সরাসরি পর্বে যান
পর্ব ৩১ · ৬৬-এর মধ্যে · মডিউল ৬

Message queue — producer, consumer, acknowledgement, retry আর dead letter queue

Message Queues — Producers, Consumers, Acknowledgements, Retries & Dead Letter Queues

Taught in Bangla, this lesson explains why to put a queue between services, the point-to-point model with producers, brokers and competing consumers, how fast a backlog grows and drains with worked numbers, auto versus manual acks, visibility timeouts and redelivery, why at-least-once needs idempotent consumers, retries with backoff, poison messages and dead letter queues, FIFO and message groups, prefetch, priority, delay, TTL, durability and autoscaling on queue depth.

এই পর্বে যা শিখবেন

  • Queue কেন — decoupling, ভিড় শোষণ, async কাজ আর load levelling; আর λ > N·μ হলে queue কত দ্রুত বাড়ে আর কত সময়ে সরে
  • Point-to-point মডেল — producer, broker, consumer আর competing consumers
  • Acknowledgement — auto বনাম manual ack, visibility timeout, ack/nack আর redelivery; কেন duplicate আসে আর idempotent consumer কেন বাধ্যতামূলক
  • Retry আর backoff, poison message, max receive count আর dead letter queue
  • ক্রম (FIFO বনাম best-effort, message group), prefetch, priority, delay, TTL, durability, queue depth দেখে autoscaling, আর queue বনাম pub/sub বনাম log

১ · Queue কেন — সরাসরি ডাকার বদলে মাঝখানে একটি সারি

একটি ব্যস্ত রেস্তোরাঁর কথা ভাবুন। ওয়েটার অর্ডার নিয়ে রান্নাঘরের দরজার হুকে স্লিপটি ঝুলিয়ে দেন, তারপর পরের টেবিলে যান। বাবুর্চি হুক থেকে একটি একটি স্লিপ নেন। ওয়েটারকে রান্না শেষ হওয়া পর্যন্ত দাঁড়িয়ে থাকতে হয় না। একজন বাবুর্চি চায়ের বিরতিতে গেলেও অর্ডার নেওয়া থামে না। আর সন্ধ্যার ভিড়ে স্লিপ হুকে জমে, রান্নাঘর ভাঙে না। এই হুকটিই একটি message queue.

Software-এ একই ঘটনা ঘটে। একটি অনলাইন দোকানের (কাল্পনিক) “অর্ডার দিন” বোতামের পেছনে অনেক কাজ: database-এ অর্ডার লেখা, SMS রসিদ, ইমেইল, গুদামকে খবর, লয়্যালটি পয়েন্ট, analytics. সব কাজ যদি একই অনুরোধের ভেতরে একটির পর একটি ডাকা হয় (synchronous call), তিনটি সমস্যা হয়:

  • Latency যোগ হয়: SMS gateway ২ সেকেন্ড নিলে ব্যবহারকারীও ২ সেকেন্ড বেশি অপেক্ষা করেন — অথচ SMS-এর জন্য অপেক্ষার কোনো দরকার ছিল না।
  • Availability গুণ হয়: পাঁচটি সেবা প্রত্যেকে ৯৯.৯% চালু থাকলে সবাই একসাথে চালু থাকে প্রায় ৯৯.৫% সময় (পর্ব ৫)। ইমেইল সেবা বন্ধ বলে অর্ডারই ব্যর্থ — এটি অন্যায্য।
  • গতি বাঁধা থাকে: ঈদের সেলে অর্ডার দশ গুণ হলে SMS gateway-কেও তখনই দশ গুণ চাপ নিতে হয়, যদিও SMS পাঁচ মিনিট পরে গেলেও চলত।

Queue এই তিনটি বাঁধন খোলে। অর্ডার সার্ভিস শুধু একটি বার্তা রাখে — “অর্ডার ১০৪২-এর SMS পাঠাতে হবে” — আর সঙ্গে সঙ্গে ব্যবহারকারীকে বলে “অর্ডার গৃহীত”. বাকি কাজ পেছনে worker-রা করে। একে বলে asynchronous কাজ।

⏳ সময়ের বাঁধন খোলে

Producer আর consumer-কে একই মুহূর্তে চালু থাকতে হয় না। Consumer ১০ মিনিট বন্ধ থাকলেও বার্তা জমে থাকে — ডাকঘরের চিঠির মতো।

decoupling in time

🌊 ভিড় শোষণ করে

হঠাৎ ঢেউ এলে queue লম্বা হয়, consumer স্থির গতিতেই চলে — ফেরিঘাটের সিরিয়ালের মতো: গাড়ি যত আসুক, ফেরি একবারে নির্দিষ্ট কয়টিই নেয়।

buffering · load levelling

🔀 গতি আর সংখ্যা আলাদা

Producer কতজন আর consumer কতজন, কেউ কাউকে চেনে না। Consumer বাড়ালে ক্ষমতা বাড়ে, producer-এর কোড বদলাতে হয় না।

decoupling in rate

🛟 ব্যর্থতা আটকায়

SMS gateway বিকল হলে অর্ডার ব্যর্থ হয় না; SMS-এর বার্তা অপেক্ষা করে, gateway ফিরলে যায়। ব্যর্থতা এক জায়গায় আটকে থাকে, ছড়ায় না।

failure isolation
মূল ধারণা — queue কত দ্রুত বাড়ে

ধরা যাক বার্তা আসে সেকেন্ডে λ, consumer N জন, প্রত্যেকে সেকেন্ডে μ টি শেষ করে। মোট ক্ষমতা N·μ.

  1. λ < N·μ: গড়ে queue ছোট থাকে; শুধু এলোমেলো ঢেউয়ে একটু জমে আর সরে।
  2. λ > N·μ, T সেকেন্ড ধরে: জমা B ≈ (λ − N·μ) × T. প্রতি সেকেন্ডে যত বেশি আসে, তত জমে — সরল যোগ-বিয়োগ।
  3. ঢেউ শেষে চাপ λ′ (< N·μ): জমা সরে (N·μ − λ′) হারে, তাই খালি হতে লাগে B ÷ (N·μ − λ′) সেকেন্ড।
  4. ঢেউয়ের শেষে আসা বার্তার অপেক্ষা (FIFO): তার সামনের B টি বার্তা শেষ হতে ≈ B ÷ (N·μ) সেকেন্ড।

উদাহরণ: N = ৪, μ = ১০ (ক্ষমতা ৪০), স্বাভাবিক চাপ ২৫, ঢেউ ৮০ — ২০ সেকেন্ড ধরে। জমা ≈ (৮০ − ৪০) × ২০ = ৮০০; খালি হতে ৮০০ ÷ (৪০ − ২৫) ≈ ৫৩ সেকেন্ড; ঢেউয়ের শেষ বার্তার অপেক্ষা ৮০০ ÷ ৪০ = ২০ সেকেন্ড। অংশ ৩-এর simulator এটি যাচাই করবে।

লক্ষ করুন, queue ক্ষমতা বাড়ায় না — শুধু সময় কেনে। দীর্ঘ সময়ের গড়ে λ যদি N·μ ছাড়ায়, queue অসীমভাবে বাড়বে; তখন consumer বাড়াতেই হবে, নয়তো উৎসকে থামাতে হবে — পর্ব ১৮-এর bounded queue আর backpressure দিয়ে। Queue তখনই কাজের, যখন গড় চাপ ক্ষমতার নিচে কিন্তু peak ওপরে।

সব কাজ queue-তে যায় না। ব্যবহারকারী যে উত্তরের জন্য অপেক্ষা করছেন — ব্যালান্স দেখা, লগইন — তা queue-র পেছনে রাখলে শুধু latency আর জটিলতা বাড়ে। Queue-র জায়গা “পরে হলেও চলবে” কাজে।

২ · Point-to-point মডেল — producer, broker আর competing consumers

সবচেয়ে প্রচলিত queue হলো point-to-point: প্রতিটি বার্তা একজন consumer-ই পায় আর প্রক্রিয়া করে। চরিত্র তিনটি:

  • Producer — বার্তা বানায় আর পাঠায় — একে বলে publish বা send. বার্তায় থাকে একটি অনন্য id, কাজের বিবরণ (body, প্রায়ই JSON বা Protobuf — পর্ব ১২) আর কিছু header (কখন বানানো, কতবার চেষ্টা হলো)।
  • Broker — মাঝের সেবা, যা বার্তা গ্রহণ করে, জমা রাখে, consumer-কে দেয় আর কাজ শেষের খবর (ack) পেলে মোছে। RabbitMQ, Amazon SQS-ধাঁচের cloud queue, বা Redis — উদাহরণ অংশ ৮-এ।
  • Consumer — বার্তা তুলে নিয়ে কাজ করে, তারপর ack পাঠায়। একই queue-তে অনেক consumer থাকলে তারা বার্তা ভাগ করে নেয় — কে আগে খালি, সে পরেরটি পায়।

শেষেরটির নাম competing consumers: ব্যাংকের টোকেন ব্যবস্থার মতো। একটি সারি, কয়েকটি কাউন্টার; যে কাউন্টার খালি হয়, পরের টোকেন সেখানে ডাকা হয়। কাউন্টার বাড়ালে সারি দ্রুত চলে, আর কোনো টোকেন দুই কাউন্টারে যায় না। Consumer stateless হলে (পর্ব ১৩) এভাবে horizontal scaling সবচেয়ে সহজ।

Producer → broker-এর queue (+ DLQ) → competing consumers

Point-to-point queue: each message goes to exactly one consumer; failed messages move to a dead letter queue

Producer, broker queue, dead letter queue আর competing consumers Producer Order API Producer Payment Broker জমা রাখে, ভাগ করে queue sms-jobs depth = 4 ৩ বার ব্যর্থ → DLQ DLQ dead letter Consumer ১ worker Consumer ২ worker Consumer ৩ worker প্রতিটি বার্তা একজন consumer-ই পায় publish — producer বার্তা রাখে deliver — খালি consumer পরেরটি পায় ack — কাজ শেষ, এবার মুছুন বারবার ব্যর্থ বার্তা DLQ-তে আলাদা
ছবি ৩১.১ — Producer-রা consumer-দের চেনে না, শুধু queue-র নাম জানে। Broker প্রতিটি বার্তা একজন খালি consumer-কে দেয় আর ack-এর অপেক্ষা করে। কয়েকবার চেষ্টাতেও যে বার্তা প্রক্রিয়া হয় না, সেটি dead letter queue-তে আলাদা হয়, যাতে বাকিদের পথ আটকে না থাকে।

এবার একটি বার্তার পুরো জীবন দেখুন — producer থেকে ack পর্যন্ত। ধরা যাক broker-এর visibility timeout ৩০ সেকেন্ড (অংশ ৪-এ বিস্তারিত)।

  1. ProducerOrder APIবার্তা বানায়
  2. Brokersms-jobsdisk-এ লেখে
  3. Consumer C1receiveবার্তা লুকানো
  4. কাজSMS gateway১.২ সেকেন্ড
  5. Ackm-1042কাজ শেষের খবর
