০০ যে প্রশ্নগুলোর উত্তর খুঁজবো
সেশন ০৮ এ আমরা শিখেছি, ধীর কাজ সাথে সাথে না করে queue তে রেখে দেওয়া যায়। পরে worker রা সেটা তুলে নিয়ে শেষ করে। কিন্তু সেখানে worker কে আমরা একটা বাক্স হিসেবেই দেখেছি। ভেতরে কী হয়, সেটা দেখা হয়নি।
এই সেশনে আমরা সেই বাক্সটা খুলবো। কতজন worker রাখবো? কাজ fail করলে কখন আবার চেষ্টা করবো? Deploy এর সময় worker হঠাৎ বন্ধ হলে অর্ধেক করা কাজের কী হবে? আর রাত ২টার কাজ ভুল করে দুইবার চললে কী হবে? প্রতিটা প্রশ্ন আমরা দেখবো একটা payroll startup এর একটা কঠিন সকালের ভেতর দিয়ে।
- ১ ৫০,০০০ salary slip একটা loop এ বানালে সমস্যা কোথায়? (background job)
- ২ Job কত রকমের হয়? (one-off, delayed, cron, recurring)
- ৩ অনেক job একসাথে চালাবো কীভাবে? (worker pool)
- ৪ Worker pool কত বড় হবে? (CPU-bound বনাম IO-bound)
- ৫ Worker বাড়ালেই কি কাজ সবসময় দ্রুত হয়? (concurrency limit)
- ৬ Job fail করলে কখন আবার চেষ্টা করবো? (exponential backoff, jitter)
- ৭ একটা job আটকে গেলে কে টের পায়? (job timeout, job status)
- ৮ Deploy এর সময় চলতি job গুলোর কী হয়? (graceful shutdown)
- ৯ দুই server এ একই cron job চললে কী হয়? (distributed lock)
০১ ৫০,০০০ salary slip একটা loop এ বানালে সমস্যা কোথায়?
নভেম্বরের ১ তারিখ, সকাল ৮টা ৫৫। তানিয়া অফিসে ঢুকে laptop খুলতেই দেখে, support inbox এ ৪০টা নতুন message। প্রায় সবগুলোর কথা এক: “আমার বেতনের slip এখনো আসেনি।” তানিয়ার startup দেশের ৬০টা কোম্পানির payroll চালায়, সব মিলিয়ে ৫০,০০০ কর্মী। নিয়ম হলো, প্রতি মাসের ১ তারিখ সকাল ৯টার আগে প্রত্যেকের email এ তার salary slip এর PDF পৌঁছাবে। তানিয়া script এর log খুললো। রাত ২টায় চালু হওয়া script টা এখন মাত্র ১২,৪৫০ নম্বর slip এ আছে। তার বুকটা ধক করে উঠলো।
Script টা দেখতে খুবই সরল ছিলো। একটা loop সব কর্মীর তালিকা ধরে একজন একজন করে এগোয়। প্রতিজনের জন্য আগে PDF বানায়, তারপর email পাঠায়, তারপর পরের জনে যায়। হিসাবটা করলেই সমস্যা পরিষ্কার হয়:
একটা slip: PDF render 0.8s + email 1.2s = 2s এক loop এ সব: 50,000 × 2s = 100,000s ≈ 27.8 ঘণ্টা হাতে সময়: রাত 2:00 থেকে সকাল 9:00 = 7 ঘণ্টা = 25,200s 9টার আগে হয়: 25,200 ÷ 2s = 12,600 slip বাকি থাকে: 50,000 - 12,600 = 37,400 জন কর্মী
এক loop এ পুরো কাজ শেষ করতে প্রায় ২৮ ঘণ্টা লাগে। অথচ হাতে আছে মাত্র ৭ ঘণ্টা। কিন্তু সমস্যা শুধু ধীর হওয়া নয়:
- একটাই লাইন: Loop একবারে একটা slip নিয়েই ব্যস্ত থাকে। Machine এর বাকি core গুলো চুপচাপ বসে থাকে। এই loop টাই এখানে bottleneck (সেশন ০১), অর্থাৎ পুরো system এর গতি যে একটা জায়গায় আটকে আছে।
- মাঝপথে crash হলে সব গুলিয়ে যায়: রাত ৪টায় script crash করলে কোন কোন slip গেছে, তার কোনো হিসাব থাকে না। আবার শুরু থেকে চালালে প্রথম কয়েক হাজার মানুষ দুইটা করে email পান।
- ভুল জায়গায় চলে: Script টা চলছিলো web server এ। রাতে যে HR ম্যানেজাররা dashboard খোলেন, তাদের page ও তখন ধীর হয়ে যায়।
সমাধানের প্রথম ধাপ হলো ভাবনাটা বদলানো। “৫০,০০০ slip এর একটা বিশাল কাজ” না ভেবে ভাবুন “৫০,০০০টা ছোট, আলাদা কাজ”। প্রতিটা slip একটা আলাদা background job।
Background job: এমন একটা কাজ, যেটা কোনো user এর request এর ভেতরে চলে না। Web server এর বাইরে আলাদা একটা process (worker) কাজটা করে। সাধারণত job টা আগে একটা queue তে জমা হয়, তারপর worker সেখান থেকে তুলে নেয়।
Queue, ACK আর idempotency সেশন ০৮ এ দেখেছি। সেখানে প্রশ্ন ছিলো, কাজকে পরে করার জন্য কোথায় রাখবো। এই সেশনের প্রশ্ন হলো, সেই পরে করার কাজগুলো ঠিক সময়ে, ঠিক গতিতে আর নিরাপদে কীভাবে চালাবো।
একটা বড় কাজ একটা loop এ চালালে তিনটা জিনিস একসাথে হারায়: গতি, crash এর পরে হিসাব, আর web server এর শান্তি। কাজটাকে অনেকগুলো ছোট job এ ভাঙলে প্রতিটা job আলাদাভাবে চালানো, retry করা আর গোনা যায়।
০২ Job কত রকমের হয়?
তানিয়ার system এ শুধু মাসের শুরুর slip ই নেই। আরো কয়েক রকম কাজ পেছনে চলে। কোন job কখন চলবে, সেটা দেখে job কে চার ভাগে ভাগ করা যায়:
- One-off job: একবারই চলবে, আর এখনই। ধরুন HR একজন কর্মীর বেতন ঠিক করে “slip আবার পাঠান” button চাপলেন। একটা job queue তে গেলো, worker সেটা সাথে সাথে চালিয়ে দিলো।
- Delayed job: কাজটা এখন ঠিক হলো, কিন্তু চলবে একটু পরে। যেমন HR বেতন approve করলেন, আর ৩০ মিনিট পরে কর্মীদের একটা reminder email যাবে। Job টা এখনই queue তে যায়, সাথে লেখা থাকে “এই সময়ের আগে চালাবেন না”। বেশিরভাগ queue এই সুবিধা দেয় (যেমন Amazon SQS এর delay)।
- Scheduled job (cron): ক্যালেন্ডারের নির্দিষ্ট সময়ে চলে। প্রতি মাসের ১ তারিখ রাত ২টায় slip বানানো এমন একটা কাজ।
- Recurring job: একটা নির্দিষ্ট বিরতিতে বারবার চলে। যেমন প্রতি ১৫ মিনিটে ব্যাংক থেকে খোঁজ নেওয়া, কোন বেতন account এ পৌঁছেছে।
Cron: ঘড়ি দেখে কাজ চালানো
Cron হলো Unix এর একটা পুরোনো আর খুব জনপ্রিয় scheduler, মানে এমন একটা program যেটা ঘড়ি দেখে নির্দিষ্ট সময়ে কাজ চালু করে। কখন চলবে, সেটা পাঁচটা ঘরে লেখা হয়। এই লেখাটাকে বলে cron expression। তানিয়ার payroll job এর cron expression হলো 0 2 1 * *:
| মিনিট | ঘণ্টা | মাসের দিন | মাস | সপ্তাহের দিন |
|---|---|---|---|---|
0 | 2 | 1 | * | * |
| ০ মিনিটে | রাত ২টায় | ১ তারিখে | প্রতি মাসে | সপ্তাহের যেকোনো দিন |
* মানে “যেকোনো”। তাই পুরো লাইনটার মানে: প্রতি মাসের ১ তারিখ রাত ২টা ০ মিনিটে কাজটা চলবে। প্রতি ১৫ মিনিটের recurring job টা লেখা যায় */15 * * * * দিয়ে।
খেয়াল করুন, cron নিজে PDF বানায় না। সে শুধু ঠিক সময়ে ঘুম ভেঙে ৫০,০০০টা job queue তে রেখে দেয়। আসল কাজ করে worker রা। এই ভাগটা মনে রাখুন, সেশনের শেষে এটা খুব কাজে লাগবে।
চার রকম job এর পার্থক্য শুধু কখন চলবে, সেখানে। কীভাবে চলবে, সেটা সবার জন্য একই। সবাই queue তে যায়, আর একই worker রা সবাইকে চালায়।
০৩ অনেক job একসাথে চালাবো কীভাবে?
এখন queue তে ৫০,০০০টা job। একজন worker একটা একটা করে চালালে আবার সেই ২৮ ঘণ্টা। তাই তানিয়া একসাথে অনেক worker চালানোর সিদ্ধান্ত নিলো। এই worker দের দলকে বলে worker pool।
Worker pool: নির্দিষ্ট সংখ্যক worker এর একটা দল, যারা একই queue থেকে job নেয়। প্রত্যেকে নিজের হাতের job শেষ করে ACK পাঠায়, তারপর queue থেকে পরের job নেয়। Worker এর সংখ্যা আগে থেকেই ঠিক করা থাকে।
চায়ের দোকানের কথা ভাবুন। দোকানে চারজন কর্মী। Customer এলে কেউ নতুন লোক ডেকে আনে না। যে কর্মী হাতের কাপ শেষ করেছেন, তিনিই পরের অর্ডার নেন। কর্মীর সংখ্যা ঠিক থাকে, অর্ডার যত খুশি আসুক। Worker pool ঠিক এভাবেই কাজ করে।
প্রশ্ন আসতে পারে, প্রতিটা job এর জন্য একটা করে নতুন process খুললেই তো হয়। ৫০,০০০ job মানে ৫০,০০০ process? প্রতিটা process কিছু memory নেয়। এত process একসাথে খুললে machine এর memory শেষ হয়ে যায়, আর machine টা crash করে। Pool এ worker এর সংখ্যা বাঁধা থাকে, তাই কাজ যত বাড়ুক, machine এর ওপর চাপ একটা সীমার ভেতরে থাকে।
এবার হিসাব। একটা slip এ ২ সেকেন্ড লাগে। ৫০,০০০ slip মানে মোট ১,০০,০০০ সেকেন্ডের কাজ। এটা এক ঘণ্টায় (৩,৬০০ সেকেন্ড) শেষ করতে চাইলে একসাথে লাগবে ১,০০,০০০ ÷ ৩,৬০০ ≈ ২৮ জন worker।
Queue বলে দেয় কী কী কাজ বাকি। Pool বলে দেয় একসাথে কতজন কাজ করবে। দুটো আলাদা রাখলে কাজের পরিমাণ যত বাড়ুক, machine এর ওপর চাপ নিয়ন্ত্রণে থাকে।
০৪ Worker pool কত বড় হবে?
২৮ সংখ্যাটা শুনে তানিয়ার টিমের একজন বললো, “তাহলে একটা ৮ core এর machine এ ২৮টা worker চালিয়ে দিই।” চালানোর পর দেখা গেলো, গতি প্রায় বাড়েইনি। কেন?
উত্তর লুকিয়ে আছে slip এর দুই অংশের চরিত্রে। একটা slip এর ২ সেকেন্ডের মধ্যে দুটো আলাদা রকম কাজ হয়:
- PDF বানানো (০.৮ সেকেন্ড): পুরো সময়টা CPU হিসাব করে। টেবিল সাজায়, বাংলা font বসায়, ছবি আঁকে। এমন কাজকে বলে CPU-bound। কাজের গতি ঠিক করে CPU কত দ্রুত হিসাব করতে পারে।
- Email পাঠানো (১.২ সেকেন্ড): CPU কাজ করে মাত্র ২০ মিলিসেকেন্ড, email টা গুছিয়ে network এ ছেড়ে দিতে। বাকি ১.১৮ সেকেন্ড শুধু অপেক্ষা, কখন email provider উত্তর দেবে। এমন কাজকে বলে IO-bound। IO মানে input/output: network, disk বা database এর সাথে লেনদেন। কাজের গতি ঠিক করে অন্য পক্ষ কত দ্রুত উত্তর দেয়।
একটা core এর সময় দুই রকম কাজে কীভাবে খরচ হয়, দেখুন:
CPU-bound কাজে core সবসময় ব্যস্ত। ৮ core এ ২৮টা PDF worker চালালে একসাথে চলতে পারে ৮টাই। বাকি ২০টা পালার জন্য অপেক্ষা করে। উল্টো এদের মধ্যে বারবার পালা বদলাতে CPU এর কিছু সময় নষ্ট হয়, আর memory ও বেশি লাগে।
IO-bound কাজে core বেশিরভাগ সময় খালি। একটা email যখন উত্তরের অপেক্ষায়, core তখন আরেকটা email এর কাজ করতে পারে। তাই এখানে core এর চেয়ে অনেক বেশি worker রাখা যায়। হিসাবটা এরকম:
CPU-bound (PDF render, 0.8s পুরোটাই CPU)
worker সংখ্যা ≈ core সংখ্যা
8 core এর machine: 8 worker
1 worker: 1 ÷ 0.8s = 1.25 slip/s
8 worker: 10 slip/s; 3টা machine: 30 slip/s
50,000 ÷ 30 ≈ 1,667s ≈ 28 মিনিট
IO-bound (email, 20ms CPU + 1,180ms অপেক্ষা)
worker সংখ্যা ≈ core × (1 + অপেক্ষা ÷ CPU সময়)
= 8 × (1 + 1180 ÷ 20) = 8 × 60 = 480
কিন্তু আসল সীমা ঠিক করে email provider (পরের অংশ দেখুন)
এই হিসাব থেকে তানিয়া একটা গুরুত্বপূর্ণ সিদ্ধান্ত নিলো। একটা slip এর দুই অংশ একই worker এ রাখলে কোনো মাপই ঠিক হয় না। ৮টা worker রাখলে email এর সময় core খালি পড়ে থাকে। ৪৮০টা রাখলে PDF এর সময় সবাই core এর জন্য ঠেলাঠেলি করে। তাই সে কাজটা দুই ভাগ করলো। একটা queue (pdf-render) শুধু PDF এর জন্য, যার worker সংখ্যা core এর সমান। আরেকটা queue (send-email) শুধু email এর জন্য, যেখানে worker অনেক বেশি। PDF worker কাজ শেষ করে একটা email job পরের queue তে রেখে দেয়।
খরচটাও দেখার মতো। Cloud এ একটা ৮ core এর machine ভাড়া ঘণ্টায় মোটামুটি ৪০ টাকা। তিনটা machine আধা ঘণ্টা চালালে মাসে খরচ ৬০ টাকার মতো। কাজ শেষ হলে machine গুলো বন্ধ করে দেওয়া যায়।
CPU-bound কাজে worker রাখুন core এর সমান। IO-bound কাজে core এর অনেক গুণ রাখা যায়, কারণ বেশিরভাগ সময় worker শুধু অপেক্ষা করে। একই job এ দুই রকম কাজ থাকলে job টা ভেঙে দুটো queue তে দিন।
০৫ Worker বাড়ালেই কি কাজ সবসময় দ্রুত হয়?
হিসাব বলছিলো, email এর জন্য ৪৮০ জন worker রাখা যায়। তানিয়া সাবধানে ১০০ দিয়ে শুরু করলো। পাঁচ মিনিটের মাথায় log ভরে গেলো একই error এ: 429 Too Many Requests।
তানিয়ার email provider এর একটা নিয়ম আছে। একটা account থেকে সেকেন্ডে ৪০টার বেশি email নেবে না। ১০০ জন worker প্রত্যেকে ১.২ সেকেন্ডে একটা করে পাঠালে সেকেন্ডে যায় প্রায় ৮৩টা। সীমার দ্বিগুণ। Provider বাড়তি request গুলো ফিরিয়ে দিচ্ছিলো।
এখানে তানিয়ার machine এ কোনো সমস্যা নেই। Bottleneck সরে গেছে অন্য পক্ষের কাছে। এর সমাধান হলো concurrency limit।
Concurrency limit: একটা নির্দিষ্ট ধরনের job একসাথে সর্বোচ্চ কয়টা চলতে পারবে, তার সীমা। Pool এ যত worker ই থাকুক, এর বেশি job একসাথে চলবে না। বাকিরা পালার জন্য অপেক্ষা করবে।
সীমা কত হবে, সেটা হিসাব করে বের করা যায়। একটা email এ ১.২ সেকেন্ড লাগে। তাই একসাথে ৪৮টা email চললে সেকেন্ডে ঠিক ৪০টা যায় (৪০ কে ১.২ দিয়ে গুণ করে ৪৮)। একটু জায়গা রেখে তানিয়া limit দিলো ৪৫। এতে সেকেন্ডে প্রায় ৩৭টা email যায়। ৫০,০০০ email পাঠাতে লাগে ২৩ মিনিটের মতো।
Database নিয়েও একই সাবধানতা দরকার। তানিয়ার database একসাথে ২০০টা connection নিতে পারে। এর মধ্যে ১২০টা web server এর লাগে। Worker রা প্রত্যেকে একটা করে connection খুললে web server এর জন্য connection বাকি থাকে না, আর HR dashboard খুলতেই পারে না। তাই worker দের database connection এর জন্যও আলাদা একটা সীমা রাখা হলো: সব worker মিলিয়ে সর্বোচ্চ ৬০টা।
Worker বাড়ালে কাজ দ্রুত হয়, যতক্ষণ না অন্য কোনো জিনিস সীমায় পৌঁছায়। সেটা email provider, database বা অন্য কোনো API হতে পারে। প্রতিটা ভাগ করা জিনিসের সামনে একটা concurrency limit বসান, যাতে আপনার worker রা নিজেরাই অন্য system কে চাপে না ফেলে।
০৬ Job fail করলে কখন আবার চেষ্টা করবো?
রাত ২টা ১৪। Email provider এর নিজের system এ একটা গোলমাল হলো। তিন মিনিট ধরে সব request এ তারা ফেরত দিলো 503 Service Unavailable। এর মধ্যে তানিয়ার ৩,০০০টা email job fail করলো।
সেশন ০৮ থেকে আমরা জানি, ACK না হওয়া job আবার ফিরে আসে। কিন্তু কখন ফিরবে? সাথে সাথে retry করলে কী হয়, ভাবুন। ৩,০০০ job এক সেকেন্ডের মধ্যে আবার provider এর দরজায় ধাক্কা দেয়। Provider তখনো ঠিক হয়নি, তাই আবার fail। আবার সাথে সাথে retry। যে system টা সুস্থ হওয়ার চেষ্টা করছে, তার ওপর এই চাপ সুস্থ হওয়াটাকেই পিছিয়ে দেয়।
Exponential backoff: প্রতিবার একটু বেশি অপেক্ষা
এর প্রথম সমাধান হলো প্রতিবার fail করার পর আগের চেয়ে দ্বিগুণ সময় অপেক্ষা করা। প্রথমবার ২ সেকেন্ড, তারপর ৪, তারপর ৮, ১৬, ৩২। এই নিয়মের নাম exponential backoff। ছোটখাটো সমস্যা হলে প্রথম দুই এক চেষ্টাতেই কাজ হয়ে যায়। বড় সমস্যা হলে অপেক্ষা লম্বা হতে থাকে, আর অন্য পক্ষ দম ফেলার সময় পায়। অপেক্ষা যেন অসীম না হয়, তাই একটা ছাদ (cap) থাকে, যেমন ৫ মিনিট। আর মোট চেষ্টারও একটা সীমা থাকে, যেমন ৬ বার। ৬ বার fail হলে job টা failed হয়, আর সেশন ০৮ এর Dead Letter Queue (DLQ) তে যায়।
Jitter: সবাই যেন একসাথে না ফেরে
শুধু backoff এ একটা সমস্যা থেকে যায়। ৩,০০০টা job তো একই মুহূর্তে fail করেছিলো। তাই সবাই একসাথে ২ সেকেন্ড অপেক্ষা করে, একসাথে ফেরে। তারপর একসাথে ৪ সেকেন্ড অপেক্ষা করে, আবার একসাথে ফেরে। চাপ কমেনি, শুধু ঢেউয়ের মতো দফায় দফায় আসছে।
এর সমাধান jitter। প্রতিটা job অপেক্ষার সীমার ভেতরে একটা random সময় বেছে নেয়। কেউ ০.৩ সেকেন্ড পরে ফেরে, কেউ ১.৭ সেকেন্ড পরে। ঢেউটা ভেঙে ছড়িয়ে পড়ে:
Office ছুটির পর লিফটের কথা ভাবুন। সবাই ঠিক ৫টায় বের হলে লিফটের সামনে বিশাল ভিড় হয়। কেউ ৫টা ২ মিনিটে, কেউ ৫টা ৭ মিনিটে বের হলে লিফট সবাইকে আরামে নামাতে পারে। Jitter ঠিক এই কাজটাই করে। হিসাবটা লেখা হয় এভাবে:
wait = random(0, min(cap, base × 2^attempt)) # full jitter base = 1s, cap = 300s, max attempt = 6 attempt jitter ছাড়া jitter সহ (এই সীমার যেকোনো মান) 1 2s 0 - 2s 2 4s 0 - 4s 3 8s 0 - 8s 4 16s 0 - 16s 5 32s 0 - 32s 6 64s 0 - 64s এরপর status = failed, job যায় DLQ তে
কোন error এ retry করবেন, কোনটায় নয়
সব error retry করার মতো নয়। Provider এর 503 বা network timeout সাময়িক সমস্যা, একটু পরে ঠিক হয়ে যায়। এদের বলে transient error। এগুলোতে retry করুন। কিন্তু কোনো কর্মীর email address ভুল হলে ছয়বার চেষ্টা করেও লাভ নেই। এমন স্থায়ী error এ job টা সরাসরি failed করে দিন, যাতে HR address টা ঠিক করতে পারেন।
Retry এর আরেকটা ঝুঁকি আছে। Email আসলে চলে গিয়েছিলো, কিন্তু provider এর উত্তর আসার আগেই connection কেটে গেলো। Worker ভাবলো fail হয়েছে, আর আবার পাঠালো। কর্মী দুইটা slip পেলেন। তাই প্রতিটা job এর একটা idempotency key থাকে (সেশন ০৮), যেমন slip:2026-11:emp-48213। Worker পাঠানোর আগে দেখে নেয়, এই key এর email আগে গেছে কিনা। একটা user request এর ভেতরে অন্য service কে call করার সময় retry কীভাবে সামলাতে হয়, সেটা সেশন ২১ এ দেখবো।
প্রতিবার অপেক্ষা দ্বিগুণ করুন (exponential backoff)। অপেক্ষায় একটু random মেশান (jitter)। মোট চেষ্টা আর সর্বোচ্চ অপেক্ষার সীমা রাখুন, তারপর DLQ। আর শুধু সাময়িক error এ retry করুন, তাও idempotency key সহ।
০৭ একটা job আটকে গেলে কে টের পায়?
রাত ২টা ১৮। একটা কোম্পানির slip এর সাথে ৪০০ পাতার attendance report জোড়া ছিলো। PDF বানাতে গিয়ে library টা কোথাও আটকে গেলো। Error দিলো না, crash ও করলো না। শুধু চলতেই থাকলো।
Crash হলে সমস্যা ছিলো না, সেশন ০৮ এর ACK আর visibility timeout সেটা সামলে নিতো। কিন্তু আটকে থাকা worker তো চালু আছে। Queue ভাবছে কাজ চলছে। Worker টা অন্য কোনো job ও নিচ্ছে না। একই রকম আরো কয়েকটা slip এলে একে একে সব worker আটকে যায়:
Job timeout
সমাধান হলো প্রতিটা job এর জন্য একটা সময়সীমা বেঁধে দেওয়া। এই সময় পার হলে worker নিজেই job টা থামিয়ে দেয়, failed হিসেবে লেখে, আর retry তে পাঠায়। এর নাম job timeout।
Timeout কত হবে? তানিয়া গত মাসের হিসাব দেখলো। সাধারণ একটা PDF এ লাগে ০.৮ সেকেন্ড। সবচেয়ে ধীর ১%ও ৩ সেকেন্ডের মধ্যে শেষ হয়। তাই সে timeout দিলো ৩০ সেকেন্ড। স্বাভাবিক কোনো কাজ এতে কাটা পড়বে না, কিন্তু আটকে যাওয়া কাজ বেশিক্ষণ worker দখল করে রাখতে পারবে না।
একটা নিয়ম মনে রাখুন: job timeout সবসময় queue এর visibility timeout এর চেয়ে ছোট হবে। তানিয়ার queue তে visibility timeout ৬০ সেকেন্ড। উল্টো হলে কী হতো? ধরুন visibility timeout ২০ সেকেন্ড, আর job ২৫ সেকেন্ড ধরে চলছে। ২০ সেকেন্ডে queue ভাববে worker crash করেছে, আর job টা আরেকজনকে দেবে। তখন একই slip দুইজন worker একসাথে বানাবে।
Job status: প্রতিটা job এখন কোথায়
সকালে HR জানতে চাইবেন, “আমাদের কোম্পানির সবার slip কি গেছে?” Queue এই প্রশ্নের উত্তর দিতে পারে না। Queue শুধু জানে কোন job এখনো বাকি। যে job শেষ হয়ে গেছে, সেটা queue থেকে মুছে গেছে।
তাই তানিয়া database এ একটা jobs table রাখলো। প্রতিটা job এর একটা row, আর তাতে তার job status:
| id | type | status | attempts | last_error |
|---|---|---|---|---|
slip:2026-11:emp-48213 | send-email | done | ১ | |
slip:2026-11:emp-48214 | send-email | retrying | ৩ | 503 from provider |
slip:2026-11:emp-51007 | pdf-render | failed | ৬ | timeout after 30s |
Status বদলায় একটা নির্দিষ্ট পথে:
- queued: queue তে আছে, worker এর অপেক্ষায়।
- running: একজন worker কাজ করছে।
- retrying: সাময়িক error এ fail করেছে, backoff শেষে আবার queued হবে।
- done: সফলভাবে শেষ।
- failed: সব চেষ্টা শেষ, বা স্থায়ী error। মানুষকে দেখতে হবে।
এই table থেকে HR এর dashboard এ এক লাইনে দেখানো যায়: “৪৯,৯৯২টা পাঠানো হয়েছে, ৮টা আটকে আছে।” আরেকটা লাভ আছে। কোনো job ১০ মিনিট ধরে running থাকলে বোঝা যায় কিছু একটা গোলমাল। একটা ছোট recurring job প্রতি ৫ মিনিটে এমন job খুঁজে বের করে আবার queue তে দেয়।
Crash করা worker কে queue নিজেই ধরে ফেলে। আটকে থাকা worker কে কেউ ধরে না, যদি না আপনি timeout দেন। আর status table না থাকলে কোন job কোথায় আছে, সেটা কেউ বলতে পারে না।
০৮ Deploy এর সময় চলতি job গুলোর কী হয়?
রাত ২টা ২০। তানিয়ার এক সহকর্মী timeout এর fix টা merge করলেন। নিয়ম অনুযায়ী নতুন code সাথে সাথে deploy হতে শুরু করলো। পুরোনো worker গুলো বন্ধ হবে, নতুন worker চালু হবে। কিন্তু পুরোনো worker দের হাতে তখন অর্ধেক বানানো slip।
তানিয়ার worker গুলো চলে Kubernetes এ। Kubernetes হলো এমন একটা system, যেটা অনেক machine জুড়ে application চালায়, আর deploy এর সময় পুরোনোগুলো সরিয়ে নতুনগুলো বসায়। একটা worker বন্ধ করার আগে Kubernetes তাকে একটা সংকেত পাঠায়, যার নাম SIGTERM। মানে, “এবার গুছিয়ে বন্ধ হওয়ার সময়।” তারপর সে ৩০ সেকেন্ড অপেক্ষা করে। এই সময়টাকে বলে grace period। ৩০ সেকেন্ডেও worker বন্ধ না হলে Kubernetes পাঠায় SIGKILL। এতে process টা সাথে সাথে থেমে যায়, কিছু গোছানোর সুযোগ থাকে না।
তানিয়ার পুরোনো worker গুলো SIGTERM এর কোনো খেয়ালই করতো না। ফলে প্রতিবার deploy এ যা চলছিলো, সব মাঝপথে কাটা পড়তো। কোনো slip এর email চলে গেছে কিন্তু status এ done লেখা হয়নি। কোনো job ৬০ সেকেন্ডের visibility timeout শেষ না হওয়া পর্যন্ত কারো হাতেই নেই।
Graceful shutdown
ভালো worker SIGTERM পেলে ভদ্রভাবে বিদায় নেয়। এই আচরণকে বলে graceful shutdown:
- নতুন job নেওয়া বন্ধ: Queue থেকে আর কিছু তোলে না।
- হাতের কাজ শেষ: যে slip গুলো চলছিলো, সেগুলো শেষ করে ACK পাঠায় আর status এ
doneলেখে। - লম্বা কাজ ফেরত: কোনো job grace period এর মধ্যে শেষ হওয়ার মতো না হলে worker সেটা নিজেই queue তে ফেরত দেয়। Visibility timeout এর জন্য অপেক্ষা না করে আরেকজন worker সাথে সাথে সেটা তুলে নিতে পারে।
- নিজে বন্ধ হওয়া: সব গুছিয়ে worker নিজেই exit করে। SIGKILL এর দরকারই পড়ে না।
এখানে আগের অংশের timeout আবার কাজে লাগে। তানিয়ার job timeout ৩০ সেকেন্ড, grace period ও ৩০ সেকেন্ড। কোনো job ই timeout এর বেশি চলতে পারে না, তাই হাতের কাজ শেষ করার জন্য grace period যথেষ্ট। Job যত ছোট, deploy তত নিরাপদ। এটাও প্রতিটা slip কে আলাদা job বানানোর একটা বড় লাভ।
Worker প্রতি সপ্তাহে কয়েকবার deploy হয়। তাই worker থামানো একটা নিয়মিত ঘটনা, দুর্ঘটনা নয়। SIGTERM পেলে নতুন job নেওয়া থামান, হাতের কাজ শেষ করুন, আর না পারলে job টা ফেরত দিন।
০৯ দুই server এ একই cron job চললে কী হয়?
নভেম্বরের মাঝামাঝি তানিয়ার টিম আরেকটা পরিবর্তন করলো। এতদিন cron চলতো একটাই server এ। সেটা down হলে কোনো slip ই যেতো না, অর্থাৎ ঐ server টা ছিলো একটা SPOF (single point of failure, সেশন ০১)। তাই তারা application টা দুইটা server এ চালু করলো (সেশন ০২ এর redundancy)।
পরদিন সকালে একজন HR ম্যানেজার জানালেন, “ব্যাংকের status email প্রতিবার দুইটা করে আসছে।” তানিয়া log দেখে বুঝলো ঘটনাটা। প্রতি ১৫ মিনিটের recurring job টা এখন দুই server এই চলছে। দুই server এ একই code, তাই একই cron schedule। ঠিক তখনই তার মনে পড়লো, ডিসেম্বরের ১ তারিখ রাত ২টায় payroll job টাও দুই জায়গায় চলবে:
দুইটা server এ ৫০,০০০ করে মোট ১,০০,০০০ job। প্রত্যেক কর্মী দুইটা slip পাবেন। ১ তারিখের আগে এটা ঠিক করতেই হবে।
Distributed lock
একটা machine এর ভেতরে দুইটা thread যেন একই কাজ একসাথে না করে, তার জন্য lock ব্যবহার হয়। কিন্তু এখানে দুইটা আলাদা machine। তাদের lock রাখতে হবে এমন জায়গায়, যেটা দুজনেই দেখতে পায়। এমন lock কে বলে distributed lock।
Distributed lock: এমন একটা lock, যেটা অনেকগুলো machine এর মধ্যে ভাগ করা একটা জায়গায় (যেমন Redis) থাকে। একসাথে শুধু একটা machine এটা ধরে রাখতে পারে। যে পায়, সে কাজটা চালায়। বাকিরা কাজটা বাদ দেয়।
তানিয়া Redis ব্যবহার করলো (সেশন ০৪ এ cache হিসেবে দেখা সেই Redis)। রাত ২টায় দুই server ই Redis এর কাছে একই key চায়:
# রাত 2:00, server A আর server B দুজনেই এই command পাঠায় SET lock:payroll:2026-11 server-A NX EX 900 server A পায়: OK # lock তার, সে 50,000 job বানাবে server B পায়: nil # আগেই কেউ নিয়েছে, তাই skip NX key আগে থেকে না থাকলে তবেই set হয় EX 900 900s (15 মিনিট) পরে key নিজে থেকেই মুছে যায় # দ্বিতীয় পাহারা: database এ একটা unique row INSERT INTO payroll_runs (company_id, month) VALUES (17, '2026-11'); # একই company_id আর month আবার ঢোকাতে গেলে database error দেয়
NX এর কারণে দুজনের মধ্যে শুধু প্রথমজন key টা পায়। Redis একটা একটা করে command চালায়, তাই দুজনে একসাথে চাইলেও একজনই জেতে।
EX 900 অংশটা খুব জরুরি। ধরুন server A lock নিয়েই crash করলো। Lock এর মেয়াদ না থাকলে সেটা চিরদিন আটকে থাকবে, আর পরের মাসে কোনো server ই job চালাতে পারবে না। মেয়াদ থাকলে ১৫ মিনিট পরে lock নিজে থেকেই খুলে যায়। মেয়াদটা কাজের সময়ের চেয়ে বেশি রাখতে হয়। এখানে cron এর কাজ শুধু ৫০,০০০ job queue তে রাখা, যাতে এক মিনিটের মতো লাগে। ১৫ মিনিট তাই যথেষ্ট।
Lock এর পাশে দ্বিতীয় পাহারা
Distributed lock পুরোপুরি নিখুঁত নয়। ধরুন server A lock নিলো, তারপর কোনো কারণে ২০ মিনিট থেমে রইলো। ততক্ষণে lock এর মেয়াদ শেষ, server B ও lock পেয়ে গেছে। এখন দুজনেই ভাবছে lock তার। এমন বিরল ঘটনা ঠেকাতে তানিয়া database এ একটা payroll_runs table রাখলো। Table এ company_id আর month মিলে unique। প্রতিটা কোম্পানির প্রতি মাসের run একবারই লেখা যায়। দ্বিতীয়বার লিখতে গেলে database নিজেই error দেয়। Lock সাধারণ দিনে দুইবার চলা ঠেকায়, আর unique row ঠেকায় বিরল দিনেরটা।
আরো দুইটা পথ আছে, দুটোরই নিজের downside আছে। একটা হলো cron শুধু একটা আলাদা scheduler server এ চালানো। কিন্তু তাহলে সেই server আবার SPOF হয়ে যায়। আরেকটা হলো server গুলো নিজেদের মধ্যে একজন leader বেছে নেবে, আর শুধু leader cron চালাবে। Leader কীভাবে বাছাই হয়, সেটা সেশন ১৬ এর বিষয়।
সব মিলিয়ে: ডিসেম্বরের ১ তারিখ
রাত ২টা ৩১। Dashboard এ শেষ সংখ্যাটা ভেসে উঠলো: ৫০,০০০টা done, ০টা failed। নভেম্বরের failed তালিকা দেখে HR আগেই ভুল email address গুলো ঠিক করে রেখেছিলেন। Email provider একবার আধা মিনিটের জন্য 503 দিয়েছিলো। Backoff আর jitter চুপচাপ সেটা সামলে নিয়েছে। মাঝে একবার deploy ও হয়েছে, কোনো slip হারায়নি। তানিয়া laptop বন্ধ করে ঘুমাতে গেলো। সকাল ৯টায় support inbox এ একটাও নতুন message ছিলো না।
Background job চালানো মানে শুধু একটা queue আর কিছু worker নয়। কাজটাকে ছোট job এ ভাঙুন। কাজের চরিত্র দেখে pool এর মাপ ঠিক করুন, আর অন্যের সীমা মেনে concurrency limit দিন। Fail করলে backoff আর jitter দিয়ে retry করুন, আটকে গেলে timeout দিন, আর প্রতিটা job এর status রাখুন। Deploy এ ভদ্রভাবে বন্ধ হোন, আর একাধিক server থাকলে lock দিয়ে নিশ্চিত করুন যে cron একবারই চলে।
১০ মগজে প্রেশার দিন
আগে নিজে ভাবুন। তারপর উত্তর খুলে দেখুন: বেশিরভাগ মানুষ কীভাবে ভাবে, সেই চিন্তায় কী বাদ পড়ে, আর আসল কথাটা কোথায়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Worker যত বেশি, কাজ তত দ্রুত। ২৮ জনে এক ঘণ্টা লাগলে ৫০০ জনে কয়েক মিনিটেই শেষ হবে।”
এই হিসাবে একটা ধরে নেওয়া কথা লুকিয়ে আছে: worker রা একে অপরের পথ আটকায় না।
যে জায়গাটা যোগ করা দরকার
Worker বাড়ালে গতি বাড়ে, যতক্ষণ না কোনো ভাগ করা জিনিস সীমায় পৌঁছায়। PDF এর মতো CPU-bound কাজে সীমা হলো core এর সংখ্যা। ৮ core এ ৫০০ worker চালালেও একসাথে চলে ৮টাই। Email এর মতো IO-bound কাজে সীমা ঠিক করে অন্য পক্ষ। তানিয়ার provider সেকেন্ডে ৪০টার বেশি নেয় না, তাই ৫০০ worker শুধু 429 error বাড়াবে। আর প্রতিটা worker একটা database connection নিলে web server এর connection ফুরিয়ে যাবে।
তাই প্রথম প্রশ্ন হওয়া উচিত, “এই কাজে সবচেয়ে ধীর ভাগ করা জিনিস কোনটা?” সেটার মাপেই pool আর concurrency limit ঠিক হয়।
Worker সংখ্যা ঠিক করে কাজের চরিত্র (CPU-bound না IO-bound) আর সবচেয়ে সংকীর্ণ ভাগ করা জিনিস। এর বেশি worker শুধু লাইনে দাঁড়ায়, বা অন্য system কে চাপে ফেলে।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Fail করলে ১০ সেকেন্ড পর পর ৩ বার retry করবো। তারপরও না হলে বাদ দেবো।”
নিয়মটা শুনতে যুক্তিসঙ্গত। কিন্তু ৫ মিনিটের outage এ এটা দুই দিক থেকেই ভুল করে।
যে জায়গাটা যোগ করা দরকার
প্রথমত, ৩ বার × ১০ সেকেন্ড মানে ৩০ সেকেন্ডেই সব চেষ্টা শেষ। Provider ফিরবে ৫ মিনিট পরে, ততক্ষণে হাজার হাজার job বাদ পড়ে গেছে। Exponential backoff এ অপেক্ষা ২, ৪, ৮, ১৬, ৩২, ৬৪ সেকেন্ড করে বাড়ে, তাই ৬ বারের চেষ্টা প্রায় দুই মিনিট জুড়ে ছড়ায়। চেষ্টা আরেকটু বাড়ালে বা cap বড় রাখলে ৫ মিনিটের outage ও পার হয়ে যায়।
দ্বিতীয়ত, হাজার হাজার job একই মুহূর্তে fail করেছে। একই নির্দিষ্ট বিরতিতে retry করলে সবাই একসাথে ফেরে। Provider যেই উঠে দাঁড়ায়, প্রথম ঢেউটাই তাকে আবার overloaded করে ফেলে। তাই অপেক্ষায় jitter মেশাতে হয়।
এর সাথে বাকি তিনটা অংশ: শুধু 503 আর timeout এর মতো সাময়িক error এ retry, সব চেষ্টা শেষে DLQ, আর প্রতিটা email এর idempotency key, যাতে retry তে কেউ দুইটা slip না পান।
Retry এর নিয়ম মানে শুধু “কতবার” নয়। কতক্ষণ পর (backoff), সবাই একসাথে কিনা (jitter), কোন error এ (transient), আর শেষে কোথায় (DLQ), চারটাই বলতে হয়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“সমস্যা হলো cron তিনবার চলবে। সমাধান সহজ: দুইটা server এ cron বন্ধ রেখে শুধু একটায় চালু রাখবো।”
সমস্যাটা ঠিক ধরা হয়েছে। কিন্তু সমাধানটা নতুন একটা সমস্যা তৈরি করে।
যে জায়গাটা যোগ করা দরকার
শুধু একটা server এ cron রাখলে সেই server একটা SPOF। মাসের ১ তারিখ রাতে ঐ server down থাকলে কোনো slip ই যাবে না, আর কেউ টেরও পাবে না। তিনটা server রাখার উদ্দেশ্যটাই তখন হারিয়ে যায়।
ভালো উত্তর হলো, তিনটা server এই cron চালু থাকুক, কিন্তু কাজ শুরুর আগে সবাই একটা distributed lock চাইবে। যে পাবে, শুধু সে চালাবে। Lock এর একটা মেয়াদ (TTL) থাকবে, যাতে lock নেওয়া server crash করলেও পরের বার কাজ আটকে না যায়। আর lock পুরোপুরি নিখুঁত নয় বলে কাজটা নিজেও idempotent হবে: database এ (company, month) এর একটা unique row, যাতে দ্বিতীয় run শুরুতেই থেমে যায়।
“একটাই server এ cron” মানে SPOF। সব server এ cron, সাথে মেয়াদওয়ালা distributed lock, আর কাজের ভেতরে একটা unique key: এই তিনটা মিলে cron একবারই চলে, একটা server down থাকলেও।
১১ যে টার্মগুলো শিখলাম
| টার্ম | মানে |
|---|---|
| Background job | User এর request এর বাইরে, আলাদা worker এ চলা একটা কাজ। সাধারণত queue থেকে আসে। |
| One-off job | একবারই চলে, আর যত দ্রুত সম্ভব। |
| Delayed job | এখন queue তে যায়, কিন্তু একটা নির্দিষ্ট সময় পরে চলে। |
| Scheduled job | ক্যালেন্ডারের নির্দিষ্ট সময়ে চলে, যেমন প্রতি মাসের ১ তারিখ রাত ২টা। |
| Recurring job | একটা নির্দিষ্ট বিরতিতে বারবার চলে, যেমন প্রতি ১৫ মিনিটে। |
| Cron | ঘড়ি দেখে নির্দিষ্ট সময়ে কাজ চালু করার পুরোনো আর জনপ্রিয় scheduler। |
| Cron expression | পাঁচ ঘরের একটা লেখা (মিনিট, ঘণ্টা, দিন, মাস, সপ্তাহের দিন), যেটা বলে কাজ কখন চলবে। যেমন 0 2 1 * *। |
| Worker pool | নির্দিষ্ট সংখ্যক worker এর দল, যারা একই queue থেকে একটার পর একটা job নেয়। |
| CPU-bound | যে কাজের পুরো সময় CPU হিসাব করে। Worker রাখুন core এর সমান। |
| IO-bound | যে কাজের বেশিরভাগ সময় network, disk বা database এর উত্তরের অপেক্ষায় যায়। Core এর অনেক গুণ worker রাখা যায়। |
| Concurrency limit | একটা ধরনের job একসাথে সর্বোচ্চ কয়টা চলতে পারবে, তার সীমা। |
| Transient error | সাময়িক error, একটু পরে নিজে থেকেই ঠিক হয়ে যায় (যেমন 503, timeout)। |
| Exponential backoff | প্রতিবার fail করার পর আগের চেয়ে দ্বিগুণ সময় অপেক্ষা করে retry করা। |
| Jitter | Retry এর অপেক্ষায় একটু random সময় মেশানো, যাতে সব job একসাথে না ফেরে। |
| Job timeout | একটা job সর্বোচ্চ যতক্ষণ চলতে পারে। সময় পার হলে job থামিয়ে retry তে পাঠানো হয়। |
| Job status | একটা job এখন কোন অবস্থায় আছে: queued, running, retrying, done বা failed। |
| SIGTERM | Process কে গুছিয়ে বন্ধ হতে বলার সংকেত। |
| SIGKILL | Process কে সাথে সাথে থামিয়ে দেওয়ার সংকেত। এতে গোছানোর কোনো সুযোগ থাকে না। |
| Grace period | SIGTERM আর SIGKILL এর মাঝের সময়। Kubernetes এ সাধারণত ৩০ সেকেন্ড। |
| Graceful shutdown | বন্ধ হওয়ার আগে নতুন কাজ নেওয়া থামানো, হাতের কাজ শেষ করা, আর বাকিটা ফেরত দেওয়া। |
| Distributed lock | অনেক machine এর মধ্যে ভাগ করা একটা lock, যেটা একসাথে শুধু একজন ধরে রাখতে পারে। |
| Lock TTL | Lock এর মেয়াদ। এই সময় পরে lock নিজে থেকেই খুলে যায়, যাতে crash করা machine চিরদিন lock আটকে না রাখে। |
১২ শর্ট সামারি
- বড় কাজকে অনেকগুলো ছোট background job এ ভাঙুন। Cron শুধু ঠিক সময়ে job গুলো queue তে রাখে, আর একটা worker pool সেগুলো একসাথে চালায়।
- Pool এর মাপ ঠিক করে কাজের চরিত্র। CPU-bound কাজে worker থাকে core এর সমান, IO-bound কাজে অনেক বেশি। আর বাইরের system এর সীমা মানতে প্রতিটা ধরনের job এ একটা concurrency limit দিতে হয়।
- Fail করা job retry করুন exponential backoff আর jitter দিয়ে, শুধু সাময়িক error এ, আর idempotency key সহ। আটকে যাওয়া job থামান timeout দিয়ে, আর প্রতিটা job এর status database এ রাখুন।
- Deploy এর সময় worker SIGTERM পেলে graceful shutdown করবে। একাধিক server এ cron চললে distributed lock আর একটা unique key মিলে নিশ্চিত করে যে কাজটা একবারই চলে।
১৩ মনে রাখুন
- একটা বড় loop নয়, অনেক ছোট job। ছোট job আলাদাভাবে চালানো, retry করা, গোনা আর ফেরত দেওয়া যায়।
- Cron শুধু job বানায়, কাজ করে worker। তাই cron চলে এক মিনিট, আর ভারী কাজ pool এ ছড়িয়ে পড়ে।
- CPU-bound এ core এর সমান worker, IO-bound এ অনেক বেশি। একই job এ দুটোই থাকলে job টা ভেঙে দুই queue তে দিন।
- Worker বাড়ানোর আগে সবচেয়ে ধীর ভাগ করা জিনিস খুঁজুন। তার সামনে একটা concurrency limit বসান।
- Retry মানে backoff, jitter, সীমা আর DLQ। আর শুধু সাময়িক error এ, idempotency key সহ।
- Job timeout হবে visibility timeout এর চেয়ে ছোট। নইলে একই job দুইজন worker একসাথে চালাবে।
- SIGTERM পেলে গুছিয়ে বন্ধ হোন, আর cron এর সামনে lock রাখুন। Deploy আর একাধিক server দুটোই স্বাভাবিক ঘটনা, দুর্ঘটনা নয়।