پست

الگوی مدیریت وظایف طولانی‌مدت در مصاحبه‌های طراحی سیستم

جداسازی تأیید فوری از پردازش پس‌زمینه با صف پیام و worker poolها، به‌همراه مصالحه‌ها، انتخاب فناوری، مدیریت شکست، Dead Letter Queue، idempotency، backpressure و صف‌های جدا برای بارهای ترکیبی

الگوی مدیریت وظایف طولانی‌مدت در مصاحبه‌های طراحی سیستم

مدیریت وظایف طولانی‌مدت

در این فصل الگوی وظایف طولانی‌مدت (Long-Running Tasks) و نحوه استفاده از آن در طراحی سیستم را یاد می‌گیریم.

الگوی Managing Long-Running Tasks درخواست‌های API را به دو مرحله تقسیم می‌کند: تأیید فوری و پردازش پس‌زمینه. وقتی کاربران کارهای سنگین (مثل encoding ویدیو) را ارسال می‌کنند، وب‌سرور فوراً درخواست را تأیید می‌کند، یک job به صف می‌فرستد (Redis یا RabbitMQ) و در چند میلی‌ثانیه یک job ID برمی‌گرداند. در همین حال، فرایندهای worker جداگانه مدام صف را بررسی می‌کنند، jobهای معلق را برمی‌دارند، کار زمان‌بر واقعی را اجرا می‌کنند و وضعیت job را در پایگاه داده به‌روز می‌کنند.

مسئله

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

حالا فرض کنید به‌جای گرفتن اطلاعات، باید گزارش PDF از فعالیت سالانه کاربر تولید کنیم. این کار شامل کوئری روی چند جدول، تجمیع داده در میلیون‌ها سطر، رسم نمودار و تولید یک سند قالب‌بندی‌شده است. کل فرایند دست‌کم ۴۵ ثانیه طول می‌کشد.

مشکل پردازش هم‌زمان

با پردازش هم‌زمان (synchronous)، مرورگر کاربر ۴۵ ثانیه منتظر می‌ماند. بیشتر وب‌سرورها و Load Balancerها timeoutهایی حدود ۳۰ تا ۶۰ ثانیه اعمال می‌کنند، پس ممکن است درخواست اصلاً کامل نشود. حتی اگر کامل شود، تجربه کاربری بد است. کاربر به یک نشانگر loading خیره شده و هیچ بازخوردی از پیشرفت ندارد.

گزارش PDF منحصربه‌فرد نیست. مثلاً آپلود ویدیو encoding می‌خواهد که چند دقیقه طول می‌کشد. آپلود عکس پروفایل resize، crop و ساخت thumbnail می‌خواهد. عملیات انبوه مثل فرستادن خبرنامه برای هزاران کاربر یا import فایل‌های CSV بزرگ حتی بیشتر طول می‌کشد. همه این عملیات خیلی بیش از چیزی است که کاربران منطقاً منتظرش می‌مانند.

حتی اگر بتوانید بی‌نهایت مقیاس بدهید، پردازش هم‌زمان بازخورد کاربری وحشتناکی می‌دهد. کاربر روی «Generate Report» کلیک می‌کند و بعد… هیچ. کار کرد؟ شکست خورد؟ باید refresh کند؟ بعد از ۳۰ ثانیه انتظار، بسیاری از کاربران فکر می‌کنند چیزی خراب است و دوباره تلاش می‌کنند. این کار job تکراری می‌سازد و مسئله را بدتر می‌کند.

خب، پردازش هم‌زمان وقتی عملیات بیش از چند ثانیه طول بکشد به‌وضوح محدودیت دارد. جایگزین چیست؟

راه‌حل

به‌جای این‌که کاربر را هنگام تولید PDF منتظر نگه داریم، عملیات را به دو بخش تقسیم می‌کنیم. وقتی روی «Generate Report» کلیک می‌کند، بلافاصله درخواستش را در صف ذخیره می‌کنیم و پاسخی برمی‌گردانیم: «در حال تولید گزارش شما هستیم. وقتی آماده شد خبرتان می‌کنیم.» این کار میلی‌ثانیه طول می‌کشد، نه چند دقیقه.

کار وب‌سرور حالا خیلی ساده است. فقط درخواست را تأیید می‌کند، به صف اضافه‌اش می‌کند و job ID را برمی‌گرداند. تولید واقعی PDF کاملاً جای دیگری انجام می‌شود.