বার্তা m-1042: “অর্ডার ১০৪২-এর SMS রসিদ পাঠান”
সময়
বার্তার অবস্থা
Queue: অপেক্ষায় / in-flight
Receive count
ব্যবহারকারী কী দেখছেন
  1. Publish. অর্ডার database-এ লেখার পরে Order API একটি বার্তা বানায়: id m-1042, ধরন send-sms, অর্ডার নম্বর ১০৪২। অর্ডার লেখা আর বার্তা পাঠানো একসাথে নিশ্চিত করার কৌশল outbox, পর্ব ২৫-এ।
  2. Broker জমা রাখে আর নিশ্চিত করে। Broker বার্তাটি disk-এ (আর replica থাকলে সেখানেও) লিখে তারপর producer-কে “পেয়েছি” জানায় — একে বলে publisher confirm. এই নিশ্চয়তা পেলে তবেই Order API ব্যবহারকারীকে “অর্ডার গৃহীত” বলে — মোট প্রায় ৩ ms. বার্তা এখন অপেক্ষায়, যেকোনো consumer দেখতে পায়।
  3. C1 বার্তাটি নেয়। Broker বার্তাটি মোছে না, শুধু অন্যদের কাছ থেকে লুকিয়ে রাখে — একে বলে in-flight বা unacked. Receive count হলো ১। ৩০ সেকেন্ডের মধ্যে ack না এলে বার্তা আবার দেখা যাবে।
  4. কাজ। C1 SMS gateway ডাকে; উত্তর আসে ১.২ সেকেন্ডে। এই পুরো সময় বার্তাটি in-flight — অন্য কোনো consumer এটি পায় না।
  5. Ack — কাজ শেষের পরে, আগে নয়। SMS সফল হলে C1 ack পাঠায়। আগে ack দিয়ে তারপর কাজ করলে, মাঝপথে crash-এ বার্তাটি চিরতরে হারাত।
  6. মুছে ফেলা। Ack পেয়ে broker বার্তাটি মোছে; queue খালি। ব্যবহারকারী অপেক্ষা করেছেন মাত্র প্রায় ৩ ms, অথচ কাজটি নির্ভরযোগ্যভাবে শেষ হলো ১.২ সেকেন্ডে — producer আর consumer একে অপরকে চেনেই না।

Operating system-এর ভেতরেও একই ধারণা আছে: একই যন্ত্রের দুটি process-এর মধ্যে message queue (Operating Systems · Inter-process communication), আর একটি সীমিত buffer-এ producer-consumer সমস্যা (Operating Systems · ক্লাসিক সমস্যা)। System design-এর queue তারই network-জোড়া, টেকসই রূপ, যেখানে যেকোনো অংশ যেকোনো মুহূর্তে বিকল হতে পারে।

৩ · ভিড়ে queue কত বাড়ে — consumer-এর সংখ্যা হিসাব করা

অংশ ১-এর সূত্রটি এবার একটি simulation-এ যাচাই করুন। স্বাভাবিক চাপ সেকেন্ডে ২৫টি বার্তা; ৬০ থেকে ৮০ সেকেন্ডে একটি ঢেউ, সেকেন্ডে ৮০টি। প্রতিটি consumer গড়ে ১০০ ms নেয় (সেকেন্ডে ১০টি)। সময় আর আগমন দুটোই এলোমেলো — exponential service আর Poisson আগমন। Consumer ২, ৪ আর ৮ জন হলে কী হয়?

আগে মাথায় হিসাব করুন। N = ২-এ ক্ষমতা ২০, যা স্বাভাবিক চাপ ২৫-এরও কম — queue সারাক্ষণ বাড়বে। N = ৪-এ ক্ষমতা ৪০: স্বাভাবিক দিনে ঠিক, ঢেউয়ে প্রায় ৮০০ জমবে। N = ৮-এ ক্ষমতা ৮০: ঢেউয়ের সমান, তাই সামান্য জমা।

