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

Resilience pattern — timeout, retry, backoff, circuit breaker আর bulkhead

Resilience Patterns — Timeouts, Retries, Backoff, Circuit Breakers & Bulkheads

Taught in Bangla, this lesson shows how failures cascade through call chains and how to contain them: choosing timeouts and propagating deadlines, safe retries with retry budgets, exponential backoff with full, equal and decorrelated jitter, hedged requests, circuit breaker closed, open and half-open states, bulkheads and cells, fallbacks and graceful degradation, health-check traps and chaos engineering, with hands-on simulators.

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

  • Call-এর শিকলে ব্যর্থতা কীভাবে ছড়ায় — resource exhaustion আর ধীর বনাম মৃত dependency
  • Timeout আর deadline বাছাই — percentile, thread pool-এর সীমা আর deadline propagation
  • Retry-র নিয়ম — retry-যোগ্য ত্রুটি, idempotency, স্তরে স্তরে গুণফল, retry budget আর তিন রকম jitter
  • Hedged request, circuit breaker-এর তিন অবস্থা, bulkhead, cell আর fallback-এর নকশা
  • Health check-এর ফাঁদ, chaos engineering আর game day, আর library বনাম service mesh-এ resilience

১ · ব্যর্থতা কীভাবে ছড়ায় — cascading failure আর “ধীর, কিন্তু জীবিত” dependency

একটি রেস্তোরাঁয় ১০ জন ওয়েটার। আজ মিষ্টির কাউন্টারের কারিগর অসুস্থ, প্রতিটি দইয়ে ১০ মিনিট লাগছে, আর যে ওয়েটারই দইয়ের অর্ডার নেন, কাউন্টারে দাঁড়িয়ে থাকেন। আধা ঘণ্টায় ১০ জনই দইয়ের লাইনে; এক কাপ চায়ের অর্ডার নেওয়ারও কেউ নেই। দই না থাকলে ক্ষতি সামান্য ছিল; দই ধীর বলেই পুরো রেস্তোরাঁ থামল।

সফটওয়্যারে ওয়েটারের জায়গায় thread, connection বা memory — সীমিত সম্পদ। অন্য সার্ভিসকে (dependency) ডেকে অপেক্ষার সময় একটি thread আটকে থাকে। কতগুলো, তা বলে পর্ব ৪-এর Little-এর সূত্র:

ব্যস্ত thread L = call-এর হার λ × প্রতিটি call-এর সময় W

সেকেন্ডে ৪০০টি call, প্রতিটি ৫০ ms হলে ব্যস্ত মাত্র ২০টি thread. একই call ৩ সেকেন্ড নিলে দরকার ১,২০০টি। Pool-এ ২০০টি থাকলে কয়েক মুহূর্তে সব ভরে যায় — একে বলে resource exhaustion. তখন যে অনুরোধের ওই dependency লাগেই না, সেটিও thread পায় না।

মূল ধারণা — ধীর কেন মৃতের চেয়ে খারাপ

মৃত সার্ভিসের যন্ত্র চালু থাকলে OS সাধারণত প্রায় ১ ms-এ “connection refused” দেয়; caller দ্রুত ত্রুটি পায়, thread মুক্ত হয়। ধীর সার্ভিস কোনো সংকেত দেয় না, আর অপেক্ষা Little-এর সূত্রে thread খায়। বাইরে থেকে দুটি আলাদা করা যায় না (পর্ব ৩৮), তাই নকশার কাজ ধীরতাকে দ্রুত ব্যর্থতায় বদলানো।

নিচের stepper-এ একটি কাল্পনিক অনলাইন দোকানের শিকল দেখুন। Gateway সেকেন্ডে ২,০০০টি অনুরোধ নেয়; তার ১,০০০টি যায় অর্ডার সার্ভিস X-এ। X-এর ২০০টি thread; প্রতিটি অনুরোধে নিজের কাজ ২০ ms, আর ৪০০টি অনুরোধ (cart-এর পাতা) অফার সার্ভিস D-কে ডাকে। বাকি ৬০০টি (অর্ডারের অবস্থা দেখা) D-কে ডাকেই না।

ব্যবহারকারীসেকেন্ডে 2,000
Gatewaypool 500
অর্ডার সার্ভিস Xpool 200
অফার সার্ভিস Dসেকেন্ডে 400 call
X: সেকেন্ডে 1,000 অনুরোধ, নিজের কাজ 20 ms; এর 400টি D-কে ডাকে
D-র উত্তরের সময়
X-এ দরকারি thread (λW)
X-এর pool (200)
অর্ডারের অবস্থা (D লাগে না)
Gateway-এর pool (500)
ব্যবহারকারী দেখেন
  1. স্বাভাবিক দিন। X-এ ব্যস্ত thread = ১,০০০ × ০.০২ + ৪০০ × ০.০৫ = ২০ + ২০ = ৪০টি, pool-এর ২০০-এর মধ্যে আরামে। Gateway-এ X-এর জন্য প্রায় ৪০ আর অন্য সার্ভিসের ১,০০০টি অনুরোধের জন্য প্রায় ৩০ (তাদের উত্তর গড়ে ৩০ ms ধরে), মোট প্রায় ৭০টি thread.
  2. D ধীর হলো, মরেনি। প্রতিটি call এখন ৩ সেকেন্ড। দরকার ২০ + ৪০০ × ৩ = ১,২২০টি thread. ফাঁকা ১৬০টি thread সেকেন্ডে ৪০০ হারে আটকায়, তাই প্রায় ০.৪ সেকেন্ডে pool ভরা।
  3. সুস্থ কাজও থামল। অর্ডারের অবস্থা দেখার ৬০০টি অনুরোধের D লাগে না, কিন্তু thread-ও নেই — queue-তে দাঁড়িয়ে timeout. X-এর health check-ও thread পায় না, তাই load balancer X-কে অসুস্থ ভাবে।
  4. শিকল ওপরে উঠল। Gateway X-এর উত্তরের জন্য ১০ সেকেন্ড পর্যন্ত অপেক্ষা করে: দরকার ১,০০০ × ১০ = ১০,০০০টি thread, আছে ৫০০ — প্রায় আধা সেকেন্ডে ভরা। এবার অন্য সার্ভিসের অনুরোধও gateway পেরোতে পারে না। একটি অফার-সার্ভিসের ধীরতায় পুরো সাইট বন্ধ — cascading failure.
  5. তুলনা: D যদি মরেই যেত। প্রতিটি call ১ ms-এ “connection refused”: ৪০০ × ০.০০১ = ০.৪টি thread. শুধু অফারের অংশ খালি, বাকি সব চলে।
  6. প্রতিরোধ। D-র call-এ ছোট timeout, D-র জন্য আলাদা ৩০টি thread-এর bulkhead, আর circuit breaker: ব্যর্থতার হার সীমা ছাড়ালে D-কে ডাকাই বন্ধ, সাথে সাথে fallback — cache-এর জনপ্রিয় তালিকা। X-এর thread খরচ আবার প্রায় ২০।

Cascading failure-এর আরও দুটি চেনা পথ: একটি সার্ভার পড়লে তার ভার বাকিদের ঘাড়ে গিয়ে তাদেরও ফেলা (পর্ব ৫, ১৮), আর স্তরে স্তরে retry-র ঢেউ (অংশ ৩)। এই পর্বের সব pattern-এর লক্ষ্য একটাই — ঢাকার মোড়ে বিকল বাসের জটকে সেই মোড়েই আটকে রাখা, পুরো শহরে ছড়াতে না দেওয়া।

২ · Timeout আর deadline — কতক্ষণ অপেক্ষা, আর পুরো শিকলের সময়-বাজেট

প্রথম নিয়ম সবচেয়ে সরল, তবু সবচেয়ে বেশি ভাঙা হয়: network-এর ওপারে প্রতিটি call-এর একটি timeout চাই। অনেক HTTP client আর database driver-এর ডিফল্ট timeout খুব বড় বা অসীম (লাইব্রেরিভেদে আলাদা)।

দুই রকম timeout, এক রকম deadline

  • Connect timeout: সংযোগ স্থাপনের সীমা; একই ডেটা সেন্টারে এটি ছোট রাখা যায়।
  • Request timeout: উত্তরের সীমা; retry থাকলে প্রতিটি চেষ্টার আলাদা — per-try timeout.
  • Deadline: “কতক্ষণ” নয়, “কখন পর্যন্ত”। অনুরোধ এলে একবার ঠিক হয়, তারপর প্রতিটি নিচের call জানে কত সময় বাকি — deadline propagation.

Timeout কত হবে — তিনটি সীমা

Timeout বাছাইয়ের তিন শর্ত
  1. নিচের সীমা — percentile থেকে: dependency-র স্বাভাবিক দিনের p99 বা p99.9-এর (পর্ব ৪) একটু ওপরে। এর নিচে দিলে স্বাভাবিক ধীর উত্তরও ব্যর্থ ধরা হয়, আর retry বাড়ে। পর্ব ৩৮-এর simulator দেখিয়েছিল, খুব ছোট timeout-এর বেশিরভাগই আসলে “ধীর, কিন্তু সফল”।
  2. ওপরের সীমা — pool থেকে: dependency পুরো ঝুলে গেলে ব্যস্ত thread = λ × timeout. তাই timeout ≤ এই dependency-র ভাগের thread ÷ λ। ৩০টি thread আর সেকেন্ডে ৪০০ call হলে timeout ৭৫ ms-এর বেশি হলে ঝুলে যাওয়া dependency পুরো ভাগ দখল করবে।
  3. বাজেটের সীমা — deadline থেকে: timeout কখনো বাকি সময়ের বেশি নয়: কার্যকর timeout = min(নিজের timeout, deadline − এখন − ছোট একটি margin).

শর্ত ১ আর ২ না মিললে timeout একা সমাধান নয়: ভাগ বড় করুন, অথবা circuit breaker দিয়ে ঝুলে থাকার সময় ছোট করুন।

আরেকটি সাধারণ ভুল উল্টো timeout: gateway ২ সেকেন্ডে হাল ছাড়ে, অথচ নিচের সার্ভিস ৫ আর database ১০ সেকেন্ড অপেক্ষা করে — caller চলে যাওয়ার পরেও কাজ চলে। নিয়ম: শিকলে যত নিচে, timeout তত ছোট।

নিচের stepper-এ একটি পেমেন্টের শিকলে ১,০০০ ms-এর deadline কীভাবে ভাগ হয়, দেখুন। Payment সার্ভিস ব্যাংকের gateway-কে ডাকে, যা আজ প্রায় ২ সেকেন্ড নিচ্ছে।

  1. Apptimeout 1 sব্যবহারকারী
  2. Gatewaydeadline ঠিক করেauth 20 ms
  3. OrderDB 100 msবাকি দেখে ডাকে
  4. Paymentper-try 400 msব্যাংক আজ ধীর
Deadline = শুরু + 1,000 ms; প্রতি hop-এ network প্রায় 5 ms
সময় t
বাকি সময় (deadline − t)
কে কাজ করছে
সিদ্ধান্ত
  1. Deadline জন্মায়। App ১ সেকেন্ড অপেক্ষা করবে। Gateway অনুরোধ পেয়ে একবারই লিখে রাখে: deadline = এখন + ১,০০০ ms.
  2. প্রথম hop. Auth-এ ২০ ms গেল। Gateway Order-কে ডাকে বাকি ৯৮০ ms জানিয়ে — gRPC-তে এটি grpc-timeout header হিসেবে যায় (পর্ব ৯), HTTP-তে সাধারণত নিজেদের একটি header-এ।
  3. খরচের আগে হিসাব। Order ২৫ ms-এ অনুরোধ পেল, database-এ ১০০ ms খরচ করল; t = ১২৫, বাকি ৮৭৫। Payment-এর স্বাভাবিক p99 ৩০০ ms, বাকি সময়ে আঁটে, তাই ডাকে। বাকি যদি ২০০ ms হতো, Order না ডেকেই সাথে সাথে ত্রুটি দিত (fail fast).
  4. Per-try timeout আর deadline একসাথে। Payment ১৩০ ms-এ অনুরোধ পেয়ে ব্যাংককে ডাকল, প্রতিটি চেষ্টার সীমা ৪০০ ms. প্রথম চেষ্টা ৫৩০ ms-এ timeout. বাকি ৪৭০, যা এক চেষ্টার চেয়ে বেশি; তাই একই idempotency key-সহ আরেকবার — যাতে প্রথম চেষ্টা আসলে কাজ করে থাকলেও টাকা দুবার না কাটে।
  5. সময় ফুরাল, সবাই থামল। দ্বিতীয় চেষ্টাও ৯৩০ ms-এ timeout. বাকি ৭০ ms-এ ব্যাংকের একটি চেষ্টাও আঁটে না, তাই আর retry নেই — DEADLINE_EXCEEDED ওপরে যায়, আর প্রায় ৯৪০ ms-এ ব্যবহারকারী স্পষ্ট বার্তা পান: “পেমেন্ট নিশ্চিত হয়নি, অবস্থা দেখে নিন”। পরে কেউ বাতিল করলে সেই cancellation নিচেও পৌঁছায়।
  6. Propagation না থাকলে। Payment-এর নিজের timeout ৫ সেকেন্ড, ৩ বার চেষ্টা: ব্যবহারকারী চলে যাওয়ার পরেও ১৫ সেকেন্ড পর্যন্ত ব্যাংককে ডাকতে থাকে। এদিকে app ১ সেকেন্ডে retry করে দ্বিতীয় একটি শিকল শুরু করেছে। নষ্ট কাজ দ্বিগুণ, আর idempotency key না থাকলে দুবার টাকা কাটার ঝুঁকি।