این را الگوی مدیریت وظایف طولانی‌مدت می‌نامیم. ایده اصلی جدا کردن پذیرش درخواست از پردازش درخواست است. وب‌سرورهای شما به روترهای سبک درخواست تبدیل می‌شوند که کار را سریع تأیید می‌کنند و تحویل می‌دهند. مجموعه جداگانه‌ای از فرایندهای worker کار سنگین واقعی را انجام می‌دهند. این workerها jobها را از یک صف مشترک برمی‌دارند، با سرعت خودشان پردازش می‌کنند و بعد از اتمام، وضعیت job را به‌روز می‌کنند. بخش «async» یعنی درخواست اصلی HTTP بدون منتظر ماندن برای تمام شدن کار، فوراً کامل می‌شود. بخش «worker» به مجموعه فرایندهایی اشاره دارد که برای اجرای این jobهای پس‌زمینه تخصیص داده شده‌اند.

صف مثل بافر بین وب‌سرورها و workerها عمل می‌کند. jobها را به‌صورت بادوام نگه می‌دارد تا اگر workerای از کار بیفتد، چیزی از بین نرود. انتخاب‌های محبوب برای صف شامل Redis (با Bull یا BullMQ)، AWS SQS، RabbitMQ یا Kafka برای مقیاس بزرگ‌تر است. صف همچنین نشان می‌دهد چند job در انتظار است، چقدر طول می‌کشد و آیا jobای شکست خورده است یا نه.

این جداسازی مشکلات مقیاس‌دهی را که قبلاً دیدیم حل می‌کند. سرورهای وب شما دیگر لازم نیست فقط چون بعضی درخواست‌ها پردازش ویدیو را راه می‌اندازند، GPU گران داشته باشند. می‌توانید وب‌سرورها را روی نمونه‌های کوچک و ارزان اجرا کنید، در حالی که workerهای ویدیو از ماشین‌های بهینه برای GPU استفاده می‌کنند. وقتی تولید PDF در گزارش‌های پایان ماه backlog می‌شود، بدون دست زدن به ناوگان وب‌سرور، worker بیشتری راه می‌اندازید. هر جزء بر اساس نیاز خودش مقیاس می‌گیرد.

تجربه کاربری هم چشمگیر بهتر می‌شود. به‌جای خیره شدن به مرورگر منجمد، کاربران فوراً تأیید می‌گیرند که درخواستشان دریافت شده است. می‌توانند بعداً دوباره بررسی کنند تا جایگاهشان در صف یا پیشرفت پردازش را ببینند. وقتی کار کامل شد، از طریق ایمیل، push notification یا به‌روزرسانی WebSocket خبرشان می‌کنید. آزادند که بروند، لپ‌تاپ را ببندند و وقتی کار واقعاً تمام شد برگردند.

این الگو به‌طور گسترده برای هر عملیاتی که بیش از چند ثانیه طول می‌کشد کاربرد دارد. پردازش تصویر، encoding ویدیو، import انبوه داده، تماس با APIهای شخص ثالث با rate limit سخت، تولید گزارش و کمپین ایمیلی، همه از پردازش ناهمزمان سود می‌برند. فناوری صف و پیاده‌سازی worker ممکن است فرق کند، اما الگوی اصلی همان است: سریع بپذیرید، ناهمزمان پردازش کنید، بعد از تکمیل خبر بدهید.

مصالحه‌ها

مدیریت وظایف طولانی‌مدت جادو نیست. بعضی مشکلات را حل می‌کند و بعضی را می‌سازد. این چیزی است که وارد آن می‌شوید.

آنچه به دست می‌آورید

  • زمان پاسخ سریع برای کاربر: فراخوانی API به‌جای timeout شدن بعد از ۳۰ ثانیه، در میلی‌ثانیه برمی‌گردد. کاربران فوراً تأیید می‌گیرند که درخواستشان دریافت شده است.
  • مقیاس‌دهی مستقل: وب‌سرورها و workerها جداگانه مقیاس می‌گیرند. در اوج پردازش worker بیشتری اضافه کنید، بدون پرداخت برای وب‌سرورهای بیکار.
  • جداسازی خطا: خرابی worker هنگام پردازش یک ویدیو کل API شما را از کار نمی‌اندازد. jobهای شکست‌خورده را می‌توان بدون اثر روی سرویس‌های رو به کاربر دوباره امتحان کرد.
  • استفاده بهتر از منابع: workerهای CPU-bound روی نمونه‌های بهینه برای محاسبه اجرا می‌شوند. jobهای پرمصرف حافظه ماشین پرحافظه می‌گیرند. وب‌سرورها از نمونه‌های ارزان همه‌منظوره استفاده می‌کنند.

آنچه از دست می‌دهید

  • پیچیدگی سیستم: حالا صف، worker و ردیابی وضعیت job برای مدیریت دارید. اجزای متحرک بیشتر یعنی چیزهای بیشتری که می‌توانند خراب شوند.
  • سازگاری نهایی: وقتی API برمی‌گردد، کار انجام نشده است. کاربران ممکن است تا کامل شدن پردازش پس‌زمینه داده کهنه ببینند.
  • ردیابی وضعیت job: برای ذخیره وضعیت jobها، مدیریت retry و ارائه endpoint وضعیت به زیرساخت نیاز دارید. این بار پایگاه داده و پیچیدگی API را بیشتر می‌کند.
  • سربار مانیتورینگ: عمق صف، سلامت workerها، نرخ شکست job و تأخیر پردازش. به‌جای چرخه ساده درخواست و پاسخ، یک سیستم توزیع‌شده را زیر نظر دارید.

الگوهای ناهمزمان حالت‌های خرابی جدیدی هم می‌سازند. وقتی صف پر می‌شود چه می‌شود؟ با پیام‌های معیوب (poison message) که مدام workerها را crash می‌کنند چه می‌کنید؟ کی از retry کردن job شکست‌خورده دست می‌کشید؟ حل این مشکلات غیرممکن نیست، اما برنامه‌ریزی می‌خواهد که سیستم‌های هم‌زمان به آن نیاز ندارند.

نحوه پیاده‌سازی

در هسته، دو فناوری برای این کار لازم است.

  • Message Queue: صف پیام سیستمی است که اجازه می‌دهد بین بخش‌های مختلف سیستم پیام بفرستید و دریافت کنید. راهی برای جدا کردن فرستنده و گیرنده پیام است تا مستقل از هم عمل کنند.
  • Worker Pool: مجموعه‌ای از workerها گروهی از فرایندها هستند که برای پردازش پیام‌های یک صف تخصیص داده شده‌اند. مسئولیتشان برداشتن پیام از صف، پردازش آن و به‌روزرسانی وضعیت job است.

هر جزء و انتخاب‌های رایج فناوری را که در مصاحبه درباره‌شان صحبت می‌کنید مرور می‌کنیم.

Message Queue

صف باید بادوام باشد. اگر از کار بیفتد، نباید همه jobهای معلق را از دست بدهید. همچنین باید دسترسی هم‌زمان چند worker را بدون تکراری شدن کار پشتیبانی کند. این گزینه‌های استاندارد است.

  • Redis با Bull یا BullMQ: بسیاری از استارتاپ‌ها از آن استفاده می‌کنند. Redis ذخیره‌سازی را فراهم می‌کند و Bull معناشناسی صف job را روی آن اضافه می‌کند: retry خودکار، jobهای تأخیری، صف‌های اولویت‌دار. راه‌اندازی آن ساده است و برای ۹۰٪ موارد «فقط کار می‌کند». با این‌که Redis گزینه‌های ماندگاری (persistence) دارد، همچنان اول از همه درون‌حافظه‌ای است. پس در crash سخت ممکن است jobها را از دست بدهید. برای دوام واقعی، SQS یا RabbitMQ می‌خواهید.
  • AWS SQS: سربار عملیاتی را کاملاً حذف می‌کند، که خوب است. آمازون زیرساخت را مدیریت می‌کند، مقیاس‌دهی را انجام می‌دهد و تحویل پیام را تضمین می‌کند. به ازای هر پیام پول می‌دهید که در حجم کم عالی است ولی در مقیاس بزرگ ممکن است گران شود. محدودیت اندازه پیام ۲۵۶ کیلوبایت (طبق متن اصلی) یعنی داده job را جای دیگری ذخیره می‌کنید و فقط شناسه‌ها را از صف رد می‌کنید.
  • RabbitMQ: با الگوهای مسیریابی پیچیده کنترل بیشتری می‌دهد، اما باید خودتان میزبانی کنید. در محیط‌های سازمانی آزموده شده و workflowهای پیچیده را خوب مدیریت می‌کند. بار عملیاتی هم واقعی است. باید clusterها را مدیریت کنید، ارتقا بدهید و مصرف دیسک را زیر نظر بگیرید.
  • Kafka: به‌خصوص وقتی استفاده می‌شود که stack شما از قبل از event streaming استفاده می‌کند. log فقط‌اضافه‌شونده‌اش اجازه می‌دهد پیام‌ها را replay کنید، برای چند مصرف‌کننده بفرستید و داده را برای بازه‌های retention طولانی نگه دارید، در حالی که هنوز حجم عظیم را با تضمین‌های ترتیب دقیق مدیریت می‌کند.

Workerها

workerها jobها را از صف برمی‌دارند و اجرا می‌کنند. تصمیم کلیدی این است که این workerها چطور میزبانی شوند.

سرورهای «معمولی» ساده‌ترین روش‌اند. ۱۰ تا ۲۰ فرایند worker را روی چند ماشین اجرا می‌کنید که هرکدام در یک حلقه jobها را انجام می‌دهند. این کار کنترل کامل روی محیط می‌دهد و دیباگ را ساده می‌کند، چون می‌توانید با SSH وارد جعبه شوید و ببینید چه می‌گذرد. وقتی زیرساخت را کنترل می‌کنید، jobهای طولانی مشکلی نیستند. نقطه ضعف این است که خودتان سرورها را مدیریت می‌کنید و برای ظرفیت بیکار در دوره‌های آرام پول می‌دهید.

1
2
3
4
5
6
# Simplified worker loop
while True:
    job = queue.pop()  # Blocks until job available
    if job:
        process_job(job)
        mark_complete(job.id)

توابع serverless (Lambda و Cloud Functions) مدیریت سرور را حذف می‌کنند. هر job اجرای یک تابع را راه می‌اندازد که خودکار مقیاس می‌گیرد. این برای بارهای کاری پراکنده خوب کار می‌کند، جایی که ممکن است در یک دقیقه ۱۰۰۰ job پردازش کنید و در دقیقه بعد صفر. فقط برای زمان اجرای واقعی پول می‌دهید. اما محدودیت‌ها اینجا معنادارند. به اجراهای ۱۵ دقیقه‌ای محدود می‌شوید، cold start تأخیر می‌سازد و ذخیره‌سازی محلی حداقلی است.

workerهای مبتنی بر کانتینر (روی Kubernetes یا ECS) حد وسط را ارائه می‌دهند. workerها را به‌صورت کانتینر Docker بسته‌بندی می‌کنید و بگذارید orchestrator مقیاس و استقرار را مدیریت کند. این رویکرد از سرورهای ساده پیچیده‌تر است، اما انعطاف بیشتری از serverless می‌دهد. هنوز می‌توانید jobهای طولانی اجرا کنید و هم‌زمان با auto-scaling از منابع بهتر استفاده کنید.

کنار هم گذاشتن

این‌طور همه‌چیز با هم کار می‌کند.

  1. وب‌سرور درخواست را تأیید می‌کند و رکورد job را در پایگاه داده با وضعیت «در انتظار» (pending) می‌سازد.
  2. وب‌سرور پیامی حاوی job ID (نه داده کامل job) به صف می‌فرستد.
  3. وب‌سرور بلافاصله job ID را به کلاینت برمی‌گرداند.
  4. worker پیام را از صف برمی‌دارد و جزئیات job را از پایگاه داده می‌گیرد.
  5. worker وضعیت job را به «در حال پردازش» (processing) به‌روز می‌کند.
  6. worker کار واقعی را انجام می‌دهد.
  7. worker نتیجه‌ها را ذخیره می‌کند (فایل‌ها در S3، metadata در پایگاه داده و مانند آن).
  8. worker وضعیت job را به «کامل‌شده» (completed) یا «شکست‌خورده» (failed) به‌روز می‌کند.

نکته مهم این است که هر جزء می‌تواند مستقل از کار بیفتد و سیستم همچنان کار کند. اگر صف از کار بیفتد، وب‌سرورها همچنان درخواست‌ها را می‌پذیرند و jobها در پایگاه داده به‌صورت «در انتظار» جمع می‌شوند. وقتی workerها crash می‌کنند، jobها در صف می‌مانند تا workerهای جدید آنلاین شوند. اگر پایگاه داده مشکل داشته باشد، workerها می‌توانند تا بازیابی دوباره تلاش کنند.

در مصاحبه این را بیش از حد پیچیده نکنید. Kafka (یا صفی که خوب می‌شناسید) را برای صف انتخاب کنید، مگر دلیل مشخصی برای غیر از آن باشد. برای workerها از فرایندهای سرور معمولی استفاده کنید، مگر مصاحبه‌گر برای serverless فشار بیاورد. به‌جای بحث درباره مزیت‌های فناوری‌های مختلف صف، روی نشان دادن اینکه تفکیک دغدغه‌ها را می‌فهمید تمرکز کنید.

زمان استفاده در مصاحبه

منتظر نمانید مصاحبه‌گر درباره مدیریت وظایف طولانی‌مدت بپرسد. نکته کلیدی این است که مسائلی را که فریاد می‌زنند «پردازش ناهمزمان» تشخیص دهید و فعالانه آن را پیشنهاد کنید.

چند نشانه رایج که باید دنبالشان باشید و فکرتان را درباره مدیریت وظایف طولانی‌مدت راه بیندازند:

  • وقتی عملیات کند مشخصی نام برده می‌شود: لحظه‌ای که «encoding ویدیو»، «پردازش تصویر»، «تولید PDF»، «ارسال ایمیل انبوه» یا «export داده» را می‌شنوید، نشانه شماست. این عملیات از چند ثانیه تا چند دقیقه طول می‌کشند. فوراً وارد شوید: «تبدیل encoding ویدیو چند دقیقه طول می‌کشد، پس job ID را فوراً برمی‌گردانم و ناهمزمان پردازش می‌کنم.»
  • وقتی حساب‌ها جور درنمی‌آیند: اگر می‌گویند «روزی ۱ میلیون تصویر پردازش می‌کنیم» و می‌دانید هر پردازش تصویر ۱۰ ثانیه طول می‌کشد، حساب سریع را با صدای بلند انجام دهید: «این حدود ۱۲ تصویر در ثانیه است، یعنی ۱۲۰ ثانیه زمان پردازش در هر ثانیه. حدود ۱۲۰ worker هم‌زمان لازم داریم. این اصلاً در درخواست هم‌زمان جا نمی‌شود، پس از worker ناهمزمان استفاده می‌کنم.»
  • وقتی عملیات مختلف سخت‌افزار متفاوتی می‌خواهند: اگر مسئله هم APIهای ساده درخواست و پاسخ دارد و هم jobهای سنگین GPU (مثل inference یادگیری ماشین یا پردازش ویدیو)، نشانه روشن پردازش ناهمزمان است. «نباید بارهای GPU را روی همان سرورهایی اجرا کنیم که درخواست‌های login را مدیریت می‌کنند. این‌ها را با workerهای ناهمزمان روی نمونه‌های GPU جدا می‌کنم.»
  • وقتی درباره مقیاس یا خرابی می‌پرسند: سوال‌هایی مثل «اگر سرور حین پردازش خراب شود چه؟» یا «ترافیک ۱۰ برابر را چطور مدیریت می‌کنید؟» فرصت‌های خوبی برای معرفی workerهای ناهمزمان‌اند. «با workerهای ناهمزمان، اگر یکی وسط کار crash کند، worker دیگری job را از صف برمی‌دارد. هیچ درخواست کاربری گم نمی‌شود.»

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

مثال‌ها

ببینیم مدیریت وظایف طولانی‌مدت در سوال‌های محبوب مصاحبه طراحی سیستم چطور ظاهر می‌شود. برای هر کدام تحلیل‌های دقیقی داریم که نشان می‌دهد workerهای ناهمزمان دقیقاً کجای طراحی بزرگ‌تر قرار می‌گیرند.

  • YouTube و پلتفرم‌های ویدیو: آپلود ویدیو چند job ناهمزمان را راه می‌اندازد: encoding به رزولوشن‌های مختلف (1080p، 720p، 480p)، ساخت thumbnail، استخراج زیرنویس و اجرای moderation محتوا. هر ویدیو ممکن است ساعت‌ها پردازش بین چند worker لازم داشته باشد.
  • Instagram و اشتراک‌گذاری عکس: آپلود عکس jobهای ناهمزمانی برای ساخت اندازه‌های متعدد، اعمال فیلتر، استخراج metadata، اجرای تشخیص تصویر برای تگ‌گذاری و بررسی سیاست محتوا ایجاد می‌کند. مسئله fan-out هم از workerهای ناهمزمان استفاده می‌کند. وقتی کسی با میلیون‌ها دنبال‌کننده پست می‌گذارد، workerها آن پست را در فید دنبال‌کننده‌ها منتشر می‌کنند.
  • Uber و سرویس سواری: تطبیق سفر ناهمزمان اجرا می‌شود. کاربر «در حال پیدا کردن رانندگان نزدیک شما…» را می‌بیند، در حالی که workerها دسترس‌پذیری رانندگان را ارزیابی می‌کنند، مسیرهای بهینه را حساب می‌کنند و قیمت را تعیین می‌کنند. به‌روزرسانی‌های موقعیت راننده هم از طریق workerهای ناهمزمان پردازش می‌شوند تا اپلیکیشن‌های مسافر بدون فشار بیش از حد روی API اصلی به‌روز شوند.
  • Stripe و پردازش پرداخت: APIهای پرداخت بلافاصله با وضعیت معلق (pending) برمی‌گردند. workerهای ناهمزمان تلاش واقعی برای شارژ، تشخیص تقلب، تأیید 3D Secure و اعلان webhook را انجام می‌دهند. این جداسازی جلوی مسدود شدن جریان پرداخت تاجر توسط APIهای کند بانک را می‌گیرد.
  • Dropbox و همگام‌سازی فایل: آپلود فایل پردازش ناهمزمان را راه می‌اندازد: اسکن ویروس، ایندکس برای جستجو، ساخت پیش‌نمایش و همگام‌سازی با دستگاه‌های دیگر. پردازش فایل‌های بزرگ ممکن است چند دقیقه طول بکشد، اما API آپلود بلافاصله برمی‌گردد.

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

بررسی‌های عمیق رایج

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

مدیریت شکست‌ها

مصاحبه‌گر ممکن است با یک سوال پایه شروع کند، مثل «اگر worker وسط کار crash کند چه می‌شود؟». اگر از این الگو پیروی کرده باشیم، پاسخ ساده است: job توسط worker دیگری دوباره امتحان می‌شود.

اما از کجا بفهمیم worker crash کرده است؟ معمولاً مکانیزم heartbeat دارید که به‌صورت دوره‌ای با صف چک می‌کند تا بداند worker هنوز زنده است. اگر worker اعلام حضور نکند، صف فرض می‌کند مرده و job را دوباره امتحان می‌کند.

فاصله heartbeat یک تصمیم کلیدی طراحی است. اگر خیلی طولانی باشد، خرابی‌ها یعنی jobها قابل‌توجه تأخیر می‌خورند (صف خوش‌بینانه فرض می‌کند worker بیشتر از آنچه واقعاً هست زنده بوده). اگر خیلی کوتاه باشد، پیام‌های غیرضروری زیادی به صف می‌فرستید یا بدتر، ممکن است jobهایی را شکست‌خورده علامت بزنید در حالی که هنوز در حال اجرا هستند (مثلاً به‌خاطر توقف garbage collection).

هر سیستم صف روش متفاوتی برای مدیریت این دارد. مثلاً در SQS می‌توانید visibility timeout تنظیم کنید. در RabbitMQ می‌توانید فاصله heartbeat را تنظیم کنید. در Kafka می‌توانید session timeout بگذارید. بلندترین فاصله‌ای را انتخاب کنید که کاربران یا مشتریان شما از نظر تأخیر اضافه اجازه می‌دهند. برای بیشتر سیستم‌ها ۱۰ تا ۳۰ ثانیه نقطه شروع خوبی است.

مدیریت شکست‌های مکرر

مصاحبه‌گر می‌پرسد: «اگر یک job مدام شکست بخورد چه؟ شاید باگی در کد شما یا داده ورودی بدی باشد که هر بار worker را crash می‌کند.»

بدون مدیریت، jobهای شکست‌خورده برای همیشه دوباره امتحان می‌شوند. workerهای شما روی jobهای محکوم به شکست منابع هدر می‌دهند، در حالی که jobهای معتبر در صف عقب می‌مانند. بدتر این‌که یک poison message که worker را crash می‌کند می‌تواند کل ناوگان worker شما را نابود کند، چون هرکدام تلاش می‌کند آن را پردازش کند و می‌میرد.

راه‌حل Dead Letter Queue (DLQ) است. بعد از این‌که job تعداد مشخصی (معمولاً ۳ تا ۵ بار) شکست خورد، به‌جای retry آن را به صف جداگانه‌ای منتقل می‌کنید. این کار jobهای مشکل‌دار را ایزوله می‌کند و اجازه می‌دهد کار سالم ادامه یابد. DLQ شما مجموعه jobهایی می‌شود که به بررسی انسانی نیاز دارند. شاید باگی برای رفع یا داده‌ای برای پاک‌سازی باشد. بعد از رفع مشکل، می‌توانید jobها را برای پردازش دوباره به صف اصلی برگردانید.

بیشتر سیستم‌های صف پشتیبانی داخلی از DLQ دارند. در SQS یک redrive policy تنظیم می‌کنید. در RabbitMQ یک dead letter exchange راه می‌اندازید. نکته کلیدی این است که DLQ خودتان را مانیتور کنید، چون dead letter queue در حال رشد معمولاً نشانه باگی در سیستم است که توجه فوری می‌خواهد.

جلوگیری از jobهای تکراری

مصاحبه‌گر ادامه می‌دهد: «کاربر بی‌حوصله می‌شود و سه بار روی “Generate Report” کلیک می‌کند. حالا سه job یکسان در صف دارید. چطور از اجرای چندباره یک کار جلوگیری می‌کنید؟»

بدون deduplication، منابع را برای پردازش jobهای یکسان هدر می‌دهید. بدتر این‌که ممکن است سه ایمیل برای یک کاربر بفرستید یا سه بار کارت اعتباری او را شارژ کنید. وقتی jobها idempotent نباشند، عوارض جانبی بزرگ‌تر می‌شود.

راه‌حل کلید idempotency است. هنگام پذیرش job، به شناسه یکتایی نیاز دارید که آن عملیات را نشان دهد. برای اقدام‌هایی که کاربر آغاز می‌کند، شناسه کاربر + اقدام + timestamp را ترکیب کنید (احتمالاً گرد شده به بازه زمانی‌ای که می‌خواهید برای جلوگیری از کار تکراری لحاظ کنید). برای jobهایی که سیستم تولید می‌کند، از شناسه‌های قطعی (deterministic) بر اساس داده ورودی استفاده کنید. قبل از ساختن job، بررسی کنید آیا jobای با این کلید از قبل وجود دارد یا نه. اگر دارد، به‌جای ساختن job ID جدید، job ID موجود را برگردانید.

1
2
3
4
5
6
7
8
9
10
11
def submit_job(user_id, job_type, job_data, idempotency_key):
    # Check if job already exists
    existing_job = db.get_job_by_key(idempotency_key)
    if existing_job:
        return existing_job.id

    # Create new job
    job_id = create_job(user_id, job_type, job_data)
    db.store_idempotency_key(idempotency_key, job_id)
    queue.push(job_id)
    return job_id

باید خود job را هم idempotent کنید. اگر jobای وسط کار شکست بخورد و دوباره امتحان شود، باید بتواند دوباره اجرا شود. این ممکن است یعنی قبل از ادامه بررسی کنید ایمیلی قبلاً فرستاده شده یا فایلی قبلاً پردازش شده است.

مدیریت Backpressure صف

سوال بعدی: «جمعه سیاه است و ناگهان ۱۰ برابر بیشتر از معمول job می‌گیرید. workerهای شما نمی‌توانند جلو بیایند. صف به میلیون‌ها job معلق می‌رسد. چه می‌کنید؟»

وقتی workerها نمی‌توانند jobها را به‌اندازه کافی سریع پردازش کنند، صف‌ها بدون محدودیت رشد می‌کنند. مصرف حافظه منفجر می‌شود. زمان انتظار job به ساعت‌ها می‌رسد. jobهای جدید رد می‌شوند چون صف پر است. کاربران از انتظار برای نتیجه‌ای که هرگز نمی‌آید ناامید می‌شوند.

راه‌حل backpressure نام دارد: وقتی workerها غرق شده‌اند، پذیرش job را کند کنید. می‌توانید محدودیت عمق صف بگذارید و وقتی صف خیلی عمیق است، jobهای جدید را رد کنید و به‌جای پذیرفتن کاری که از پسش برنمی‌آیید، فوراً پاسخ «سیستم مشغول است» بدهید.

باید workerها را بر اساس عمق صف هم مقیاس بدهید. وقتی صف از آستانه‌ای فراتر رفت، worker بیشتری بالا بیاورید. وقتی کوچک شد، کم کنید. در AWS، آلارم CloudWatch همراه Auto Scaling Group این کار را ساده می‌کند. معیار اصلی عمق صف است، نه مصرف CPU. تا وقتی CPU بالا برود، صف شما از قبل backlog شده است.

مدیریت بارهای کاری ترکیبی

مصاحبه‌گر بررسی می‌کند: «بعضی گزارش‌های PDF شما ۵ ثانیه طول می‌کشند، اما گزارش‌های پایان سال ۵ ساعت. همه در یک صف‌اند. این چه مشکلاتی می‌سازد؟»

jobهای طولانی jobهای کوتاه را مسدود می‌کنند (head-of-line blocking). کاربری که گزارش ساده می‌خواهد، ساعت‌ها پشت export عظیم پایان سال یک نفر منتظر می‌ماند. استفاده از workerها نامتوازن می‌شود، چون بعضی workerها صدها job سریع پردازش می‌کنند و بعضی دیگر در jobهای طولانی گیر کرده‌اند. وقتی نمی‌توانید پیش‌بینی کنید job چقدر طول می‌کشد، auto-scaling هم دردسر بزرگی می‌شود.

راه‌حل این است که صف‌ها را بر اساس نوع job یا مدت مورد انتظار جدا کنید. گزارش‌های سریع به صف «fast» با workerهای زیاد می‌روند. گزارش‌های پیچیده به صف «slow» با workerهای کمتر و قوی‌تر. این کار جلوی head-of-line blocking را می‌گیرد و اجازه می‌دهد هر صف را مستقل تنظیم کنید. یا می‌توانید jobهای بزرگ را به تکه‌های کوچک‌تر بشکنید که از همان زیرساخت صف استفاده می‌کنند، مثل تقسیم News Feed به دسته‌هایی از دنبال‌کننده‌ها.

1
2
3
4
5
6
7
8
9
10
queues:
  fast:
    max_duration: 60s
    worker_count: 50
    instance_type: t3.medium

  slow:
    max_duration: 6h
    worker_count: 10
    instance_type: c5.xlarge

هنگام ارسال، jobها را به صف مناسب هدایت کنید. اگر نمی‌توانید مدت را از قبل پیش‌بینی کنید، jobها را در صف fast شروع کنید و اگر از محدودیت زمانی فراتر رفتند، به صف slow منتقل کنید. این کار صف‌های fast را پاسخگو نگه می‌دارد و به jobهای طولانی منابعی را که لازم دارند می‌دهد.

هماهنگی وابستگی‌های job

در نهایت: «اگر تولید گزارش سه مرحله دارد، یعنی گرفتن داده، تولید PDF و بعد ایمیل کردن آن، چطور jobهایی را که به jobهای دیگر وابسته‌اند مدیریت می‌کنید؟»

بدون orchestration درست، به کد اسپاگتی می‌رسید. workerهای مرحله ۱ کارشان را تمام می‌کنند و مستقیم مرحله ۲ را در صف می‌گذارند. فهمیدن و تغییر دادن جریان سخت می‌شود. مدیریت خطا کابوس می‌شود. اگر مرحله ۲ شکست بخورد، فقط همان را دوباره امتحان کنید یا از اول شروع کنید؟ مانیتور کردن این‌که کدام مرحله و چرا شکست خورده، به کند و کاو در logها نیاز دارد.

برای زنجیره‌های ساده، هر worker می‌تواند job بعدی را قبل از علامت زدن کامل بودن job خودش در صف بگذارد. context کامل را در هر job بگذارید تا مرحله‌ها را بتوان مستقل دوباره امتحان کرد:

1
2
3
4
5
6
7
8
9
{
  "workflow_id": "report_123",
  "step": "generate_pdf",
  "previous_steps": ["fetch_data"],
  "context": {
    "user_id": 456,
    "data_s3_url": "s3://bucket/data.json"
  }
}

برای workflowهای پیچیده با مرحله‌های شاخه‌ای یا موازی، از orchestrator مثل AWS Step Functions، Temporal یا Airflow استفاده کنید. این ابزارها اجازه می‌دهند workflowها را به‌صورت کد تعریف کنید، برای هر مرحله retry داشته باشید و دید بدهند که workflowها کجا گیر می‌کنند. مصالحه پیچیدگی بیشتر است، پس فقط وقتی سراغ orchestration بروید که وابستگی jobها واقعاً پیچیده شد. درباره این موضوع در الگوی فرایندهای چندمرحله‌ای بیشتر می‌خوانید.

جمع‌بندی

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

یادتان باشد که در مصاحبه می‌خواهید خودتان این مشکلات را شناسایی کنید، نه این‌که منتظر بمانید مصاحبه‌گر به آن‌ها اشاره کند. وقتی آپلود ویدیو یا تولید PDF را مطرح می‌کنند، فوراً بگویید «این برای یک درخواست هم‌زمان خیلی طول می‌کشد، پس از workerهای ناهمزمان استفاده می‌کنم.» نشان دهید از همان ابتدا درباره timeout، مصرف منابع و تجربه کاربری فکر می‌کنید.

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

از همه مهم‌تر، زیاد به آن فکر نکنید. این الگو تقریباً در هر شرکتی در مقیاس بزرگ ظاهر می‌شود، از تطبیق سفر Uber تا fan-out فید اینستاگرام. بر این الگو مسلط شوید و با اطمینان با سلسله‌ای از مسائل طراحی سیستم روبه‌رو خواهید شد.