// Bursty producer -> one queue -> N competing consumers (FIFO). Seeded, repeatable.
// Normal load 25 msg/s; a burst of 80 msg/s from t = 60 s to t = 80 s; run 180 s.
// Each consumer needs 100 ms per message on average (exponential), i.e. 10 msg/s.
function rng(seed) {                                   // mulberry32
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const rate = (t) => (t >= 60 && t < 80 ? 80 : 25), END = 180, S = 0.1;
const SAMPLES = [30, 60, 70, 80, 100, 120, 150, 180];
function run(N) {
  const rand = rng(7), exp = (m) => -Math.log(1 - rand()) * m;
  const free = new Array(N).fill(0), arr = [], start = [];
  for (let t = exp(1 / rate(0)); t < END; t += exp(1 / rate(t))) {
    let k = 0; for (let i = 1; i < N; i++) if (free[i] < free[k]) k = i;  // first free consumer
    const s = Math.max(t, free[k]); free[k] = s + exp(S);
    arr.push(t); start.push(s);
  }
  const depth = SAMPLES.map((x) => arr.filter((a, i) => a <= x && start[i] > x).length);
  const waits = arr.map((a, i) => start[i] - a).sort((a, b) => a - b);
  const p99 = waits[Math.ceil(0.99 * waits.length) - 1];
  return { depth, p99, n: arr.length };
}
console.log("depth at t(s):  " + SAMPLES.map((x) => String(x).padStart(5)).join("") + "   wait p99");
for (const N of [2, 4, 8]) {
  const r = run(N);
  console.log(("N = " + N + " (" + N * 10 + "/s)").padEnd(16) + r.depth.map((d) => String(d).padStart(5)).join("") +
    (r.p99 < 1 ? (r.p99 * 1000).toFixed(0) + " ms" : r.p99.toFixed(1) + " s").padStart(11));
}

আউটপুট:

depth at t(s):     30   60   70   80  100  120  150  180   wait p99
N = 2 (20/s)      233  445 1022 1597 1618 1708 1787 1940     96.0 s
N = 4 (40/s)        2    0  342  676  324   51    0    0     15.7 s
N = 8 (80/s)        0    0    2    3    0    0    0    0     213 ms

N = ২: ঢেউয়ের আগেই ৬০ সেকেন্ডে ৪৪৫টি জমা, কারণ গড় চাপ ক্ষমতার ওপরে। ঢেউয়ে বাড়ে সেকেন্ডে প্রায় ৫৮ (হিসাবে ৮০ − ২০ = ৬০), তারপরও থামে না — ১৮০ সেকেন্ডে ১,৯৪০টি। এখানে কোনো ভিড় “শোষিত” হচ্ছে না; queue শুধু সমস্যাটি লুকিয়ে রাখছে, আর p99 অপেক্ষা ৯৬ সেকেন্ড।

N = ৪: ঢেউয়ের আগে প্রায় খালি। ৮০ সেকেন্ডে ৬৭৬ — সূত্রের ৮০০-এর কাছাকাছি (এলোমেলো আগমন আর service-এ কিছুটা কম)। তারপর সরে সেকেন্ডে প্রায় ১৫ হারে: ১২০ সেকেন্ডে ৫১, ১৫০-এ শূন্য — সূত্রে খালি হওয়ার কথা ৮০ + ৬৭৬ ÷ ১৫ ≈ ১২৫ সেকেন্ডে। p99 অপেক্ষা ১৫.৭ সেকেন্ড। ঢেউয়ের সময় কাজগুলো দেরিতে হলো, কিন্তু কোনোটি হারাল না, আর consumer-রা একই গতিতে চলল — এটিই load levelling.

N = ৮: ক্ষমতা ঢেউয়ের সমান, তাই জমা মাত্র ২–৩টি, p99 ২১৩ ms. দাম: বাকি সময় ক্ষমতার ৬৯% অলস (২৫ ÷ ৮০ = ৩১% ব্যস্ত)। কোন N বাছবেন, তা নির্ভর করে কাজটি কতটা দেরি সয় তার ওপর: SMS রসিদে ১৫ সেকেন্ড চলে, OTP-তে চলে না — এটি একটি requirement (পর্ব ২)।

কতজন consumer — Little-এর সূত্রে

একটি consumer process প্রায়ই একসাথে কয়েকটি বার্তা প্রক্রিয়া করে, thread বা async দিয়ে। তখন প্রশ্ন হয় “একসাথে কতগুলো কাজ চলবে?” পর্ব ৪-এর Little-এর সূত্র L = λW সরাসরি উত্তর দেয়: সেকেন্ডে ২০০টি SMS, প্রতিটি gateway call ০.৫ সেকেন্ড — একসাথে চলবে গড়ে ২০০ × ০.৫ = ১০০টি কাজ। প্রতিটি process ২০টি concurrent কাজ চালালে অন্তত ৫টি process, আর ৭০–৮০% ব্যস্ততার লক্ষ্যে ৭টি। নিচের ধাপ (gateway, database) সেই ১০০টি একসাথে নিতে পারবে কি না, সেটিও দেখুন — নইলে consumer বাড়ানো শুধু চাপ সরায়।

৪ · Acknowledgement — বার্তা কখন মুছবে, আর crash হলে কী হয়

Queue-র সবচেয়ে গুরুত্বপূর্ণ প্রশ্ন: broker কখন বুঝবে কাজটি শেষ, আর বার্তাটি মোছা নিরাপদ? উত্তর acknowledgement বা ack — consumer-এর পাঠানো “কাজ শেষ” সংকেত। দুটি ধরন:

ধরনকখন মোছেConsumer crash করলেনিশ্চয়তা
Auto ackপাঠানোর মুহূর্তেই, কাজের আগেবার্তা হারায় — কেউ আর করে নাসর্বোচ্চ একবার (at-most-once)
Manual ackConsumer কাজ শেষে ack দিলেবার্তা আবার আসে, অন্য কেউ করেঅন্তত একবার (at-least-once)

Auto ack দ্রুত আর সহজ, কিন্তু শুধু সেখানে চলে যেখানে বার্তা হারালে ক্ষতি নেই — যেমন প্রতি সেকেন্ডের তাপমাত্রার রিডিং, যার পরেরটি এক সেকেন্ড পরেই আসছে। টাকা, অর্ডার আর নোটিফিকেশনে manual ack.

Broker কীভাবে বোঝে consumer মারা গেছে — দুই ধাঁচ

  • Visibility timeout (SQS-ধাঁচ): consumer বার্তা নিলে সেটি নির্দিষ্ট সময়ের জন্য লুকানো থাকে। সময়ের মধ্যে consumer বার্তাটি মুছে দিলে (delete — এখানে এটিই ack) শেষ; না মুছলে সময় শেষে বার্তা আবার দেখা যায়, আর অন্য কেউ পায়। Broker consumer-এর সাথে কোনো সংযোগ ধরে রাখে না; সময়ই একমাত্র সংকেত — একটি lease-এর মতো (পর্ব ৪৩)। Amazon SQS-এর ডকুমেন্টেশন অনুযায়ী, লেখার সময়ে ডিফল্ট ৩০ সেকেন্ড আর সর্বোচ্চ ১২ ঘণ্টা, আর লম্বা কাজে consumer মাঝপথে সময় বাড়াতে পারে।
  • সংযোগ-ভিত্তিক ack (RabbitMQ-ধাঁচ): consumer একটি খোলা সংযোগে বার্তা পায়। কাজ শেষে ack দেয়; প্রক্রিয়া করতে না পারলে nack বা reject দেয় — সাথে বলে আবার queue-তে ফেরাবে (requeue) কি না। সংযোগ বা channel কেটে গেলে ack-না-পাওয়া সব বার্তা আবার queue-তে ফেরে। RabbitMQ-র নতুন সংস্করণে ack-এর জন্য একটি সময়সীমাও আছে (মান সংস্করণ আর setting-ভেদে আলাদা)।

Redis Streams-এ তৃতীয় একটি রূপ: consumer group-এ পড়া বার্তা একটি pending তালিকায় থাকে, XACK-এ সরে; অনেকক্ষণ pending থাকা বার্তা অন্য consumer দাবি করে নিতে পারে — XCLAIM বা XAUTOCLAIM দিয়ে। নাম আলাদা, ধারণা এক: ack না আসা পর্যন্ত বার্তা কারো দায়িত্বে থাকে, আর দায়িত্বপ্রাপ্ত হারিয়ে গেলে বার্তা ফেরে।

এখানেই redelivery-র জন্ম, আর তার সাথে duplicate-এর। নিচের stepper-এ consumer কাজ শেষ করে, কিন্তু ack দেওয়ার আগেই মারা যায়। কাজ: গ্রাহক ৮৮-এর হিসাবে ৫০ লয়্যালটি পয়েন্ট যোগ করা। Visibility timeout ৩০ সেকেন্ড।

Brokerm7 · VT 30 s
Consumer C1প্রথম গ্রহণ
Consumer C2দ্বিতীয় গ্রহণ
Databasepoints + processed
বার্তা m7: “গ্রাহক ৮৮-কে ৫০ পয়েন্ট দিন”
সময়
m7-এর অবস্থা
Receive count
processed টেবিলে m7
পয়েন্ট — idempotent consumer
পয়েন্ট — dedupe ছাড়া
  1. C1 বার্তা নিল। t = ০-তে C1 m7 পেল; broker সেটি t = ৩০ সেকেন্ড পর্যন্ত লুকিয়ে রাখবে। Receive count ১। গ্রাহক ৮৮-এর পয়েন্ট এখন ১০০।
  2. কাজ হলো। t = ৪-এ C1 একটি local transaction-এ দুটি কাজ করল: পয়েন্ট ১০০ থেকে ১৫০, আর processed টেবিলে m7-এর id. দুটি একসাথে commit হয়, নয়তো কোনোটিই না।
  3. Ack-এর আগে crash. t = ৫-এ C1-এর process মারা গেল — deploy-এর সময় kill, memory শেষ, বা যন্ত্র বিকল। Ack যায়নি। Broker জানে না কাজ হয়েছে কি না; সে শুধু জানে ack আসেনি।
  4. Visibility timeout শেষ। t = ৩০-এ m7 আবার দেখা যায়। Broker-এর দিক থেকে এটিই একমাত্র নিরাপদ পথ: বার্তা হারানোর চেয়ে দুবার দেওয়া ভালো। এটিই at-least-once.
  5. C2 একই বার্তা পেল। Receive count ২। C2 transaction শুরু করে আগে processed-এ m7 ঢোকাতে চায় — আগেই আছে (unique key), তাই পয়েন্ট যোগ বাদ, ১৫০-ই থাকে। Dedupe না থাকলে পয়েন্ট দ্বিতীয়বার যোগ হতো: ২০০ — গ্রাহক ৫০ পয়েন্ট বেশি পেলেন, হিসাব ভুল।
  6. C2 ack দিল, m7 মুছে গেল। শিক্ষা: broker দেয় “অন্তত একবার”; “ঠিক একবারের ফল” consumer-এর দায়িত্ব — বার্তার id আর effect একই transaction-এ। এর পূর্ণ আলোচনা — at-most-once, exactly-once আর তাদের দাম — পর্ব ৩৪-এর delivery semantics-এ পাবেন।

Crash ছাড়াও duplicate আসে: কাজ visibility timeout-এর চেয়ে লম্বা হলে প্রথম consumer কাজ করছে, অথচ বার্তা আবার দেখা দেয় আর দ্বিতীয়জনও শুরু করে। Confirm হারালে producer-ও একই বার্তা আবার পাঠায়। তাই নিয়ম সরল: যেকোনো queue-র consumer ধরে নেবে যে প্রতিটি বার্তা একাধিকবার আসতে পারে।

// Illustrative consumer loop. "queue" and "db" are made-up clients, not a real library.
while (running) {
  const batch = queue.receive({ max: 10, waitSeconds: 20 });   // long poll, small batch
  for (const msg of batch) {
    try {
      db.transaction((tx) => {
        if (!tx.insertIfAbsent("processed", msg.id)) return;      // seen before: skip effect
        tx.addPoints(msg.body.customer, msg.body.points);         // the real effect
      });                                                         // both commit together
      queue.ack(msg);                                             // ack only AFTER commit
    } catch (e) {
      if (isPermanent(e)) queue.deadLetter(msg, e.message);       // bad message: stop retrying
      else queue.retryLater(msg, backoffWithJitter(msg.receiveCount)); // transient: wait, retry
    }
  }
}

তিনটি জিনিস লক্ষ করুন: effect আর “দেখা হয়েছে” চিহ্ন একই transaction-এ; ack commit-এর পরে; আর ত্রুটি দুই রকম — ক্ষণিকের (network, timeout) আর স্থায়ী (ভুল ডেটা)। বাইরের সেবার effect (SMS, পেমেন্ট gateway) একই transaction-এ রাখা যায় না; সেখানে সেবাটির idempotency key (পর্ব ৮) পাঠান, যাতে দ্বিতীয় অনুরোধ নতুন কাজ না বানায়।

৫ · Retry, backoff, poison message আর dead letter queue

কাজ ব্যর্থ হলে আবার চেষ্টা স্বাভাবিক — SMS gateway এক মুহূর্ত ব্যস্ত ছিল, database failover চলছিল। কিন্তু কীভাবে আবার চেষ্টা, তা গুরুত্বপূর্ণ:

  • সঙ্গে সঙ্গে requeue নয়: nack-এ বার্তা সঙ্গে সঙ্গে ফিরলে consumer সেটি আবার মুহূর্তেই পায় — gateway তখনো বিকল, আবার ব্যর্থ। সেকেন্ডে হাজারবার এই ঘূর্ণি CPU পোড়ায় আর log ভরায়, আর বিকল সেবার ওপর চাপ বাড়ায় (retry storm, পর্ব ১৮)।
  • Exponential backoff আর jitter: প্রতিবার অপেক্ষা দ্বিগুণ — যেমন ১০, ২০, ৪০ সেকেন্ড — সাথে এলোমেলো অংশ, যাতে হাজারো বার্তা একই মুহূর্তে না ফেরে। Queue-তে এর উপায়: visibility timeout বাড়িয়ে দেওয়া (SQS-ধাঁচ), বা delay-সহ আলাদা retry queue (RabbitMQ-তে TTL আর dead-letter exchange দিয়ে প্রচলিত কৌশল)।
  • চেষ্টার সীমা: প্রতিটি বার্তার একটি max receive count — যেমন ৩ বা ৫। সীমা পেরোলে বার্তা আর মূল queue-তে নয়।

Poison message — যে বার্তা কখনো সফল হবে না

কিছু বার্তা যতবারই চেষ্টা করুন, ব্যর্থ হবে: JSON ভাঙা, পুরোনো schema, টাকার ঘরে সংখ্যার বদলে লেখা, বা এমন গ্রাহক যিনি মুছে গেছেন। এমন বার্তার নাম poison message. সীমা না থাকলে এটি চিরকাল ঘোরে: প্রতিবার consumer-এর সময় খায়, কখনো consumer-কেই crash করায়। আর FIFO queue-তে (অংশ ৬) পেছনের সবাইকে আটকে রাখে।

সমাধান dead letter queue (DLQ): সীমা পেরোনো বার্তা একটি আলাদা queue-তে সরে, সাথে ত্রুটির কারণ। মূল queue চলতে থাকে; মানুষ বা একটি tool পরে DLQ দেখে, কোড ঠিক করে, তারপর বার্তাগুলো মূল queue-তে ফেরত পাঠায় — একে বলে redrive. ডাকঘরের “ঠিকানা পাওয়া যায়নি” তাকের মতো — চিঠি ফেলে দেওয়া হয় না, আবার সাধারণ বাছাইয়ের পথও আটকায় না।

  1. মূল queuepaymentsm9 আসে
  2. Consumerparse errorবারবার ব্যর্থ
  3. DLQpayments-dlqআলাদা তাক
  4. AlertDLQ depth > 0মানুষ দেখেন
  5. Redriveঠিক কোডআবার মূল queue
m9-এর body-তে amount = "১২০০ টাকা" (সংখ্যা নয়, লেখা) · max receive count 3
সময়
m9 কোথায়
Receive count
ফল
DLQ depth
বাকি বার্তাগুলো
  1. প্রথম চেষ্টা। m9-এর amount-এ সংখ্যার বদলে লেখা “১২০০ টাকা”, পুরোনো একটি অ্যাপের পাঠানো। Parser ত্রুটি দিল; ক্ষণিকের কি না নিশ্চিত নয়, তাই ১০ সেকেন্ড পরে আবার চেষ্টার জন্য ফেরত। Count ১।
  2. দ্বিতীয় চেষ্টা, backoff দ্বিগুণ। প্রায় ১০ সেকেন্ডে আবার এল, একই ত্রুটি; এবার অপেক্ষা ২০ সেকেন্ড। Count ২। এই পুরো সময় অন্য consumer-রা বাকি বার্তা প্রক্রিয়া করছে।
  3. তৃতীয় চেষ্টা। প্রায় ৩০ সেকেন্ডে, count ৩, একই ত্রুটি। একই কোড, একই ডেটা — ফল বদলানোর কোনো কারণ নেই।
  4. DLQ-তে সরানো। Count সীমা ৩ ছুঁয়েছে, তাই broker (বা consumer নিজে) m9-কে payments-dlq-তে সরাল, সাথে ত্রুটির কারণ আর কতবার চেষ্টা হলো। ঠিক কোন মুহূর্তে সরে — তৃতীয় ব্যর্থতার পরেই, না পরের গ্রহণের চেষ্টায় — broker-ভেদে আলাদা। মূল queue-র পথ এখন পরিষ্কার।
  5. Alert. DLQ-তে একটি বার্তাও থাকলে বা সবচেয়ে পুরোনোটির বয়স কয়েক মিনিট পেরোলে alert বাজে। কেউ না দেখলে DLQ একটি কবরস্থান — পেমেন্টটি চুপচাপ হারাত।
  6. ঠিক করে redrive. Consumer-এর কোড এখন “১২০০ টাকা”-র মতো পুরোনো ধাঁচও পড়তে পারে (পর্ব ১২-এর tolerant reader-এর ধারণায়)। DLQ থেকে বার্তাটি মূল queue-তে ফেরত গেল, count নতুন করে শুরু, এবার সফল। Consumer idempotent বলে redrive-এ ভয় নেই — কোনো বার্তা আগে আংশিক হয়ে থাকলেও দ্বিতীয়বার effect হবে না।
# SQS-style: a redrive policy on the main queue (a JSON queue attribute)
RedrivePolicy      = {"deadLetterTargetArn": "<ARN of payments-dlq>", "maxReceiveCount": "3"}
VisibilityTimeout  = 60          # seconds; longer than the slowest normal job

# RabbitMQ-style: arguments when declaring a quorum queue
x-queue-type            = quorum
x-dead-letter-exchange  = payments.dlx    # where rejected or expired messages go
x-delivery-limit        = 3               # quorum queues: give up after this many tries
x-message-ttl           = 86400000        # milliseconds (1 day)

এবার সব একসাথে: ১০,০০০টি বার্তা, কাজ গড়ে ৩ সেকেন্ড (exponential), প্রতিটি গ্রহণে ২% সম্ভাবনায় কাজের আগে crash আর ২% সম্ভাবনায় কাজের পরে কিন্তু ack-এর আগে crash; ০.৩% বার্তা poison. Visibility timeout (VT) ৫, ১৫ আর ৬০ সেকেন্ড হলে কী হয়?

// At-least-once queue with a visibility timeout (VT) and max receive count 3 -> DLQ.
// 10,000 messages. Work takes ~3 s on average (exponential). Per receive:
// 2% the consumer crashes BEFORE doing the work, 2% crashes AFTER the work but BEFORE ack.
// If work takes longer than VT, the message becomes visible again and is redelivered
// (the slow consumer still finishes, so the work happens twice). 0.3% are poison messages:
// they crash the consumer every time.
function rng(seed) {                                   // mulberry32
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const MSGS = 10000, MAX_RECEIVE = 3;
function run(VT) {
  const rand = rng(31);
  let deliveries = 0, effects = 0, dupEffects = 0, dlq = 0, dlqHealthy = 0;
  for (let m = 0; m < MSGS; m++) {
    const poison = rand() < 0.003; let done = 0, acked = false;
    for (let r = 1; r <= MAX_RECEIVE && !acked; r++) {
      deliveries++;
      const u = rand(), work = -Math.log(1 - rand()) * 3;     // seconds
      if (poison || u < 0.02) continue;                       // crash before work: no effect
      done++; effects++; if (done > 1) dupEffects++;          // the side effect happened
      if (u < 0.04) continue;                                 // crash after work, no ack
      if (work > VT) continue;                                // too slow: redelivered meanwhile
      acked = true;                                           // ack in time: message deleted
    }
    if (!acked) { dlq++; if (!poison) dlqHealthy++; }
  }
  return [VT + " s", deliveries, effects, dupEffects, dlq, dlqHealthy];
}
const w = [6, 12, 9, 13, 6, 13];
console.log(["VT", "deliveries", "effects", "dup effects", "DLQ", "DLQ healthy"].map((h, i) => h.padStart(w[i])).join(""));
for (const VT of [5, 15, 60]) console.log(run(VT).map((v, i) => String(v).padStart(w[i])).join(""));

আউটপুট:

    VT  deliveries  effects  dup effects   DLQ  DLQ healthy
   5 s       12693    12361         2392   135          104
  15 s       10494    10204          234    30            0
  60 s       10440    10147          176    29            0

VT = ৫ সেকেন্ড — খুব ছোট: গড় ৩ সেকেন্ডের কাজে ৫ সেকেন্ড ছাড়ানোর সম্ভাবনা e−5/3, প্রায় ১৯ শতাংশ। সেগুলো consumer শেষ করে, কিন্তু ততক্ষণে বার্তা আরেকজনের কাছে — effect দুবার। তাই ২,৩৯২টি duplicate effect (প্রায় ২৪%), আর ১০৪টি সুস্থ বার্তা DLQ-তে, শুধু ধীর ছিল বলে। হিসাব: একটি গ্রহণে ack না হওয়ার সম্ভাবনা q = ০.০৪ + ০.৯৬ × ০.১৮৯ ≈ ০.২২১; তিনবারই না হওয়ার সম্ভাবনা q³ ≈ ০.০১০৮, অর্থাৎ ৯,৯৭০ সুস্থ বার্তায় প্রায় ১০৮টি — simulator-এ ১০৪।

VT = ১৫ আর ৬০: ধীরতার কারণে redelivery প্রায় নেই; duplicate ২৩৪ আর ১৭৬ — বাকিগুলো আসে “কাজের পরে, ack-এর আগে crash” থেকে (প্রায় ২%, অর্থাৎ ২০০-র কাছাকাছি)। এগুলো কোনো timeout দিয়ে সরানো যায় না; শুধু idempotent consumer এদের নিরীহ করে। DLQ-তে ২৯–৩০টি, সবই আসল poison.

শিক্ষা: VT রাখুন স্বাভাবিক কাজের p99.9-এর ওপরে, আর লম্বা কাজে heartbeat দিয়ে সময় বাড়ান। খুব বড় VT-তে মৃত consumer-এর বার্তা ততক্ষণ আটকে থাকে। আর duplicate কখনো শূন্য হয় না।

৬ · ক্রম — FIFO বনাম best-effort, আর message group

অনেক queue বার্তার ক্রম কঠোরভাবে রাখে না। SQS-এর standard queue-র মতো সেবাগুলো ডকুমেন্টেশনে স্পষ্ট বলে: ক্রম best-effort, আর বার্তা কখনো একাধিকবার আসতে পারে। কেন? সবচেয়ে সরল কারণ competing consumers নিজেই:

  • বার্তা ১ গেল C1-এ, বার্তা ২ গেল C2-তে। C1 ধীর, তাই ২-এর কাজ আগে শেষ। Queue ক্রমে দিলেও কাজ ক্রমে হলো না।
  • বার্তা ১ ব্যর্থ হয়ে retry-তে গেল; ততক্ষণে ২, ৩, ৪ শেষ। Retry ক্রম ভাঙে।

সব কাজে ক্রম লাগেও না — একশো জনের SMS কে আগে গেল, তাতে কিছু আসে যায় না। কিন্তু একই অ্যাকাউন্টের “জমা ৫০০” আর “তোলা ৩০০” উল্টো ক্রমে হলে মাঝখানে ব্যালান্স ঋণাত্মক দেখাতে পারে। এর দুটি সমাধান:

🚦 FIFO queue আর message group

FIFO queue একটি message group-এর (যেমন account id) ভেতরে ক্রম রাখে: আগেরটি ack না হওয়া পর্যন্ত একই group-এর পরেরটি কাউকে দেওয়া হয় না। ভিন্ন group সমান্তরালে চলে। SQS-এর FIFO queue-তে একে MessageGroupId বলে, আর একটি নির্দিষ্ট সময়ের (লেখার সময়ে ৫ মিনিট) মধ্যে একই deduplication id-এর দ্বিতীয় বার্তা বাদ দেয়।

group-এর ভেতরে ক্রম, বাইরে সমান্তরাল

🔢 Consumer-এ ক্রম যাচাই

প্রতিটি বার্তায় একটি version বা sequence number; consumer পুরোনো version এলে তা উপেক্ষা করে (“এই অ্যাকাউন্টের শেষ প্রক্রিয়া করা version ১৭, এটি ১৫ — বাদ”)। অথবা বার্তায় শুধু “অ্যাকাউন্ট ৪২ বদলেছে” লেখা থাকে, আর consumer সর্বশেষ অবস্থা database থেকে পড়ে।

ক্রম না মেনেও সঠিক ফল

Message group-এর দাম head-of-line blocking: একটি group-এর মাথার বার্তা ব্যর্থ হলে সেই group-এর বাকি সবাই অপেক্ষা করে। Poison message এখানে আরও বিপজ্জনক — সীমা আর DLQ ছাড়া একটি অ্যাকাউন্টের সব লেনদেন চিরকাল আটকে থাকত। আর group যত কম, সমান্তরালতা তত কম: একটিমাত্র group মানে একজন consumer-এর গতি।

Key দিয়ে ভাগ — log-এর পথ: Kafka-ধাঁচের log-ভিত্তিক stream একই ধারণা অন্যভাবে করে — key-এর hash দিয়ে বার্তা একটি partition-এ যায়, আর partition-এর ভেতরে ক্রম কঠোর। Partition, offset আর consumer group-এর বিস্তারিত পর্ব ৩৩-এর log-ভিত্তিক streaming-এ পাবেন।

৭ · Prefetch, priority, delay, TTL আর durability

Prefetch, batching আর consumer concurrency

একটি একটি করে বার্তা আনলে প্রতিটির জন্য broker পর্যন্ত একটি round trip লাগে। কাজ ৫ ms আর round trip ২ ms হলে consumer-এর প্রায় ২৯% সময় অপেক্ষায় যায়। Prefetch এর সমাধান: broker আগেভাগে কয়েকটি বার্তা consumer-এর কাছে পাঠিয়ে রাখে (RabbitMQ-তে prefetch count, SQS-ধাঁচে এক অনুরোধে সর্বোচ্চ কয়েকটি বার্তা আর long polling, যাতে খালি queue-তে বারবার জিজ্ঞেস করতে না হয়)। Ack-ও একসাথে কয়েকটির জন্য দেওয়া যায় — batching.

কিন্তু prefetch বড় হলে দুটি সমস্যা: একটি consumer অনেক বার্তা ধরে বসে থাকে, অন্যরা অলস — বণ্টন অসম; আর সে crash করলে সবগুলো একসাথে ফেরে, যাদের কিছু অর্ধেক হয়ে থাকতে পারে। শুরুর নিয়ম: prefetch ≈ একসাথে যতগুলো কাজ consumer সত্যিই চালায়, তার একটু বেশি। লম্বা কাজে (মিনিটের হিসাবে) prefetch ১।

Priority queue — জরুরি কাজ আগে

OTP আর মাসিক বিবরণীর ইমেইল একই queue-তে থাকলে, ১০ লাখ বিবরণীর পেছনে একটি OTP ঘণ্টাখানেক আটকে থাকতে পারে। Priority queue-তে বার্তার অগ্রাধিকার থাকে, বেশি অগ্রাধিকারের বার্তা আগে দেওয়া হয় — ভেতরে প্রায়ই একটি heap-এর মতো কাঠামো — বিস্তারিত DSA · Heap আর priority queue-এ। RabbitMQ-র কিছু queue ধরন এটি দেয় (সীমিত কয়েকটি স্তরে, সংস্করণভেদে আলাদা)। তবে অনেক দল এর চেয়ে সরল পথ নেয়: আলাদা queue, আলাদা consumer pool — জরুরিদের নিজস্ব worker থাকে, তাই নিচু শ্রেণির ভিড়ে তারা আটকায় না (পর্ব ১৮-এর bulkhead-এর ধারণা)। কঠোর priority-তে নিচু শ্রেণি চিরকাল অপেক্ষা করতে পারে (starvation); সেটিও মাপুন।

Delayed message আর message TTL

  • Delayed message: “৩০ মিনিট পরে দেখুন পেমেন্ট এসেছে কি না, না এলে অর্ডার বাতিল।” বার্তা queue-তে ঢোকে, কিন্তু নির্দিষ্ট সময় পরে দেখা দেয়। SQS-ধাঁচে delay-র সীমা ছোট (লেখার সময়ে সর্বোচ্চ ১৫ মিনিট); RabbitMQ-তে plugin বা TTL আর dead-letter-এর কৌশল। দিন বা মাসের হিসাবের সময়সূচির জন্য scheduler বা workflow engine ভালো — পর্ব ৩৭-এর background job আর workflow-এ দেখবেন।
  • Message TTL: একটি বার্তার মেয়াদ। “তিন মিনিটের OTP” পাঁচ মিনিট পরে পাঠানো অর্থহীন, বরং বিভ্রান্তিকর; মেয়াদ পেরোলে broker সেটি ফেলে দেয় বা DLQ-তে পাঠায়। Consumer-এর দিকেও যাচাই রাখুন: বার্তায় “এর পরে বাতিল” সময় লিখে রাখুন। এটি পর্ব ১৮-এর deadline-এর ধারণাই।

Durability — বার্তা কি broker-এর restart-এ টিকে থাকবে?

Persistent বার্তা broker disk-এ লেখে (প্রায়ই fsync সহ), তাই restart-এ টেকে; transient বার্তা শুধু memory-তে — দ্রুত, কিন্তু restart-এ হারায়। RabbitMQ-র ক্লাসিক জগতে queue “durable” আর বার্তা “persistent” — দুটিই আলাদা করে ঠিক করতে হয়; একটি না থাকলে restart-এ বার্তা হারাতে পারে।

কিন্তু একটি disk-ও নষ্ট হয়। তাই অনেক broker বার্তা একাধিক node-এ replicate করে (পর্ব ২২)। RabbitMQ-র quorum queue এর জন্য Raft consensus ব্যবহার করে (পর্ব ৪২): বার্তা গ্রহণের নিশ্চয়তা আসে বেশিরভাগ replica লেখার পরে। Durability-র শিকল সম্পূর্ণ হয় তখনই, যখন তিনটিই আছে: producer confirm-এর অপেক্ষা করে, broker disk আর replica-তে লেখে, consumer কাজের পরে ack দেয়। যেকোনো একটি বাদ পড়লে শিকল সেখানেই ছেঁড়ে।

৮ · চালানো আর মাপা — queue depth, autoscaling আর broker বাছাই

একটি queue-র স্বাস্থ্য বোঝার দুটি সংখ্যা: queue depth (কতগুলো বার্তা অপেক্ষায়) আর সবচেয়ে পুরোনো বার্তার বয়স। প্রথমটি বলে কত কাজ জমা, দ্বিতীয়টি বলে ব্যবহারকারী কত দেরি টের পাচ্ছেন। Log-ভিত্তিক stream-এ একই জিনিসের নাম consumer lag — লেখা আর পড়ার অবস্থানের ব্যবধান (পর্ব ৩৩)। Consumer-এর CPU এখানে প্রায়ই ভুল সংকেত: SMS gateway-র অপেক্ষায় বসে থাকা consumer-এর CPU ৫%, অথচ queue-তে এক লাখ বার্তা।

পর্ব ১৩-এ দেখেছেন, পেছনের worker-এর জন্য autoscaling-এর সেরা metric queue-র দৈর্ঘ্য। এবার চারটি নীতি পাশাপাশি চালান: স্বাভাবিক চাপ সেকেন্ডে ৩০, ৩০০ থেকে ৯০০ সেকেন্ডে ১৫০; প্রতিটি consumer সেকেন্ডে ১০; নতুন consumer চালু হতে ৬০ সেকেন্ড।

// Fixed consumers vs autoscaling on queue depth. 1-second steps, fluid model.
// Arrivals: 30 msg/s, 150 msg/s from t = 300 s to 900 s, then 30 msg/s; run 3600 s.
// One consumer handles 10 msg/s. Autoscaler checks every 30 s (min 2, max 20);
// new consumers need 60 s to start; scale in by at most 1 consumer per check.
const RATE = (t) => (t >= 300 && t < 900 ? 150 : 30), END = 3600, MU = 10;
const POLICIES = [
  { name: "fixed 8", fixed: 8 },
  { name: "fixed 15", fixed: 15 },
  { name: "depth only", want: (depth, inRate) => Math.ceil(depth / 100) },
  { name: "depth + in-rate", want: (depth, inRate) => Math.ceil(inRate / (0.8 * MU) + depth / 100) }];
function run(p) {
  let depth = 0, n = p.fixed || 2, maxDepth = 0, cost = 0, A = 0, D = 0, actions = 0;
  const starting = [], arrived = [], served = [];
  for (let t = 0; t < END; t++) {
    while (starting.length && starting[0] <= t) { starting.shift(); n++; }
    depth += RATE(t); A += RATE(t);
    const s = Math.min(depth, n * MU); depth -= s; D += s;
    arrived.push(A); served.push(D); maxDepth = Math.max(maxDepth, depth);
    cost += n + starting.length;                          // starting ones are paid too
    if (p.want && t % 30 === 0 && t >= 30) {
      const inRate = (A - arrived[t - 30]) / 30;          // measured, last 30 s
      const desired = Math.min(20, Math.max(2, p.want(depth, inRate))), total = n + starting.length;
      if (desired > total) { actions++; for (let k = total; k < desired; k++) starting.push(t + 60); }
      else if (desired < total && starting.length === 0) { actions++; n--; }
    }
  }
  let maxWait = 0, j = 0;                                 // FIFO: when is message #A(t) served?
  for (let t = 0; t < END; t++) { while (served[j] < arrived[t]) j++; maxWait = Math.max(maxWait, j - t); }
  return [p.name, maxDepth, maxWait + " s", (cost / 3600).toFixed(2), actions];
}
console.log("policy           max depth  max wait  consumer-hours  scale actions");
for (const p of POLICIES) {
  const r = run(p);
  console.log(r[0].padEnd(16) + String(r[1]).padStart(10) + r[2].padStart(10) + r[3].padStart(16) + String(r[4]).padStart(15));
}

আউটপুট:

policy           max depth  max wait  consumer-hours  scale actions
fixed 8              42000     525 s            8.00              0
fixed 15                 0       0 s           15.00              0
depth only           10790      72 s            8.52            102
depth + in-rate       7190      48 s            7.92             26

Fixed ৮: ক্ষমতা ৮০, ঢেউ ১৫০ — জমে সেকেন্ডে ৭০, ৬০০ সেকেন্ডে (১৫০ − ৮০) × ৬০০ = ৪২,০০০; সর্বোচ্চ অপেক্ষা ৪২,০০০ ÷ ৮০ ≈ ৫২৫ সেকেন্ড — প্রায় ৯ মিনিট। Fixed ১৫: জমা নেই, কিন্তু খরচ প্রায় দ্বিগুণ, আর বেশিরভাগ সময় ৮০% ক্ষমতা অলস।

শুধু depth দেখে (চাই = ⌈depth ÷ ১০০⌉): ঢেউয়ে সর্বোচ্চ অপেক্ষা ৭২ সেকেন্ড, কিন্তু ১০২টি scaling কাজ! স্বাভাবিক সময়ে depth শূন্য, তাই নিয়ম বলে “২ জনই যথেষ্ট”; ২ জনে ক্ষমতা ২০ < ৩০, queue জমে, আবার বাড়ায় — বারবার ওঠানামা, যাকে বলে flapping. Depth শূন্য বলে না কতজন লাগবে, শুধু বলে এখন জমা নেই।

Depth আর আসার হার দুটো দেখে (চাই = ⌈আসার হার ÷ (০.৮ × ১০) + depth ÷ ১০০⌉): স্বাভাবিক সময়ে স্থির ৪ জন (৩০ ÷ ৮ = ৩.৭৫ → ৪), ঢেউয়ে দ্রুত বাড়ে। সর্বোচ্চ জমা ৭,১৯০, অপেক্ষা ৪৮ সেকেন্ড, খরচ fixed ৮-এর চেয়েও কম, আর scaling কাজ মাত্র ২৬টি। শিক্ষা: আসার হার বলে স্থির অবস্থায় কতজন লাগে, depth বলে জমা সরাতে কতজন বাড়তি।

Dashboard-এ (পর্ব ৫০) রাখুন depth, সবচেয়ে পুরোনো বার্তার বয়স, আসা ও শেষ হওয়ার হার, redelivery-র হার আর DLQ depth. Alert দিন বয়সে — “সবচেয়ে পুরোনো বার্তা ৫ মিনিটের বেশি” — কারণ ব্যবহারকারী সংখ্যা নয়, দেরি টের পান।

Broker-এর উদাহরণ — কোনটি কেমন

Brokerমূল ধাঁচAck ও redeliveryকখন মানানসই
RabbitMQ (open-source)Exchange থেকে queue-তে routing; AMQP 0-9-1 সহ কয়েকটি protocolসংযোগে ack/nack; prefetch; dead-letter exchange; quorum queue-তে replicationনমনীয় routing, task queue, নিজে চালাতে পারেন এমন দল
Amazon SQS-ধাঁচের cloud queueপরিচালিত সেবা, HTTP API, pollingVisibility timeout, delete = ack, redrive policy আর DLQ; standard আর FIFOসার্ভার চালাতে চান না; প্রায় সীমাহীন scale; খরচ অনুরোধ-প্রতি
Redis listLPUSH আর blocking popনিজে কোনো ack নেই; “reliable queue” কৌশলে (BLMOVE দিয়ে processing তালিকায় সরানো) হাতে বানাতে হয়ছোট, সরল কাজ, যেখানে মাঝে মাঝে হারানো চলে
Redis StreamsAppend-only stream, consumer groupPending তালিকা, XACK, delivery count, XAUTOCLAIMRedis আগেই আছে, মাঝারি পরিমাণ; durability Redis-এর persistence setting-এর ওপর নির্ভর

Kafka-কেও প্রায়ই “queue” বলা হয়, কিন্তু তার মূল কাঠামো একটি টেকসই log, যেখানে পড়লে বার্তা মোছে না — সেই ভিন্নতা নিচের ছকে, আর পূর্ণ আলোচনা পর্ব ৩৩-এ। কোনো broker-এর সীমা বা ডিফল্ট সংস্করণভেদে বদলায়; অফিসিয়াল ডকুমেন্টেশন দেখুন।

Queue বনাম pub/sub বনাম log — একটি আগাম ছক

প্রশ্নQueue (point-to-point)Pub/subLog (stream)
একটি বার্তা কে পায়?একজন consumerপ্রতিটি subscriber একটি করে কপিপ্রতিটি consumer group, নিজের অবস্থান থেকে
পড়ার পরে বার্তা?Ack-এ মুছে যায়Subscriber-এর কাছে পৌঁছালে শেষ (টেকসই subscription-এ জমে থাকে)Retention পর্যন্ত থাকে; আবার পড়া যায়
ক্রমসাধারণত best-effort; FIFO/group-এ সীমিতব্যবস্থাভেদেPartition-এর ভেতরে কঠোর
Scale কীভাবেConsumer যোগ (competing)Subscriber যোগ, প্রত্যেকে আলাদা কপিPartition-সংখ্যা পর্যন্ত consumer
সাধারণ কাজTask: ইমেইল, ছবি ছোট করা, রিপোর্ট“অর্ডার হয়েছে” খবর অনেক সেবাকেEvent-এর ইতিহাস, CDC, analytics, replay
বিস্তারিতএই পর্বপর্ব ৩২ (pub/sub)পর্ব ৩৩ (log streaming)

আরও গভীরে: Gregor Hohpe আর Bobby Woolf-এর Enterprise Integration Patterns (২০০৩) বইয়ে এই পর্বের অনেক নাম প্রথম গোছানো রূপ পায় — Point-to-Point Channel, Competing Consumers, Dead Letter Channel, Idempotent Receiver, Guaranteed Delivery. RabbitMQ-র quorum queue Raft-এর ওপর গড়া, আর তার ডকুমেন্টেশনে publisher confirm, consumer ack আর poison message-এর সীমা নিয়ে বিস্তারিত আলোচনা আছে। Queue-ভিত্তিক load levelling-এর গাণিতিক ভিত্তি queueing theory; একাধিক সেবাদাতার M/M/c মডেলে অপেক্ষার সম্ভাবনা Erlang-C সূত্রে হিসাব হয় — টেলিফোন এক্সচেঞ্জের জন্য A. K. Erlang-এর বিশ শতকের শুরুর কাজ থেকে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Message queue কেন ব্যবহার করবেন? Producer আর consumer-কে সময়, গতি আর ব্যর্থতায় আলাদা করা; ভিড় শোষণ; ধীর কাজ ব্যবহারকারীর পথ থেকে সরানো।
  • λ = ৫০০/সেকেন্ড, প্রতি consumer ২০/সেকেন্ড, ৭০% ব্যস্ততার লক্ষ্যে কতজন? ৫০০ ÷ (০.৭ × ২০) = ৩৫.৭ → ৩৬ জন।
  • Visibility timeout কী, আর ছোট রাখলে কী হয়? গ্রহণের পরে বার্তা লুকানোর সময়; কাজের চেয়ে ছোট হলে কাজ চলাকালেই অন্য consumer বার্তা পায় — duplicate, এমনকি সুস্থ বার্তা DLQ-তে।
  • Poison message আর DLQ? যে বার্তা বারবার ব্যর্থ হয়; max receive count পেরোলে DLQ-তে আলাদা, alert, ঠিক করে redrive.

৯ · AI-এর যুগে queue-র নকশা যাচাই

“অর্ডারের পরে SMS ধীর, কী করব?” — AI প্রায়ই বলে “Kafka বসান, Redis দিন, microservice-এ ভাগ করুন।” কিন্তু আসল প্রশ্ন থেকে যায়: বার্তা হারালে, দুবার এলে বা কখনো সফল না হলে কী হবে? সাধারণ ভুলগুলো:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“Kafka দিলেই exactly-once”Broker-এর ভেতরের নিশ্চয়তা আর আপনার database-এর effect এক নয়জিজ্ঞেস করুন: consumer crash-এর পরে effect দুবার হলে কে আটকায়?
Ack-এর সময় না বলা, বা কাজের আগে ackCrash-এ বার্তা হারায়কোডে ack commit-এর পরে কি না দেখুন
Retry আছে, সীমা বা DLQ নেইPoison message চিরকাল ঘোরে, FIFO group আটকায়Max receive count, backoff আর DLQ-র alert খুঁজুন
Consumer-এর সংখ্যার হিসাবে ভুলλ, μ আর অপেক্ষার বাজেট মেলে না(λ − N·μ) × T আর L = λW নিজে হিসাব করুন
পণ্যের সীমা বানিয়ে বলাVisibility timeout, বার্তার আকার, delay-র সীমা সেবা আর সময়ভেদে আলাদাঅফিসিয়াল ডকুমেন্টেশনের সাথে মেলান
সব কিছুতে queueব্যবহারকারী যে উত্তরের অপেক্ষায়, তা queue-তে রাখলে latency আর জটিলতা বাড়েপ্রতিটি কাজের জন্য জিজ্ঞেস করুন: পরে হলে চলবে?

AI-কে সংখ্যা আর ব্যর্থতার পথ দিয়ে বাঁধুন:

তুমি একজন অভিজ্ঞ backend প্রকৌশলী। একটি কাল্পনিক অনলাইন দোকানে অর্ডারের পরে SMS, ইমেইল আর লয়্যালটি পয়েন্টের কাজ একটি message queue দিয়ে করতে চাই। স্বাভাবিক দিনে সেকেন্ডে ৫০টি অর্ডার, সেলের ১০ মিনিটে সেকেন্ডে ৪০০টি। একটি consumer সেকেন্ডে ১০টি বার্তা শেষ করে; SMS gateway-র p99 প্রায় ৩ সেকেন্ড, মাঝে মাঝে কয়েক মিনিট বিকল থাকে। ১. সেলের সময় queue কত বাড়বে আর কত consumer লাগবে, যাতে কোনো বার্তা ৬০ সেকেন্ডের বেশি অপেক্ষা না করে — প্রতিটি ধাপ দেখাও। ২. Ack কখন দেবে, visibility timeout কত হবে আর কেন, retry-র backoff আর max receive count কত। ৩. একটি consumer কাজের পরে কিন্তু ack-এর আগে crash করলে কী হয়, আর পয়েন্ট যেন দুবার যোগ না হয় তার নকশা (টেবিল আর transaction সহ)। ৪. কোন কাজে ক্রম দরকার, কোনটিতে নয়; poison message কোথায় যাবে, কে জানবে। ৫. নতুন কোনো tool যোগ না করে কী করা যায়, আগে সেটা বলো। কোনো সেবার সীমা বা ডিফল্ট বললে ডকুমেন্টেশনের সূত্র দাও; না জানলে বলো "জানি না"।

AI-এর queue-পরিকল্পনা হাতে পেলে — যাচাইয়ের চেকলিস্ট

Queue কাজকে পরে পাঠায়, দায়িত্বকে নয়।
প্রতিটি বার্তার জন্য জিজ্ঞেস করুন: হারালে কী, দুবার এলে কী, কখনো সফল না হলে কোথায়?

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। মুখস্থ নয়, প্রতিটি নিশ্চয়তার পেছনের দাম চেনাই লক্ষ্য।

প্র ০১ব্যাংক আর আর্থিক প্রতিষ্ঠান কেন সবার আগে message queue-তে গিয়েছিল? আর “guaranteed delivery” আসলে কী প্রতিশ্রুতি দেয়, কী দেয় না?

১৯৯০-এর দশকে বড় ব্যাংকগুলোর একটি সমস্যা ছিল। Mainframe-এর হিসাবের ব্যবস্থা, শাখার কম্পিউটার, কার্ড আর বৈদেশিক লেনদেনের ব্যবস্থা — সবাই আলাদা যন্ত্রে, আলাদা সময়ে চালু থাকত। সরাসরি একে অপরকে ডাকলে একটির বিরতিতে পুরো শিকল থামত। IBM-এর MQSeries (১৯৯৩ সালের দিকে বাজারে আসে, পরে নাম IBM MQ) এর জন্য একটি সরল প্রতিশ্রুতি দিল: বার্তা একবার queue-তে নিশ্চিত হলে, প্রাপক ঘণ্টা পরে চালু হলেও পৌঁছাবে। পরে ২০০০-এর দশকে JPMorgan Chase-এ শুরু হওয়া উদ্যোগ থেকে AMQP নামে একটি খোলা protocol আসে, যার ১.০ সংস্করণ ২০১২ সালে OASIS মান হয়। RabbitMQ (২০০৭) এর প্রথম দিকের একটি বাস্তবায়ন, Erlang ভাষায় লেখা।

“Guaranteed delivery” কথাটি কিন্তু সতর্কভাবে পড়তে হয়। এর আসল অর্থ: broker নিশ্চিত করা বার্তা হারাবে না, আর ack না আসা পর্যন্ত আবার দেবে। এর ভেতরে তিনটি কথা লুকানো:

  • দুবারও আসতে পারে: crash-এর পরে broker জানে না কাজ হয়েছে কি না। তাই আবার দেয়।
  • নিশ্চয়তা শুধু broker পর্যন্ত: producer confirm-এর অপেক্ষা না করলে বার্তা broker-এ পৌঁছানোর আগেই হারাতে পারে।
  • “পৌঁছানো” মানে “কাজ সঠিক হওয়া” নয়: বার্তা consumer-এ পৌঁছাল, কিন্তু কাজটি ভুলভাবে হলো, বা অর্ধেক হলো।

Saltzer, Reed আর Clark-এর ১৯৮৪ সালের প্রবন্ধ “End-to-End Arguments in System Design” এর কারণ বলে। মাঝের কোনো স্তর পুরো কাজের সঠিকতা নিশ্চিত করতে পারে না; সেটি পারে শুধু দুই প্রান্ত। মাঝের স্তরের নির্ভরযোগ্যতা কাজকে দ্রুত আর সস্তা করে, কিন্তু প্রান্তের যাচাইয়ের বদলি নয়। Queue-র ক্ষেত্রে সেই প্রান্তের যাচাই হলো idempotent consumer আর হিসাব মিলানো (reconciliation), যেমন দিনের শেষে অর্ডার আর পাঠানো SMS-এর সংখ্যা।

মূল উপলব্ধি: Message queue ব্যর্থতার সময় আলাদা করে — প্রাপক পরে চালু হলেও চলে। কিন্তু ব্যর্থতার অর্থ সরায় না। “হারাবে না” পাওয়া যায় broker থেকে; “ঠিক একবার, সঠিকভাবে” আসে আপনার নিজের নকশা থেকে।

প্র ০২Visibility timeout আর সংযোগ-ভিত্তিক ack — একই সমস্যার দুটি সমাধান। কোনটির কী দাম, আর লম্বা কাজে কোনটি নিরাপদ?

দুটি পদ্ধতিই একটি প্রশ্নের উত্তর দেয়: “consumer মারা গেছে” broker কীভাবে বুঝবে? Distributed system-এ এর নিখুঁত উত্তর নেই। ধীর consumer আর মৃত consumer বাইরে থেকে একই দেখায় (পর্ব ৩৮)।

সংযোগ-ভিত্তিক ack (RabbitMQ-ধাঁচ): consumer একটি খোলা TCP সংযোগে বার্তা পায়। সংযোগ কাটলে broker সব unacked বার্তা ফেরায়। সুবিধা হলো দ্রুত সাড়া। Process মারা গেলে OS সংযোগ বন্ধ করে, আর বার্তা প্রায় সঙ্গে সঙ্গে ফেরে। কাজ যত লম্বাই হোক, সংযোগ বেঁচে থাকলে বার্তা consumer-এর কাছেই থাকে। দুর্বলতা দুটি। প্রথমত, process আটকে গেল (deadlock) কিন্তু সংযোগ বেঁচে আছে — বার্তা চিরকাল আটকে থাকতে পারত। তাই নতুন সংস্করণে ack-এর একটি সময়সীমা যোগ হয়েছে। দ্বিতীয়ত, network-এর ক্ষণিকের বিচ্ছেদে সংযোগ কাটলে কাজ-চলা বার্তাও ফেরে, তাই duplicate আসে।

Visibility timeout (SQS-ধাঁচ): broker কোনো সংযোগ রাখে না। গ্রহণ মানে একটি lease: “এত সেকেন্ড বার্তাটি এই consumer-এর।” HTTP-ভিত্তিক, stateless, বিশাল scale-এ সহজ। দাম হলো একটি সংখ্যা বাছাই। ছোট হলে ধীর কাজ duplicate হয়, যেমন অংশ ৫-এর simulator-এ ৫ সেকেন্ডে ২৪% duplicate আর সুস্থ বার্তা DLQ-তে। বড় হলে মৃত consumer-এর বার্তা অনেকক্ষণ আটকে থাকে।

লম্বা কাজে (ভিডিও transcode, ১০ মিনিট) দুই পথেরই নিরাপদ রূপ একই: heartbeat দেওয়া। কাজ চলাকালে consumer মাঝে মাঝে lease বাড়ায়, যেমন প্রতি ৩০ সেকেন্ডে আরও ৬০ সেকেন্ড। Consumer মারা গেলে heartbeat থামে, আর বার্তা অল্প সময়েই ফেরে। এটি পর্ব ৪৩-এর lease-এর ধারণা। সাথে মনে রাখুন, lease শেষ হওয়ার পরেও পুরোনো consumer কাজ করে যেতে পারে, যেমন GC-র বিরতি বা ধীর network-এর কারণে। তাই effect-এর জায়গায় যাচাই লাগে। Idempotent consumer, বা fencing token-এর মতো version, যাতে পুরোনো মালিকের দেরির লেখা নতুনটিকে মুছতে না পারে।

মূল উপলব্ধি: “মৃত” আর “ধীর”-এর মধ্যে নিশ্চিত পার্থক্য করা যায় না, শুধু একটি সময়ের অনুমান করা যায়। তাই যেকোনো redelivery-ব্যবস্থা কখনো কখনো জীবিত কারো কাজ অন্যকে দেবে। নকশাকে সেই duplicate সইতে হবে।

প্র ০৩অনেক broker “exactly-once” বা “deduplication” দেয় বলে। তবুও consumer-কে idempotent হতে হয় কেন?

দুই সেনাপতির সমস্যা (Two Generals) একটি পুরোনো চিন্তা-পরীক্ষা। দুই পাহাড়ে দুই সেনাপতি, মাঝের উপত্যকায় দূত ধরা পড়তে পারে। কোনো সসীম সংখ্যক বার্তা দিয়ে দুজন নিশ্চিতভাবে একমত হতে পারেন না, কারণ শেষ বার্তাটি পৌঁছাল কি না, প্রেরক কখনো জানেন না। Queue-তে এর রূপ হলো consumer-এর ack বার্তা। Consumer কাজ করল, ack পাঠাল, আর ack হারাল। Broker-এর দিক থেকে এটি আর “কাজ হয়নি” একই দেখায়। তাই broker আবার দেয়।

তাহলে “exactly-once” দাবিগুলো কী? সেগুলো সাধারণত নির্দিষ্ট সীমার ভেতরের নিশ্চয়তা।

  • Producer-এর দিকে deduplication: যেমন SQS FIFO queue একটি নির্দিষ্ট সময়ের (লেখার সময়ে ৫ মিনিট) মধ্যে একই deduplication id-এর দ্বিতীয় বার্তা গ্রহণ করে না। ৬ মিনিট পরের retry কিন্তু ধরা পড়ে না। আর consumer-এর দিকের redelivery এতে থামে না।
  • Broker-এর ভেতরের transaction: Kafka-র transaction “পড়া-প্রক্রিয়া-লেখা” কাজকে Kafka-র ভেতরেই একবার করতে পারে। কিন্তু consumer যদি বাইরের database-এ লেখে বা SMS পাঠায়, সেই effect Kafka-র transaction-এর বাইরে (পর্ব ৩৩–৩৪)।

অর্থাৎ delivery কখনো সত্যিই “ঠিক একবার” নয়। যা পাওয়া যায় তা হলো effectively once: বার্তা কয়েকবার এলেও ফল একবার। এর উপায় সবসময় প্রান্তে। বার্তার id একটি unique constraint-সহ টেবিলে, effect-এর সাথে একই transaction-এ, অংশ ৪-এর stepper-এর মতো। অথবা effect নিজেই idempotent, যেমন “পয়েন্ট যোগ করুন”-এর বদলে “অর্ডার ১০৪২-এর পয়েন্ট = ৫০” লেখা। বাইরের সেবায় idempotency key (পর্ব ৮)।

Dedupe টেবিলেরও নকশা লাগে: সেকেন্ডে ১,০০০ বার্তার id ৭ দিন রাখলে প্রায় ৬০ কোটি সারি। কতদিন রাখবেন আর কীভাবে মুছবেন, তা পর্ব ৩৪-এ।

মূল উপলব্ধি: Network-এ “ঠিক একবার পৌঁছানো” প্রমাণযোগ্যভাবে অসম্ভব, কিন্তু “ঠিক একবারের ফল” নকশা করা যায়। সেই নকশা broker-এর বিজ্ঞাপনে থাকে না, থাকে consumer-এর transaction-এ।

প্র ০৪কঠোর ক্রম আর সমান্তরালতা কেন একে অপরের শত্রু? একটি chat বা ব্যাংকিং সেবায় ক্রমের প্রয়োজন কীভাবে ঠিক করবেন?

পুরো queue-তে কঠোর FIFO মানে বার্তা ২-এর কাজ বার্তা ১ শেষ হওয়ার আগে শুরু হতে পারবে না। নইলে ২ আগে শেষ হতে পারে। তাহলে একসাথে একটিই কাজ চলে, আর ক্ষমতা হয় একজন consumer-এর। ৮ জন consumer রাখলেও তা ১ জনের সমান। এটিই মূল দ্বন্দ্ব: ক্রম মানে অপেক্ষা, আর অপেক্ষা মানে সমান্তরালতা নেই।

বাস্তবে প্রায় কখনো সব বার্তার মধ্যে ক্রম দরকার হয় না। দরকার হয় একই সত্তার বার্তার মধ্যে। একই অ্যাকাউন্টের জমা-তোলা, একই chat-এর বার্তা, একই অর্ডারের “তৈরি → পরিশোধিত → পাঠানো”। ভিন্ন অ্যাকাউন্টের মধ্যে ক্রম অর্থহীন। তাই সমাধান হলো ক্রমের পরিধি ছোট করা: message group বা partition key হিসেবে account id, chat id বা order id নেওয়া। ১০ লাখ অ্যাকাউন্ট থাকলে ১০ লাখ স্বাধীন সারি, যা সমান্তরালে চলে।

এর দুটি দাম আছে, যা নকশায় মাপতে হয়।

  • Hot group: একটি বড় merchant-এর অ্যাকাউন্টে সেকেন্ডে ৫০০ লেনদেন হলে সেই group একজন consumer-এর গতির বেশি নিতে পারে না।
  • Head-of-line blocking: group-এর মাথার বার্তা ব্যর্থ হলে পুরো group থামে। তাই poison message-এর সীমা আর DLQ এখানে আরও জরুরি। আর DLQ-তে সরালে ক্রমও ভাঙে, যা ব্যবসায়িকভাবে মেনে নিতে হয়।

Chat সেবায় (পর্ব ৫৫) একটি প্রচলিত পথ হলো সার্ভারে প্রতিটি chat-এ একটি sequence number বসানো। গ্রাহক সেই নম্বর দেখে সাজায়, ফাঁক থাকলে অপেক্ষা করে বা চেয়ে নেয়। ফলে transport-এর ক্রমের ওপর পুরো ভরসা করতে হয় না। ব্যাংকিং-এ অনেক সময় আরও সরল পথ চলে। বার্তা শুধু জানায় “অ্যাকাউন্ট ৪২ বদলেছে”, আর consumer database থেকে সর্বশেষ অবস্থা পড়ে। পুরোনো বার্তা দেরিতে এলেও ক্ষতি নেই।

মূল উপলব্ধি: প্রশ্নটি “ক্রম লাগবে কি না” নয়, “কার মধ্যে ক্রম লাগবে”। ক্রমের পরিধি যত ছোট, সমান্তরালতা তত বেশি। আর যেখানে সম্ভব, consumer-কে ক্রম-নিরপেক্ষ করাই সবচেয়ে শক্ত নকশা।

প্র ০৫Queue কখন উপকারের বদলে ক্ষতি করে? “সব কিছু async করুন” পরামর্শের বিপদ কী?

Queue একটি শক্তিশালী হাতিয়ার, তাই এর অপব্যবহারও সাধারণ। চারটি চেনা ফাঁদ আছে।

১. Queue overload লুকিয়ে রাখে। অংশ ৩-এর N = ২-এর মতো, গড় চাপ ক্ষমতার ওপরে হলে queue কোনো ভিড় শোষণ করে না, শুধু ব্যর্থতা পিছিয়ে দেয়। Dashboard-এ ত্রুটি শূন্য, সবাই খুশি। অথচ সবচেয়ে পুরোনো বার্তার বয়স ঘণ্টায় গড়াচ্ছে, আর ব্যবহারকারীরা ৩ ঘণ্টা পরে OTP পাচ্ছেন। পর্ব ১৮-এর শিক্ষা এখানেও খাটে: queue-র সীমা আর বয়সের alert চাই। মেয়াদ পেরোনো কাজ ফেলে দেওয়াই প্রায়ই ঠিক।

২. ব্যবহারকারী যে উত্তরের অপেক্ষায়। লগইন বা ব্যালান্স দেখার অনুরোধ queue-র পেছনে রাখলে request-reply-এর জন্য দুটি queue, correlation id আর timeout লাগে। সাথে সরাসরি call-এর চেয়ে বেশি latency, আর ব্যর্থতার খবর দেরিতে আসে। Synchronous কাজ synchronous থাকাই ভালো, সাথে timeout আর circuit breaker (পর্ব ৪৮)।

৩. ডিবাগ করা কঠিন হয়। Synchronous call-এ একটি stack trace পুরো গল্প বলে। Queue-র পরে একটি অনুরোধের গল্প পাঁচটি সেবা আর কয়েক মিনিটে ছড়ানো। তাই প্রতিটি বার্তায় একটি correlation id বহন করুন (পর্ব ৫০)।

৪. Queue-কে database বানানো। বার্তা খুঁজে বের করা, মাঝখান থেকে বদলানো, সপ্তাহের পর সপ্তাহ রেখে দেওয়া — queue এর জন্য তৈরি নয়। “কোন অর্ডারের SMS বাকি” জানতে চাইলে সেই অবস্থা database-এ রাখুন, আর queue শুধু কাজের সংকেত দিক।

সাথে আছে broker চালানোর খরচ। ছোট সেবায় database-এর একটি “jobs” টেবিল আর SELECT … FOR UPDATE SKIP LOCKED-এর মতো কৌশল (PostgreSQL আর MySQL ৮-এ আছে) অনেক সময় যথেষ্ট, আর লেখার সাথে একই transaction-এ থাকে।

মূল উপলব্ধি: Queue সময় কেনে, ক্ষমতা নয়; আর দায়িত্ব সরায় না, শুধু পরে পাঠায়। প্রতিটি queue-র জন্য তিনটি প্রশ্ন করুন: কাজটি কি সত্যিই পরে হলে চলবে, কতক্ষণ পরে পর্যন্ত, আর দেরি সেই সীমা ছাড়ালে কে জানবে?

অনুশীলন

কাগজ-কলমে হিসাব করুন, তারপর উত্তর মেলান। তৃতীয় প্রশ্নে আগে অনুমান লিখুন, তারপর simulator-এ সংখ্যা বদলে চালিয়ে যাচাই করুন।

  1. সেলের ভিড়ের হিসাব: একটি কাল্পনিক অনলাইন দোকানে অর্ডার-নিশ্চিতকরণ ইমেইলের queue. স্বাভাবিক দিনে সেকেন্ডে ৫০টি বার্তা; ১০ মিনিটের ফ্ল্যাশ সেলে সেকেন্ডে ৪০০টি। ২৫ জন consumer, প্রত্যেকে সেকেন্ডে ১০টি। (ক) সেলের শেষে queue-তে কতগুলো জমা? (খ) সেলের পরে খালি হতে কত সময়? (গ) সেলের শেষ মুহূর্তে আসা বার্তার অপেক্ষা কত (FIFO)? (ঘ) কোনো বার্তা যেন ৬০ সেকেন্ডের বেশি অপেক্ষা না করে, তার জন্য সেলের সময় সবচেয়ে কম কতজন consumer লাগবে? (ঙ) ৮০% ব্যস্ততার লক্ষ্যে সেলের জন্য কতজন?
    • (ক) ক্ষমতা ২৫ × ১০ = ২৫০। জমে (৪০০ − ২৫০) = সেকেন্ডে ১৫০; ৬০০ সেকেন্ডে ৯০,০০০টি।
    • (খ) সেলের পরে আসে ৫০, সরে (২৫০ − ৫০) = সেকেন্ডে ২০০; ৯০,০০০ ÷ ২০০ = ৪৫০ সেকেন্ড (সাড়ে ৭ মিনিট)।
    • (গ) সামনে ৯০,০০০টি, ক্ষমতা ২৫০: ৯০,০০০ ÷ ২৫০ = ৩৬০ সেকেন্ড (৬ মিনিট)। এটিই সর্বোচ্চ অপেক্ষা, কারণ জমা এর পরে কমতে থাকে।
    • (ঘ) ক্ষমতা C হলে জমা (৪০০ − C) × ৬০০, অপেক্ষা তার ÷ C. শর্ত (৪০০ − C) × ৬০০ ≤ ৬০ × C, অর্থাৎ ২,৪০,০০০ ≤ ৬৬০ C, C ≥ ৩৬৩.৬। প্রতি consumer ১০ বলে ৩৭ জন। যাচাই: ৩৭ জনে জমা ৩০ × ৬০০ = ১৮,০০০, অপেক্ষা ১৮,০০০ ÷ ৩৭০ ≈ ৪৮.৬ সেকেন্ড ✓; ৩৬ জনে ২৪,০০০ ÷ ৩৬০ ≈ ৬৬.৭ সেকেন্ড ✗।
    • (ঙ) ৪০০ ÷ (০.৮ × ১০) = ৫০ জন — কোনো জমাই হবে না। (ঘ)-র ৩৭ জন সস্তা, কিন্তু ভিড়ে এক মিনিট দেরি মেনে নেয়। কোনটি ঠিক, তা দেরির সহনশীলতা আর খরচের প্রশ্ন; সেলের সময় জানা থাকলে scheduled scaling দিয়ে consumer আগেই চালু রাখুন।
  2. নকশার সমালোচনা করুন: একটি কাল্পনিক mobile banking সেবার লেনদেন-নোটিফিকেশন worker এভাবে বানানো: (১) auto ack; (২) ব্যর্থ হলে সঙ্গে সঙ্গে requeue, কোনো সীমা নেই; (৩) visibility timeout ১০ সেকেন্ড, অথচ SMS gateway-র p99 প্রায় ২৫ সেকেন্ড; (৪) সব গ্রাহকের সব বার্তা একটি FIFO group-এ, “যাতে ক্রম ঠিক থাকে”; (৫) পর্যবেক্ষণ শুধু consumer-এর CPU. প্রতিটির সমস্যা আর প্রতিকার লিখুন।
    • (১) Auto ack: worker crash বা deploy-এ হাতে থাকা বার্তা চিরতরে হারায় — গ্রাহক লেনদেনের SMS পান না। প্রতিকার: manual ack, কাজ সফল হওয়ার পরে।
    • (২) সীমাহীন তাৎক্ষণিক requeue: gateway বিকল হলে সেকেন্ডে হাজারবার ব্যর্থ চেষ্টা, gateway-র ওপর retry storm; poison message চিরকাল ঘোরে। প্রতিকার: exponential backoff আর jitter, max receive count (যেমন ৫), DLQ আর তার alert; স্থায়ী ত্রুটি (ভুল নম্বর) সরাসরি DLQ-তে বা বাতিল।
    • (৩) VT ১০ সেকেন্ড বনাম p99 ২৫ সেকেন্ড: ধীর SMS-এর কাজ চলাকালেই বার্তা অন্য worker পায় — একই SMS দুবার; সুস্থ বার্তাও DLQ-তে যেতে পারে। প্রতিকার: VT স্বাভাবিক কাজের সবচেয়ে ধীর অংশের ওপরে (যেমন ৬০ সেকেন্ড) বা heartbeat; gateway call-এ নিজস্ব timeout (যেমন ২০ সেকেন্ড), যা VT-র চেয়ে ছোট; আর gateway যদি idempotency key বা client reference নেয়, বার্তার id সেখানে পাঠান।
    • (৪) একটি FIFO group: পুরো সেবার ক্ষমতা একজন worker-এর গতি; একটি poison message সব গ্রাহকের নোটিফিকেশন আটকায়। আর ভিন্ন গ্রাহকের মধ্যে ক্রম অর্থহীন। প্রতিকার: group = account id; নোটিফিকেশনে প্রায়ই ক্রমের চেয়ে লেনদেনের সময় আর id দেখানোই যথেষ্ট।
    • (৫) শুধু CPU: gateway-র অপেক্ষায় CPU কম থাকে, queue জমলেও alert বাজে না। প্রতিকার: queue depth, সবচেয়ে পুরোনো বার্তার বয়স (যেমন ২ মিনিটে alert), redelivery-র হার, DLQ depth আর শেষ-থেকে-শেষ latency; autoscaling depth আর আসার হার দেখে।
  3. আউটপুট অনুমান করুন: অংশ ৫-এর visibility_dlq.js-এ MAX_RECEIVE = 3 বদলে MAX_RECEIVE = 5 করলে VT = ৫ সেকেন্ডের সারিতে DLQ, DLQ healthy, deliveries আর dup effects কোন দিকে সরবে? DLQ healthy মোটামুটি কত হবে, সূত্র দিয়ে আন্দাজ করুন। তারপর চালিয়ে মেলান।

    আন্দাজ: একটি গ্রহণে ack না হওয়ার সম্ভাবনা q ≈ ০.২২১, তাই সুস্থ বার্তার DLQ-তে যাওয়ার সম্ভাবনা q³ ≈ ১.০৮% থেকে নামবে q⁵ ≈ ০.০৫৩%-এ — ৯,৯৭০টিতে প্রায় ৫টি। Poison বার্তা তবুও DLQ-তে যাবে, কিন্তু প্রতিটি এখন ৫ বার চেষ্টা খাবে, তাই deliveries বাড়বে। ধীর বার্তা বেশিবার চেষ্টা পায় বলে duplicate-ও একটু বাড়বে।

        VT  deliveries  effects  dup effects   DLQ  DLQ healthy
       5 s       12866    12477         2507    31            1
      15 s       10574    10209          242    33            0
      60 s       10519    10148          181    33            0
    • DLQ healthy: ১০৪ থেকে ১ (আন্দাজ প্রায় ৫; এলোমেলো সংখ্যার ধারা বদলায় বলে ঠিক মেলে না)। DLQ এখন প্রায় শুধু poison: ৩১।
    • Deliveries: ১২,৬৯৩ থেকে ১২,৮৬৬; dup effects ২,৩৯২ থেকে ২,৫০৭।
    • শিক্ষা: সীমা বাড়ালে সুস্থ বার্তা কম হারায়, কিন্তু poison বেশি সময় খায় আর duplicate বাড়ে। আসল রোগ ছোট VT — সীমা বাড়ানো শুধু উপসর্গ ঢাকে। VT ১৫ সেকেন্ডে সীমা ৩-এই সুস্থ বার্তা DLQ-তে শূন্য।
  4. Visibility timeout বাছাই করুন: একটি ছবি-ছোট-করার worker-এর কাজের সময় মোটামুটি exponential, গড় ৩ সেকেন্ড। (ক) কত সেকেন্ডের VT-তে ধীরতার কারণে redelivery প্রতি ১,০০০ গ্রহণে ১টির কম হবে? (খ) VT ৩০ সেকেন্ড হলে ধীরতার কারণে redelivery-র সম্ভাবনা কত? (গ) কিছু ছবি বিশাল, তাদের কাজ ৫ মিনিট পর্যন্ত লাগে — VT ৫ মিনিট করে দেওয়ার দাম কী, আর ভালো বিকল্প কী?
    • (ক) Exponential-এ P(কাজ > VT) = e−VT/3, তাই শর্ত e−VT/3 < ০.০০১ → VT > ৩ × ln ১০০০ ≈ ৩ × ৬.৯১ = ২০.৭ সেকেন্ড। তাই প্রায় ২১ সেকেন্ড বা বেশি; ২১ সেকেন্ডে e−7 ≈ ০.০৯১ শতাংশ।
    • (খ) e−10 ≈ ০.০০০০৪৫, অর্থাৎ প্রায় ১০ লাখে ৪৫টি। বাস্তবে বণ্টন exponential-এর চেয়ে লম্বা লেজের হতে পারে; তাই মাপা p99.9 দেখে ঠিক করুন।
    • (গ) সব বার্তার VT ৫ মিনিট হলে একটি worker মারা গেলে তার হাতের বার্তা ৫ মিনিট আটকে থাকে, যদিও বেশিরভাগ কাজ ৩ সেকেন্ডের। ভালো বিকল্প: ছোট VT (যেমন ৩০ সেকেন্ড) আর কাজ চলাকালে heartbeat দিয়ে সময় বাড়ানো (SQS-ধাঁচে visibility বদলানো); worker মরলে heartbeat থামে, বার্তা দ্রুত ফেরে। আরেকটি পথ: বড় ছবির জন্য আলাদা queue আর আলাদা VT রাখা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমManual ack আর idempotent consumer-সহ একটি বার্তার জীবনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📤 Producer বার্তা পাঠায়
  2. 💾 Broker বার্তা disk-এ লেখে আর producer-কে confirm দেয়
  3. 📥 Consumer বার্তা নেয়; broker সেটি অন্যদের কাছ থেকে লুকায়
  4. 🧾 Consumer একই transaction-এ effect আর বার্তার id লেখে
  5. ✅ Consumer ack পাঠায়
  6. 🗑️ Broker বার্তাটি মুছে ফেলে

একটি queue-তে ১০ মিনিট ধরে সেকেন্ডে ৩০০টি বার্তা আসছে। ২০ জন consumer, প্রত্যেকে সেকেন্ডে ১২টি বার্তা শেষ করে। শুরুতে queue খালি হলে ১০ মিনিট পরে মোটামুটি কতগুলো বার্তা জমা থাকবে?

  • প্রায় ১,৮০,০০০
  • প্রায় ৩৬,০০০
  • প্রায় ১,৪৪,০০০
  • প্রায় শূন্য, কারণ consumer-রা ব্যস্ত

একটি SQS-ধাঁচের queue-র visibility timeout ১০ সেকেন্ড, কিন্তু অনেক কাজ ২০ সেকেন্ড নেয়। কোনো consumer crash না করলেও কী ঘটবে?

  • ১০ সেকেন্ড পরে বার্তা মুছে যাবে, কাজটি হারাবে
  • কিছুই না, consumer তো কাজ করছে
  • লম্বা কাজগুলো একাধিক consumer করবে — duplicate effect, এমনকি সুস্থ বার্তা DLQ-তে
  • Producer নতুন বার্তা পাঠানো বন্ধ করবে

একটি consumer auto ack ব্যবহার করে (বার্তা পাঠানোর মুহূর্তেই broker সেটি মুছে ফেলে)। কাজের মাঝপথে consumer crash করলে বার্তাটির কী হবে?

  • বার্তাটি হারাবে; কেউ আর কাজটি করবে না
  • বার্তাটি আবার queue-তে ফিরবে
  • বার্তাটি DLQ-তে যাবে
  • Producer বার্তাটি আবার পাঠাবে

একটি বার্তার JSON ভাঙা, তাই প্রতিবার consumer ত্রুটি দেয়। Queue-তে max receive count বা dead letter queue নেই, আর ব্যর্থ হলে বার্তা সঙ্গে সঙ্গে requeue হয়। কী ঘটবে?

  • Broker নিজে থেকে ভাঙা বার্তা মুছে দেবে
  • কয়েকবার পরে বার্তাটি সফল হবে
  • বার্তাটি স্বয়ংক্রিয়ভাবে DLQ-তে যাবে
  • বার্তাটি অনন্তকাল ঘুরবে, consumer-এর সময় নষ্ট করবে, আর FIFO হলে পেছনের বার্তা আটকে রাখবে

একটি best-effort ক্রমের (standard) queue-তে ৫ জন competing consumer. একই অ্যাকাউন্টের “জমা ৫০০” আর “তোলা ৩০০” প্রক্রিয়া ক্রমে হতে হবে। সবচেয়ে উপযুক্ত সমাধান কোনটি?

  • Consumer কমিয়ে ১ জন করা
  • FIFO queue-তে account id-কে message group করা
  • Prefetch বাড়ানো
  • Visibility timeout বাড়ানো

পেছনের worker-দের autoscaling শুধু queue depth দেখে হয় (চাই = ⌈depth ÷ ১০০⌉, সর্বনিম্ন ২ জন)। স্বাভাবিক চাপ সেকেন্ডে ৩০, প্রতি worker সেকেন্ডে ১০। স্বাভাবিক সময়ে কী ঘটার সম্ভাবনা বেশি?

  • ২ জন worker-এ স্থির থাকবে, কোনো জমা নেই
  • সবসময় সর্বোচ্চ সংখ্যায় থাকবে
  • বারবার বাড়া-কমা (flapping), কারণ depth শূন্য মানে “কম লাগবে” নয়
  • CPU বাড়লে worker বাড়বে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Queue producer আর consumer-কে সময়, গতি আর ব্যর্থতায় আলাদা করে, আর ভিড় শোষণ করে। কিন্তু ক্ষমতা বাড়ায় না: জমা (λ − N·μ) × T, খালি হয় (N·μ − λ′) হারে।
  2. Point-to-point queue-তে প্রতিটি বার্তা একজন consumer পায়; competing consumers যোগ করে scale. Consumer-এর concurrency Little-এর সূত্রে: L = λW.
  3. Ack দিন কাজ commit-এর পরে। Ack না এলে বার্তা ফেরে (visibility timeout বা সংযোগ কাটা) — তাই স্বাভাবিক নিশ্চয়তা at-least-once, আর consumer-কে idempotent হতে হবে: বার্তার id আর effect একই transaction-এ।
  4. Retry-তে backoff আর jitter, একটি max receive count, আর সীমা পেরোলে DLQ — alert আর redrive-সহ। Visibility timeout স্বাভাবিক কাজের সবচেয়ে ধীর অংশের ওপরে, লম্বা কাজে heartbeat.
  5. ক্রম দরকার শুধু একই সত্তার মধ্যে — message group বা version. Prefetch, priority, delay, TTL আর durability সচেতনভাবে ঠিক করুন; আর queue-র নাড়ি দেখুন depth আর সবচেয়ে পুরোনো বার্তার বয়সে, autoscaling-এ আসার হারসহ।

পরের পর্বে: এখানে প্রতিটি বার্তা একজনই পায়। কিন্তু “অর্ডার হয়েছে” খবরটি যদি পাঁচটি সেবার দরকার হয়? পর্ব ৩২-এ Pub/sub আর event-driven architecture — fan-out, topic আর subscription, আর সেবাগুলোর সমন্বয়ে choreography বনাম orchestration.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান