System Design

برنامه‌ریزی توزیع‌شده وظایف: مهندسی صف‌های کار و سیستم‌های Cron قابل اعتماد در مقیاس بزرگ

با رشد برنامه‌ها از ساختارهای تک‌جزئی به میکروسرویس‌های توزیع‌شده، رویکرد ساده اجرای اسکریپت‌ها روی یک سرور دیگر کافی نیست. چه در حال پردازش میلیون‌ها تصویر باشید، چه ارسال ایمیل‌های تراکنشی یا همگام‌سازی پایگاه‌های داده در سراسر مناطق، به یک مکانیزم قدرتمند برای مدیریت کارهای ناهمگام نیاز دارید. این پست به بررسی الگوهای معماری برای ساخت زمان‌بندهای وظایف توزیع‌شده قابل اعتمادی می‌پردازد که می‌توانند پیچیدگی و مقیاس زیرساخت‌های مدرن را مدیریت کنند.

چالش‌های اصلی زمان‌بندی توزیع‌شده

هنگام انتقال از یک وظیفه cron محلی به یک سیستم توزیع‌شده، سه چالش اصلی ظاهر می‌شوند:

  1. هماهنگی: چگونه اطمینان حاصل کنید که یک وظیفه دقیقاً یک بار یا حداقل یک بار اجرا می‌شود، زمانی که چندین گره برای انجام کار رقابت می‌کنند؟
  2. قابلیت اطمینان: اگر یک کارگر (Worker) در میانه پردازش دچار خطا شود، چه اتفاقی می‌افتد؟ وظیفه باید بازیابی و مجدداً اجرا شود بدون اینکه باعث فساد داده‌ها گردد.
  3. قابلیت مشاهده (Observability): شما به بینش‌های بلادرنگ درباره عمق صف، تأخیر پردازش و نرخ خطاها برای حفظ سلامت سیستم نیاز دارید.

یک پیاده‌سازی ساده ممکن است از یک جدول پایگاه داده به عنوان صف استفاده کند. اگرچه این روش ساده است، اما تحت بار کاری بالا با مشکلات قفل‌گذاری (Lock contention) و مقیاس‌پذیری مواجه می‌شود. در عوض، ما به واسط‌های پیام (Message brokers) یا سیستم‌های صف‌بندی تخصصی متکی می‌شویم.

معماری با استفاده از واسط‌های پیام

استاندارد صنعت برای صف‌بندی کارهای قابل اعتماد، جداسازی تولیدکننده (کسی که وظیفه را زمان‌بندی می‌کند) از مصرف‌کننده (کسی که آن را اجرا می‌کند) از طریق یک واسط پیام مانند RabbitMQ، Apache Kafka یا AWS SQS است. این سیستم‌ها تضمین‌های دوام را فراهم می‌کنند و اطمینان حاصل می‌کنند که پیام‌ها حتی اگر مصرف‌کنندگان آفلاین باشند، حفظ می‌شوند.

فرض کنید سناریویی داریم که نیاز به پردازش آپلودهای کاربران داریم. ما نمی‌خواهیم API در حالی که تصویر در حال تغییر اندازه است، متوقف بماند. در عوض، یک وظیفه را به صف ارسال می‌کنیم.

در اینجا یک مثال مفهومی با استفاده از ساختار شبه‌کد پایتون برای انتشار یک وظیفه در یک صف دوام‌دار آورده شده است:

class JobPublisher:
    def __init__(self, queue_service):
        self.queue = queue_service

    def schedule_image_resize(self, image_id, dimensions):
        """
        یک وظیفه تغییر اندازه را به صف توزیع‌شده منتشر می‌کند.
        شامل متادیتا برای منطق تلاش مجدد و ردیابی است.
        """
        job_payload = {
            "task_type": "resize_image",
            "image_id": image_id,
            "target_dimensions": dimensions,
            "timestamp": datetime.utcnow().isoformat(),
            "retry_count": 0
        }
        
        # انتشار با تنظیم پرچم 'durable' روی true
        # این اطمینان حاصل می‌کند که پیام پس از راه‌اندازی مجدد واسط زنده می‌ماند
        self.queue.publish(
            exchange="task_exchange",
            routing_key="image.resize",
            body=json.dumps(job_payload),
            persistent=True
        )

تضمین ایدمپوتنسی و منطق تلاش مجدد

در یک محیط توزیع‌شده، تحویل «حداقل یک‌بار» (At-least-once) رایج‌ترین تضمین است. این بدان معناست که اگر یک مصرف‌کننده پس از پردازش اما قبل از تأیید پیام دچار خطا شود، ممکن است همان وظیفه را دو بار دریافت کند. برای جلوگیری از عوارض جانبی تکراری (مانند شارژ دوباره کاربر یا تغییر اندازه دوباره تصویر)، وظایف باید ایدمپوتنت (Idempotent) باشند.

ایدمپوتنسی اغلب با استفاده از یک شناسه منحصر‌به‌فرد برای هر وظیفه به دست می‌آید. کارگر بررسی می‌کند که آیا شناسه وظیفه قبلاً در یک انبار تکمیل (مانند Redis یا DynamoDB) پردازش شده است یا خیر، قبل از انجام پردازش سنگین.

برای خطاهای گذرا، از بازگشت نمایی (Exponential backoff) استفاده کنید. به جای شکست فوری یا تلاش مجدد بلافاصله، تأخیرها را وارد کنید:

def process_with_retry(task, max_retries=5):
    for attempt in range(max_retries):
        try:
            execute_task(task)
            return True
        except TransientError as e:
            delay = 2 ** attempt * random.uniform(0, 1)
            time.sleep(delay)
            log_warning(f"Attempt {attempt + 1} failed: {e}")
            
    raise FatalError("Exhausted all retry attempts")

زمان‌بندی با Cron توزیع‌شده

در حالی که واسط‌های پیام وظایف رویداد-محور را مدیریت می‌کنند، شما به زمان‌بندی مبتنی بر زمان نیز نیاز دارید. اجرای یک سرور cron واحد، نقطه شکست واحد (Single point of failure) ایجاد می‌کند. برای مقیاس‌پذیری cron، باید از مشکلات «گله طوفانی» (Thundering herd) که در آن همه کارگران همزمان بیدار می‌شوند، اجتناب کنید.

الگوهای محبوب شامل استفاده از یک زمان‌بنده متمرکز مانند Kubernetes CronJobs برای استقرارهای بومی کوبرنِتس، یا استفاده از کتابخانه‌هایی مانند Celery Beat یا Hangfire است که مسئولیت زمان‌بندی را بین چندین گره توزیع می‌کنند. نکته کلیدی این است که تنها یک نمونه از زمان‌بنده باید وظیفه را فعال کند که اغلب از طریق قفل‌های توزیع‌شده (مانند Redis SETNX) اعمال می‌شود.

نتیجه‌گیری

ساخت سیستم‌های وظایف توزیع‌شده قابل اعتماد تنها به انتخاب ابزار مناسب نیست؛ بلکه طراحی برای شکست است. با پذیرش ایدمپوتنسی، استفاده از واسط‌های پیام دوام‌دار و پیاده‌سازی مکانیزم‌های تلاش مجدد قوی، می‌توانید سیستم‌هایی بسازید که تحت بار کاری مقاوم باقی بمانند. هنگام مقیاس‌پذیری، به یاد داشته باشید که قابلیت مشاهده (Observability) تور ایمنی شماست—صف‌های خود را نظارت کنید و همیشه از سلامت برنامه خود آگاه خواهید بود.

Share: