এই পর্বে যা শিখবেন
- Overload কেন ধস নামায় — throughput বনাম goodput, retry storm আর congestion collapse-এর বক্ররেখা
- Queueing-এর হিসাবে ধস কখন শুরু হয়, bounded queue কত বড় হবে, আর ফেরানোর খরচ কেন গুরুত্বপূর্ণ
- Backpressure-এর কৌশল — blocking producer, bounded channel, TCP flow control, request(n), pull বনাম push
- Load shedding-এর নকশা — অগ্রাধিকার আর খরচ দেখে ফেরানো, CoDel-ধাঁচের queue, adaptive LIFO আর concurrency limit
- Retry budget, deadline, graceful degradation, brownout, metastable failure আর load test-এ “হাঁটু” খোঁজা
১ · Overload কী — সিস্টেম কেন ধীরে না নেমে হঠাৎ ভেঙে পড়ে
একটি ফোনে-অর্ডার-নেওয়া রেস্তোরাঁর রান্নাঘর ঘণ্টায় ৬০টি খাবার বানাতে পারে। ঈদের আগের রাতে ঘণ্টায় ৯০টি অর্ডার এলো। রান্নাঘর একটু ধীর হলো না — প্রতি ঘণ্টায় ৩০টি অর্ডার জমতে লাগল। অনেক ক্রেতা অপেক্ষা না করে অর্ডার বাতিল করলেন, কিন্তু স্লিপগুলো হুকে ঝুলেই রইল, আর বাবুর্চি সেগুলোও রাঁধছেন। খাবার তৈরি হচ্ছে, নেওয়ার কেউ নেই।
সফটওয়্যারে একে বলে overload: কাজ আসার হার λ সামর্থ্য μ-এর চেয়ে বেশি, অল্প সময়ের জন্য হলেও। ব্যস্ততা ρ = ০.৯ মানে কষ্টে হলেও সামলানো যাচ্ছে (পর্ব ৪: গড় response time তখন service time-এর দশ গুণ)। কিন্তু ρ > ১ হলে কোনো স্থির অবস্থাই নেই: লাইন শুধু বাড়ে।
Throughput = প্রতি সেকেন্ডে কতগুলো কাজ শেষ হলো। Goodput = তার মধ্যে কতগুলো কাজে লাগল — ক্লায়েন্ট হাল ছাড়ার আগেই সফল উত্তর। Overload-এ throughput সর্বোচ্চে থাকে, অথচ goodput শূন্যের দিকে নামতে পারে: সার্ভার এমন কাজে ব্যস্ত, যার ফল কেউ নেবে না।
নিচের ছবিটি এই পর্বের মূল ছবি: ক্ষমতা সেকেন্ডে প্রায় ১০০, ক্লায়েন্ট ১ সেকেন্ডে হাল ছাড়ে, আর চাপ ৫০ থেকে ২০০। সংখ্যাগুলো অংশ ২-এর simulator থেকে।
Congestion collapse-এর বক্ররেখা — goodput বনাম আসা চাপ
Goodput vs offered load: capacity ≈ 100 req/s, client timeout 1 s (simulated, 30 s per load level)
এই নিচে নামা ঢালের নাম congestion collapse. Overload কোনো একদিন হবেই; লক্ষ্য হলো ক্ষমতার পরে বক্ররেখা সমতল রাখা — বাড়তিরা ফেরত যাক, গৃহীতরা সময়মতো শেষ হোক।
কেন ধস — তিনটি ফিরতি চক্র
- নষ্ট কাজ: অপেক্ষা ক্লায়েন্টের timeout ছাড়ায়; ক্লায়েন্ট চলে যায়, কিন্তু সার্ভার পুরোনো অনুরোধে সময় খরচ করতেই থাকে।
- বাড়তি চেষ্টা: timeout হলে আবার চেষ্টা; একই চাহিদা কয়েক গুণ অনুরোধ হয়ে ফেরে — retry storm.
- কাজের দাম বাড়ে: ভিড়ে garbage collection, thread-এর পালাবদল, lock-এর লাইন আর নতুন TLS সংযোগে μ নিজেই কমে। অপারেটিং সিস্টেমে এর চেনা রূপ thrashing (Operating Systems · thrashing আর working set).
নিচের stepper-এ চক্রটি দেখুন: ক্ষমতা সেকেন্ডে ১০০, timeout ১ সেকেন্ড, ব্যর্থ হলে সর্বোচ্চ ৩ বার retry.
- চাপ
λ বনাম μকত আসছে - Queue লম্বা
অপেক্ষা ↑লাইন জমে - Timeout
goodput ↓নষ্ট কাজ - Retry
চাপ ↑↑একই চাহিদা ৪ গুণ - আটকে যাওয়া
metastableকারণ নেই, ধস আছে - চক্র ভাঙা
budget + বাদফিরে আসা
| নতুন চাহিদা (প্রতি সেকেন্ডে) | |
| Retry (প্রতি সেকেন্ডে) | |
| সার্ভারে মোট আসছে | |
| Queue-র অবস্থা | |
| নতুন অনুরোধের অপেক্ষা | |
| Goodput (সময়মতো সফল/সেকেন্ড) |
- স্বাভাবিক দিন। সেকেন্ডে ৮০টি, ক্ষমতা ১০০, ρ = ০.৮। পর্ব ৪-এর M/M/1-এ গড় response time 1/(100 − 80) সেকেন্ড = ৫০ ms, ভেতরে গড়ে ৪টি। Goodput = ৮০।
- চাহিদা ১১০। শেষ হয় ১০০, তাই প্রতি সেকেন্ডে ১০টি জমে। ১০ সেকেন্ডে জমা প্রায় ১০০, নতুনের অপেক্ষা ১০০ ÷ ১০০ = ১ সেকেন্ড। এতক্ষণ উত্তর সময়মতো, goodput প্রায় ১০০।
- Timeout শুরু। অপেক্ষা ১ সেকেন্ড ছাড়াল; প্রতিটি উত্তর পৌঁছায় ক্লায়েন্ট চলে যাওয়ার পরে। Throughput ১০০, goodput প্রায় শূন্য — নষ্ট কাজ।
- Retry যোগ হলো। প্রতিটি ব্যর্থ অনুরোধ আরও ৩ বার চেষ্টা করে: ১১০ × ৪ = প্রায় ৪৪০, যার প্রায় ৩৩০টি retry. জমা বাড়ে সেকেন্ডে ৩৪০; আরও ১০ সেকেন্ডে প্রায় ৩,৫০০, অপেক্ষা প্রায় ৩৫ সেকেন্ড।
- ধাক্কা গেল, ধস রইল। চাহিদা আবার ৮০, কিন্তু সবাই এখনো ৪ বার চেষ্টা করে: ৮০ × ৪ = ৩২০, যার প্রায় ২৪০টি retry; জমা বাড়ছে সেকেন্ডে ২২০। কারণ নেই, retry নিজেই চাপ ধরে রেখেছে — metastable failure.
- চক্র ভাঙা। Retry budget-এ retry নতুনের ১০%-এর মধ্যে: সর্বোচ্চ ৮টি, মোট ৮৮ — ক্ষমতার নিচে। আর সার্ভার কাজ শুরুর আগে দেখে অনুরোধের ১ সেকেন্ডের মেয়াদ পেরিয়েছে কি না; পেরোলে বিনা খরচে বাদ, তাই জমা মুহূর্তে সরে। গড় response time ৫০ ms (চাপ ৮০) থেকে ৮৩ ms (চাপ ৮৮)। অংশ ৬-এর simulator এটি মাপে।
“Congestion collapse” কথাটি John Nagle-এর ১৯৮৪ সালের RFC 896-এর। ১৯৮৬ সালে ইন্টারনেট সত্যিই এভাবে ধসে পড়ে: link পুরো ব্যস্ত, অথচ কাজের ফল প্রায় হাজার গুণ কম, কারণ বেশিরভাগই একই packet-এর বারবার পাঠানো কপি। ১৯৮৮ সালে Van Jacobson আর Michael Karels-এর congestion control এর সমাধান করে (Computer Networks · TCP congestion control). Retransmission আর retry একই প্রাণী, ভিন্ন স্তরে।
২ · Queueing-এর চোখে overload — অসীম queue একটি bug
পর্ব ৪-এ M/M/1 আর Little-এর সূত্র দেখেছেন: ρ < ১ হলে গড় সময় W = S/(1 − ρ)। ρ > ১ হলে সূত্রটি আর খাটে না, কারণ কোনো স্থির গড়ই নেই। তখন সরল একটি হিসাব কাজে আসে।
- প্রতি সেকেন্ডে আসে λ, শেষ হয় μ (λ > μ)। তাই t সেকেন্ড পরে জমা B(t) ≈ (λ − μ) × t.
- এই মুহূর্তে আসা অনুরোধকে (FIFO-তে) আগের সবার কাজ শেষ হওয়া পর্যন্ত অপেক্ষা করতে হবে: অপেক্ষা ≈ B(t) ÷ μ = (λ − μ) × t ÷ μ।
- ক্লায়েন্টের timeout D হলে অপেক্ষা D ছোঁয় সেই মুহূর্তে: T* = D × μ ÷ (λ − μ)। এর পর থেকে প্রতিটি উত্তর দেরিতে পৌঁছায় — goodput প্রায় শূন্য, যদিও throughput μ।
μ = ১০০, D = ১ সেকেন্ড ধরলে: λ = ১২০-এ T* = ১০০ ÷ ২০ = ৫ সেকেন্ড; λ = ১৫০-এ ২ সেকেন্ড; λ = ২০০-এ মাত্র ১ সেকেন্ড। অর্থাৎ চাপ যত বেশি, ধস তত দ্রুত।
অসীম বা খুব বড় queue তাই একটি bug. কারণগুলো: জমা অনুরোধ memory খায়, একসময় process crash করে আর সব কাজ একসাথে হারায়। FIFO-তে সবচেয়ে পুরোনো অনুরোধ আগে সেবা পায় — ঠিক যাদের ক্লায়েন্ট আগে চলে গেছে। চাপ কমলেও জমা সরে কেবল μ − λ দিয়ে: ৩,০০০টি জমা আর বাড়তি ক্ষমতা সেকেন্ডে ২০ হলে আড়াই মিনিট। আর কেউ ফেরত পায় না বলে ত্রুটির হার শূন্য দেখায়, alert বাজে দেরিতে।
রেলস্টেশনের কাউন্টারে আজকের সিট শেষ হলে বুদ্ধিমান কর্মী জানালায় লিখে দেন “আজকের টিকিট শেষ”। পেছনের ৫০০ জন দুই ঘণ্টা দাঁড়িয়ে খালি হাতে ফেরেন না; এক মিনিটেই জেনে বাসস্ট্যান্ডে রওনা দেন। দ্রুত বলা “না”-ও একটি সেবা — এটিই fail fast.
Queue কত বড় হবে — memory নয়, সময় দিয়ে মাপুন
Queue-র কাজ ক্ষণিকের ভিড় শোষণ, overload জমানো নয়। Little-এর সূত্র থেকে: K ≈ μ × সর্বোচ্চ গ্রহণযোগ্য অপেক্ষা। ক্ষমতা ১০০ আর অপেক্ষা সর্বোচ্চ ০.৫ সেকেন্ড হলে K ≈ ৫০। এর বেশি জমা মানে নতুন অনুরোধ এমনিতেই সময়মতো শেষ হবে না, তাই এখনই জানানো ভালো।
ধরা যাক একটি অনুরোধের কাজের সময় S, আর একটি অনুরোধ ফেরাতে লাগে তার c অংশ (connection নেওয়া, TLS, header পড়া, log লেখা)। আসছে λ, গ্রহণ করা হলো a. সার্ভারের পুরো এক সেকেন্ড খরচ হলে:
S = ১০ ms আর c = ০.১ ধরলে: λ = ২০০-এ a ≈ ৮৮.৯; λ = ৫০০-এ a ≈ ৫৫.৬; λ = ১,০০০-এ a = ০ — পুরো সময় “না” বলতেই যায়। তাই ফেরান যত আগে আর যত সস্তায় সম্ভব: database আর বড় body-র আগে, পারলে load balancer বা gateway-তে; দরজাও ভেসে গেলে ক্লায়েন্টকেই কম পাঠাতে হয় (অংশ ৫)।
এবার তিন নকশা পাশাপাশি চালান: ক্ষমতা ১০০ (গড় service ১০ ms, এলোমেলো), timeout ১ সেকেন্ড, প্রতিটি চাপে ৩০ সেকেন্ড, ৩০% অনুরোধ জরুরি। নকশা: (১) অসীম queue; (২) ভেতরে সর্বোচ্চ ৫০, তারপর সঙ্গে সঙ্গে 503; (৩) অগ্রাধিকার — সাধারণ অনুরোধ ভেতরে ২০টি থাকলেই ফেরত, জরুরি ৫০ পর্যন্ত।
// Overload: one server, capacity 100 req/s (mean service 10 ms), client timeout 1 s.
// Each load level runs 30 s from empty. 30% of requests are critical (e.g. payments).
function rng(seed) { // mulberry32: seeded, repeatable
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 T = 30, TIMEOUT = 1, S = 0.010;
const LIMIT = { // most requests allowed inside the server: [critical, other]
"unbounded queue": [Infinity, Infinity],
"bounded queue, fail fast": [50, 50],
"priority shedding": [50, 20] };
function run(policy, load) {
const rand = rng(42), exp = (mean) => -Math.log(1 - rand()) * mean;
const fin = [], lat = []; let head = 0, t = 0, free = 0, rej = 0, crit = 0, critOk = 0;
while ((t += exp(1 / load)) < T) {
const isCrit = rand() < 0.3; crit += isCrit;
while (head < fin.length && fin[head] <= t) head++; // who has left by now
if (fin.length - head >= LIMIT[policy][isCrit ? 0 : 1]) { rej++; continue; } // 503 now
free = Math.max(free, t) + exp(S); fin.push(free); // FIFO: wait, then serve
if (free - t <= TIMEOUT && free <= T) { lat.push(free - t); critOk += isCrit; } // in time
} // (late answers = wasted work)
lat.sort((a, b) => a - b);
const p99 = lat.length ? lat[Math.ceil(0.99 * lat.length) - 1] * 1000 : 0;
return [load, (lat.length / T).toFixed(1), p99.toFixed(0), rej,
(100 * critOk / crit).toFixed(0) + "%", fin.length - head];
}
Object.keys(LIMIT).forEach((policy, k) => {
console.log((k ? "\n" : "") + policy + "\nload good/s p99 ms rejected critical ok queue at 30 s");
for (const load of [50, 80, 100, 120, 150, 200]) {
const r = run(policy, load), w = [4, 8, 8, 10, 13, 15];
console.log(r.map((v, i) => String(v).padStart(w[i])).join(""));
}
});আউটপুট:
unbounded queue
load good/s p99 ms rejected critical ok queue at 30 s
50 51.2 105 0 100% 3
80 80.7 231 0 100% 2
100 99.4 852 0 100% 10
120 20.2 996 0 16% 511
150 7.8 992 0 4% 1372
200 5.8 934 0 2% 2853
bounded queue, fail fast
load good/s p99 ms rejected critical ok queue at 30 s
50 51.2 105 0 100% 3
80 80.7 231 0 100% 2
100 97.3 568 16 99% 27
120 101.9 644 429 87% 46
150 95.1 683 1580 64% 50
200 98.4 669 2956 50% 50
priority shedding
load good/s p99 ms rejected critical ok queue at 30 s
50 51.2 105 0 100% 3
80 79.0 226 11 100% 1
100 93.0 279 162 100% 4
120 98.8 288 599 99% 19
150 96.2 317 1524 99% 22
200 99.4 304 2821 100% 20
অসীম queue: ১২০-এ goodput ২০.২। প্রমাণের T* = ৫ সেকেন্ড; সেই ৫ সেকেন্ডে আসা প্রায় ৬০০টিই সফল, ৩০ সেকেন্ডে ভাগ করলে প্রায় ২০ — হিসাব মিলল। ২০০-এ goodput ৫.৮, শেষে ২,৮৫৩টি লাইনে। কেউ ফেরত পায়নি, প্রায় কেউ সেবাও পায়নি।
Bounded queue: চাপ দ্বিগুণেও goodput ৯৫–১০২ (এলোমেলো service-এ কখনো ১০০-এর একটু বেশি), p99 প্রায় ৫৭০–৬৮০ ms (৫০ × ১০ ms অপেক্ষা আর কাজ)। কিন্তু দরজা অন্ধ: ২০০-এ জরুরিরও অর্ধেক ফেরত (critical ok ৫০%).
অগ্রাধিকার: একই goodput, জরুরি ৯৯–১০০% সফল, p99 প্রায় ৩০০ ms. দাম: চাপ ৮০-তেও ১১টি সাধারণ অনুরোধ ফেরত, কারণ এলোমেলো ভিড়ে মাঝে মাঝে ২০টি জমে। অনুশীলন ৪-এ [50, 50]-কে [10, 10] করে আগে অনুমান করুন।
৩ · Backpressure — “একটু থামুন” বার্তা উৎস পর্যন্ত পৌঁছানো
Bounded queue একটি সীমা টানে। কিন্তু queue ভরে গেলে উৎসকে কী বলা হবে? এখানেই backpressure: পেছনের ধীর অংশের চাপ উল্টো দিকে সামনে পৌঁছে দেওয়া, যাতে উৎস কম পাঠায় বা থেমে থাকে।
রান্নাঘরের স্লিপ-হুকে ১০টির জায়গা। হুক ভরলে বাবুর্চি ওয়েটারকে বলেন, “এখন আর না।” ওয়েটার নতুন টেবিলে বলেন, “২০ মিনিট লাগবে, অপেক্ষা করবেন?” চাপ রান্নাঘর থেকে ওয়েটারে, ওয়েটার থেকে ক্রেতায় পৌঁছাল; কেউ অনন্তকাল বসে রইলেন না, রান্নাঘরও ভাঙল না।
Backpressure-এর চেনা রূপ
| কৌশল | কীভাবে থামায় | কোথায় দেখবেন |
|---|---|---|
| Blocking producer | Queue ভরা থাকলে put() জায়গা খালি হওয়া পর্যন্ত অপেক্ষা করে, তাই উৎসও থেমে থাকে | Java-র BlockingQueue; OS-এর producer-consumer সমস্যা (Operating Systems) |
| Bounded channel | নির্দিষ্ট ধারণক্ষমতার channel; ভরলে পাঠানো আটকে যায়, বা “try” ধরনের পাঠানো সঙ্গে সঙ্গে ব্যর্থ হয় | Go-র buffered channel, নানা ভাষার async channel |
| TCP flow control | প্রাপক ছোট receive window জানায়; window শূন্য হলে প্রেরক থামে | প্রতিটি TCP সংযোগ (Computer Networks · TCP flow control) |
| HTTP/2 আর gRPC-র flow control | প্রতিটি stream আর সংযোগের নিজস্ব window | Streaming RPC (পর্ব ৯) |
Reactive Streams-এর request(n) | গ্রাহক জানায় “আর n-টি নিতে পারব”; উৎস তার বেশি পাঠায় না | RxJava, Project Reactor, Akka Streams; Java 9-এর Flow API (খুঁটিনাটি লাইব্রেরিভেদে আলাদা) |
| Pull model | গ্রাহক প্রস্তুত হলে তবেই কাজ টেনে নেয় | Queue থেকে worker-এর কাজ তোলা; log-ভিত্তিক stream-এ consumer-এর poll (পর্ব ৩১, ৩৩) |
Pull বনাম push: push-এ উৎস নিজের গতিতে পাঠায়, তাই গ্রাহককে সংকেত দিতে হয় (window, credit, request(n)). Pull-এ backpressure আপনাআপনি: ধীর গ্রাহক কম টানে, আর জমা থাকে মাঝের queue-তে, যা সীমিত বা disk-এ টেকসই হতে হবে (message queue, পর্ব ৩১)। Disk-এ জমা ইমেইল পাঠানোর মতো পেছনের কাজে চলে, স্ক্রিনের সামনে অপেক্ষারত ব্যবহারকারীর অনুরোধে নয়।
নিচের ছবিতে অনুরোধ নিচে নামে (নীল), backpressure-এর সংকেত ধাপে ধাপে ওপরে ওঠে (কমলা ড্যাশ)।
অনুরোধ নিচে নামে, backpressure ওপরে ওঠে
Client → API (admission control) → bounded queue → workers → database
এবার সংখ্যায়: প্রতিটি অনুরোধে একটি database query, ২০টি worker; Little-এর সূত্রে ক্ষমতা = worker-সংখ্যা ÷ query-র সময়।
| DB-তে একটি query-র সময় | |
| Worker-দের ক্ষমতা (20 ÷ query সময়) | |
| ব্যস্ত worker | |
| Queue (সর্বোচ্চ 100) | |
| আসছে (নতুন + retry) | |
| API গ্রহণ করছে | |
| দ্রুত ফেরত (503) |
- স্বাভাবিক। Query ২০ ms, তাই ক্ষমতা ২০ ÷ ০.০২ = সেকেন্ডে ১,০০০। আসছে ৬০০; Little-এর সূত্রে ব্যস্ত worker ৬০০ × ০.০২ = ১২টি। Queue প্রায় খালি।
- Database ধীর। Query এখন ১০০ ms, ক্ষমতা ২০ ÷ ০.১ = ২০০; ২০টি worker-ই আটকে (চিহ্নিত: worker আর DB). ৬০০ আসে, ২০০ বেরোয়, queue ভরে সেকেন্ডে ৪০০ করে।
- Queue ভরা। ১০০টি জায়গা ভরল ১০০ ÷ ৪০০ = ০.২৫ সেকেন্ডে। নতুন enqueue সঙ্গে সঙ্গে ব্যর্থ — এটিই backpressure-এর সংকেত। অসীম queue-তে কোনো সংকেত আসত না, শুধু memory বাড়ত।
- API দরজা বন্ধ করল। বাড়তি ৪০০টি database ছোঁয়ার আগেই প্রায় ১ ms-এ 503 আর
Retry-Afterপায়। গৃহীত ২০০টির অপেক্ষা ১০০ ÷ ২০০ = ০.৫ সেকেন্ড, timeout-এর মধ্যে; goodput স্থির ২০০। - ক্লায়েন্ট পিছোল। অ্যাপ
Retry-Afterমানে, jitter দেয়, retry রাখে নতুনের ১০%-এ (সর্বোচ্চ ৬০)। আসছে সর্বোচ্চ ৬৬০, গৃহীত ২০০, ফেরত প্রায় ৪৬০ — retry storm নেই। - Database সেরে উঠল। Query আবার ২০ ms; বাড়তি ক্ষমতা ১,০০০ − ৬০০ = ৪০০, তাই জমা ১০০টি সরে ০.২৫ সেকেন্ডে। কোথাও অসীম জমা ছিল না, তাই ফিরে আসাও তাৎক্ষণিক।
Backpressure মানে “অনন্তকাল অপেক্ষা” নয়। API queue-তে জায়গার জন্য thread আটকে বসে থাকলে API-র thread-ই ভরে, তার caller-রা timeout হয় — overload শুধু এক ধাপ ওপরে সরে। তাই প্রতিটি অপেক্ষার সময়সীমা চাই, পেরোলে fail fast. আর internet-এর ব্যবহারকারীকে “block” করা যায় না; শেষ দরজায় backpressure রূপ নেয় ফেরত দেওয়া (load shedding) বা virtual waiting room-এ (পর্ব ৬)। WebSocket-এর ধীর গ্রাহকের সমাধান পর্ব ১০-এ।
৪ · Load shedding — আগে, সস্তায় আর অগ্রাধিকার বুঝে ফেরানো
বাংলাদেশে “লোডশেডিং” সবার চেনা: বিদ্যুতের চাহিদা উৎপাদন ছাড়ালে কিছু এলাকার সংযোগ পালা করে বন্ধ রাখা হয়, যাতে পুরো গ্রিড বসে না যায়; হাসপাতালের মতো জরুরি লাইন সাধারণত সবার শেষে। সফটওয়্যারের load shedding হুবহু এই ভাবনা। চারটি নিয়ম: আগে — দামি কাজ শুরুর আগে, পারলে load balancer বা gateway-তে (পর্ব ১১); সস্তায় — অংশ ২-এর হিসাব মনে রেখে; স্পষ্ট করে — ঠিক কোড আর কখন আবার চেষ্টা; আর বুঝে-শুনে — অগ্রাধিকার আর খরচ দেখে।
কোন কোড — 429, না 503?
| কোড | কখন | ক্লায়েন্ট কী করবে |
|---|---|---|
429 Too Many Requests | এই ক্লায়েন্ট নিজের সীমা (rate limit বা quota) ছাড়িয়েছে — সমস্যা তার দিকে | Retry-After মেনে কম পাঠাবে; অন্যদের ওপর প্রভাব নেই (rate limiting-এর algorithm পর্ব ৪৯-এ) |
503 Service Unavailable | সার্ভার নিজেই এখন ভারে — সবার জন্য | Backoff আর jitter দিয়ে পরে চেষ্টা, অন্য replica বা region, অথবা হালকা বিকল্প দেখানো |
Timeout বা 504 | উত্তর আসেনি; কাজটি হয়েছে কি না অজানা | কাজটি idempotent হলে তবেই আবার চেষ্টা (পর্ব ৮) |
HTTP/1.1 503 Service Unavailable
Retry-After: 5
Content-Type: application/problem+json
Cache-Control: no-store
{
"type": "https://api.shop.example/problems/overloaded",
"title": "Service is temporarily overloaded",
"status": 503,
"detail": "Please retry after about 5 seconds."
}Retry-After সেকেন্ডে বা তারিখ-সময়ে লেখা যায়; ভালো ক্লায়েন্ট এটি মানে আর একটু jitter যোগ করে। উত্তরটি ছোট আর আগে থেকে তৈরি রাখুন — ফেরানোর মুহূর্তে লম্বা log লেখাও খরচ।
অগ্রাধিকার দেখে ফেরানো
প্রতিটি অনুরোধের একটি criticality বা অগ্রাধিকারের শ্রেণি থাকে। একটি কাল্পনিক অনলাইন দোকানে:
| শ্রেণি | উদাহরণ | Overload-এ আচরণ |
|---|---|---|
| A · জরুরি | পেমেন্ট নিশ্চিত করা, OTP যাচাই, অর্ডার দেওয়া | সবার শেষে ফেরত; এদের জন্য আলাদা ক্ষমতা সংরক্ষিত থাকে |
| B · সাধারণ | পণ্য দেখা, অর্ডারের অবস্থা, search | A-র পরে; চাপ খুব বেশি হলে অংশত ফেরত বা কম ফল |
| C · ছেড়ে দেওয়া যায় | “আপনার জন্য প্রস্তাবিত”, analytics event, আগাম prefetch, রিপোর্ট export | সবার আগে ফেরত, বা স্থির বিকল্প; পেছনের কাজ পরে |
শ্রেণিটি header-এ নিচের সার্ভিসেও যায়; নইলে জরুরি পেমেন্ট নিচে গিয়ে “অজানা” হয়ে আগে ফেরত যেতে পারে (priority inversion). জরুরিদের জন্য আলাদা queue-ও চাই, নইলে তারা C-র লম্বা লাইনের পেছনে দাঁড়ায়। নিচে ভিড়ের সময়ে সার্ভিসটি দেখুন; নিয়ম: ক্ষমতা যতক্ষণ থাকে, আগে A, তারপর B, তারপর C. ডানের সংখ্যা “সেবা পেল / এসেছে”, প্রতি সেকেন্ডে।
- স্বাভাবিক সন্ধ্যা। A ১৫০, B ৪০০, C ২৫০ — মোট ৮০০, ক্ষমতা ১,০০০। সবাই সেবা পায়, হাতে ২০০-র বাড়তি জায়গা (headroom) থাকে।
- ঈদের সেল শুরু। A ২৫০ + B ৭০০ = ৯৫০, তাই C-র ৫৫০-এর মধ্যে থাকে মাত্র ৫০; ৫০০টি ফেরত। অ্যাপ তখন CDN থেকে জনপ্রিয় পণ্যের স্থির তালিকা দেখায়।
- ভিড়ের চূড়া। মোট ২,০০০, ক্ষমতার দ্বিগুণ। A ৩০০ পুরো, B পায় বাকি ৭০০ (২০০ ফেরত), C পুরো ফেরত। মোট ১,০০০ ফেরত, কিন্তু টাকা আনা একটি অনুরোধও হারায়নি।
- একটি সার্ভার বিকল। ক্ষমতা ৭৫০। A ৩০০ পুরো; B পায় ৭৫০ − ৩০০ = ৪৫০, ফেরত ৪৫০; মোট ফেরত ১,২৫০। জরুরি এখন ক্ষমতার ৪০%; A নিজেই ক্ষমতা ছাড়ালে বাড়তি ক্ষমতাই একমাত্র পথ (autoscaling, পর্ব ১৩)।
- ভিড় কমল। মোট ৮৫০, সবাই সেবা পায়। C খুলুন ধীরে — ব্যস্ততা কিছুক্ষণ ৮০%-এর নিচে স্থির থাকলে তবে। এই দেরি (hysteresis) না থাকলে ব্যবস্থা চালু-বন্ধে দুলতে থাকে।
অনুরোধ নয়, খরচ গুনুন
ধরা যাক একটি প্রোফাইল দেখতে ২ ms CPU লাগে, ৫০০টি ফলসহ একটি search-এ ২০০ ms — একটি search = ১০০টি প্রোফাইল। তাই অনেক ব্যবস্থা অনুরোধের বদলে খরচের একক গোনে: endpoint আর parameter (যেমন page size) দেখে আগে আন্দাজ, পরে আসল খরচ মিলিয়ে নেওয়া। Overload-এ দামি আর কম জরুরি অনুরোধ আগে ফেরত যায়, আর প্রতি ক্লায়েন্টের আলাদা ভাগ থাকে — যে বেশি পাঠাচ্ছে, আগে তাকেই ফেরানো।
৫ · Queue-কে বুদ্ধিমান করা — CoDel, LIFO আর concurrency limit
“কতজন লাইনে” দেখে সিদ্ধান্ত নেওয়া সহজ, কিন্তু অসম্পূর্ণ। ১০০ জনের লাইন দ্রুত চললে কোনো সমস্যা নেই; ২০ জনের লাইনও সমস্যা, যদি কাউন্টার আটকে থাকে। তাই আধুনিক queue দেখে প্রতিটি অনুরোধ লাইনে কতক্ষণ ছিল — sojourn time.
ভালো queue আর খারাপ queue — CoDel-এর ভাবনা
Kathleen Nichols আর Van Jacobson ২০১২ সালে ACM Queue-তে “Controlling Queue Delay” প্রবন্ধে CoDel প্রস্তাব করেন। ভালো queue ক্ষণিকের ভিড় শুষে নিজেই খালি হয়; খারাপ queue সবসময় ভরা থেকে শুধু দেরি করায়। চেনার উপায়: একটি সময়-জানালা (interval) ধরে সবচেয়ে কম sojourn time-ও লক্ষ্যমাত্রার (target) ওপরে থাকলে লাইনটি স্থায়ী — তখন ফেলা শুরু। Network-এ প্রস্তাবিত মান মোটামুটি target ৫ ms, interval ১০০ ms; আসল CoDel ফেলার ফাঁক ধীরে কমায় (interval ÷ √count). পরে এটি RFC 8289 (২০১৮) হয়।
উল্টো ব্যবস্থা tail drop: queue ভরা থাকলে নতুনটি ফেলে দেওয়া, নইলে লাইনের শেষে রাখা। বড় buffer-এ এটি প্রায় কিছুই ফেলে না, কিন্তু সবাইকে দীর্ঘ লাইনে দাঁড় করায় — নেটওয়ার্কে এর নাম bufferbloat. নিচের simulator-এর CoDel-ধাঁচে অনুরোধ তোলার সময় অপেক্ষা ৫০ ms ছাড়ালে ঘড়ি চালু হয়; টানা ১০০ ms ওপরে থাকলে অনুরোধটি বিনা কাজে ফেরত যায়। এখানে প্রতিটি কাজে ১০ ms লাগে, তাই target-ও network-এর ৫ ms-এর চেয়ে বড় ধরা হয়েছে।
// One server, 10 ms per request (100 req/s). Arrivals: 80 req/s, plus a burst that
// makes it 200 req/s from t = 2 s to 5 s. Both queues may hold up to 300 requests.
function rng(seed) { // mulberry32: seeded, repeatable
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 S = 0.010, LIMIT = 300, TARGET = 0.050, INTERVAL = 0.100, END = 12;
function run(codel) {
const rand = rng(5), arr = [], wait = [], q = [], zeros = () => new Array(END).fill(0);
for (let t = 0; t < END; ) { t += -Math.log(1 - rand()) / (t >= 2 && t < 5 ? 200 : 80); arr.push(t); }
const drops = zeros(), delay = zeros(), served = zeros();
let i = 0, now = 0, aboveSince = null;
while (now < END) {
while (i < arr.length && arr[i] <= now) { // arrivals up to now join the queue
if (q.length >= LIMIT) drops[Math.floor(arr[i])]++; else q.push(arr[i]); i++; }
if (!q.length) { now = arr[i]; aboveSince = null; continue; } // queue empty: idle
const t = q.shift(), sojourn = now - t; // oldest waiting request
if (codel && sojourn > TARGET) {
if (aboveSince === null) aboveSince = now; // above target: start the clock
else if (now - aboveSince >= INTERVAL) { drops[Math.floor(now)]++; continue; } // drop
} else aboveSince = null; // under target again: reset
wait.push(sojourn); delay[Math.floor(now)] += sojourn; served[Math.floor(now)]++; now += S;
}
wait.sort((a, b) => a - b);
const p99 = 1000 * wait[Math.ceil(0.99 * wait.length) - 1], dropped = drops.reduce((a, b) => a + b);
return { cell: (s) => (1000 * delay[s] / served[s]).toFixed(0) + " ms / " + drops[s],
sum: `served ${wait.length}, dropped ${dropped}, p99 ${p99.toFixed(0)} ms, ` +
`waited > 1 s: ${wait.filter((w) => w > 1).length}` };
}
const td = run(false), cd = run(true);
console.log("second tail-drop: delay / drops CoDel-like: delay / drops");
for (let s = 0; s < END; s++) console.log(String(s).padStart(4) + td.cell(s).padStart(22) + cd.cell(s).padStart(26));
console.log("tail-drop : " + td.sum + "\nCoDel-like: " + cd.sum);আউটপুট:
second tail-drop: delay / drops CoDel-like: delay / drops
0 8 ms / 0 8 ms / 0
1 15 ms / 0 15 ms / 0
2 291 ms / 0 74 ms / 110
3 828 ms / 0 75 ms / 101
4 1355 ms / 38 77 ms / 93
5 1854 ms / 0 32 ms / 32
6 2393 ms / 0 34 ms / 3
7 2881 ms / 0 13 ms / 0
8 2905 ms / 0 8 ms / 0
9 2808 ms / 0 20 ms / 0
10 2616 ms / 0 7 ms / 0
11 2395 ms / 0 17 ms / 0
tail-drop : served 1161, dropped 38, p99 2997 ms, waited > 1 s: 821
CoDel-like: served 1029, dropped 339, p99 116 ms, waited > 1 s: 0
Tail drop: ভিড়ে অপেক্ষা ২৯১ থেকে ১,৩৫৫ ms, ৪ সেকেন্ডে queue ভরে ৩৮টি ফেলে। আসল ক্ষতি পরে: ভিড় থেমেছে ৫ সেকেন্ডে, অথচ ৮ সেকেন্ডে সেবা-পাওয়ারা গড়ে ২,৯০৫ ms আর ১১ সেকেন্ডেও ২,৩৯৫ ms অপেক্ষা করেছে, কারণ লাইন সরে সেকেন্ডে মাত্র ১০০ − ৮০ = ২০টি। ১,১৬১টি সেবার ৮২১টি ১ সেকেন্ডের বেশি অপেক্ষা করেছে — প্রায় কিছু না ফেলেও কাজের ফল কম।
CoDel-ধাঁচ: ভিড়েও গড় অপেক্ষা প্রায় ৭৫ ms; ২–৬ সেকেন্ডে ৩৩৯টি ফেরত, মোটামুটি বাড়তি চাপের সমান। স্বাভাবিক সেকেন্ডে (০, ১, ৭–১১) একটিও ফেরত নেই, কারণ ক্ষণিকের ছোট লাইন ১০০ ms-এর আগেই সরে। p99 ১১৬ ms, ১ সেকেন্ডের বেশি অপেক্ষা শূন্য জনের।
Overload-এ নতুনজন আগে — adaptive LIFO
Overload-এ FIFO-র লাইনের মাথার অনুরোধটি সবচেয়ে পুরোনো, তার ক্লায়েন্ট হয়তো চলে গেছে। কিছু ব্যবস্থা তাই লাইন জমলে সবচেয়ে নতুনটিকে আগে সেবা দেয়, আর স্বাভাবিক সময়ে আবার FIFO — adaptive LIFO. ২০১৫ সালে ACM Queue-তে প্রকাশিত Ben Maurer-এর “Fail at Scale” প্রবন্ধে Facebook-এর সার্ভারে এর ব্যবহারের বর্ণনা আছে, সাথে CoDel-ধাঁচের একটি queue timeout-এরও (খুঁটিনাটি প্রবন্ধের বর্ণনা অনুযায়ী)। দাম: পুরোনো অনুরোধ লম্বা সময় সেবা না-ও পেতে পারে, তাই সাথে মেয়াদ-বাদের নিয়ম থাকে। এটি কিছু ব্যবস্থার কৌশল, সবার নিয়ম নয়।
Admission control আর concurrency limit — দরজায় গোনা
Admission control মানে দরজায় সিদ্ধান্ত: নেব কি না। কী গুনব — সেকেন্ডে কতগুলো (rate), না এই মুহূর্তে ভেতরে কতগুলো (concurrency limit)? Little-এর সূত্র L = λW দেখায় কেন দ্বিতীয়টি প্রায়ই ভালো। সীমা ভেতরে ২০টি আর W = ০.১ সেকেন্ড হলে throughput পর্যন্ত ২০ ÷ ০.১ = ২০০; dependency ধীর হয়ে W = ০.৫ হলে একই সীমা নিজেই ৪০-এ নামে। একটি স্থির rate limit তখনও ২০০ ঢোকাত, আর ভেতরে জমত।
সীমা কত? Load test থেকে একটি সংখ্যা নেওয়া যায়, কিন্তু hardware আর dependency বদলায়, তাই অনেক ব্যবস্থা TCP-র মতো নিজে খোঁজে। AIMD-তে সব ঠিক থাকলে সীমা ধীরে বাড়ে, latency বা timeout বাড়লে অনুপাতে কমে; gradient পদ্ধতি সবচেয়ে কম দেখা latency-র সাথে এখনকারটা তুলনা করে। Netflix-এর open-source concurrency-limits লাইব্রেরি (প্রায় ২০১৮) এমন পদ্ধতি দেয়; কিছু proxy-তেও এ সুবিধা আছে (সংস্করণভেদে আলাদা)।
// Illustrative sketch only: AIMD limit on requests in flight.
let limit = 20, inFlight = 0;
const baselineMs = 40; // e.g. the usual latency seen when healthy
function tryAdmit() {
if (inFlight >= limit) return false; // reject now: 503 + Retry-After
inFlight++;
return true;
}
function onFinish(latencyMs, ok) {
inFlight--;
if (!ok || latencyMs > 2 * baselineMs) {
limit = Math.max(1, Math.floor(limit * 0.9)); // multiplicative decrease
} else {
limit += 1 / limit; // additive: about +1 per "limit" successes
}
}শেষ স্তর ক্লায়েন্ট: দরজাও ফেরাতে ফেরাতে ক্লান্ত হলে ক্লায়েন্ট নিজেই কিছু অনুরোধ পাঠানোর আগে বাতিল করে — client-side adaptive throttling. Google-এর SRE বইয়ে (২০১৬) বর্ণিত সূত্রে ক্লায়েন্ট সাম্প্রতিক পাঠানো (requests) আর গৃহীত (accepts) গোনে, আর নতুন অনুরোধ নিজের কাছেই বাতিল করে max(0, (requests − K × accepts) ÷ (requests + 1)) সম্ভাবনায়, K = ২। ১,০০০টি পাঠানো আর ৩০০ গৃহীত হলে (১,০০০ − ৬০০) ÷ ১,০০১ ≈ ০.৪০ — প্রায় ৪০% পাঠানোই হয় না।
৬ · Timeout, deadline আর retry budget — retry storm থামানো
প্রতিটি দূরের call-এ timeout লাগে, নইলে একটি ধীর dependency সব thread আটকায়। তার ওপর পুরো অনুরোধের সময়-বাজেট (ধরা যাক ১ সেকেন্ড) নিচের প্রতিটি call-কে জানাতে হয় — deadline propagation (পর্ব ৯)। Overload-এ এর বড় সুবিধা: সার্ভার কাজ শুরুর আগে দেখে deadline পেরিয়েছে কি না; পেরোলে বিনা খরচে বাদ, আর অংশ ১-এর নষ্ট কাজ বন্ধ।
Retry কত গুণ চাপ বাড়ায় — হিসাব
প্রতিটি চেষ্টা স্বাধীনভাবে p সম্ভাবনায় ব্যর্থ হলে, আর সর্বোচ্চ r বার retry হলে, একটি অনুরোধে গড় চেষ্টা:
r = ৩ হলে: p = ০.১-এ ১.১১১; ০.৫-এ ১.৮৭৫; ০.৯-এ ৩.৪৩৯; পুরো বিকল (p = ১) হলে ৪। Overload-এ p নিজেই চাপের সাথে বাড়ে, তাই retry সবচেয়ে বেশি চাপ যোগ করে ঠিক সবচেয়ে দুর্বল মুহূর্তে। স্তরও গুণ হয়: অ্যাপ → API → data service → database, প্রতিটি ৪ বার চেষ্টা করলে database-এ সর্বোচ্চ ৪ × ৪ × ৪ = ৬৪টি। তাই অনেক নির্দেশিকা একটিমাত্র স্তরে retry রাখতে বলে।
Exponential backoff আর jitter-এর ভাবনা পর্ব ৬-এ, পূর্ণ algorithm পর্ব ৪৮-এ (Resilience pattern). এখানে প্রশ্ন একটাই: টানা overload-এ backoff কি মোট চাপ কমায়? Simulator-এ নতুন অনুরোধ সেকেন্ডে ৭০, timeout ১ সেকেন্ড, সর্বোচ্চ ৩ বার retry; ক্ষমতা ১০০, কিন্তু ৩০–৬০ সেকেন্ডে ৫০ (যেমন অর্ধেক সার্ভার বিকল)। “সেরে ওঠা” = ৬০ সেকেন্ডের পরে প্রথম যে ৫ সেকেন্ডের জানালায় অন্তত ৯৫% চেষ্টা সময়মতো সফল।
// Retry storm: 70 new requests/s, client timeout 1 s, at most 3 retries per request.
// FIFO server: 100 req/s, but only 50 req/s from t = 30 s to 60 s (a capacity dip).
function rng(seed) { // mulberry32: seeded, repeatable
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 END = 240, TIMEOUT = 1, NEW = 70, cap = (t) => (t >= 30 && t < 60 ? 50 : 100);
function sim(backoff, budget, dropExpired) {
const rand = rng(9), exp = (mean) => -Math.log(1 - rand()) * mean;
const due = [], att = new Array(END).fill(0), ok = new Array(END).fill(0);
const add = (t, n) => { if (t < END) (due[Math.floor(t * 1000)] ||= []).push([t, n]); };
for (let t = exp(1 / NEW); t < END; t += exp(1 / NEW)) add(t, 1); // new requests
let free = 0, tokens = 10;
for (let ms = 0; ms < END * 1000; ms++) for (const [t, n] of due[ms] || []) {
if (n === 1) tokens = Math.min(10, tokens + 0.1); // budget: retries <= 10% of new
att[Math.floor(t)]++;
const start = Math.max(free, t), expired = dropExpired && start - t > TIMEOUT;
if (!expired) free = start + exp(1 / cap(start)); // expired: skipped at no cost
if (!expired && free - t <= TIMEOUT) { ok[Math.floor(t)]++; continue; }
if (n > 3 || (budget && tokens < 1)) continue; // give up
if (budget) tokens -= 1;
add(t + TIMEOUT + (backoff ? rand() * 0.5 * 2 ** n : 0), n + 1); // full jitter
}
const sum = (a, i, j) => a.slice(i, j).reduce((x, y) => x + y, 0);
let rec = 60; while (rec < END - 5 && sum(ok, rec, rec + 5) < 0.95 * sum(att, rec, rec + 5)) rec++;
const amp = (i, j) => { const r = sum(att, i, j) / (j - i); return r.toFixed(0) + " (" + (r / NEW).toFixed(2) + "x)"; };
return [amp(30, 60), amp(120, 180), rec < END - 5 ? rec - 60 + " s" : "never (by 240 s)"];
}
console.log("client policy load/s in dip load/s 120-180 s recovery after 60 s");
for (const [name, b, g, d] of [["immediate retry", 0, 0, 0], ["backoff + jitter", 1, 0, 0],
["backoff + jitter + budget", 1, 1, 0], [" + server drops expired", 1, 1, 1]]) {
const r = sim(b, g, d);
console.log(name.padEnd(26) + r[0].padStart(14) + r[1].padStart(18) + " " + r[2]);
}আউটপুট:
client policy load/s in dip load/s 120-180 s recovery after 60 s
immediate retry 230 (3.29x) 279 (3.99x) never (by 240 s)
backoff + jitter 217 (3.10x) 279 (3.98x) never (by 240 s)
backoff + jitter + budget 75 (1.07x) 70 (1.00x) 30 s
+ server drops expired 75 (1.07x) 70 (1.00x) 0 s
সঙ্গে সঙ্গে retry: ঘাটতির সময় চাপ ২৩০ (৩.২৯ গুণ); ক্ষমতা ফেরার পরেও ১২০–১৮০ সেকেন্ডে ২৭৯ (৩.৯৯ গুণ) — প্রায় সবাই ৪ বার চেষ্টা করে ব্যর্থ। ২৪০ সেকেন্ডেও সেরে ওঠেনি: কারণ গেছে ৬০ সেকেন্ডে, ধস রয়ে গেছে — metastable.
Backoff + jitter: ২১৭ (৩.১০ গুণ), পরে ২৭৯ (৩.৯৮ গুণ), এটিও সেরে ওঠেনি। Backoff চেষ্টাগুলো সময়ে ছড়ায়, কিন্তু প্রত্যেকে তবুও ৪ বার চেষ্টা করে, তাই টানা overload-এ হার একই। ক্ষণিকের ঝামেলায় কাজের, একা overload থামায় না।
Retry budget: চাপ মাত্র ৭৫ (১.০৭ গুণ)। ঘাটতিতে জমেছিল প্রায় (৭৫ − ৫০) × ৩০ = ৭৫০টি; বাড়তি ক্ষমতা সেকেন্ডে প্রায় ২৩–৩০ দিয়ে সরতে প্রায় ৩০ সেকেন্ড। সাথে মেয়াদ-বাদ দিলে পুরোনো কাজে সময় যায় না, ৬০ সেকেন্ডের পরের প্রথম ৫ সেকেন্ডেই ৯৫%-এর বেশি সফল: ০ সেকেন্ড।
Retry-র নিয়ম, সংক্ষেপে: শুধু idempotent কাজে; 4xx-এ নয় (429-এ শুধু Retry-After-এর পরে); প্রতি অনুরোধে ২–৩ বার; একটিমাত্র স্তরে; backoff আর jitter সহ; আর সবার ওপরে একটি retry budget — Google-এর SRE বইয়ে (২০১৬) প্রতি ক্লায়েন্টে মোট অনুরোধের ১০%-এর বর্ণনা আছে। Overload-এ hedged request-ও (পর্ব ৪) বন্ধ রাখুন।
৭ · Graceful degradation আর brownout — কম দিয়ে হলেও চালু থাকা
ফেরত দেওয়া শেষ উপায় নয়; অনেক সময় ভালো উপায় হলো কম দেওয়া। পর্ব ৫-এ graceful degradation-এর ভূমিকা দেখেছেন: কম গুরুত্বের সুবিধা ছেড়ে মূল কাজ চালু রাখা। Overload-এ এর হাতিয়ারগুলো:
| উপায় | কী বদলায় | উদাহরণ (কাল্পনিক) | দাম |
|---|---|---|---|
| পুরোনো ডেটা দেখানো | Cache বা CDN-এর stale কপি, কত পুরোনো তা লিখে | ট্রেনের সময়সূচি, জনপ্রিয় পণ্যের তালিকা (পর্ব ১৭) | সর্বশেষ নয় |
| ঐচ্ছিক সুবিধা বন্ধ | Feature flag দিয়ে বন্ধ | সুপারিশ, review, “অন্যরা যা কিনেছেন” | অভিজ্ঞতা আর বিক্রি কিছুটা কমে |
| মান কমানো | ছোট ছবি, কম bitrate, কম ফল | ভিডিও ৭২০p থেকে ৪৮০p; search-এ ৫০-এর বদলে ১০টি ফল | মান |
| আনুমানিক উত্তর | সঠিক গোনার বদলে আন্দাজ | “১০,০০০+ বিক্রি” — হুবহু সংখ্যা নয় | নির্ভুলতা |
| শুধু পড়া | লেখা সাময়িক বন্ধ, পড়া চালু | প্রোফাইল সম্পাদনা কিছুক্ষণ বন্ধ | কিছু কাজ বন্ধ |
| পরে করা | গ্রহণ করে queue-তে, পরে প্রক্রিয়া | রিপোর্ট export ইমেইলে পাঠানো (পর্ব ৩১, ৩৭) | দেরি |
| স্থির বিকল্প পাতা | CDN-এ আগে থেকে রাখা পাতা | “ফলের PDF এখানে” — ব্যক্তিগত পাতার বদলে | ব্যক্তিগত তথ্য নেই |
দুটি শর্ত: বিকল্পটি যেন সেই overloaded অংশের ওপর নির্ভর না করে, আর নিয়মিত পরীক্ষা হয় — যে fallback কখনো চলেনি, প্রয়োজনের দিনে সাধারণত সেটি কাজ করে না।
Degrade মানে সৎ থাকা। পুরোনো ডেটা দেখালে তা লিখে দিন (“৫ মিনিট আগের তথ্য”)। টাকার হিসাবে — ব্যালান্স, লেনদেনের অবস্থা — পুরোনো কপিকে কখনো এখনকার বলে দেখাবেন না। আর কোনো লেখা চুপচাপ ফেলে দেবেন না: হয় নিশ্চিত করে গ্রহণ (পরে প্রক্রিয়ার জন্য), নয় স্পষ্টভাবে ফেরত।
Brownout — বাতি নেভে না, ম্লান হয়
ভোল্টেজ কমলে বাতি ম্লান হয়, নেভে না — ইংরেজিতে brownout. ২০১৪ সালে Cristian Klein ও সহকর্মীরা ICSE সম্মেলনে software-এর জন্য একই ধারণা প্রস্তাব করেন: পাতার একটি ঐচ্ছিক অংশ (যেমন সুপারিশ) কত শতাংশ অনুরোধে যোগ হবে, তা একটি “dimmer” response time দেখে ঠিক করে — চাপ বাড়লে নামে, কমলে ওঠে। Brownout চালু-বন্ধের বদলে ধীরে সমন্বয় করে, আর হাতে বন্ধ করা feature flag-এর চেয়ে দ্রুত সাড়া দেয়। সংকেত হোক queue-তে অপেক্ষার সময় আর p99, শুধু CPU নয়; আর hysteresis রাখুন (৮৫%-এ বন্ধ, ৭০%-এর নিচে কিছুক্ষণ থাকলে চালু)।
৮ · Metastable failure, dependency সুরক্ষা আর “হাঁটু” খোঁজা
পর্ব ৬-এ দেখেছেন, Nathan Bronson ও সহকর্মীরা ২০২১ সালে HotOS কর্মশালায় “Metastable Failures in Distributed Systems” প্রবন্ধে এর নাম দেন। প্রবন্ধটি তিনটি অবস্থা আলাদা করে: stable — ছোট ধাক্কা নিজেই সামলায়; vulnerable — সব ঠিক দেখায়, কিন্তু একটি ধাক্কা (trigger) এলেই পড়ে যাবে, আর দক্ষতার জন্য অনেক সিস্টেম এই অবস্থাতেই চলে; metastable — ধাক্কা গেছে, তবু একটি টিকিয়ে রাখা প্রভাব (sustaining effect) চাপ ধরে রাখে।
সেই প্রভাব শুধু retry নয়। ঠান্ডা cache: সব অনুরোধ database-এ, database-এ timeout, আর cache ভরে শুধু সফল পড়ায়, তাই আর গরম হয় না (এর একটি অংশ cache stampede, পর্ব ২৯)। দামি ব্যর্থতার পথ: লম্বা stack trace, failover-এর খোঁজ, নতুন connection. আর হাজারো client-এর একসাথে ফেরা (reconnect storm, পর্ব ১০)।
অংশ ৬-এর simulator-এ এটিই দেখেছেন। সারানোর পথ: চাপ স্বাভাবিকের চেয়েও নিচে নামানো — জোরালো shedding, retry বন্ধ, মেয়াদোত্তীর্ণ কাজ বাদ — তারপর traffic ধাপে ধাপে ফেরানো। প্রতিরোধে headroom রাখুন, আর নকশার সময়েই প্রশ্ন করুন: কোন প্রভাব ব্যর্থতাকে নিজেই টিকিয়ে রাখতে পারে?
Dependency সুরক্ষা — bulkhead আর circuit breaker
একটি সার্ভিস একই ১০০ thread-এর pool থেকে payment (সেকেন্ডে ৫০টি call, ৪০ ms) আর recommendation (২০০টি, ৫০ ms) ডাকে। Little-এর সূত্রে ব্যস্ত thread ২ + ১০ = ১২। Recommendation ধীর হয়ে ৬০০ ms নিলে তার একারই দরকার ২০০ × ০.৬ = ১২০টি — pool-এর বেশি; সব thread আটকায়, আর মাত্র ২টি thread-এর payment-ও ব্যর্থ হয়।
প্রতিকার bulkhead: প্রতিটি dependency-র আলাদা pool (ধরা যাক recommendation ২০, payment ১০), তাই শুধু recommendation-এর ২০টি ভরে। সাথে circuit breaker: বারবার ব্যর্থ dependency-কে কিছুক্ষণ ডাকাই বন্ধ, সঙ্গে সঙ্গে বিকল্প উত্তর। দুটির পূর্ণ নকশা পর্ব ৪৮-এ।
Load test — বক্ররেখার “হাঁটু” খুঁজে রাখা
এই পর্বের প্রতিটি সীমার সংখ্যা আসে মাপা থেকে। পর্ব ৪-এর load test-এর নিয়মের সাথে overload-এর জন্য:
- Open loop-এ (উত্তরের অপেক্ষা না করে নির্দিষ্ট হারে পাঠিয়ে) চাপ ধাপে ধাপে বাড়িয়ে goodput, p99 আর ত্রুটি লিখুন; p99 যেখানে হঠাৎ খাড়া, সেটিই knee বা “হাঁটু” — নিরাপদ ক্ষমতা তার কিছুটা নিচে।
- ক্ষমতার দ্বিগুণ চাপে goodput সমতল থাকা চাই (ছবি ১৮.১-এর নীল রেখা); নেমে গেলে অংশ ১-এর কোনো চক্র লুকিয়ে আছে।
- ক্ষমতা অর্ধেক করে ৩০ সেকেন্ড পরে ফেরান — সিস্টেম ফেরে, নাকি আটকে থাকে?
- Dashboard-এ (পর্ব ৫০): queue-তে অপেক্ষার সময়, চলমান অনুরোধ, শ্রেণিভেদে ফেরত, retry-র অনুপাত, আর throughput-এর পাশে goodput.
আরও গভীরে: Google-এর Site Reliability Engineering বইয়ে (২০১৬) “Handling Overload” আর “Addressing Cascading Failures” অধ্যায়ে criticality, client-side throttling আর retry budget-এর বিস্তারিত বর্ণনা আছে। Amazon-এর প্রকাশিত Builders’ Library লেখাগুলোতে load shedding, জমে থাকা queue এড়ানো আর backoff-jitter নিয়ে ব্যবহারিক আলোচনা পাবেন। Metastable failure-এর ওপর পরের একটি গবেষণা (“Metastable Failures in the Wild”, OSDI ২০২২) প্রকাশিত বহু বাস্তব ঘটনা বিশ্লেষণ করেছে। আর CoDel-এর পুরো control law জানতে RFC 8289 পড়তে পারেন।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- Throughput আর goodput-এর পার্থক্য কী? Throughput = শেষ হওয়া কাজ; goodput = তার মধ্যে সময়মতো, কাজে লাগা উত্তর। Overload-এ throughput সর্বোচ্চ থাকলেও goodput শূন্যের দিকে নামতে পারে।
- μ = ২০০, λ = ২৫০, timeout ২ সেকেন্ড, queue অসীম — কখন থেকে সব অনুরোধ timeout? T* = D·μ ÷ (λ − μ) = ২ × ২০০ ÷ ৫০ = ৮ সেকেন্ড পরে।
- Backpressure আর load shedding-এর পার্থক্য? Backpressure উৎসকে থামায় বা ধীর করে (ভেতরের অংশে, যেখানে উৎস কথা শোনে); load shedding বাড়তি কাজ ফিরিয়ে দেয় (দরজায়, যেখানে উৎসকে থামানো যায় না)।
- Exponential backoff থাকলে retry storm কি হয় না? হয়। টানা overload-এ backoff চেষ্টা ছড়ায়, কিন্তু মোট চেষ্টা কমায় না; retry budget, circuit breaker বা সার্ভারে মেয়াদ-বাদ লাগে।
৯ · AI-এর যুগে overload-এর নকশা যাচাই
“আমাদের API ভিড়ে বসে যায়, কী করব?” — AI প্রায়ই বলে “Kafka বসান, Redis-এ cache করুন, autoscaling আর microservice.” মূল প্রশ্নের উত্তর তাতে নেই: ক্ষমতা ছাড়ালে কাকে, কোথায়, কত সস্তায় ফেরাবেন? সাধারণ ভুলগুলো:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| “বড় queue বা Kafka দিন, কিছু হারাবে না” | Interactive অনুরোধের জমা মানে timeout আর নষ্ট কাজ | T* = D·μ ÷ (λ − μ) হিসাব; queue-র সীমা সময় দিয়ে |
| “Autoscaling-ই সমাধান” | নতুন সার্ভার চালু হতে মিনিট লাগে; database-এর মতো ভাগ করা অংশ বাড়ে না | ভিড়ের প্রথম কয়েক মিনিট shedding ছাড়া কীভাবে চলবে, জিজ্ঞেস করুন |
| Budget ছাড়া retry, বা “backoff দিলেই হবে” | তিন স্তরে ৬৪ গুণ পর্যন্ত; টানা overload-এ backoff মোট চেষ্টা কমায় না | গুণফল নিজে হিসাব; অংশ ৬-এর simulator |
| সব অনুরোধ একই রকম ধরা | পেমেন্ট আর সুপারিশ একসাথে ফেরত যায় | Criticality-র শ্রেণি আর আলাদা pool আছে কি না দেখুন |
| পণ্যের সীমা আর default বানিয়ে বলা | সংস্করণ আর setup-ভেদে আলাদা | অফিসিয়াল documentation আর নিজের load test |
| Metastable অবস্থার কথা না ভাবা | কারণ সরলেও ধস টিকে থাকতে পারে | Failure-mode পর্যালোচনা: কোন প্রভাব ব্যর্থতাকে টিকিয়ে রাখে? |
AI-কে সংখ্যা আর ব্যর্থতার পথ দিয়ে বাঁধুন, যাতে উত্তরটি যাচাই করা যায়:
AI-এর overload-পরিকল্পনা হাতে পেলে — যাচাইয়ের চেকলিস্ট
অঙ্ক নিজে মেলান, ক্ষমতার দ্বিগুণ চাপে চালিয়ে দেখুন, ব্যর্থতার পথ ভাবুন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। সংখ্যা মুখস্থ নয়, ধসের পেছনের চক্রগুলো চেনাই আসল লক্ষ্য।
প্র ০১১৯৮৬ সালে ইন্টারনেটের link পুরো ব্যস্ত, অথচ কাজের ফল প্রায় শূন্য — throughput আর goodput-এর এই ফাঁক থেকে আজকের API সার্ভারের কী শেখার আছে?
১৯৮৬ সালের ঘটনায় router-এর queue লম্বা হচ্ছিল, তাই packet দেরিতে পৌঁছাচ্ছিল। প্রেরকের timer ভাবছিল packet হারিয়েছে, আর আবার পাঠাচ্ছিল। ফলে link-এ একই packet-এর একাধিক কপি ঘুরছিল: throughput পুরো, goodput প্রায় শূন্য। কাঠামোটি হুবহু অংশ ১-এর ঘূর্ণি — দেরি থেকে timeout, timeout থেকে আবার পাঠানো, তা থেকে আরও দেরি।
Van Jacobson আর Michael Karels-এর ১৯৮৮ সালের সমাধানে তিনটি অংশ ছিল। আজকের সার্ভিসে প্রতিটির একটি সরাসরি প্রতিরূপ আছে:
- ভালো timeout: মাপা RTT আর তার ওঠানামা থেকে timer ঠিক করা, যাতে অকারণে আগেভাগে আবার পাঠানো না হয়। সার্ভিসে: timeout ঠিক করুন dependency-র মাপা latency বণ্টন থেকে (p99.9-এর কিছুটা ওপরে), অনুমান থেকে নয়।
- Backoff: পরপর ব্যর্থতায় অপেক্ষা দ্বিগুণ। সার্ভিসে: retry-তে exponential backoff আর jitter.
- উৎসকে ধীর করা: packet হারানোকে চাপের সংকেত ধরে congestion window কমানো (AIMD). এটি end-to-end backpressure, কারণ router কাউকে কিছু বলে না, প্রেরক নিজেই বোঝে। সার্ভিসে: adaptive concurrency limit, যা latency বাড়লে নিজের সীমা কমায়।
পরে network-এর ভেতরেও বুদ্ধিমান queue এলো (CoDel). সার্ভারের দিকে তার প্রতিরূপ request queue-র ব্যবস্থাপনা। আর এই বক্ররেখা তৃতীয় এক জায়গায়ও আছে: অপারেটিং সিস্টেমের thrashing. Peter Denning-এর ১৯৬৮ সালের working set ধারণা দেখায়, অনেক process একসাথে চালালে CPU কাজ না করে page আনা-নেওয়ায় ব্যস্ত থাকে। OS-এর সমাধানও একই ধরনের: কিছু process সাময়িক থামিয়ে দেওয়া, অর্থাৎ admission control.
তিন জায়গায় মিলটা কাকতালীয় নয়। যেকোনো ভাগ করা সম্পদে চাপের সাথে যদি নষ্ট কাজ বা প্রতিটি কাজের খরচ বাড়ে, বক্ররেখা নিচে নামবেই।
মূল উপলব্ধি: ধস থামানোর উপায় তিন স্তরেই এক — চাপের সংকেত মাপা, উৎসকে ধীর করা, আর দরজায় সীমা টানা। নষ্ট কাজ না-করা কাজের চেয়েও খারাপ, কারণ সে অন্যের জায়গা দখল করে।
প্র ০২Queue বড় রাখলে তো কিছু হারায় না — তাহলে bufferbloat আর CoDel নিয়ে এত কথা কেন? সার্ভারের request queue কত বড় হওয়া উচিত?
২০১০ সালের দিকে Jim Gettys লক্ষ করেন, বাসার router আর modem-এ memory সস্তা হওয়ায় নির্মাতারা বিশাল buffer বসাচ্ছেন। ফল উল্টো হলো। কেউ বড় ফাইল upload করলে একই বাসার ভিডিও কল বা গেম কয়েক সেকেন্ড পিছিয়ে যেত। কারণ TCP প্রেরক packet না হারানো পর্যন্ত গতি বাড়ায়। বড় buffer তাই প্রায় সবসময় ভরা থাকে, আর প্রতিটি packet সেই স্থায়ী লাইনের পেছনে দাঁড়ায়। তিনি এর নাম দেন bufferbloat. কিছু হারাচ্ছিল না, কিন্তু সবাই দেরিতে পৌঁছাচ্ছিল।
সমাধান এলো buffer ছোট করে নয়, বুদ্ধিমান করে। CoDel (২০১২) queue-র দৈর্ঘ্য না দেখে sojourn time দেখে। ক্ষণিকের ভিড় সয়ে যায়, কিন্তু স্থায়ী লাইন হলে ফেলতে শুরু করে। পরে fair queuing-এর সাথে মিলিয়ে FQ-CoDel আসে (RFC 8290, ২০১৮), যা অনেক Linux ব্যবস্থায় ব্যবহৃত হয় (বিতরণভেদে আলাদা)।
সার্ভারে একই ভুল খুব সাধারণ। যেমন Java-র Executors.newFixedThreadPool ভেতরে একটি সীমাহীন LinkedBlockingQueue ব্যবহার করে। ভিড়ে কাজ জমতেই থাকে, কেউ ফেরত পায় না, আর অপেক্ষা মিনিটে গড়ায়। সীমা কত হবে, তার জন্য তিনটি নিয়ম:
- সময় দিয়ে মাপুন: K ≈ μ × সর্বোচ্চ গ্রহণযোগ্য অপেক্ষা। সেকেন্ডে ৫০০ ক্ষমতা আর অপেক্ষার বাজেট ২০০ ms হলে K ≈ ১০০। Byte বা memory দিয়ে নয়।
- অপেক্ষার সময় দেখুন: queue-তে কতক্ষণ ছিল তা মাপুন। একটি সীমা (CoDel-ধাঁচ বা সাধারণ queue timeout) পেরোলে কাজ শুরু না করেই ফেরত দিন।
- অগ্রাধিকার আলাদা করুন: জরুরি অনুরোধ যেন অন্যদের লম্বা লাইনের পেছনে না দাঁড়ায়।
আর ক্ষণিকের ভিড় শোষণের জন্য queue অবশ্যই দরকার। একেবারে queue না থাকলে এলোমেলো আগমনেই অকারণে ফেরত যায়; অনুশীলন ৪-এ দেখবেন খুব ছোট সীমার দাম।
মূল উপলব্ধি: Buffer ভিড় শোষণের জন্য, overload জমানোর জন্য নয়। ভালো queue নিজে খালি হয়; যে queue কখনো খালি হয় না, সে শুধু latency যোগ করে।
প্র ০৩Retry কি বন্ধু না শত্রু? বড় প্রতিষ্ঠানগুলো কেন retry-র জন্য বাজেট রাখে, আর কোন স্তরে retry করা উচিত?
বন্ধু, যখন ব্যর্থতা স্বাধীন আর ক্ষণিকের। একটি packet হারাল, একটি সার্ভার restart হচ্ছে, একটি connection কাটল — এমন ক্ষেত্রে একটি চেষ্টা ১% সময় ব্যর্থ হলে দুটি চেষ্টাই ব্যর্থ হয় মাত্র ০.০১% সময়। অল্প খরচে বিশাল লাভ।
শত্রু, যখন ব্যর্থতা চাপের সাথে জড়িত। Overload-এ একটি চেষ্টা ব্যর্থ হয় কারণ সার্ভার ভারী, আর retry সেই ভার আরও বাড়ায়। p যত বাড়ে, গড় চেষ্টা (1 − pr+1) ÷ (1 − p) তত বাড়ে। এর ওপর স্তরের গুণ: তিন স্তরে ৪টি করে চেষ্টা মানে ৬৪ গুণ। ঠিক যখন নিচের database সবচেয়ে দুর্বল, তখনই তার ওপর সবচেয়ে বেশি চাপ পড়ে।
Google-এর SRE বই (২০১৬) এই দ্বন্দ্বের একটি ব্যবহারিক সমাধান বর্ণনা করে। প্রতি অনুরোধে চেষ্টার একটি সীমা থাকে। প্রতি ক্লায়েন্টে retry মোট অনুরোধের প্রায় ১০%-এর মধ্যে রাখা হয়। আর overload-এ থাকা backend এমন একটি ত্রুটি ফেরত দেয়, যা ওপরের স্তরকে জানায় যে এখানে আর retry চলবে না, যাতে গুণফল তৈরি না হয়। কিছু cloud SDK-তে একই ভাবনা একটি retry token bucket হিসেবে থাকে: সফল অনুরোধে token জমে, retry-তে খরচ হয়। ব্যাপক ব্যর্থতায় token ফুরায়, তাই retry নিজে থেকেই থামে।
কোন স্তরে retry? সাধারণ পরামর্শ হলো একটিমাত্র স্তরে। প্রায়ই সেটি ব্যর্থতার সবচেয়ে কাছের স্তর, যে জানে কাজটি idempotent কি না আর অন্য replica আছে কি না। বাকি স্তরগুলো দ্রুত ত্রুটি ওপরে পাঠায়।
আরও শর্ত: শুধু idempotent কাজে retry (পর্ব ৮-এর Idempotency-Key); deadline পেরোলে আর চেষ্টা নয়; hedged request-ও (পর্ব ৪) একই বাজেটে গোনা; আর dependency স্পষ্টত অসুস্থ হলে circuit breaker (পর্ব ৪৮) দিয়ে চেষ্টাই বন্ধ।
মূল উপলব্ধি: Retry একটি ধার — সুস্থ সময়ে নিন, কিন্তু সংকটে ধারের সীমা চাই। Budget ছাড়া retry overload-এর গুণক; budget-সহ retry ক্ষণিকের ব্যর্থতার ওষুধ।
প্র ০৪শুরুর কারণ সরে যাওয়ার পরও সিস্টেম কেন সেরে ওঠে না? Metastable failure ঠেকানো আর সারানোর উপায় কী?
ঢাকার একটি চার রাস্তার মোড়ে একটি বাস পাঁচ মিনিট বিকল হলো — trigger. পেছনের লাইন পাশের রাস্তার মুখ আটকাল, সেই লাইন আরেকটির। বাস সরানোর পরেও মোড় খোলে না, কারণ প্রতিটি লাইন অন্যটির পথ বন্ধ রেখেছে — sustaining effect. ট্রাফিক পুলিশ তখন চারদিক থেকে ঢোকা বন্ধ করে মোড় খালি করেন, তারপর একটি একটি দিক খোলেন: load shedding আর ধাপে ধাপে traffic ফেরানো।
Bronson ও সহকর্মীদের ২০২১ সালের প্রবন্ধের একটি গুরুত্বপূর্ণ কথা: vulnerable অবস্থা প্রায়ই সবচেয়ে “দক্ষ” অবস্থা। বেশি ব্যস্ততায় খরচ কম, cache-এর ওপর বেশি ভরসায় database ছোট রাখা যায়, retry-তে ছোট ব্যর্থতা ঢাকা পড়ে। এই প্রতিটি অপ্টিমাইজেশন স্বাভাবিক দিনে কাজের, আর খারাপ দিনে ব্যর্থতার টিকিয়ে রাখা প্রভাব হয়ে দাঁড়াতে পারে। ২০২২ সালের একটি পরের গবেষণা প্রকাশিত বহু বাস্তব ঘটনা বিশ্লেষণ করে দেখায়, retry আর এই ধরনের চক্র সেগুলোর বড় অংশে ভূমিকা রেখেছে।
কেন “সব সার্ভার restart” প্রায়ই কাজ করে না? কারণ restart-এ cache আর connection pool খালি হয়। চাপ একই থাকলে ঠান্ডা সিস্টেম আরও দুর্বল, তাই চক্রটি আরও শক্ত হয়। সারানোর পরিকল্পনা আগে থেকে লিখে রাখুন:
- চাপ স্বাভাবিকের নিচে নামান: জোরালো shedding, retry আর ঐচ্ছিক সুবিধা বন্ধ (feature flag).
- জমা queue খালি বা মেয়াদোত্তীর্ণ কাজ বাদ দিন।
- Traffic ধাপে ধাপে ফেরান, যেমন ১০% → ২৫% → ৫০% → ১০০%. প্রতিটি ধাপে cache গরম হতে দিন।
প্রতিরোধে: headroom; প্রতিটি টিকিয়ে রাখা প্রভাবের সামনে বাঁধ (retry budget, request coalescing, সস্তা ত্রুটির পথ); ধাক্কা দিয়ে পরীক্ষা; আর নতুন অনুরোধের তুলনায় মোট চেষ্টার অনুপাত মাপা।
মূল উপলব্ধি: খারাপ অবস্থায় পড়ার পথ আর ভালো অবস্থায় ফেরার পথ এক নয়। ফেরার জন্য প্রায়ই স্বাভাবিকের চেয়েও কম চাপ লাগে, তাই সেই পথ নকশার সময়েই বানিয়ে রাখতে হয়।
প্র ০৫কাকে আগে ফেরাবেন — কে ঠিক করে কোন অনুরোধ “ছেড়ে দেওয়া যায়”? আর FIFO-র ন্যায্যতা বনাম LIFO-র কার্যকারিতা, কোনটি কখন?
বিদ্যুৎ ব্যবস্থার উদাহরণ এখানেও শিক্ষণীয়। উৎপাদন হঠাৎ কমে গেলে গ্রিডের frequency স্বাভাবিক মানের (বাংলাদেশে ৫০ Hz) নিচে নামতে থাকে। অনেক গ্রিডে তখন স্বয়ংক্রিয় relay ধাপে ধাপে আগে থেকে ঠিক করা feeder বিচ্ছিন্ন করে; জরুরি লাইন তালিকার শেষে বা বাইরে। মূল কথা: ক্রমটি সংকটের মুহূর্তে ঠিক হয় না, আগে থেকে লেখা থাকে। সংকটের সময় কেউ বসে ভাবতে পারেন না।
Software-এও criticality আগে ঠিক করতে হয়, আর এটি শুধু প্রকৌশলের সিদ্ধান্ত নয়: কোন অনুরোধ টাকা আনে (পেমেন্ট), কোনটি নিরাপত্তার দায় (login-এর OTP), কোনটি না পেলে কেউ টেরই পান না (আগাম prefetch)? Google-এর SRE বইয়ে বর্ণিত ব্যবস্থায় অনুরোধের সাথে কয়েক স্তরের criticality মান (CRITICAL, SHEDDABLE-এর মতো নাম) নিচের সব call-এ যায়। সতর্কতা: সবাই নিজেকে “জরুরি” বলতে চায়, তাই জরুরি শ্রেণির মোট পরিমাণে সীমা চাই; আর প্রতি ক্লায়েন্টের আলাদা ভাগ।
FIFO বনাম LIFO-র প্রশ্নের উত্তর নির্ভর করে লাইনটি কে দেখছে তার ওপর। একটি ভেতরের API-তে overload-এর সময় লাইনের মাথার অনুরোধটির ক্লায়েন্ট সম্ভবত চলে গেছে। তখন নতুনজনকে আগে সেবা দিলে (adaptive LIFO) বেশি অনুরোধ সফল হয়, আর মেয়াদ-বাদ দিয়ে পুরোনোদের দ্রুত ফেরত দেওয়া হয়।
কিন্তু যেখানে মানুষ নিজের জায়গা দেখেন — ঈদের টিকিট বিক্রির virtual waiting room — সেখানে ন্যায্যতাই পণ্য। “আপনার আগে ১২,৪০০ জন” দেখালে মানুষ লম্বা লাইনও মানেন, কিন্তু পরে এসে কেউ আগে পেলে আস্থা ভাঙে। তাই সেখানে FIFO, আর অবস্থানের সৎ তথ্য।
মূল উপলব্ধি: অগ্রাধিকার একটি পণ্য-সিদ্ধান্ত, যা শান্ত দিনে লিখে রাখতে হয়। আর সেবার ক্রম ঠিক হয় কার কাছে কী গুরুত্বপূর্ণ তা দেখে: যন্ত্রের জন্য সময়মতো উত্তর, মানুষের জন্য ন্যায্যতা।
অনুশীলন
কাগজ-কলমে হিসাব করুন, তারপর উত্তর মেলান। শেষ প্রশ্নে আগে অনুমান, তারপর simulator চালিয়ে যাচাই।
-
Retry কত গুণ চাপ বাড়ায়: একটি সার্ভিসে সেকেন্ডে ১,০০০টি নতুন অনুরোধ আসে। প্রতিটি চেষ্টা স্বাধীনভাবে p সম্ভাবনায় ব্যর্থ হয়, আর ক্লায়েন্ট সর্বোচ্চ ৩ বার retry করে। (ক) p = ০.১ আর p = ০.৬ হলে সার্ভিসে সেকেন্ডে মোট কতগুলো চেষ্টা আসে, আর p = ০.৬-এ কত শতাংশ ব্যবহারকারী শেষ পর্যন্ত সফল হন? (খ) সার্ভিস পুরো বিকল হলে (p = ১)? (গ) অ্যাপ → API → data service → database — তিনটি স্তরের প্রত্যেকে নিচেরটিকে এই নীতিতে ডাকলে, পুরো বিকল database একটি ব্যবহারকারী-অনুরোধে সর্বোচ্চ কতগুলো চেষ্টা পায়? (ঘ) ক্লায়েন্টে ১০% retry budget দিলে (ক)-র দুটি ক্ষেত্রে মোট চাপ কত?
সূত্র: গড় চেষ্টা E = 1 + p + p² + p³।
- (ক) p = ০.১: E = ১.১১১, তাই সেকেন্ডে ১,১১১টি চেষ্টা। p = ০.৬: E = ১ + ০.৬ + ০.৩৬ + ০.২১৬ = ২.১৭৬, তাই ২,১৭৬টি। p = ০.৬-এ সফল ১ − ০.৬৪ = ১ − ০.১২৯৬ ≈ ৮৭% — স্বাধীন ব্যর্থতায় retry সত্যিই সাহায্য করে।
- (খ) p = ১: সেকেন্ডে ৪,০০০টি চেষ্টা, কেউ সফল নন — বিকল সার্ভিস পায় চার গুণ চাপ।
- (গ) ৪ × ৪ × ৪ = ৬৪টি database চেষ্টা; সেকেন্ডে ১,০০০ অনুরোধে ৬৪,০০০ পর্যন্ত। প্রতিকার: retry একটিমাত্র স্তরে।
- (ঘ) Retry সর্বোচ্চ নতুনের ১০% = সেকেন্ডে ১০০, তাই দুই ক্ষেত্রেই মোট ১,১০০ (দরকার ছিল ১১১ আর ১,১৭৬টি retry). স্বাভাবিক দিনে budget প্রায় কিছুই কাটে না, সংকটে গুণফল ১.১-এ আটকে দেয়।
-
Shedding নীতি নকশা করুন: একটি কাল্পনিক অনলাইন বাস-টিকিট সেবার API-র ক্ষমতা সেকেন্ডে ১,২০০ অনুরোধ। ঈদের আগের peak-এ আসে: A — পেমেন্ট নিশ্চিত করা, সেকেন্ডে ৩০০; B — সিট দেখা আর বুকিং শুরু, ৮০০; C — “আপনার জন্য প্রস্তাবিত রুট” আর analytics, ৬০০। তিন শ্রেণির জন্য একটি load shedding নীতি দিন: কে কখন ফেরত যাবে, কী উত্তর পাবে, কোথায় ফেরানো হবে। তারপর একটি সার্ভার বিকল হয়ে ক্ষমতা ৯০০ হলে কী হয়, হিসাব করুন।
একটি সম্ভাব্য নীতি — সীমাগুলো ব্যস্ততার অনুপাতে, যাতে ক্ষমতা বদলালেও নিয়ম একই থাকে:
- C: ব্যস্ততা ৭৫%-এর বেশি হলে gateway-তেই ফেরত; অ্যাপ CDN থেকে জনপ্রিয় রুটের স্থির তালিকা দেখায়, analytics ফোনে জমে পরে যায়।
- B: ব্যস্ততা ৯৫%-এর বেশি হলে
503আরRetry-After; ভিড় দীর্ঘ হলে virtual waiting room. - A: শুধু ১০০%-এ ফেরত; ক্ষমতার ৩৫% আলাদা pool-এ সংরক্ষিত (bulkhead), আর শ্রেণিটি header-এ নিচের সার্ভিসেও যায়।
ক্ষমতা ১,২০০: আসছে ১,৭০০। A ৩০০ আর B ৮০০ মিলে ১,১০০ ≤ ৯৫% × ১,২০০ = ১,১৪০, তাই দুজনেই পুরো (মোট ৯১.৭%). ব্যস্ততা ৭৫%-এর ওপরে, তাই C-র ৬০০টি ফেরত বা degrade.
ক্ষমতা ৯০০: A ৩০০ পুরো (সংরক্ষিত ৩১৫ যথেষ্ট)। B পায় ৯৫% × ৯০০ − ৩০০ = ৫৫৫, ফেরত ২৪৫ (প্রায় ৩১%). C পুরো ফেরত। সাথে autoscaling (পর্ব ১৩) আর waiting room. পেমেন্ট দ্বিগুণ হয়ে ৬০০ হলে সংরক্ষিত ৩৫% আর যথেষ্ট নয়, তাই A-র peak আলাদা করে মেপে রাখুন।
-
Metastable-এর ঝুঁকি খুঁজুন: একটি কাল্পনিক পরীক্ষার ফল দেখার সেবা: ১০টি API সার্ভার, সামনে একটি look-aside cache (hit ratio ৯৫%), পেছনে একটি database, যা সেকেন্ডে ২,০০০ query সামলায়। Peak-এ সেকেন্ডে ২০,০০০ অনুরোধ আসে। মোবাইল অ্যাপ ২ সেকেন্ড timeout-এ সঙ্গে সঙ্গে ৩ বার retry করে। API সার্ভার database query-তে ৫ সেকেন্ড অপেক্ষা করে, আর নিজেও ২ বার retry করে। Cache-এ মান বসে শুধু সফল database-পড়ার পরে। এক রাতে peak-এর সময় cache cluster restart হলো। কী ঘটবে, আর কেন cache ফিরে এলেও সেবা না-ও ফিরতে পারে? নকশায় কী বদলাবেন?
স্বাভাবিক দিনে: database পায় ২০,০০০ × ৫% = ১,০০০ query, ক্ষমতার ৫০% — আরামে, কিন্তু vulnerable, কারণ পুরো ভরসা cache-এ।
Trigger আর গুণফল: cache খালি, তাই ২০,০০০টিই database-এ — ক্ষমতার ১০ গুণ। অ্যাপ ২ সেকেন্ডে হাল ছাড়ে, অথচ API ৫ সেকেন্ড অপেক্ষা করে (নষ্ট কাজ)। অ্যাপের ৪টি × API-র ৩টি চেষ্টা = অনুরোধপ্রতি সর্বোচ্চ ১২টি query, সেকেন্ডে ২,৪০,০০০ পর্যন্ত।
Sustaining effect: cache ভরে শুধু সফল database-পড়ায়, আর প্রায় সব পড়া timeout. তাই restart শেষ হলেও cache ঠান্ডা, চাপও কমে না — metastable.
বদল: API-র deadline অ্যাপের বাকি সময়ের কম (যেমন ১.৫ সেকেন্ড), মেয়াদ-পেরোনো query শুরুই হয় না; retry একটি স্তরে, budget-সহ; একই key-এ একটিই পড়া (request coalescing); database-এর সামনে concurrency limit, যাতে যতটুকু হয় সফল হয় আর cache ধীরে ভরে; প্রকাশের আগে cache warming আর CDN-এ স্থির ফলের পাতা; peak-এ cache restart নয়, করলে traffic ধাপে ধাপে।
-
আউটপুট অনুমান করুন: অংশ ২-এর
overload_policies.js-এ “bounded queue, fail fast”-এর সীমা[50, 50]বদলে[10, 10]করলে ওই টেবিলে p99, rejected, goodput আর critical ok কোন দিকে সরবে? চাপ ৮০-তে কি কিছু ফেরত যাবে? বাকি দুটি টেবিল বদলাবে কি? আগে লিখে রাখুন, তারপর চালিয়ে মেলান।বাকি দুটি টেবিল হুবহু একই থাকবে, কারণ সেগুলোর সীমা বদলায়নি আর seed একই। “bounded queue, fail fast” টেবিলটি হবে:
bounded queue, fail fast load good/s p99 ms rejected critical ok queue at 30 s 50 51.2 105 0 100% 3 80 77.1 151 69 97% 2 100 91.4 163 232 91% 9 120 96.6 170 698 80% 10 150 97.3 190 1471 68% 10 200 97.9 202 2980 49% 10- p99 অনেক কমে: ৫৬৮–৬৮৩ ms থেকে ১৫১–২০২ ms — ভেতরে সর্বোচ্চ ১০টি মানে অপেক্ষা মোটামুটি ১০০ ms.
- স্বাভাবিক চাপেও ফেরত: চাপ ৮০-তে ৬৯টি (প্রায় ২.৯%), ১০০-তে ২৩২টি — আগে ০ আর ১৬। সীমা-সহ queue-র সূত্রে (M/M/1/K, K = ১০) প্রায় ২.৩৫% হওয়ার কথা।
- Goodput আর critical ok: চাপ ৮০-তে goodput ৮০.৭ থেকে ৭৭.১, critical ok ৯৭%; উচ্চ চাপে goodput প্রায় একই।
শিক্ষা: queue-র সীমা latency আর অকারণ ফেরতের মধ্যে trade-off. K ≈ μ × অপেক্ষার বাজেট দিয়ে শুরু করে load test-এ দুই দিক মাপুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমঅসীম queue আর retry থাকা একটি সার্ভিসে overload-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📈 আসা অনুরোধের হার ক্ষমতা ছাড়ায়
- 🧾 Queue লম্বা হয়, প্রতিটি নতুন অনুরোধের অপেক্ষা বাড়ে
- ⏱️ অপেক্ষা ক্লায়েন্টের timeout ছাড়ায়; সার্ভার তবুও পুরোনো কাজ করে চলে
- 🔁 ক্লায়েন্টরা retry করে, মোট চাপ কয়েক গুণ হয়
- 🌀 শুরুর কারণ সরে গেলেও retry চাপ ধরে রাখে — সিস্টেম আটকে থাকে
একটি সার্ভার প্রতি সেকেন্ডে ১০০টি অনুরোধের কাজ শেষ করছে, কিন্তু প্রতিটি উত্তর ক্লায়েন্টের ১ সেকেন্ডের timeout পেরোনোর পরে পৌঁছায়। Throughput আর goodput কত?
- দুটোই সেকেন্ডে ১০০
- Throughput ১০০, goodput প্রায় ০
- Throughput ০, goodput ১০০
- দুটোই প্রায় ০
একটি সার্ভারের ক্ষমতা সেকেন্ডে ২০০ অনুরোধ, আসছে সেকেন্ডে ২৫০, queue অসীম (FIFO), আর ক্লায়েন্টের timeout ২ সেকেন্ড। খালি queue থেকে শুরু হলে মোটামুটি কত সেকেন্ড পর থেকে নতুন অনুরোধগুলো timeout হতে শুরু করবে?
- প্রায় ০.৫ সেকেন্ড
- প্রায় ২ সেকেন্ড
- প্রায় ৮ সেকেন্ড
- কখনোই না, সার্ভার তো কাজ করছে
টানা overload-এ সব চেষ্টা ব্যর্থ হচ্ছে, আর প্রতিটি অনুরোধ সর্বোচ্চ ৩ বার retry করে। সঙ্গে সঙ্গে retry-র বদলে exponential backoff আর jitter দিলে দীর্ঘ সময়ের গড় চাপ নতুন অনুরোধের তুলনায় কেমন হবে?
- প্রায় ১ গুণে নামবে
- প্রায় একই থাকবে — নতুনের প্রায় ৪ গুণ
- শূন্যে নামবে, কারণ সবাই অপেক্ষা করছে
- প্রায় ১৬ গুণ হবে
কোন ক্ষেত্রে উৎসকে থামিয়ে বা ধীর করে (backpressure) সমস্যা মেটানো যায় না, বরং দরজায় বাড়তি অনুরোধ ফিরিয়ে দিতে (load shedding) হয়?
- একই প্রোগ্রামের একটি producer thread একটি bounded queue-তে কাজ রাখছে
- একটি TCP সংযোগের প্রেরক আর প্রাপক
- Internet-এর লক্ষ লক্ষ ব্যবহারকারীর পাঠানো HTTP অনুরোধ
- Reactive Streams-এর একটি গ্রাহক, যে request(n) দিয়ে চাহিদা জানায়
CoDel-ধাঁচের queue ব্যবস্থাপনা কোন শর্তে packet বা অনুরোধ ফেলে দেওয়া শুরু করে?
- Queue-তে নির্দিষ্ট সংখ্যার বেশি packet বা অনুরোধ থাকলে
- Queue-তে অপেক্ষার সময় (sojourn time) পুরো একটি interval ধরে লক্ষ্যমাত্রার ওপরে থাকলে
- প্রতি দশম অনুরোধ এলোমেলোভাবে
- সার্ভারের CPU ৯০% ছাড়ালে
একটি সার্ভিস একই ১০০ thread-এর pool থেকে দুটি dependency ডাকে: payment (সেকেন্ডে ৫০টি call, প্রতিটি ৪০ ms) আর recommendation (সেকেন্ডে ২০০টি call). Recommendation-এর latency ৫০ ms থেকে বেড়ে ৬০০ ms হলে কী ঘটবে?
- কিছুই না, ১০০টি thread যথেষ্ট
- Recommendation-এর দরকার প্রায় ১২০টি thread; pool ফুরিয়ে payment-ও আটকে যাবে
- শুধু payment ধীর হবে, recommendation ঠিক থাকবে
- Recommendation-এর দরকার মাত্র ১২টি thread
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Overload-এ throughput সর্বোচ্চ থাকলেও goodput ধসে পড়ে — নষ্ট কাজ, retry আর বাড়তে থাকা কাজের দামের তিন চক্রে। লক্ষ্য: ক্ষমতার পরেও সমতল goodput.
- অসীম queue একটি bug: λ > μ হলে ধস শুরু T* = D·μ ÷ (λ − μ) সেকেন্ডে। সীমা K ≈ μ × অপেক্ষার বাজেট, আর ভরলে fail fast.
- Backpressure ভেতরে (bounded channel, TCP flow control, request(n), pull), সবসময় সময়সীমাসহ; দরজায় load shedding — আগে, সস্তায়, অগ্রাধিকার আর খরচ দেখে, 503 বা 429 আর Retry-After দিয়ে।
- Queue দেখুন অপেক্ষার সময়ে (CoDel-ধাঁচ), দরজায় গুনুন চলমান অনুরোধ (concurrency limit, AIMD). Retry-কে বাঁধুন budget, backoff-jitter আর deadline দিয়ে, একটিমাত্র স্তরে।
- কম দিয়ে হলেও চালু থাকুন (graceful degradation, brownout); dependency আলাদা রাখুন bulkhead আর circuit breaker দিয়ে; metastable চক্র আগেই খুঁজুন আর load test-এ ক্ষমতার দ্বিগুণ চাপও দিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৯ · Storage engine — B-tree বনাম LSM-tree, WAL, row বনাম column storeপরবর্তী পর্বমডিউল ৪-এর শুরু: database ভেতরে ডেটা কীভাবে রাখে আর খোঁজে।
- পর্ব ০৪ · Latency, throughput আর percentile — p99, tail latency আর Little-এর সূত্রভিত্তিM/M/1, Little-এর সূত্র আর ব্যস্ততার সাথে অপেক্ষার বিস্ফোরণ।
- পর্ব ০৬ · একটি নকশা কীভাবে এগোয় — requirement থেকে API, data model আর deep diveআগে দেখাফল প্রকাশের মুহূর্তের thundering herd আর retry storm-এর প্রথম পরিচয়।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Computer Networks · TCP congestion control — slow start, AIMD, CUBIC আর BBRপাশের ট্র্যাক১৯৮৬-এর ধস আর end-to-end backpressure-এর আসল নকশা।
- Operating Systems · Frame allocation, thrashing আর working setগভীরে যানএকই ধসের বক্ররেখা, অপারেটিং সিস্টেমের memory-তে।