gRPC-তে deadline প্রায় আপনাআপনি নিচে যায় — যেমন Go-তে একই context পরের call-এ পাঠালে (খুঁটিনাটি ভাষাভেদে আলাদা)। HTTP-তে সর্বজনীন মান নেই, তাই দলকে একটি header আর নিয়ম ঠিক করে নিতে হয়।

Timeout মানে “ব্যর্থ” নয়, মানে “জানি না”। উত্তর আসেনি, কিন্তু কাজটি ওপারে হয়ে থাকতে পারে (পর্ব ৩৮)। তাই timeout-এর পরে কী করবেন, তা কাজের ধরন ঠিক করে: পড়া হলে আবার চেষ্টা নিরাপদ; টাকা পাঠানো হলে idempotency key ছাড়া নয় (পর্ব ৮, ৩৪)।

৩ · Retry — কোন ত্রুটিতে, কতবার, আর স্তরে স্তরে গুণফল

ব্যাংকের কাউন্টারে টোকেন ডাকার সময় আপনি পানি খেতে গিয়েছিলেন। ফিরে এসে আবার টোকেন নেওয়া যুক্তিসংগত — সমস্যাটি ক্ষণিকের। কিন্তু কর্মকর্তা যদি বলেন “এই হিসাবে টাকা নেই”, দশবার লাইনে দাঁড়ালেও উত্তর বদলাবে না। Retry-র প্রথম প্রশ্ন তাই: ত্রুটিটি কি নিজে থেকে সেরে যেতে পারে?

ত্রুটিRetry?কারণ ও শর্ত
Connection refused, অনুরোধ পাঠানোর আগেই সংযোগ কাটাহ্যাঁঅনুরোধ সার্ভারে পৌঁছায়নি, তাই প্রায় সবসময় নিরাপদ; অন্য replica-তে পাঠানো ভালো
503, 429 (সাথে Retry-After)হ্যাঁ, অপেক্ষার পরেসার্ভার নিজেই বলছে “পরে আসুন” — Retry-After মানতে হবে (পর্ব ১৮, ৪৯)
Timeout, 502, 504, উত্তরের মাঝে সংযোগ কাটাশুধু idempotent হলেকাজটি ওপারে হয়ে থাকতে পারে; টাকা বা অর্ডারে idempotency key লাগবে
500সীমিতক্ষণিকের হতে পারে, আবার একটি bug-ও হতে পারে যা প্রতিবার ঘটবে
400, 401, 403, 404, 409, 422নাঅনুরোধেই সমস্যা — non-retryable error; একই অনুরোধ একই উত্তর পাবে
gRPC: UNAVAILABLEহ্যাঁসাধারণ transient error; DEADLINE_EXCEEDED বা RESOURCE_EXHAUSTED-এ সাবধানে, INVALID_ARGUMENT-এ না

দ্বিতীয় শর্ত idempotency: একই অনুরোধ দুবার পৌঁছালেও ফল একবারের মতো। GET, PUT আর DELETE সাধারণত স্বভাবেই এমন; POST-এ client একটি idempotency key পাঠায় (পর্ব ৮), বার্তায় dedup store (পর্ব ৩৪)। এই শর্ত ছাড়া retry একটি নতুন bug.

স্তরে স্তরে retry — গুণফলের অঙ্ক

অ্যাপ → API → সার্ভিস → database. প্রতিটি caller নিচেরটিকে সর্বোচ্চ ৩ বার ডাকে (১টি চেষ্টা + ২টি retry). Database পুরো বিকল হলে সার্ভিস তাকে ৩ বার ডাকে; API সার্ভিসকে ৩ বার, তাই database-এ ৩ × ৩ = ৯; অ্যাপ API-কে ৩ বার, তাই ৩ × ৩ × ৩ = ২৭টি database-চেষ্টা, একটিমাত্র ব্যবহারকারী-অনুরোধে। সাধারণভাবে n স্তরে প্রতিটিতে k বার চেষ্টা মানে kn. পর্ব ১৮-এ প্রতিটিতে ৪ বার চেষ্টায় একই হিসাব দাঁড়িয়েছিল ৬৪-এ। এর নাম retry amplification, আর এটি আসে ঠিক তখন, যখন সবচেয়ে নিচের অংশটি সবচেয়ে দুর্বল।

প্রতিকার দুটি। এক, একটি স্তরে retry — সাধারণত ব্যর্থতার সবচেয়ে কাছের স্তরে; বাকিরা দ্রুত ত্রুটি ওপরে পাঠায়। দুই, retry budget — প্রতিটি client বা স্তরে retry প্রথম-চেষ্টার একটি অনুপাতের (ধরা যাক ১০%) মধ্যে। Google-এর SRE বইয়ে (২০১৬) প্রতি client-এ প্রায় ১০% retry ratio-র বর্ণনা আছে; gRPC-র retry throttling আর কিছু cloud SDK-র retry quota একই ভাবনার token bucket (খুঁটিনাটি implementation-ভেদে আলাদা)। নিচে দুটি নীতি পাশাপাশি:

// Chain: app -> API -> service -> database. Each of the 3 callers makes up to 3 attempts
// (1 try + 2 retries) on the layer below. Only the database fails: each attempt with prob. p.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const LAYERS = 3, TRIES = 3, USERS = 1000;
function run(p, budget) {
  const r = mulberry32(48);
  const tokens = Array(LAYERS).fill(10);   // budget: per layer, retries <= 10% of its first tries
  let dbAttempts = 0;
  function call(layer) {                   // returns true if the call to `layer` succeeded
    if (layer === LAYERS) { dbAttempts++; return r() >= p; }    // the database itself
    for (let attempt = 1; attempt <= TRIES; attempt++) {
      if (attempt === 1) tokens[layer] = Math.min(10, tokens[layer] + 0.1);
      else if (budget) { if (tokens[layer] < 1) return false; tokens[layer] -= 1; }
      if (call(layer + 1)) return true;
    }
    return false;
  }
  let ok = 0;
  for (let u = 0; u < USERS; u++) if (call(0)) ok++;
  return [dbAttempts, (dbAttempts / USERS).toFixed(2) + "x", (100 * ok / USERS).toFixed(1) + "%"];
}
console.log("p(db fails)   no budget: db calls  per user  users ok |  10% budget: db calls  per user  users ok");
for (const p of [0.01, 0.1, 0.5, 0.9, 1.0]) {
  const a = run(p, false), b = run(p, true);
  console.log(String(p).padEnd(12) + String(a[0]).padStart(20) + a[1].padStart(10) + a[2].padStart(10) +
    " |" + String(b[0]).padStart(21) + b[1].padStart(10) + b[2].padStart(10));
}

আউটপুট:

p(db fails)   no budget: db calls  per user  users ok |  10% budget: db calls  per user  users ok
0.01                        1013     1.01x    100.0% |                 1013     1.01x    100.0%
0.1                         1115     1.11x    100.0% |                 1115     1.11x    100.0%
0.5                         2022     2.02x    100.0% |                 1360     1.36x     66.8%
0.9                         9410     9.41x     94.5% |                 1361     1.36x     12.5%
1                          27000    27.00x      0.0% |                 1361     1.36x      0.0%

বাজেট ছাড়া: database পুরো বিকল (p = ১) হলে ১,০০০ ব্যবহারকারীর জন্য ২৭,০০০ চেষ্টা — ঠিক ২৭ গুণ, কেউ সফল নন। p = ০.৯-এ ৯.৪১ গুণ চাপে ৯৪.৫% সফল — স্বাধীন ব্যর্থতায় retry সত্যিই কাজের। কিন্তু বাস্তবে ৯০% ব্যর্থতা সাধারণত overload, আর ৯.৪ গুণ চাপে p তখন ১-এর দিকেই যাবে।

১০% বাজেটে: প্রতিটি স্তর সর্বোচ্চ ১.১ গুণ পাঠায়, তাই তিন স্তরে প্রায় ১.১৩ = ১.৩৩১; শুরুর জমা ১০টি token-এর জন্য দেখা গেল ১.৩৬। দাম: p = ০.৫-এ সফল ১০০% থেকে নেমে ৬৬.৮%. Budget ব্যাপক ব্যর্থতায় চাপ আটকায়, আর সেই কারণেই সেখানে কম ব্যবহারকারীকে বাঁচায় — এটি সচেতন trade-off. p = ০.০১ আর ০.১-এ দুই নীতির সংখ্যা হুবহু এক: স্বাভাবিক দিনে budget প্রায় কিছুই কাটে না।

৪ · Exponential backoff আর jitter — full, equal আর decorrelated

এবার কখন। প্রতিবার অপেক্ষা বাড়ানো হয়, সাধারণত দ্বিগুণ করে: base × 2k, সাথে একটি ঊর্ধ্বসীমা cap — এটিই exponential backoff. কিন্তু শুধু এটুকুতে একটি ফাঁদ থাকে।

ঈদের আগে বাস কাউন্টার সকাল ৯টায় খুলল না। একশো জন যাত্রী চায়ের দোকানে গিয়ে ঠিক করলেন “১০ মিনিট পরে আবার দেখব”। ১০ মিনিট পরে একশো জন একসাথে জানালায়, কাউন্টার সামলায় মাত্র দুজন। আবার সবাই একসাথে ২০ মিনিটের বিরতি, আবার একসাথে ফেরা। কেউ ৩ মিনিটে, কেউ ১৪ মিনিটে এলে কাউন্টার সারাক্ষণ ধীরে ধীরে কাজ করে যেত।

একসাথে ব্যর্থ হওয়া ক্লায়েন্টরা একই নিয়মে অপেক্ষা করলে একসাথেই ফেরে — ঢেউয়ের মতো। এই ঢেউ ভাঙে অপেক্ষার সময়ে এলোমেলো অংশ, অর্থাৎ jitter. AWS Architecture Blog-এ Marc Brooker-এর ২০১৫ সালের “Exponential Backoff And Jitter” লেখাটি simulation দিয়ে তিনটি রূপ তুলনা করে কৌশলটিকে জনপ্রিয় করে (পর্ব ৬)। k-তম retry-র আগে অপেক্ষা (rand(a, b) = a থেকে b-এর মধ্যে এলোমেলো):

রূপঅপেক্ষাবৈশিষ্ট্য
Jitter ছাড়াt = min(cap, base·2^k)সবাই একই মুহূর্তে ফেরে — ঢেউ থেকে যায়
Full jitterrand(0, t)সবচেয়ে বেশি ছড়ায়; কখনো প্রায় সাথে সাথেই আবার চেষ্টা
Equal jittert/2 + rand(0, t/2)অন্তত অর্ধেক অপেক্ষা নিশ্চিত; ছড়ানো কম
Decorrelated jittermin(cap, rand(base, 3·আগের))আগের অপেক্ষা থেকে পরেরটি; k গুনতে হয় না

এবার সংখ্যায়। ১০০টি ক্লায়েন্ট t = ০-তে একটি ক্ষণিকের বিভ্রাটে একসাথে ত্রুটি পেল। তারপর থেকে সার্ভার সুস্থ, কিন্তু প্রতি ১০০ ms-এ মাত্র ২টি call নিতে পারে (সেকেন্ডে ২০); বাড়তিগুলো সাথে সাথে ফেরত যায় (পর্ব ১৮-এর fail fast), আর সেই ক্লায়েন্ট আবার backoff করে। base = ২০০ ms, cap = ১০ সেকেন্ড। তাত্ত্বিক সর্বনিম্ন: ১০০ ÷ ২০ = ৫ সেকেন্ড, ১০০টি call.

// 100 clients all get an error at t = 0 (a brief outage). From then on the server is up but
// accepts at most 2 calls per 100 ms slot (20/s); extra calls are rejected at once and back off.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const BASE = 0.2, CAP = 10, CLIENTS = 100, PER_SLOT = 2, SLOT = 0.1;       // seconds
const WAIT = {                                  // delay before retry number k (k = 1, 2, ...)
  "no jitter":    (k, prev, r) => Math.min(CAP, BASE * 2 ** k),
  "full jitter":  (k, prev, r) => r() * Math.min(CAP, BASE * 2 ** k),
  "equal jitter": (k, prev, r) => { const t = Math.min(CAP, BASE * 2 ** k); return t / 2 + r() * t / 2; },
  "decorrelated": (k, prev, r) => Math.min(CAP, BASE + r() * (prev * 3 - BASE)),
};
console.log("strategy      calls  peak/100ms  peak/s  all done   calls in each of the first 8 s");
for (const [name, wait] of Object.entries(WAIT)) {
  const r = mulberry32(7), served = new Map(), inSlot = new Map(), perSec = [];
  const pending = [];
  let calls = 0, last = 0;
  for (let id = 0; id < CLIENTS; id++) { const d = wait(1, BASE, r); pending.push({ t: d, k: 1, prev: d }); }
  while (pending.length) {
    pending.sort((a, b) => a.t - b.t);
    const c = pending.shift(), slot = Math.floor(c.t / SLOT), sec = Math.floor(c.t);
    calls++; perSec[sec] = (perSec[sec] || 0) + 1; inSlot.set(slot, (inSlot.get(slot) || 0) + 1);
    if ((served.get(slot) || 0) < PER_SLOT) { served.set(slot, (served.get(slot) || 0) + 1); last = c.t; continue; }
    const d = wait(c.k + 1, c.prev, r);         // rejected: wait, then retry number k + 1
    pending.push({ t: c.t + d, k: c.k + 1, prev: d });
  }
  const hist = Array.from({ length: 8 }, (_, s) => String(perSec[s] || 0).padStart(4)).join("");
  console.log(name.padEnd(12) + String(calls).padStart(7) + String(Math.max(...inSlot.values())).padStart(12) +
    String(Math.max(...perSec.filter(Boolean))).padStart(8) + (last.toFixed(1) + " s").padStart(10) + "  " + hist);
}

আউটপুট:

strategy      calls  peak/100ms  peak/s  all done   calls in each of the first 8 s
no jitter      2550         100     100   462.4 s   100  98  96   0   0   0  94   0
full jitter     347          38     202     8.8 s   202  67  27  22  16   3   4   5
equal jitter    372          57     167    19.5 s   167  51  62  14  29  19   0   2
decorrelated    333          31     142     9.8 s   142  80  48  31  14   5   6   0

Jitter ছাড়া: ১০০ জনই ০.৪ সেকেন্ডে একসাথে ফেরে (১০০ ms-এর একটি খোপে peak ১০০), মাত্র ২ জন সফল; বাকি ৯৮ জন ১.২ সেকেন্ডে, ৯৬ জন ২.৮-এ, ৯৪ জন ৬.০-তে — প্রথম ৮ সেকেন্ডের histogram-এ ঠিক এই চারটি ঢেউ। প্রতিটি ঢেউয়ে ২ জন, তাই ৫০টি ঢেউ আর মোট ১০০ + ৯৮ + … + ২ = ২,৫৫০টি call; অপেক্ষা ০.৪ + ০.৮ + ১.৬ + ৩.২ + ৬.৪ = ১২.৪ সেকেন্ড, তারপর ৪৫টি ঢেউ cap-এর ১০ সেকেন্ড করে: ৪৬২.৪ সেকেন্ড। সার্ভার প্রায় সারাক্ষণ অলস, অথচ ভিড় সরে না।

Jitter-সহ: মোট call ৩৩৩–৩৭২, শেষ ৮.৮–১৯.৫ সেকেন্ডে — ন্যূনতম ৫ সেকেন্ডের কাছে। খোপপ্রতি peak ১০০ থেকে নেমে ৩১–৫৭। লক্ষ করুন, সেকেন্ডপ্রতি peak jitter-এ বরং বেশি (প্রথম সেকেন্ডে full jitter-এ ২০২): ছোট অপেক্ষায় ক্লায়েন্টরা প্রথম সেকেন্ডেই কয়েকবার চেষ্টা করে, কিন্তু সময়ে ছড়িয়ে। সার্ভারকে বসায় একই মুহূর্তের ধাক্কা, তাই খোপপ্রতি peak-ই আসল মাপ। Equal jitter এখানে সবচেয়ে ধীর (১৯.৫ s); full আর decorrelated-এর ক্রম seed বদলালে ওঠানামা করে। Brooker-এর লেখার সিদ্ধান্তও মোটামুটি এমন: jitter-ছাড়া রূপ স্পষ্টত সবচেয়ে খারাপ, equal jitter পিছিয়ে, বাকি দুটি কাছাকাছি।

তিনটি ব্যবহারিক কথা। Cap রাখুন, নইলে দশম retry-র অপেক্ষা ঘণ্টায় গড়ায়; আর সর্বোচ্চ চেষ্টার সংখ্যা বা মোট সময়ের একটি সীমাও। Backoff একা overload থামায় না: পর্ব ১৮-এর simulator দেখিয়েছে, টানা overload-এ প্রত্যেকে তবুও সব কটি চেষ্টা করে, তাই গড় চাপ একই থাকে — budget বা circuit breaker লাগে। আর নিয়মটি library-তে বাঁধুন: প্রতিটি দল নিজে retry লিখলে কেউ না কেউ jitter ভুলবেই; মোবাইলের reconnect (পর্ব ১০) আর webhook (পর্ব ৩২)-এও একই নিয়ম।

এলোমেলো backoff নতুন নয়: ১৯৭০-এর দশকের ALOHA network-এ সংঘর্ষের পরে প্রেরক এলোমেলো সময় অপেক্ষা করত, আর ১৯৭৬ সালে Metcalfe ও Boggs-এর Ethernet-এ এলো binary exponential backoff: n-তম সংঘর্ষের পরে ০ থেকে 2n − ১-এর মধ্যে এলোমেলো সংখ্যক slot অপেক্ষা — আজকের ভাষায় প্রায় হুবহু full jitter (Computer Networks · ALOHA, CSMA/CD আর CSMA/CA). TCP-র retransmission timer-ও প্রতি timeout-এ দ্বিগুণ হয় (Computer Networks · TCP-র নির্ভরযোগ্যতা)। সমস্যাটি গণিতে একই।

৫ · Hedged request — retry নয়, tail ছাঁটার কৌশল

Retry ব্যর্থতার পরে আসে; কিন্তু প্রায়ই সমস্যা ধীরতা — ১০০টির একটি উত্তর GC pause-এ দশ গুণ দেরিতে আসে। Jeffrey Dean আর Luiz André Barroso ২০১৩ সালে Communications of the ACM-এ “The Tail at Scale” প্রবন্ধে দেখান, বহু সার্ভারে ছড়ানো (fan-out) একটি অনুরোধে এই বিরল ধীরতাই প্রায় নিয়ম হয়ে দাঁড়ায় — ১০০টি অংশের প্রত্যেকে ১% সময় ধীর হলে পুরো অনুরোধের প্রায় ৬৩% ধীর (১ − ০.৯৯১০০; পর্ব ৪)।

তাঁদের প্রস্তাবিত একটি কৌশল hedged request: উত্তর প্রত্যাশিত সময়ের মধ্যে — যেমন এই ধরনের অনুরোধের p95 — না এলে একই অনুরোধ আরেকটি replica-তে পাঠানো; যে আগে উত্তর দেয় সেটি নেওয়া, অন্যটি বাতিল। পর্ব ৪-এর simulator-এ এতে p99 প্রায় ২০০ থেকে ৬২ ms-এ নেমেছিল, দাম মাত্র প্রায় ৫% বাড়তি অনুরোধ। প্রবন্ধটি আরও সূক্ষ্ম একটি রূপও বর্ণনা করে — tied request: অনুরোধ একসাথে দুই সার্ভারের queue-তে, প্রত্যেকে অন্যটির খবর রাখে; যে আগে কাজ শুরু করে, সে অন্যটিকে বাতিল করায়। Queue-তে অপেক্ষাই যেখানে ধীরতার কারণ, সেখানে এটি বাড়তি কাজ আরও কমায়।

RetryHedged request
কখন পাঠানোব্যর্থতা বা timeout-এর পরেউত্তরের অপেক্ষার মাঝে, নির্দিষ্ট দেরির পরে
লক্ষ্যক্ষণিকের ত্রুটি ঢাকাtail latency (p99, p99.9) ছোট করা
শর্তidempotent, budget-এর মধ্যেএকই, সাথে বাতিল করার উপায় আর একাধিক replica
Overload-এbudget-এ বাঁধাবন্ধ রাখুন — বাড়তি অনুরোধ আগুনে ঘি

gRPC-র service config-এ retryPolicy-র পাশে hedgingPolicy-ও আছে, দুটিই একই retry throttling-এ গোনা (সমর্থন সংস্করণভেদে আলাদা) — hedge আর retry একই বাজেট থেকে খরচ করে।

৬ · Circuit breaker — closed, open আর half-open

বাড়ির মিটারের পাশের circuit breaker short circuit হলে নিজেই লাইন কেটে দেয়; মিস্ত্রি পরে আবার তোলেন, সমস্যা থাকলে আবার পড়ে। সফটওয়্যারের circuit breaker-ও একটি dependency বারবার ব্যর্থ হলে কিছুক্ষণ তাকে ডাকাই বন্ধ রাখে — caller সঙ্গে সঙ্গে ত্রুটি বা fallback পায়, thread আটকায় না, আর অসুস্থ dependency সেরে ওঠার সুযোগ পায়।

Michael Nygard-এর বই Release It! (প্রথম সংস্করণ ২০০৭, দ্বিতীয় ২০১৮) circuit breaker, bulkhead আর timeout-কে “stability pattern” হিসেবে জনপ্রিয় করে (ইতিহাস ভাবনার প্রশ্ন ৪-এ)। তিনটি অবস্থা:

🟢 Closed — স্বাভাবিক

সব call যায়; শেষ N-টি call বা শেষ কয়েক সেকেন্ডের ফল একটি sliding window-এ গোনা হয়।

খোলে: ব্যর্থতার হার ≥ সীমা

🔴 Open — ডাকা বন্ধ

কোনো call যায় না; প্রতিটি সঙ্গে সঙ্গে ত্রুটি বা fallback পায়। নির্দিষ্ট cool-down সময় পরে half-open.

dependency বিশ্রাম পায়

🟡 Half-open — পরীক্ষা

অল্প কয়েকটি probe call যেতে দেয়, বাকিরা তখনো বন্ধ। সব probe সফল হলে closed; একটিও ব্যর্থ হলে আবার open.

half-open

খোলার সিদ্ধান্ত নেয় failure-rate threshold: window-এ ব্যর্থতার অনুপাত সীমা (ধরা যাক ৫০%) ছুঁলে, সাথে একটি সর্বনিম্ন call-সংখ্যা, যাতে দুটির একটি ব্যর্থ হলেই না খোলে। Timeout আর খুব ধীর call-ও ব্যর্থতা হিসেবে গোনা ভালো; কিন্তু 400 বা 404-এর মতো client-এর ভুল নয়।

CLOSEDসব call যায়
OPENসঙ্গে সঙ্গে fallback
HALF-OPEN৩টি probe
Window: শেষ 10টি call · সীমা 50% · cool-down 5 s · probe 3টি · timeout 1 s
সময় t
শেষ 10টি call (✓ সফল, ✗ ব্যর্থ)
ব্যর্থতার হার
Dependency-তে যাচ্ছে
Caller পায়
  1. Closed, স্বাভাবিক দিন। শেষ ১০টির একটি ব্যর্থ — ১০%, সীমার অনেক নিচে। সব call dependency-তে যায়।
  2. Dependency ঝুলে গেল। ১২ সেকেন্ড থেকে প্রতিটি call ১ সেকেন্ডের timeout-এ ব্যর্থ। ১৩.২ সেকেন্ডে শেষ ১০টির ৫টি ব্যর্থ (আগের একটিসহ, নতুন চারটি timeout): ৫০% ≥ ৫০%, আর call-সংখ্যা ১০ — সর্বনিম্ন শর্তও পূর্ণ। Breaker খুলল। তার আগের call-গুলো পুরো timeout অপেক্ষা করেছে: breaker ব্যর্থতা দেখে, আগাম জানে না।
  3. Open. পরের ৫ সেকেন্ড কোনো call যায় না। Caller ১ ms-এরও কম সময়ে fallback পায়, আর dependency চাপমুক্ত হয়ে সেরে ওঠার সময় পায়।
  4. Half-open, প্রথম পরীক্ষা। ১৮.২ সেকেন্ডে cool-down শেষ। ৩টি probe যায়, বাকিরা তখনো fallback. প্রথম probe ১৯.২ সেকেন্ডে timeout — dependency এখনো অসুস্থ। Breaker আবার open, নতুন cool-down ২৪.২ সেকেন্ড পর্যন্ত।
  5. Half-open, দ্বিতীয় পরীক্ষা। ২৪.২ সেকেন্ডে আবার ৩টি probe; এবার তিনটিই প্রায় ২০ ms-এ সফল।
  6. আবার closed. ২৪.৩ সেকেন্ড থেকে সব call যায়, window নতুন করে শুরু। বড় সিস্টেমে traffic ধাপে ধাপে ফেরানো ভালো, যাতে সদ্য সেরে ওঠা dependency-তে thundering herd না নামে।

এবার একটি দীর্ঘ call-ধারায়: সেকেন্ডে ৫০টি call, ৬০ সেকেন্ড; dependency ১০ থেকে ৩০ সেকেন্ড ঝুলে থাকে। Window শেষ ২০টি, সর্বনিম্ন ১০টি, সীমা ৫০%, cool-down ৫ সেকেন্ড, ৩টি probe.

// 50 calls/s for 60 s. Normally a call takes ~20 ms and fails 2% of the time. From t = 10 s
// to t = 30 s the dependency hangs: every call waits for the 1 s timeout and fails.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const r = mulberry32(5), TIMEOUT = 1, WINDOW = 20, MIN_CALLS = 10, THRESHOLD = 0.5, COOL = 5, PROBES = 3;
let state = "CLOSED", openedAt = 0, recent = [], probesSent = 0, probesOk = 0;
let pending = [], scOutage = 0, scHealthy = 0, waited = 0, sent = 0;
const go = (to, t, why) => { console.log(t.toFixed(2).padStart(6) + " s  " + state.padEnd(10) + "-> " + to.padEnd(10) + why);
  state = to; if (to === "OPEN") openedAt = t; if (to === "HALF_OPEN") probesSent = probesOk = 0; if (to === "CLOSED") recent = []; };
function settle(res) {                                // a call finished: update the breaker
  if (state === "HALF_OPEN" && res.probe) {
    if (!res.ok) return go("OPEN", res.end, "a probe failed");
    if (++probesOk === PROBES) go("CLOSED", res.end, PROBES + " probes succeeded");
  } else if (state === "CLOSED") {
    recent.push(res.ok); if (recent.length > WINDOW) recent.shift();
    const rate = recent.filter(ok => !ok).length / recent.length;
    if (recent.length >= MIN_CALLS && rate >= THRESHOLD)
      go("OPEN", res.end, "failure rate " + (100 * rate).toFixed(0) + "% of last " + recent.length);
  }
}
for (let t = 0; t < 60; t += -Math.log(1 - r()) / 50) {
  pending.sort((a, b) => a.end - b.end);
  while (pending.length && pending[0].end <= t) settle(pending.shift());
  if (state === "OPEN" && t - openedAt >= COOL) go("HALF_OPEN", t, "cool-down of " + COOL + " s over");
  const probe = state === "HALF_OPEN" && probesSent < PROBES;
  const hung = t >= 10 && t < 30;
  if (state === "OPEN" || (state === "HALF_OPEN" && !probe)) { hung ? scOutage++ : scHealthy++; continue; } // fallback
  if (probe) probesSent++;
  const ok = !hung && r() >= 0.02, dur = hung ? TIMEOUT : 0.01 + r() * 0.02;
  sent++; waited += dur; pending.push({ end: t + dur, ok, probe });
}
console.log("calls sent " + sent + "; short-circuited: " + scOutage + " during the outage, " + scHealthy + " after it ended");
console.log("thread-seconds spent waiting: " + waited.toFixed(0) + " with the breaker, about " +
  (waited + scOutage * TIMEOUT + scHealthy * 0.02).toFixed(0) + " without it");

আউটপুট:

 11.34 s  CLOSED    -> OPEN      failure rate 50% of last 20
 16.35 s  OPEN      -> HALF_OPEN cool-down of 5 s over
 17.35 s  HALF_OPEN -> OPEN      a probe failed
 22.36 s  OPEN      -> HALF_OPEN cool-down of 5 s over
 23.36 s  HALF_OPEN -> OPEN      a probe failed
 28.37 s  OPEN      -> HALF_OPEN cool-down of 5 s over
 29.37 s  HALF_OPEN -> OPEN      a probe failed
 34.41 s  OPEN      -> HALF_OPEN cool-down of 5 s over
 34.46 s  HALF_OPEN -> CLOSED    3 probes succeeded
calls sent 1890; short-circuited: 950 during the outage, 227 after it ended
thread-seconds spent waiting: 108 with the breaker, about 1063 without it

খোলার দেরি: dependency ১০ সেকেন্ডে ঝুলেছে, কিন্তু প্রথম ব্যর্থতা জানা যায় ১১ সেকেন্ডে (১ সেকেন্ডের timeout), আর শেষ ২০টির অর্ধেক ব্যর্থ হতে হতে ১১.৩৪ সেকেন্ড। এর মধ্যে পাঠানো সব call পুরো ১ সেকেন্ড অপেক্ষা করেছে। ছোট timeout breaker-কেও দ্রুত করে।

Probe-এর চক্র: ১৬.৩৫, ২২.৩৬ আর ২৮.৩৭ সেকেন্ডে half-open; প্রতিবার একটি probe ১ সেকেন্ড পরে ব্যর্থ, তাই আবার open. শেষ চক্রের probe ২৯.৩৭ সেকেন্ডে ব্যর্থ হলো — dependency সেরেছে ৩০ সেকেন্ডে — তাই breaker ৩৪.৪১ পর্যন্ত বন্ধ রইল, আর তিনটি probe ৩৪.৪৬-এ সফল হয়ে closed.

লাভ আর দাম: বিভ্রাটের ৯৫০টি call সঙ্গে সঙ্গে fallback পেয়েছে; thread-এর অপেক্ষা মোট ১০৮ সেকেন্ড, breaker না থাকলে প্রায় ১,০৬৩ — প্রায় দশ গুণ। দাম ২২৭টি call, যারা dependency সেরে ওঠার পরেও প্রায় ৪.৪ সেকেন্ড অকারণে fallback পেয়েছে। অনুশীলন ৩-এ cool-down ছোট করে দেখবেন।

Library-তে এটি দেখতে কেমন

Java-র জনপ্রিয় open-source library resilience4j-এ (Hystrix-এর পরে আসা, তার দ্বারা অনুপ্রাণিত) উপরের নিয়মগুলো config-এ লেখা যায়। নিচের মানগুলো এই পর্বের উদাহরণের, library-র ডিফল্ট নয়:

resilience4j:
  circuitbreaker:
    instances:
      offers:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20              # last 20 calls
        minimumNumberOfCalls: 10
        failureRateThreshold: 50           # percent
        slowCallDurationThreshold: 500ms   # a call slower than this counts as "slow"
        slowCallRateThreshold: 50          # percent of slow calls that also opens it
        waitDurationInOpenState: 5s        # cool-down
        permittedNumberOfCallsInHalfOpenState: 3
  bulkhead:
    instances:
      offers:
        maxConcurrentCalls: 10             # at most 10 calls to "offers" at once
        maxWaitDuration: 0ms               # no free slot: fail fast
  retry:
    instances:
      offers:
        maxAttempts: 2                     # 1 try + 1 retry
        waitDuration: 200ms

একটি config-এ তিনটি pattern: breaker ধীর call-ও গোনে, bulkhead একসাথে সর্বোচ্চ ১০টি call রাখে আর জায়গা না থাকলে সাথে সাথে ফেরত দেয়, retry মাত্র একবার। এগুলো কোন ক্রমে জোড়া লাগে, তা documentation-এ দেখে নিন। .NET-এ একই কাজ করে open-source Polly (নতুন সংস্করণে “resilience pipeline” নামে)।

Breaker বনাম outlier ejection — আর নামের বিভ্রান্তি

Breaker সাধারণত পুরো dependency-র জন্য একটি — “অফার সার্ভিস অসুস্থ”। কিন্তু সমস্যা প্রায়ই একটিমাত্র instance-এ। তখন ভালো উপায় সেই instance-কে load balancing pool থেকে কিছুক্ষণ সরানো — outlier ejection (পর্ব ১৪-এর passive health check-এর উন্নত রূপ), একসাথে কত শতাংশ সরানো যাবে তার সীমাসহ।

একই নাম, ভিন্ন জিনিস। Envoy-র documentation-এ “circuit breaking” মানে সর্বোচ্চ connection, অপেক্ষমাণ ও চলমান অনুরোধ আর retry-র সংখ্যার সীমা — এই পর্বের ভাষায় bulkhead আর retry budget-এর কাছাকাছি। তিন-অবস্থার breaker-এর কাছের জিনিস সেখানে outlier detection. “Circuit breaker” শুনলে জিজ্ঞেস করুন: কোনটি?

৭ · Bulkhead আর cell — ব্যর্থতাকে এক কক্ষে আটকে রাখা

জাহাজের খোল কয়েকটি পানিরোধী কক্ষে (bulkhead) ভাগ করা; একটিতে ফুটো হলে শুধু সেটিই ভরে। অংশ ১-এর রেস্তোরাঁয় এর মানে: ১০ জন ওয়েটারের সর্বোচ্চ ২ জন মিষ্টির কাউন্টারে যাবেন — দই দেরি হলেও চা ঠিকই আসবে।

সফটওয়্যারে bulkhead মানে প্রতিটি dependency-র আলাদা সীমিত সম্পদ: আলাদা thread pool, বা একসাথে-চলা call-এর সর্বোচ্চ সংখ্যা (semaphore); আলাদা connection pool আর queue-ও একই ভাবনা। আকার Little-এর সূত্রে: ভাগ ≈ call-এর হার × স্বাভাবিক p99 latency × কিছুটা বাড়তি জায়গা।

একটি সার্ভিস, তিনটি dependency, প্রতিটির নিজের কক্ষ

Per-dependency bulkhead (thread pool) + circuit breaker + timeout + fallback

প্রতিটি dependency-র নিজের bulkhead আর circuit breaker API gateway অর্ডার সার্ভিস নিজের কাজের thread আলাদা Payment · pool 30 breaker: CLOSED timeout 800 ms · retry 1 Inventory · pool 20 breaker: CLOSED timeout 300 ms অফার · pool 10 breaker: OPEN fallback: cache-এর জনপ্রিয় তালিকা Payment সুস্থ Inventory সুস্থ অফার ধীর · 3 s কক্ষ (bulkhead) call বন্ধ (breaker খোলা)
ছবি ৪৮.১ — অফার সার্ভিস ধীর, কিন্তু ক্ষতি তার কক্ষেই আটকে আছে: তার breaker খোলা, তাই তার ১০টি thread-ও আটকায় না, আর পাতা অফারের জায়গায় জনপ্রিয় তালিকা দেখায়। Payment আর Inventory নিজেদের ৩০ আর ২০টি thread নিয়ে স্বাভাবিক চলছে।

সংখ্যায়: ৪০টি thread, দুটি dependency, প্রতিটিতে সেকেন্ডে ৩০০ call. “profile” সুস্থ (প্রায় ২০ ms, দরকার ৩০০ × ০.০২ = ৬টি thread); ৫ সেকেন্ড থেকে “offers” ০.২–১.০ সেকেন্ড নেয়, timeout ০.৫ সেকেন্ড। Thread না পেলে call সাথে সাথে ফেরত।

// One service, 40 worker threads, calls two dependencies: "profile" (healthy, ~20 ms) and
// "offers" (~20 ms, but from t = 5 s it takes 0.2-1.0 s; our timeout cuts it at 0.5 s).
// 300 calls/s to each. No free thread -> the call is rejected at once.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const RATE = 300, END = 20, TIMEOUT = 0.5;
function run(pools) {                           // pools: { name: thread count }, or one shared
  const r = mulberry32(48), busy = {}, stat = { profile: [0, 0], offers: [0, 0] };
  for (const [name, n] of Object.entries(pools)) busy[name] = new Array(n).fill(0); // busy-until
  const calls = [];
  for (const dep of ["profile", "offers"])
    for (let t = 0; t < END; t += -Math.log(1 - r()) / RATE) calls.push({ t, dep });
  calls.sort((a, b) => a.t - b.t);
  for (const { t, dep } of calls) {
    const pool = busy[dep] || busy.shared, i = pool.findIndex(u => u <= t), count = t >= 5;
    if (count) stat[dep][1]++;                  // count only after "offers" got slow
    if (i < 0) continue;                        // no thread free: rejected at once
    const slow = dep === "offers" && t >= 5, took = slow ? 0.2 + r() * 0.8 : 0.01 + r() * 0.02;
    pool[i] = t + Math.min(took, TIMEOUT);      // the thread is held until reply or timeout
    if (count && took <= TIMEOUT) stat[dep][0]++;
  }
  const pct = ([ok, all]) => (100 * ok / all).toFixed(1).padStart(6) + "%";
  return pct(stat.profile) + "   " + pct(stat.offers);
}
console.log("thread pools (t = 5..20 s)            profile ok  offers ok");
console.log("one shared pool of 40               " + run({ shared: 40 }));
console.log("bulkheads: profile 20, offers 20    " + run({ profile: 20, offers: 20 }));
console.log("bulkheads: profile 30, offers 10    " + run({ profile: 30, offers: 10 }));

আউটপুট:

thread pools (t = 5..20 s)            profile ok  offers ok
one shared pool of 40                 30.1%     11.5%
bulkheads: profile 20, offers 20     100.0%      5.7%
bulkheads: profile 30, offers 10     100.0%      3.2%

ভাগ করা pool: সুস্থ profile-এর মাত্র ৩০.১% call সফল, যদিও profile-এর কোনো দোষ নেই। কারণ Little-এর সূত্রে offers-এর দরকার প্রায় ৩০০ × ০.৪৪ ≈ ১৩৩টি thread (প্রতিটি call গড়ে প্রায় ০.৪৪ সেকেন্ড thread ধরে রাখে: ৩৭.৫% call গড়ে ০.৩৫ সেকেন্ডে শেষ, বাকিরা ০.৫-এর timeout পর্যন্ত), আর সে ৪০টি thread-এর প্রায় সবই দখল করে। Timeout থাকা সত্ত্বেও এমন হলো — timeout × হার pool-এর চেয়ে বড় (অংশ ২-এর দ্বিতীয় শর্ত)।

Bulkhead: profile ১০০% সফল, ২০ বা ৩০ যে ভাগই পাক — তার দরকার মাত্র ৬টি। দাম offers-এর: ভাগ করা pool-এ সে অন্যের thread কেড়ে ১১.৫% পেত, নিজের ২০টিতে ৫.৭%, ১০টিতে ৩.২%. Bulkhead অসুস্থ অংশকে বাঁচায় না, বাকিদের বাঁচায়। আর offers-এর জন্য এখন দরকার breaker আর fallback.

বড় মাপের bulkhead — cell আর shuffle sharding

একই ভাবনা পুরো সেবার স্তরে নিলে আসে cell-based architecture: পুরো stack-এর (সার্ভিস, database, queue) কয়েকটি স্বাধীন, একই রকম কপি — cell — আর প্রতিটি ব্যবহারকারী একটি নির্দিষ্ট cell-এ। একটি খারাপ deploy বা database-এর বিপর্যয় শুধু সেই cell-কে ছোঁয়; blast radius ধরা যাক ১০ ভাগের ১ ভাগ। দাম: সামনে একটি routing স্তর, cell-এর মধ্যে ডেটা সরানোর জটিলতা আর চালানোর বাড়তি কাজ। AWS-এর প্রকাশিত নির্দেশিকায় cell আর shuffle sharding-এর আলোচনা আছে। Shuffle sharding-এ প্রতিটি client পায় সব সার্ভারের বদলে এলোমেলো ছোট একটি দল: ৮টি সার্ভার থেকে ২টি করে নিলে ২৮ রকম জোড়া হয়, তাই একটি client-এর খারাপ traffic যে দুটি সার্ভার বসায়, অন্য একটি নির্দিষ্ট client-এর দুটিই সেগুলো হওয়ার সম্ভাবনা ২৮-এ ১।

৮ · Fallback, graceful degradation আর health check-এর ফাঁদ

Breaker খুলল, timeout পেরোল — ব্যবহারকারী কী দেখবেন? “500” সবচেয়ে সহজ, আর প্রায়ই সবচেয়ে খারাপ উত্তর। ভালো নকশা প্রতিটি dependency-র জন্য আগেই ঠিক করে: এটি না পেলে কী হবে। পর্ব ১৮-এর graceful degradation-এর হাতিয়ারের মধ্যে এখানে তিনটি সবচেয়ে কাজের:

Fallbackউদাহরণ (কাল্পনিক)কোথায় চলে না
পুরোনো ডেটা (stale data)Cache-এ ৫ মিনিট আগের পণ্যের দাম, সাথে “শেষ হালনাগাদ” লেখা; HTTP-তে stale-if-error (পর্ব ১৭, ২৯)ব্যালান্স, লেনদেনের অবস্থা, স্টকের শেষ কয়েকটি
নিরাপদ defaultসুপারিশ না এলে জনপ্রিয় পণ্যের স্থির তালিকা; ব্যক্তিগত ছাড় না এলে ছাড় ছাড়াই দামযেখানে default ভুল সিদ্ধান্ত নেয় — যেমন জালিয়াতি-যাচাই ব্যর্থ হলে “ঠিক আছে” ধরা
সুবিধা বন্ধ (feature flag)রিভিউ অংশ লুকানো, পাতার বাকিটা চালুমূল কাজ নিজেই সেই dependency-র ওপর দাঁড়িয়ে থাকলে

তিনটি নিয়ম। Fallback-ও একটি dependency — সেটি যেন একই অসুস্থ অংশের ওপর নির্ভর না করে, আর নিজে দামি না হয়। Fail open নাকি fail closed, তা ব্যবসার সিদ্ধান্ত: সুপারিশ না এলে পাতা দেখানো ঠিক, কিন্তু অনুমতি-যাচাই না এলে প্রবেশ দেওয়া বিপজ্জনক। আর যে fallback কখনো চলেনি, সেটি সম্ভবত কাজ করে না — তাই অংশ ৯-এর পরীক্ষা। Overload-এ fallback-এর আগের ধাপ load shedding (পর্ব ১৮), আর client-প্রতি সীমা পরের পর্বে (পর্ব ৪৯)।

Health check — রক্ষাকবচ যখন নিজেই আঘাত করে

পর্ব ১৪-এ health check-এর হিসাব দেখেছেন। Resilience-এর চোখে এর বিপদ একটিই: ভাগ করা dependency-র সমস্যা যদি সব instance-এর check-কে একসাথে ব্যর্থ করে। ধরা যাক প্রতিটি instance-এর /health database-এ একটি query চালায়। Database একটু ধীর হলো, ২ সেকেন্ডের check timeout পেরোল। Load balancer সব instance-কে একসাথে বাদ দিল — database-এর সামান্য ধীরতা এখন পুরো সেবার বিভ্রাট। এর নাম deep health check-এর ফাঁদ।

Probeপ্রশ্নব্যর্থ হলেকী যাচাই করবেন
LivenessProcess কি আটকে গেছে?Container restartশুধু নিজের অবস্থা — event loop সাড়া দেয়, deadlock নেই; কোনো dependency নয়, কারণ restart database সারায় না।
Readinessএখন traffic নিতে পারি?শুধু traffic থেকে সরানোConfig লোড, warm-up শেষ, নিজের queue উপচে পড়ছে না; ভাগ করা dependency নয়।
Startupচালু হওয়া শেষ?অপেক্ষা, তারপর restartধীরে চালু হওয়া অ্যাপকে সময় দেয়।

আরও দুটি রক্ষাকবচ: প্রায় সবাইকে অসুস্থ দেখলে অনেক load balancer fail open করে (পর্ব ১৪), কারণ “সবাই মৃত” সাধারণত check-এরই ভুল; আর health endpoint-কে আলাদা ছোট thread দিন, যাতে pool ভরলেও “বেঁচে আছি, কিন্তু ব্যস্ত” জানানো যায়।

৯ · Chaos engineering, game day আর library বনাম service mesh

অগ্নিনির্বাপণের মহড়া ছাড়া কেউ জানেন না বিপদের সিঁড়ি তালাবন্ধ কি না; timeout আর fallback কাজ করে কি না, তা প্রথম আসল বিভ্রাটে জানলে দেরি। Chaos engineering মানে নিয়ন্ত্রিত পরীক্ষায় ইচ্ছা করে ব্যর্থতা ঘটিয়ে দেখা।

প্রকাশিত ইতিহাস অনুযায়ী Netflix ২০১০ সালের দিকে Chaos Monkey বানায়, যা কাজের সময়ে এলোমেলোভাবে production-এর instance বন্ধ করত; ২০১২ সালে এটি open-source হয়। পরে নীতিগুলো “Principles of Chaos Engineering” নামে প্রকাশিত হয়:

  1. Steady state-এর অনুমান: “স্বাভাবিক” একটি মাপা সংখ্যায় — যেমন সফল অর্ডারের হার।
  2. বাস্তব ঘটনার মতো ব্যর্থতা: সার্ভার বন্ধ, ৫০০ ms দেরি, একটি zone বিচ্ছিন্ন।
  3. যতটা সম্ভব বাস্তব পরিবেশে, কারণ staging-এর traffic আলাদা।
  4. স্বয়ংক্রিয় আর নিয়মিত।
  5. ছোট blast radius: ১% traffic বা একটি cell থেকে শুরু, থামানোর বোতামসহ।

হাতিয়ারটি প্রায়ই fault injection: service mesh বা library দিয়ে, যেমন ১০% অনুরোধে 503 ফেরত বা ২ সেকেন্ড বাড়তি দেরি। মানুষের দিকটা পরীক্ষা করে game day: ঘোষিত দিনে পরিকল্পিত বিভ্রাট ঘটিয়ে দেখা alert বাজে কি না, runbook ঠিক কি না, কে কাকে ডাকে (Amazon-এর GameDay-র ইতিহাস ভাবনার প্রশ্ন ৫-এ)।

Chaos মানে বিশৃঙ্খলা নয়। লিখিত অনুমান, dashboard (পর্ব ৫০), থামানোর শর্ত আর প্রভাবের সীমা চাই; error budget ফুরিয়ে থাকলে পরীক্ষা পিছিয়ে দিন; আর পরীক্ষা শুধু নিজের সিস্টেমে।

Resilience কোথায় থাকবে — library, নাকি service mesh?

Library-স্তরে অ্যাপের কোডে resilience4j বা Polly; mesh-স্তরে প্রতিটি সার্ভিসের পাশের sidecar proxy (যেমন Envoy) সব call-এ নিয়ম প্রয়োগ করে, config কেন্দ্রীয় — service mesh, যেমন Istio বা Linkerd (পর্ব ৪৭)।

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: offers
spec:
  hosts: ["offers"]
  http:
  - route:
    - destination: { host: offers }
    timeout: 800ms                  # whole call, all tries included
    retries:
      attempts: 1
      perTryTimeout: 300ms
      retryOn: connect-failure,refused-stream,unavailable
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: offers
spec:
  host: offers
  trafficPolicy:
    connectionPool:                 # bulkhead-like limits
      http: { http1MaxPendingRequests: 20, maxRequestsPerConnection: 50 }
    outlierDetection:               # eject a bad instance for a while
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 30

মোট timeout ৮০০ ms, প্রতিটি চেষ্টার ৩০০ ms; retry শুধু ক্ষণিকের ত্রুটিতে। নিচে অপেক্ষমাণ অনুরোধের সীমা (bulkhead-এর কাছাকাছি) আর outlier detection: পরপর ৫টি 5xx দেওয়া instance ৩০ সেকেন্ডের জন্য বাদ, তবে একসাথে ৩০%-এর বেশি নয়। অ্যাপ-কোড বদলাতে হয়নি।

Library-স্তরMesh-স্তর
ভাষাপ্রতিটি ভাষার আলাদা library, আলাদা আচরণভাষা-নিরপেক্ষ, সব সার্ভিসে একই নিয়ম
ব্যবসার জ্ঞানজানে কোন কাজ idempotent, fallback-এ কী দেখাবেশুধু HTTP/gRPC দেখে; “জনপ্রিয় তালিকা” বানাতে পারে না
বদলানোনতুন deploy লাগেকেন্দ্রীয় config, দ্রুত বদল
বিপদদলে দলে অসংগতিLibrary আর mesh দুটিতেই retry থাকলে অজান্তে গুণফল (অংশ ৩)
খরচপ্রায় নেইপ্রতিটি call-এ বাড়তি proxy hop, চালানোর জটিলতা

অনেক দল দুটি মেশায়: সাধারণ timeout, সংযোগ-স্তরের retry আর outlier ejection mesh-এ; fallback, idempotency key আর ব্যবসা-বোঝা breaker অ্যাপে। শর্ত একটাই: retry কোন স্তরে, তা এক জায়গায় লিখিত থাকুক। আর মনে রাখুন, Netflix-এর Hystrix ২০১৮ সালে maintenance mode-এ গেছে — library বদলায়, pattern টিকে থাকে।

আরও গভীরে: Dean ও Barroso-র “The Tail at Scale” (২০১৩) hedged আর tied request ছাড়াও micro-partition-এর মতো কৌশল আলোচনা করে। Google-এর SRE বইয়ের (২০১৬) “Addressing Cascading Failures” অধ্যায় deadline, retry budget আর health check-এর ফাঁদ বিস্তারিত বলে। Amazon-এর Builders’ Library-তে timeout-retry-backoff আর shuffle sharding-এর ব্যবহারিক লেখা পাবেন।

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

  • ধীর dependency কেন মৃতের চেয়ে খারাপ? মৃত দ্রুত ত্রুটি দেয়, ধীর thread আটকায়; λ × W pool ছাড়ালে সুস্থ কাজও থামে।
  • তিন স্তরে প্রতিটিতে ৩ বার চেষ্টা, সবচেয়ে নিচের অংশ বিকল — সে কত চেষ্টা পায়? ৩৩ = ২৭টি; প্রতিকার একটি স্তরে retry আর retry budget.
  • Exponential backoff থাকলেও jitter কেন? একসাথে ব্যর্থ ক্লায়েন্টরা একই নিয়মে একসাথেই ফেরে; jitter ঢেউ ভাঙে।
  • Circuit breaker-এর তিন অবস্থা? Closed (সব যায়, ব্যর্থতা গোনে), open (কিছুই যায় না, fallback), half-open (অল্প probe; সফলে closed, ব্যর্থে open).
  • Liveness probe-এ database যাচাই কেন ভুল? Database-এর সমস্যায় সব pod একসাথে restart হয়, যা কিছুই সারায় না।

১০ · AI-এর যুগে resilience-নকশা যাচাই

“আমাদের সার্ভিসগুলো মাঝে মাঝে একসাথে বসে যায়” — AI প্রায় নিশ্চিতভাবে বলবে “retry, circuit breaker আর Kafka যোগ করুন”। সংখ্যা ছাড়া তা নকশা নয়। সাধারণ ভুলগুলো:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
প্রতিটি স্তরে “৩ বার retry”গুণফল: ৩ স্তরে ২৭ গুণস্তরগুলো গুনে kn হিসাব; retry কোন স্তরে, লিখিত নিয়ম
POST বা পেমেন্টে সরাসরি retryTimeout মানে “জানি না” — দুবার কাটতে পারেIdempotency key আছে কি না দেখুন (পর্ব ৮, ৩৪)
“Timeout ৩০ সেকেন্ড দিন, নিরাপদ”λ × timeout pool ছাড়ায়, cascading failureTimeout ≤ ভাগের thread ÷ λ; percentile আর deadline দিয়ে যাচাই
Jitter ছাড়া backoff-এর কোডঢেউ থেকে যায়অংশ ৪-এর মতো simulation চালান
Library-র ডিফল্ট বা mesh-এর field বানিয়ে বলাসংস্করণভেদে আলাদা, প্রায়ই ভুলঅফিসিয়াল documentation; নিজের staging-এ fault injection
Fallback নেই, বা fallback-ও একই dependency-তেBreaker খুললে শুধু ত্রুটিপ্রতিটি dependency-র জন্য “না পেলে কী দেখাব”

AI-কে সংখ্যা, শিকল আর ব্যর্থতার ধরন দিয়ে বাঁধুন, যাতে উত্তরটি যাচাই করা যায়:

তুমি একজন অভিজ্ঞ SRE. আমাদের একটি কাল্পনিক checkout-এর শিকল: app → API gateway → order service → payment service → বাইরের ব্যাংক gateway. Order service-এর ২০০টি thread, সেকেন্ডে ৮০০ অনুরোধ; এর মধ্যে ৩০০টি একটি recommendation service-ও ডাকে। স্বাভাবিক latency: payment p99 ৩০০ ms, recommendation p99 ১২০ ms, ব্যাংক p99 ৮০০ ms. App-এর timeout ৩ সেকেন্ড। ১. প্রতিটি call-এর timeout আর পুরো অনুরোধের deadline দাও, আর Little-এর সূত্রে দেখাও কোনো dependency পুরো ঝুলে গেলে thread pool টিকবে কি না। ২. Retry কোন স্তরে, কোন ত্রুটিতে, কতবার — তিন স্তরের গুণফল হিসাব করে দেখাও। পেমেন্টে idempotency কীভাবে নিশ্চিত হবে? ৩. Backoff-এর সূত্র (jitter-সহ) আর retry budget দাও। ৪. কোন dependency-তে circuit breaker আর bulkhead, কী সীমায়, আর breaker খুললে ব্যবহারকারী কী দেখবেন? ৫. Liveness আর readiness probe কী যাচাই করবে? ৬. কোনো library বা mesh-এর ডিফল্ট মান বললে সূত্র দাও; না জানলে বলো "জানি না"। নতুন tool যোগের আগে বিদ্যমান ব্যবস্থায় কী করা যায়, আগে সেটা বলো।

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

AI দেয় pattern-এর নাম, যাচাইয়ের দায় আপনার।
Little-এর সূত্রে thread গুনুন, retry-র গুণফল কষুন, আর ইচ্ছা করে একটি dependency ধীর করে দেখুন।

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

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

প্র ০১বড় বিভ্রাটের পর্যালোচনায় প্রায়ই দেখা যায় শুরুটা হয়েছিল একটি ছোট, কম গুরুত্বের সার্ভিসের ধীরতা থেকে। কেন ধীরতা এত বিপজ্জনক, আর নকশায় তা কীভাবে চোখে পড়ে?

Nygard-এর Release It! বইয়ের অনেক গল্পের সাধারণ সুর এটাই: বিপর্যয় আসে প্রায়ই সেই অংশ থেকে, যাকে কেউ গুরুত্বপূর্ণ ভাবেনি — একটি সুপারিশের সার্ভিস, একটি log পাঠানোর call, একটি বাইরের ঠিকানা-যাচাই। কারণটি গাণিতিক। একটি সার্ভিসের সম্পদ — thread, connection, memory — সব call ভাগ করে নেয়। Little-এর সূত্রে কোনো একটি call-এর latency দশ গুণ হলে সেই call-এর thread খরচও দশ গুণ। গুরুত্ব কম হলেও সম্পদের ভাগ সমান। ফলে কম গুরুত্বের call-ই গুরুত্বপূর্ণ call-এর thread খেয়ে ফেলে।

মৃত্যু উল্টো, প্রায় নিরীহ। মৃত সার্ভিসের দিকে সংযোগ সাথে সাথে ব্যর্থ হয়, ত্রুটির হার বাড়ে, alert বাজে, আর caller-এর সম্পদ মুক্ত থাকে। ধীরতা প্রায়ই alert-এর নিচে থাকে: ত্রুটির হার শূন্য, শুধু p99 বাড়ছে। গড় latency দেখা dashboard-এ এটি চোখেই পড়ে না (পর্ব ৪)।

নকশায় একে দেখার উপায় একটি সরল অনুশীলন। প্রতিটি dependency-র পাশে লিখুন: সেকেন্ডে কত call (λ), এর timeout কত, আর λ × timeout কতগুলো thread. তারপর সেই সংখ্যা pool-এর সাথে মেলান। কোথাও সংখ্যাটি pool-এর চেয়ে বড় হলে সেই dependency পুরো সার্ভিসকে বসিয়ে দিতে পারে। অনেক দল এই প্রশ্নটিকে design review-এর নিয়মিত অংশ করে: “এটি মারা গেলে কী হবে” নয়, “এটি দশ গুণ ধীর হলে কী হবে”।

প্রতিরোধের ক্রমটিও এখান থেকে আসে। প্রথমে timeout, যাতে অপেক্ষার সীমা থাকে। তারপর bulkhead, যাতে সীমার মধ্যেও একটি dependency সবার ভাগ না খায়। তারপর circuit breaker, যাতে ধীরতা টের পাওয়ার পরে আর অপেক্ষাই না হয়। আর শেষে fallback, যাতে ব্যবহারকারী শূন্য হাতে না ফেরেন।

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

প্র ০২ALOHA আর Ethernet থেকে আজকের cloud SDK পর্যন্ত backoff-এ এলোমেলোতা কেন বারবার ফিরে আসে? এলোমেলোতা ছাড়া একটি সুশৃঙ্খল সময়সূচি কি আরও ভালো হতো না?

১৯৭০-এর দশকের শুরুতে হাওয়াই বিশ্ববিদ্যালয়ের ALOHA network-এ কয়েকটি দ্বীপের terminal একটি বেতার চ্যানেল ভাগ করত। দুজন একসাথে পাঠালে দুটি বার্তাই নষ্ট হতো। দুজনেই যদি ঠিক একই সময় অপেক্ষা করে আবার পাঠাত, আবার সংঘর্ষ — চিরকাল। সমাধান ছিল এলোমেলো অপেক্ষা। ১৯৭৬ সালে Metcalfe আর Boggs-এর Ethernet এর সাথে exponential অংশ যোগ করে: যত বেশি সংঘর্ষ, এলোমেলো অপেক্ষার পরিসর তত দ্বিগুণ। এতে বোঝা যায় ভিড় কত বড়, তাই প্রতিটি যন্ত্র নিজেই ধীর হয়।

সুশৃঙ্খল সময়সূচি — যেমন “client 1 প্রথম ১০০ ms-এ, client 2 পরের ১০০ ms-এ” — তাত্ত্বিকভাবে নিখুঁত। কিন্তু তার জন্য প্রত্যেককে জানতে হবে মোট কতজন আছে আর নিজের ক্রম কী। মানে একটি সমন্বয়কারী, যা নিজেই একটি ভাগ করা বিন্দু আর ব্যর্থতার উৎস। Distributed system-এ লাখো মোবাইল ক্লায়েন্ট একে অপরকে চেনেই না। এলোমেলোতা সমন্বয় ছাড়াই সমন্বয়ের কাজটি করে দেয়: প্রত্যেকে স্বাধীনভাবে বাছে, তবু গড়ে সবাই ছড়িয়ে পড়ে।

Marc Brooker-এর ২০১৫ সালের লেখাটি এই পুরোনো শিক্ষাকে API client-এর ভাষায় আবার বলে। তাঁর simulation-এ jitter ছাড়া exponential backoff-ও ঢেউ রেখে দেয়, ঠিক অংশ ৪-এর মতো। Jitter থাকলে কাজ আর সময় দুটিই অনেক কমে। লেখাটির পরে অনেক cloud SDK আর library-তে jitter ডিফল্ট হয়ে যায় (খুঁটিনাটি SDK-ভেদে আলাদা)।

একই নীতি অন্য জায়গাতেও দেখবেন। Cache-এর TTL-এ jitter দেওয়া হয়, যাতে হাজারো key একসাথে মেয়াদোত্তীর্ণ না হয় (পর্ব ২৯)। Raft-এ election timeout এলোমেলো, যাতে দুজন একসাথে নেতা হতে না চায় (পর্ব ৪২)। আর cron job প্রতি ঘণ্টার শুরুতে না চালিয়ে কয়েক মিনিট ছড়িয়ে চালানো হয়।

মূল উপলব্ধি: যেখানে অংশগ্রহণকারীরা একে অপরকে জানে না, সেখানে এলোমেলোতাই সবচেয়ে সস্তা সমন্বয়। একই ঘটনায় একসাথে ব্যর্থ হওয়া মানেই একসাথে ফেরার ঝুঁকি, আর jitter সেই সম্পর্কটি ভাঙে।

প্র ০৩Hedged request p99 নাটকীয়ভাবে কমায় অল্প বাড়তি খরচে — তাহলে সব অনুরোধে কেন hedge করা হয় না? কখন এটি ক্ষতিকর?

Dean আর Barroso-র ২০১৩ সালের প্রবন্ধের মূল পর্যবেক্ষণ হলো, বড় fan-out-এ বিরল ধীরতাও সাধারণ হয়ে যায়। একটি অনুরোধ যদি ১০০টি সার্ভার ছোঁয়, আর প্রতিটি ১% সময় ধীর হয়, তবে পুরো অনুরোধের প্রায় ৬৩% ধীর। প্রতিটি সার্ভারকে নিখুঁত করা অসম্ভব: GC pause, ভাগ করা disk, পেছনের compaction, network-এর queue — এসব থাকবেই। তাই তাঁরা প্রস্তাব করেন সিস্টেমকে “tail-tolerant” করতে, ঠিক যেমন fault-tolerant নকশা নিখুঁত hardware ছাড়াই নির্ভরযোগ্য হয়। প্রবন্ধে বর্ণিত একটি পরীক্ষায় অল্প দেরির পরে hedge পাঠালে খুব কম বাড়তি অনুরোধে p99.9 অনেক গুণ কমেছিল।

কিন্তু hedge কাজ করে তিনটি শর্তে। প্রথম, ধীরতা স্বাধীন হতে হবে। দুটি replica একই কারণে ধীর হলে — একটি ভাগ করা database, বা পুরো cluster-এর overload — দ্বিতীয় কপিও ধীর, লাভ শূন্য, খরচ দ্বিগুণ। দ্বিতীয়, অনুরোধটি idempotent হতে হবে। একটি পড়া দুবার হলে ক্ষতি নেই, কিন্তু টাকা পাঠানোর অনুরোধ দুটি replica-তে কার্যকর হলে বিপদ। তৃতীয়, বাতিল করার উপায় থাকতে হবে। হেরে যাওয়া কপি শেষ পর্যন্ত চললে বাড়তি load ৫% নয়, আরও বেশি।

সবচেয়ে বড় বিপদ overload. সিস্টেম যখন চাপে ধীর, তখন প্রায় সব অনুরোধই p95 পেরোয়। ফলে প্রায় সবাই hedge পাঠায়, load প্রায় দ্বিগুণ, ধীরতা আরও বাড়ে। এটি retry storm-এরই আরেক রূপ (পর্ব ১৮)। তাই ভালো নকশায় hedge-এর একটি সীমা থাকে, যেমন মোট অনুরোধের ৫%. Hedge retry-র সাথে একই budget-এ গোনা হয়, আর চাপ বেশি হলে hedge নিজেই বন্ধ হয়।

Tied request এর একটি সূক্ষ্ম সমাধান। দুই সার্ভারের queue-তে একসাথে রাখা হয়, আর যে আগে কাজ শুরু করে, সে অন্যটিকে বাতিল করায়। Queue-তে অপেক্ষাই যেখানে মূল ধীরতা, সেখানে প্রায় বিনা খরচে tail কমে। দাম হলো সার্ভারের মধ্যে বাড়তি বার্তা আর জটিলতা।

মূল উপলব্ধি: Hedging হলো স্বাধীন ধীরতার বিরুদ্ধে একটি বিমা। পারস্পরিক সম্পর্কযুক্ত ধীরতা বা overload-এ সেই বিমার প্রিমিয়ামই সিস্টেমকে ডুবিয়ে দিতে পারে, তাই এর সীমা আর বন্ধ করার শর্ত নকশায় থাকতে হবে।

প্র ০৪Circuit breaker client-এর library-তে থাকবে, নাকি service mesh-এ? আর Hystrix থেকে resilience4j আর Envoy পর্যন্ত পথে ধারণাটি কীভাবে বদলেছে?

Nygard-এর ২০০৭ সালের বই ধারণাটিকে software-এর ভাষা দেয়। ২০১০-এর দশকের শুরুতে Netflix cloud-এ শত শত সার্ভিসে ভাগ হওয়ার সময় একটি library বানায় — Hystrix — আর ২০১২ সালে open-source করে। তাদের প্রকাশিত লেখা অনুযায়ী মূল উদ্বেগ ছিল ঠিক অংশ ১-এর সমস্যা: একটি ধীর dependency যেন সব thread খেয়ে না ফেলে। তাই Hystrix প্রতিটি dependency-র call আলাদা thread pool-এ চালাত (bulkhead), timeout দিত, breaker রাখত, আর fallback ডাকত — চারটি pattern একসাথে।

পরে দুই দিকে বদল আসে। Library-র দিকে resilience4j আরও হালকা নকশা নেয়: প্রতিটি pattern আলাদা মডিউল, আলাদা thread pool বাধ্যতামূলক নয় (semaphore দিয়েও bulkhead), আর breaker ধীর call-ও গোনে। ২০১৮ সালে Hystrix maintenance mode-এ যায়। Infrastructure-এর দিকে service mesh আসে: Envoy-এর মতো sidecar প্রতিটি call দেখে, তাই ভাষা-নিরপেক্ষভাবে timeout, retry, outlier ejection আর concurrency-সীমা দিতে পারে।

কোথায় বসাবেন, তার উত্তর নির্ভর করে breaker-কে কী জানতে হবে তার ওপর। Mesh দেখে HTTP status আর latency. সে জানে না কোন 500 আসলে ব্যবসার “না”, কোন অনুরোধ idempotent, বা breaker খুললে পাতায় কী দেখাতে হবে। Library এগুলো জানে, কিন্তু প্রতিটি ভাষায় আলাদা করে বানাতে আর মেলাতে হয়। তাই একটি প্রচলিত বিভাজন হলো: instance-স্তরের সমস্যা (একটি খারাপ pod) mesh-এর outlier ejection-এ, আর পুরো dependency-র অসুস্থতা ও fallback অ্যাপের breaker-এ।

নামের গোলমালও এই ইতিহাস থেকে। Envoy-তে “circuit breaker” বলতে concurrency আর queue-র সীমা বোঝায়, যা Hystrix-এর ভাষায় bulkhead-এর কাজ। দুটি স্তরে একই কাজ দুবার থাকলে আরেকটি বিপদ আসে। যেমন mesh-এ ২ বার আর library-তে ৩ বার retry থাকলে অজান্তেই ৯ গুণ।

মূল উপলব্ধি: Pattern স্থির, বাস্তবায়ন বদলায়। যে স্তর ব্যর্থতার ধরন আর ব্যবসার মানে সবচেয়ে ভালো জানে, সিদ্ধান্ত সেখানেই নেওয়া উচিত। আর প্রতিটি নিয়ম যেন শুধু একটি স্তরেই থাকে।

প্র ০৫Production-এ ইচ্ছা করে সার্ভার বন্ধ করা কি দায়িত্বজ্ঞানহীন নয়? Chaos engineering কীভাবে নিরাপদে শুরু করবেন, আর কোন প্রতিষ্ঠানের জন্য এটি এখনো আগাম?

প্রশ্নটি যুক্তিসংগত, আর chaos engineering-এর প্রবক্তারা নিজেরাই এর উত্তর দিয়েছেন। Netflix-এর Chaos Monkey-র পেছনের যুক্তি ছিল সরল: cloud-এ instance এমনিতেই মাঝে মাঝে হারাবে। প্রশ্ন শুধু, সেটি রাত ৩টায় অপ্রস্তুত অবস্থায় হবে, নাকি দুপুর ২টায়, যখন সব প্রকৌশলী অফিসে আর চোখ dashboard-এ। তাই tool-টি কাজের সময়ে চলত। আসল ব্যর্থতা সময় বেছে আসে না; পরীক্ষার ব্যর্থতা আসে আপনার বেছে নেওয়া সময়ে, ছোট পরিসরে, থামানোর বোতামসহ।

অগ্নিনির্বাপক দলের সাথে তুলনাটি প্রায়ই টানা হয়। Amazon-এ GameDay অনুশীলনের প্রবর্তক হিসেবে পরিচিত Jesse Robbins প্রকাশিত বক্তৃতায় অগ্নিনির্বাপণের মহড়ার অভিজ্ঞতা থেকে এই ধারণার কথা বলেছেন। মহড়ায় আসল আগুন লাগে না, কিন্তু সিঁড়ি, পাম্প আর মানুষের প্রতিক্রিয়া সত্যিই পরীক্ষা হয়।

নিরাপদে শুরুর একটি ধাপে ধাপে পথ:

  • প্রথমে কাগজে: একটি tabletop আলোচনায় “Payment ৫ মিনিট বন্ধ হলে কী হবে” ধাপে ধাপে ভাবুন।
  • তারপর staging-এ fault injection — dependency-তে দেরি যোগ করে timeout আর breaker সত্যিই কাজ করে কি না দেখুন।
  • তারপর production-এ ১% traffic বা একটি cell-এ। সাথে থাকুক steady-state সংখ্যা, স্বয়ংক্রিয় থামানোর শর্ত আর ঘোষিত সময়।
  • শেষে নিয়মিত, স্বয়ংক্রিয় পরীক্ষা।

কিছু প্রতিষ্ঠানের জন্য এটি আগাম। যার monitoring দুর্বল, সে পরীক্ষার ফলই দেখতে পাবে না। যার একটি instance হারালেই সেবা বন্ধ হয় (পর্ব ৫-এর SPOF), তার আগে redundancy দরকার, chaos নয়। আর যার কাছে কোনো error budget বাকি নেই, তার প্রথম কাজ পরিচিত সমস্যা সারানো। Chaos engineering অজানা দুর্বলতা খোঁজার হাতিয়ার, জানা দুর্বলতা নিশ্চিত করার নয়।

মূল উপলব্ধি: ব্যর্থতা ঘটবেই; বেছে নেওয়ার বিষয় শুধু কখন আর কতটা নিয়ন্ত্রণে। ছোট, ঘোষিত, পর্যবেক্ষিত পরীক্ষা বড়, অঘোষিত বিভ্রাটের চেয়ে অনেক সস্তা শিক্ষক।

অনুশীলন

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

  1. Timeout আর backoff-এর হিসাব: একটি সার্ভিস সেকেন্ডে ৪০০ বার একটি dependency ডাকে। Dependency-র স্বাভাবিক latency: গড় প্রায় ৩০ ms, p99 ১২০ ms, p99.9 ৪০০ ms. এই dependency-র জন্য bulkhead-এ ৬০টি thread. (ক) স্বাভাবিক দিনে কতগুলো thread ব্যস্ত? (খ) Dependency পুরো ঝুলে গেলে timeout ২ সেকেন্ড হলে কতগুলো thread দরকার? (গ) ৬০টি thread-এ টিকতে হলে timeout সর্বোচ্চ কত হতে পারে, আর তা p99 ও p99.9-এর তুলনায় কোথায়? ৪৫০ ms timeout রাখতে চাইলে ভাগ কত বড় হতে হবে? (ঘ) Full jitter-এ base ১০০ ms আর cap ২ সেকেন্ড হলে ৩য় আর ৫ম retry-র আগে অপেক্ষার পরিসর আর গড় কত? Equal jitter-এ ৫ম retry-র?
    • (ক) L = λW = ৪০০ × ০.০৩ = ১২টি — ৬০-এর মধ্যে আরামে।
    • (খ) ৪০০ × ২ = ৮০০টি; ৬০টির ভাগ প্রায় ৬০ ÷ ৪০০ = ০.১৫ সেকেন্ডে পূর্ণ। Bulkhead না থাকলে এটিই পুরো সার্ভিসের thread খেত।
    • (গ) Timeout ≤ ৬০ ÷ ৪০০ = ০.১৫ সেকেন্ড = ১৫০ ms. এটি p99 (১২০)-এর ওপরে কিন্তু p99.9 (৪০০)-এর নিচে, তাই স্বাভাবিক দিনেও ১%-এর কম, কিন্তু ০.১%-এর বেশি call কাটা পড়বে — একবারের retry বা hedge (budget-এর মধ্যে) সেগুলো ঢাকতে পারে। ৪৫০ ms রাখতে চাইলে ভাগ লাগবে ৪০০ × ০.৪৫ = ১৮০টি thread; না হলে breaker-কে দ্রুত খুলতে দিন, যাতে ঝুলে থাকার সময়টুকু ছোট হয়।
    • (ঘ) Full jitter-এ অপেক্ষা rand(০, min(cap, base × 2k)). k = ৩: min(২, ০.৮) = ০.৮, তাই ০ থেকে ০.৮ সেকেন্ড, গড় ০.৪। k = ৫: min(২, ৩.২) = ২, তাই ০ থেকে ২ সেকেন্ড, গড় ১.০। Equal jitter-এ k = ৫: ২ ÷ ২ + rand(০, ১), অর্থাৎ ১ থেকে ২ সেকেন্ড, গড় ১.৫।
  2. একটি নকশার সমালোচনা করুন: একটি কাল্পনিক অনলাইন বাস-টিকিট সেবার বর্তমান নিয়ম: (১) app-এর timeout ২ সেকেন্ড, booking সার্ভিসের ৫ সেকেন্ড, payment সার্ভিসের ১০ সেকেন্ড; (২) প্রতিটি স্তর যেকোনো ত্রুটিতে সঙ্গে সঙ্গে ৩ বার retry করে, POST /payments-সহ, কোনো idempotency key নেই; (৩) booking সার্ভিসের সব dependency একটি ২০০ thread-এর pool ভাগ করে; (৪) circuit breaker একটি ব্যর্থতাতেই খোলে, আর ৫ মিনিট খোলা থাকে; (৫) liveness probe database আর payment provider দুটিই যাচাই করে; (৬) সুপারিশ না এলে fallback হলো database-এ পুরো রুট-তালিকার একটি query. প্রতিটির ঝুঁকি বলুন আর একটি ভালো বিকল্প দিন।
    • (১) উল্টো timeout: app চলে যাওয়ার পরেও নিচে ৮ সেকেন্ড কাজ। প্রতিকার: deadline propagation, ছোট per-try timeout, বাকি সময় কম হলে শুরুই না।
    • (২) গুণফল আর দুবার টাকা: ৪৩ = ৬৪ গুণ, jitter নেই বলে ঢেউ, 400-এও অকারণ চেষ্টা, আর payment-এর retry-তে দুবার কাটার ঝুঁকি। প্রতিকার: এক স্তরে, ক্ষণিকের ত্রুটিতে, full jitter আর ১০% budget-সহ retry, আর প্রতিটি পেমেন্টে idempotency key (পর্ব ৮, ৬০)।
    • (৩) ভাগ করা pool: একটি ধীর dependency সব thread খাবে। প্রতিকার: Little-এর সূত্রে মাপা আলাদা ভাগ।
    • (৪) অতি-সংবেদনশীল breaker: একটি ত্রুটিতেই ৫ মিনিটের বিভ্রাট। প্রতিকার: sliding window, সর্বনিম্ন call-সংখ্যা, ৫০% সীমা, কয়েক সেকেন্ডের cool-down আর probe.
    • (৫) Deep liveness: payment provider ধীর হলেই সব pod একসাথে restart. প্রতিকার: liveness শুধু নিজের process; বাইরের dependency সামলাবে timeout আর breaker.
    • (৬) দামি fallback: সুপারিশ বিকল হলেই database-এ ভারী query-র ঢেউ। প্রতিকার: cache বা CDN-এ রাখা জনপ্রিয় রুটের ছোট স্থির তালিকা।
  3. আউটপুট অনুমান করুন: অংশ ৬-এর circuit_breaker.js-এ COOL = 5 বদলে COOL = 2 করলে (বাকি সব একই, seed-ও) কী বদলাবে? (ক) প্রথম OPEN কি একই সময়ে হবে? (খ) Half-open-এর চক্র বাড়বে না কমবে? (গ) বিভ্রাটের পরে অকারণ fallback (“after it ended”) বাড়বে না কমবে? (ঘ) পাঠানো call আর thread-এর অপেক্ষা কোন দিকে? আগে লিখে রাখুন, তারপর চালিয়ে মেলান।
     11.34 s  CLOSED    -> OPEN      failure rate 50% of last 20
     13.34 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     14.34 s  HALF_OPEN -> OPEN      a probe failed
     16.35 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     17.35 s  HALF_OPEN -> OPEN      a probe failed
     19.37 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     20.37 s  HALF_OPEN -> OPEN      a probe failed
     22.37 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     23.37 s  HALF_OPEN -> OPEN      a probe failed
     25.40 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     26.40 s  HALF_OPEN -> OPEN      a probe failed
     28.42 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     29.42 s  HALF_OPEN -> OPEN      a probe failed
     31.43 s  OPEN      -> HALF_OPEN cool-down of 2 s over
     31.50 s  HALF_OPEN -> CLOSED    3 probes succeeded
    calls sent 2038; short-circuited: 941 during the outage, 75 after it ended
    thread-seconds spent waiting: 120 with the breaker, about 1062 without it
    • (ক) হ্যাঁ, ১১.৩৪ সেকেন্ডে — খোলার আগে cool-down-এর কোনো ভূমিকা নেই, আর seed একই বলে call-ধারাও একই।
    • (খ) বাড়বে: প্রতি চক্র এখন প্রায় ২ + ১ = ৩ সেকেন্ড (cool-down আর probe-এর ১ সেকেন্ডের timeout), তাই ১১.৩৪ থেকে ৩০ সেকেন্ডের মধ্যে ৬টি ব্যর্থ half-open — আগে ছিল ৩টি।
    • (গ) কমবে: শেষ ব্যর্থ probe ২৯.৪২-এ, পরের পরীক্ষা ৩১.৪৩-এ, closed ৩১.৫০-এ। অকারণ fallback ২২৭ থেকে নেমে ৭৫।
    • (ঘ) পাঠানো call ১,৮৯০ থেকে ২,০৩৮, অপেক্ষা ১০৮ থেকে ১২০ সেকেন্ড — বেশি probe অসুস্থ dependency-তে গেছে, আর আগে closed হয়েছে। Trade-off: ছোট cool-down দ্রুত ফেরে, কিন্তু অসুস্থ dependency-কে বেশি খোঁচায়; তাই কিছু library ব্যর্থ পরীক্ষার পরে cool-down বাড়াতে দেয়।
  4. একটি স্তরে retry — হিসাব আর কোড: অংশ ৩-এর retry_amplification.js-এ শুধু সবচেয়ে নিচের caller (service → database) ৩ বার চেষ্টা করুক, ওপরের দুটি স্তর একবার। (ক) আগে হাতে হিসাব করুন: database-এর ব্যর্থতা p = ০.৫, ০.৯ আর ১ হলে ব্যবহারকারীপ্রতি database-চেষ্টা আর সফলের হার কত (budget ছাড়া)? (খ) কোডের কোন লাইন কীভাবে বদলাবেন? (গ) চালিয়ে মেলান, আর তিন স্তরের ২৭ গুণের সাথে তুলনা করুন।
    • (ক) শুধু নিচের স্তরে চেষ্টা: E = ১ + p + p², সফল = ১ − p³। p = ০.৫: ১.৭৫ গুণ, ৮৭.৫%; p = ০.৯: ১ + ০.৯ + ০.৮১ = ২.৭১ গুণ, ২৭.১%; p = ১: ৩ গুণ, ০%.
    • (খ) loop-এর শর্তটি স্তর দেখে করুন: attempt <= (layer === LAYERS - 1 ? 3 : 1).
    • (গ) চালালে budget ছাড়া কলামে পাওয়া যায় p = ০.৫-এ ১৭৬৬ (১.৭৭ গুণ, ৮৭.৬%), ০.৯-এ ২৭৪৬ (২.৭৫ গুণ, ২৪.৭%), ১-এ ঠিক ৩০০০ (৩ গুণ, ০%) — হাতের হিসাবের কাছাকাছি। পুরো বিকল database এখন ২৭ গুণের বদলে ৩ গুণ চাপ পায়; দাম, ব্যাপক ব্যর্থতায় সফল কম (p = ০.৯-এ ৯৪.৫% থেকে ২৪.৭%), যা পূরণ করে fallback আর ভালো replica বাছাই, গুণফল নয়।

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

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

ক্রমএকটি dependency বিকল হয়ে আবার সেরে ওঠার সময় circuit breaker-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🟢 Closed: সব call যায়, শেষ কয়েকটির ফল sliding window-এ গোনা হয়
  2. 📉 Window-এ ব্যর্থতার হার সীমা ছোঁয়, সর্বনিম্ন call-সংখ্যাও পূর্ণ
  3. 🔴 Open: কোনো call যায় না, সবাই সঙ্গে সঙ্গে fallback পায়
  4. ⏳ Cool-down শেষ, half-open: অল্প কয়েকটি probe যায়
  5. ✅ সব probe সফল, breaker আবার closed

একটি REST API client-এর কোন উত্তরে স্বয়ংক্রিয়ভাবে আবার চেষ্টা করা যুক্তিসংগত?

  • 400 Bad Request
  • 503 Service Unavailable, সাথে Retry-After: 5
  • 401 Unauthorized
  • 422 Unprocessable Content

App → API → service → database. তিনটি caller-এর প্রত্যেকে নিচের স্তরকে সর্বোচ্চ ৩ বার চেষ্টা করে (১টি + ২টি retry). Database পুরো বিকল হলে একটি ব্যবহারকারী-অনুরোধে database সর্বোচ্চ কতগুলো চেষ্টা পায়?

  • ৩টি
  • ৯টি
  • ২৭টি
  • ৬৪টি

Full jitter backoff-এ base = ১০০ ms আর cap = ১০ সেকেন্ড। ৪র্থ retry-র (k = ৪) আগে অপেক্ষা কোন পরিসরে, আর গড়ে কত?

  • ০ থেকে ১.৬ সেকেন্ড, গড় ০.৮
  • ০.৮ থেকে ১.৬ সেকেন্ড, গড় ১.২
  • ঠিক ১.৬ সেকেন্ড
  • ০ থেকে ১০ সেকেন্ড, গড় ৫

একটি dependency-র জন্য bulkhead-এ ২০টি thread, অপেক্ষার queue নেই। Call আসে সেকেন্ডে ২০০টি, আর dependency এখন প্রতিটি call-এ ১ সেকেন্ড নিচ্ছে (timeout-এর নিচে)। মোটামুটি কত শতাংশ call thread পাবে?

  • প্রায় ১০০%
  • প্রায় ১০%
  • প্রায় ৫০%
  • প্রায় ০%

একটি সার্ভিসের প্রতিটি pod-এর Kubernetes liveness probe একটি ভাগ করা database-এ query চালায়। Database কয়েক মিনিট ধীর হলে সবচেয়ে সম্ভাব্য ফল কী?

  • শুধু একটি pod traffic থেকে সরে যাবে
  • কিছুই হবে না, কারণ pod-গুলো নিজেরা সুস্থ
  • সব pod প্রায় একসাথে restart হবে, যা সমস্যা না সারিয়ে বিভ্রাট বাড়াবে
  • Database নিজে থেকে দ্রুত সেরে উঠবে

একটি সার্ভিসের ২০০টি thread. সেকেন্ডে ৫০০ বার একটি dependency ডাকা হয়। Dependency পুরো ঝুলে গেলেও যাতে এই call-গুলো ১০০টির বেশি thread দখল না করে, timeout সর্বোচ্চ কত হতে পারে?

  • ২০০ ms
  • ৪০০ ms
  • ১ সেকেন্ড
  • যেকোনো মান, কারণ timeout thread-এর সংখ্যা বদলায় না

শব্দকোষ

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

মনে রাখুন

  1. ধীর মৃতের চেয়ে খারাপ: ব্যস্ত thread = λ × W; একটি ধীর dependency pool ফুরিয়ে সুস্থ কাজও থামায়, আর ব্যর্থতা শিকল বেয়ে ওপরে ওঠে — cascading failure.
  2. প্রতিটি call-এ timeout, পুরো অনুরোধে deadline: timeout স্বাভাবিক p99/p99.9-এর একটু ওপরে, কিন্তু ≤ ভাগের thread ÷ λ আর ≤ বাকি deadline; শিকলের নিচে ছোট, আর deadline নিচে পৌঁছায়।
  3. Retry: শুধু ক্ষণিকের ত্রুটিতে আর idempotent কাজে; একটি স্তরে (n স্তরে k বার মানে kn), retry budget-এ বাঁধা, exponential backoff আর jitter-সহ — jitter ছাড়া একসাথে ব্যর্থরা একসাথে ফেরে।
  4. Hedge, breaker, bulkhead: hedged request স্বাধীন ধীরতায় tail ছাঁটে (overload-এ বন্ধ); circuit breaker closed → open → half-open-এ অসুস্থ dependency-কে বিশ্রাম দেয়; bulkhead আর cell ব্যর্থতাকে এক কক্ষে আটকায়।
  5. বিকল্প আর পরীক্ষা: প্রতিটি dependency-র fallback আগে থেকে লেখা আর সস্তা; liveness অগভীর; আর chaos engineering, fault injection ও game day দিয়ে ছোট blast radius-এ নিয়মিত যাচাই।

পরের পর্বে: কেউ যদি সেকেন্ডে হাজার বার ডাকে? পর্ব ৪৯-এ প্রতিটি client-এর সীমা টানার algorithm — token bucket, leaky bucket আর sliding window — আর অনেক সার্ভারে সেই সীমা মেলানো।

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

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