০০ যে প্রশ্নগুলোর উত্তর খুঁজবো
সেশন ১২ তে আমরা একটা ভয়ের ছবি দেখেছিলাম। একটা service ধীর হলে তাকে যে service গুলো call করে, সেগুলোও একে একে আটকে যায়। এই ঘটনার নাম cascading failure। সেখানে শুধু সমস্যাটা চিনেছিলাম। এই সেশনে আমরা তার প্রতিকার শিখবো।
প্রতিকার দুই রকম। প্রথম দলটা নিজের system কে অন্যের সমস্যা থেকে বাঁচায়: timeout, retry, circuit breaker, bulkhead। দ্বিতীয় দলটা system কে অতিরিক্ত request থেকে বাঁচায়: rate limiting আর load shedding। শেষে আপনি বুঝবেন, বড় sale এর রাতেও একটা ছোট dependency কেন পুরো site কে থামাতে পারে না।
- ১ SMS provider ধীর হলে পুরো checkout কেন বন্ধ হয়ে যায়? (cascading failure)
- ২ প্রতিটা call এ timeout কেন লাগে, আর কত রাখবো? (request-level timeout)
- ৩ Fail করলে আবার চেষ্টা করাই তো ভালো। তাহলে retry storm কেন হয়?
- ৪ Retry কে নিরাপদ করবো কীভাবে? (exponential backoff, jitter, retry budget)
- ৫ Circuit breaker কী, আর closed, open, half-open state কীভাবে কাজ করে?
- ৬ Circuit open থাকলে user কী পাবে, আর একটা ধীর dependency যেন সব thread দখল না করে? (fallback, bulkhead)
- ৭ Rate limiting কেন লাগে, আর fixed window ও sliding window কীভাবে গোনে?
- ৮ Token bucket আর leaky bucket এর পার্থক্য কী?
- ৯ Limit পার হলে client কে কী বলবো, আর পুরো server চাপে পড়লে কী করবো? (429, Retry-After, load shedding)
০১ SMS provider ধীর হলে পুরো checkout কেন বন্ধ হয়ে যায়?
রাত এগারোটা আটান্ন। শাওন অফিসের চেয়ারে বসে dashboard এর দিকে তাকিয়ে আছে। দুই মিনিট পর 11.11 এর sale শুরু হবে। Marketing টিম আগেই চল্লিশ লাখ মানুষকে notification পাঠিয়ে দিয়েছে। বারোটা বাজতেই checkout এর graph খাড়া উপরে উঠে গেলো: সেকেন্ডে ১,০০০টা order। শাওন একটু হাসলো, সব ঠিকঠাক চলছে। রাত বারোটা তিনে Slack এ লাল message আসা শুরু হলো: “Checkout এ শুধু spinner ঘুরছে, কোনো order হচ্ছে না!”
শাওন একে একে সব দেখলো। Database ঠিক আছে। Payment gateway ঠিক আছে। Server এর CPU মাত্র ২০%। তাহলে সমস্যা কোথায়? শেষে সে খুঁজে পেলো। Order confirm হলে checkout service একটা SMS পাঠায়: “আপনার order #4711 confirm হয়েছে।” যে কোম্পানি এই SMS পাঠায় (SMS provider), সেই রাতে তাদের নিজেদের system চাপে পড়েছে। আগে প্রতিটা SMS এ ২০০ মিলিসেকেন্ড লাগতো। এখন লাগছে ৩০ সেকেন্ড।
মাত্র একটা SMS এর জন্য পুরো checkout কীভাবে থেমে গেলো? উত্তরটা লুকিয়ে আছে thread এ। সেশন ০৮ এ দেখেছেন, thread হলো server এর ভেতরের একজন কর্মীর মতো, যে একবারে একটা request সামলায়। শাওনদের checkout service এর ৪টা instance আছে, প্রতিটায় ২০০টা thread। মানে মোট ৮০০ জন কর্মী আছে।
হিসাবটা খুব সহজ। একসাথে কয়টা thread ব্যস্ত থাকে, তা নির্ভর করে দুটো জিনিসের উপর: প্রতি সেকেন্ডে কয়টা request আসে, আর প্রতিটা request কতক্ষণ thread টা ধরে রাখে। দুটো গুণ করলেই উত্তর মেলে। এই সূত্রকে বলে Little’s Law।
একসাথে ব্যস্ত thread = প্রতি সেকেন্ডে request × প্রতিটা request এর সময় স্বাভাবিক: 1,000/s × 0.3s = 300 thread (আছে 800, আরাম) SMS 30s ঝুলছে: 1,000/s × 30.3s = 30,300 thread (আছে 800, সব আটকে) timeout 1s: 1,000/s × 1.3s = 1,300 thread (আছে 800, তবুও কম পড়ে) breaker open: 1,000/s × 0.3s = 300 thread (SMS কে আর ডাকাই হয় না)
স্বাভাবিক সময়ে ৮০০ এর মধ্যে ৩০০টা thread ব্যস্ত থাকে, তাই কোনো চিন্তা নেই। SMS ৩০ সেকেন্ড ঝুলিয়ে রাখা শুরু করতেই দরকার হয় ত্রিশ হাজার thread। আছে মাত্র ৮০০। এক সেকেন্ডের মধ্যে সব thread SMS এর উত্তরের অপেক্ষায় বসে যায়। নতুন order আর কোনো thread পায় না। যাদের SMS এর সাথে কোনো সম্পর্কই নেই, যেমন cart দেখা বা address বদলানো, তারাও আটকে যায়।
এটাই cascading failure। SMS provider এর সমস্যা ছড়িয়ে পড়লো checkout এ। Checkout এর সমস্যা ছড়িয়ে পড়লো mobile app এ। আর এর শুরুটা হলো এমন একটা কাজ থেকে, যেটা order এর জন্য জরুরিও নয়।
একটা dependency fail করলে সাধারণত বড় ক্ষতি হয় না। বিপদ হয় dependency টা ধীর হলে। Fail করা call সাথে সাথে ফিরে আসে, কিন্তু ধীর call thread টাকে ধরে রাখে। আপনার system এর thread, connection আর memory সীমিত। কেউ সেগুলো ধরে রাখলে বাকিদের জন্য আর কিছু থাকে না।
০২ প্রতিটা call এ timeout কেন লাগে, আর কত রাখবো?
সেশন ০১ এ timeout এর সাথে পরিচয় হয়েছিলো: একটা নির্দিষ্ট সময়ের পর অপেক্ষা ছেড়ে দেওয়া। এখন সেটাকে প্রতিটা network call এ বসাতে হবে। একে বলে request-level timeout। শাওনদের code এ SMS call এ কোনো timeout ছিলো না। অনেক HTTP library তে default timeout অসীম, বা কয়েক মিনিট। তাই thread টা provider এর মর্জির উপর বসে ছিলো।
Timeout কত রাখবেন? অনুমান করে নয়, মেপে ঠিক করুন। স্বাভাবিক সময়ে SMS provider এর p99 latency দেখুন। মানে ১০০টা call এর মধ্যে সবচেয়ে ধীর ১টা কত সময় নেয় (সেশন ১৯)। ধরুন সেটা ৬০০ মিলিসেকেন্ড। তাহলে timeout তার একটু উপরে রাখুন, যেমন ১ সেকেন্ড। এতে স্বাভাবিক ধীর call গুলো কাটা পড়ে না, আবার অস্বাভাবিক অপেক্ষাও থামে।
আরেকটা জিনিস মনে রাখুন। User এর পুরো checkout এর জন্য একটা মোট সময়সীমা থাকে, ধরুন ৩ সেকেন্ড। ভেতরের সব call এর timeout মিলিয়ে সেই সীমার মধ্যে থাকতে হবে। Mobile app ৩ সেকেন্ড পরে হাল ছেড়ে দিলে ভেতরে ১০ সেকেন্ডের timeout রাখার কোনো মানে নেই। তখন server এমন একটা কাজ করতে থাকে, যার উত্তর শোনার জন্য কেউ আর অপেক্ষা করছে না।
উপরের হিসাবে আবার তাকান। ১ সেকেন্ডের timeout দিলেও দরকার হয় ১,৩০০টা thread। আছে ৮০০। মানে timeout ক্ষতি অনেক কমায়, কিন্তু একা পুরো সমস্যা মেটাতে পারে না। Provider যতক্ষণ ধীর, ততক্ষণ প্রতিটা order ১ সেকেন্ড করে নষ্ট করে। এর সমাধান আসবে সেকশন ০৫ এ।
Network এর ওপারে যাওয়া প্রতিটা call এ timeout দিন: database, cache, অন্য service, third-party API, সব জায়গায়। Timeout না দিলে আপনার thread কতক্ষণ আটকে থাকবে, সেটা ঠিক করে অন্য কেউ।
০৩ Fail করলে আবার চেষ্টা করাই তো ভালো। তাহলে retry storm কেন হয়?
শাওনদের code এ timeout ছিলো না, কিন্তু retry ছিলো। SMS call fail করলে checkout আরো ৩ বার চেষ্টা করতো। Mobile app এও একই নিয়ম: checkout থেকে error এলে app নিজে থেকে আরো ৩ বার চেষ্টা করে। দুটো সিদ্ধান্তই আলাদা আলাদা টিম নিয়েছিলো, আর দুটোই আলাদাভাবে যুক্তিসংগত।
সাময়িক কোনো গোলমাল হলে retry সত্যিই কাজে দেয়। Network এ একটা packet হারিয়ে গেলো, বা একটা server restart হচ্ছিলো, এমন সমস্যা এক সেকেন্ড পরে নিজেই মিটে যায়। কিন্তু dependency টা চাপে পড়ে ধীর হয়ে গেলে retry তার উপর আরো চাপ বাড়ায়।
একজন user একবার “Place order” চাপলো। App চেষ্টা করলো ৪ বার (প্রথমবার আর ৩টা retry)। প্রতিবার checkout SMS provider কে ৪ বার করে ডাকলো। এক tap থেকে provider এর কাছে গেলো ১৬টা call। সেকেন্ডে ১,০০০টা order এর বদলে provider পেলো সেকেন্ডে ১৬,০০০টা অনুরোধ। এর উপর অধৈর্য user রা নিজেরাও বারবার button চাপছে।
যে provider সেকেন্ডে ২,০০০টা SMS সামলাতে পারে, সে আর কোনোদিন সামলে ওঠার সুযোগ পায় না। Provider এর চাপ কমলে সে সেরে উঠতো। কিন্তু retry গুলো চাপ কমতেই দেয় না। এই অবস্থার নাম retry storm: সবার retry মিলে এত load তৈরি করে যে আসল সমস্যাটা আর মিটতেই পারে না।
আরেকটা বিপদও আছে। Order এর call টা retry হলে একই order দুইবার তৈরি হতে পারে। তাই যে call retry হবে, সেটাকে idempotent হতে হবে (সেশন ০৮)। Payment এর মতো কাজে এটা আরো জরুরি, বিস্তারিত আছে সেশন ১৮ তে।
প্রতিটা retry আসলে বাড়তি load। একটা layer এ ৩ বার retry মানে চাপ ৪ গুণ। দুটো layer এ সেটা হয় ১৬ গুণ, তিনটা layer এ ৬৪ গুণ। তাই একটা call chain এ retry রাখুন শুধু একটা জায়গায়, সব layer এ নয়।
০৪ Retry কে নিরাপদ করবো কীভাবে?
Retry একেবারে বাদ দেওয়া যায় না, কারণ সাময়িক গোলমালে এটাই সবচেয়ে সহজ সমাধান। তাই retry রাখতে হবে, কিন্তু তিনটা নিয়ম মেনে।
প্রথম নিয়ম: exponential backoff আর jitter
সেশন ০৯ এ background job এর জন্য এই দুটো দেখেছেন। একই নিয়ম request এর বেলায়ও খাটে। Exponential backoff মানে প্রতিবার retry এর আগে আগের চেয়ে দ্বিগুণ অপেক্ষা: ১ সেকেন্ড, তারপর ২, তারপর ৪। Jitter মানে সেই অপেক্ষার সাথে একটু random সময় যোগ করা।
শুধু backoff দিলে অপেক্ষা বাড়ে ঠিকই। কিন্তু যে হাজার জন client একই মুহূর্তে fail করেছিলো, তারা সবাই ঠিক ১ সেকেন্ড পরে একসাথে ফিরে আসে। তারপর আবার একসাথে, ২ সেকেন্ড পরে। Provider এর কাছে load আসে ঢেউয়ের মতো। Jitter সেই ঢেউ ভেঙে দেয়। AWS এর পরামর্শ হলো “full jitter”: ০ থেকে backoff এর পুরো সময়ের মধ্যে যেকোনো একটা random সময় অপেক্ষা করা।
দ্বিতীয় নিয়ম: সব error এ retry নয়
Timeout, 503 বা connection কেটে যাওয়ার মতো সাময়িক error এ retry করা যায়। কিন্তু 400 (ভুল request) বা 401 (login নেই) এ retry করে লাভ নেই। একই ভুল request দশবার পাঠালে দশবারই fail করবে। আর retry এর সংখ্যার একটা সীমা রাখুন, সাধারণত ২ থেকে ৩ বার।
তৃতীয় নিয়ম: retry budget
Backoff আর jitter থাকলেও একটা প্রশ্ন থেকে যায়। সব request ই যখন fail করছে, তখন প্রত্যেককে ৩ বার retry করতে দেওয়ার মানে কী? তার উত্তর হলো retry budget। পুরো service এর জন্য একটা সীমা থাকে: retry এর সংখ্যা মূল request এর একটা নির্দিষ্ট অংশের বেশি হবে না। Google এর SRE বইতে এই সীমা ১০%।
নিয়ম: প্রতি সেকেন্ডে retry ≤ মূল request এর 10%
স্বাভাবিক সময়: 1,000 request/s, fail হয় ~5 টা → 5 টা retry, budget এর ভেতরে
SMS down: 1,000 request/s, fail হয় সবগুলো
budget ছাড়া: প্রত্যেকে 3 বার retry → SMS এ 1,000 + 3,000 = 4,000/s
budget সহ: মাত্র 100 টা retry/s → SMS এ 1,000 + 100 = 1,100/s
বাকি 900 টা সাথে সাথে fallback এ যায়
স্বাভাবিক সময়ে budget টের পাওয়াই যায় না, কারণ অল্প কয়েকটা request fail করে আর সবাই retry পায়। কিন্তু dependency পুরো down হলে budget নিজে থেকেই retry বন্ধ করে দেয়। Provider এর উপর load ৪,০০০ থেকে নেমে আসে ১,১০০ তে।
অপেক্ষা বাড়িয়ে আর ছড়িয়ে retry করুন (backoff আর jitter)। শুধু সাময়িক error এ retry করুন। আর পুরো service এর retry একটা budget এর মধ্যে রাখুন। এই তিনটা ছাড়া retry বিপদের সময় সাহায্য না করে উল্টো ক্ষতি করে।
০৫ Circuit breaker কী, আর closed, open, half-open state কীভাবে কাজ করে?
শাওনের মাথায় এবার একটা সহজ প্রশ্ন এলো। SMS provider যে এখন ধীর, সেটা তো আমরা জানিই। প্রতিটা order এ তাকে আবার ডেকে ১ সেকেন্ড নষ্ট করার দরকার কী? কিছুক্ষণ তাকে ডাকা বন্ধ রাখলেই তো হয়।
ঠিক এই কাজটাই করে circuit breaker। নামটা এসেছে বাসার বিদ্যুতের main switch থেকে। কোথাও short circuit হলে main switch নিজে থেকেই trip করে, মানে লাইন কেটে দেয়। তাতে পুরো বাসার wiring পুড়ে যাওয়া থেকে বাঁচে। Software এর circuit breaker ও তেমন। একটা dependency বারবার fail করলে সে কিছুক্ষণের জন্য সেই dependency কে call করা বন্ধ করে দেয়। Michael Nygard তাঁর Release It! বইতে এই pattern কে জনপ্রিয় করেন।
Breaker এর তিনটা state। শাওনদের checkout এর সংখ্যাগুলো দিয়ে দেখি:
- Closed: স্বাভাবিক অবস্থা। সব call SMS provider এর কাছে যায়। Breaker শুধু হিসাব রাখে, শেষ ২০টা call এর কয়টা fail বা timeout হলো। বিদ্যুতের ভাষায় circuit বন্ধ মানে লাইন জোড়া, তাই current চলে।
- Open: শেষ ২০টা call এর অন্তত অর্ধেক fail করলে breaker open হয়। এখন ৩০ সেকেন্ড কোনো call provider এর কাছে যায় না। প্রতিটা request সাথে সাথে fallback পায়, ১ মিলিসেকেন্ডের মধ্যে। Thread আর আটকে থাকে না, আর provider ও একটু দম নেওয়ার সময় পায়।
- Half-open: ৩০ সেকেন্ড পর breaker একটু পরখ করে দেখে। মাত্র ৩টা trial call provider এর কাছে যেতে দেয়, বাকিরা তখনো fallback পায়। তিনটাই সফল হলে breaker আবার closed হয়। একটাও fail করলে আবার open হয়, আরো ৩০ সেকেন্ডের জন্য।
Half-open state টা খুব জরুরি। এটা না থাকলে breaker কখনো জানতেই পারতো না provider সেরে উঠেছে কিনা। আবার হঠাৎ সব traffic ছেড়ে দিলে সদ্য সেরে ওঠা provider আবার চাপে পড়তো।
Code এ breaker এর মূল যুক্তি দেখতে এরকম:
def send_sms(order):
if breaker.state == OPEN:
if now() - breaker.opened_at < 30s:
return fallback(order) # 1ms, provider কে ডাকাই হলো না
breaker.state = HALF_OPEN # 30s হয়ে গেছে, এবার একটু পরখ করি
try:
sms_provider.send(order, timeout=1s)
breaker.record_success() # HALF_OPEN এ 3 টা সফল হলে CLOSED
except (Timeout, ServerError):
breaker.record_failure() # শেষ 20 call এ ≥ 50% fail হলে OPEN
return fallback(order)
নিজে বানানোর দরকার নেই। Java তে Resilience4j, .NET এ Polly এর মতো library এটা দেয়। Service mesh (সেশন ১২) ব্যবহার করলে sidecar ও এই কাজ করতে পারে।
এবার সেশনের প্রথম হিসাবটা মনে করুন। Breaker open থাকলে SMS এর জন্য আর কোনো অপেক্ষা নেই। Checkout আবার ০.৩ সেকেন্ডে শেষ হয়, আর দরকার হয় মাত্র ৩০০টা thread।
Circuit breaker দুই পক্ষকেই বাঁচায়। আপনার service আর ধীর call এর পেছনে thread নষ্ট করে না। আর অসুস্থ dependency টা বাড়তি চাপ থেকে মুক্তি পেয়ে সেরে ওঠার সময় পায়। Timeout বলে “এই call টায় আর অপেক্ষা করবো না।” Breaker বলে “এই dependency কে কিছুক্ষণ আর ডাকবোই না।“
০৬ Circuit open থাকলে user কী পাবে, আর একটা ধীর dependency যেন সব thread দখল না করে?
Fallback: পরিকল্পিত বিকল্প উত্তর
Breaker open হলে call টা আর হয় না। তখন user কে কী দেবেন? এর উত্তর আগে থেকে ঠিক করে রাখতে হয়। এই বিকল্প উত্তরের নাম fallback।
শাওনের ক্ষেত্রে উত্তরটা সহজ। SMS না গেলে order বাতিল করার কোনো কারণ নেই। SMS টা একটা queue তে রেখে দিলেই হয় (সেশন ০৮)। Provider সেরে উঠলে worker রা queue থেকে নিয়ে SMS গুলো পাঠিয়ে দেবে।
সব dependency এর fallback এক রকম নয়। কয়েকটা প্রচলিত ধরন:
| Dependency | Fallback | User কী দেখে |
|---|---|---|
| SMS, email | Queue তে রেখে পরে পাঠানো | Order confirm, SMS একটু পরে আসে |
| “আপনার পছন্দ হতে পারে” section | Cache এ রাখা পুরোনো list, বা সবার জনপ্রিয় list | একটু কম personal suggestion |
| Product review এর সংখ্যা | একটা default মান, বা section টা লুকিয়ে রাখা | Review section নেই |
| Payment | কোনো fallback নেই, পরিষ্কার error | “Payment হয়নি, একটু পরে চেষ্টা করুন” |
শেষ সারিটা খেয়াল করুন। সব কিছুর fallback হয় না। টাকা কাটা হয়েছে কিনা না জেনে “order confirm” দেখানো যায় না। এমন জায়গায় সবচেয়ে ভালো fallback হলো দ্রুত আর সৎ একটা error, যাতে user কে ৩০ সেকেন্ড spinner এর দিকে তাকিয়ে বসে থাকতে না হয়।
Bulkhead: ক্ষতিকে এক ঘরে আটকে রাখা
বড় জাহাজের তলাটা কয়েকটা আলাদা কামরায় ভাগ করা থাকে। মাঝখানের দেয়ালগুলো পানি আটকায়। একটা কামরায় ফুটো হলে শুধু সেই কামরায় পানি ঢোকে, পুরো জাহাজ ডোবে না। এই দেয়ালকে বলে bulkhead।
Software এ bulkhead মানে প্রতিটা dependency এর জন্য আলাদা thread pool বা connection pool রাখা। Pool মানে আগে থেকে তৈরি করে রাখা কয়েকটা thread বা connection এর একটা দল। শাওনদের checkout এ সব call একই ২০০টা thread এর pool ভাগ করে নিতো। Bulkhead দিলে ভাগটা এমন হয়: SMS এর জন্য ২০টা, payment এর জন্য ১০০টা, inventory এর জন্য ৮০টা।
এখন SMS provider ধীর হলে শুধু তার ২০টা thread আটকে যায়। ২১ নম্বর SMS call আর thread পায় না, সাথে সাথে fallback এ চলে যায়। Payment আর inventory এর thread গুলোর গায়ে আঁচও লাগে না। Order চলতেই থাকে।
Timeout প্রতিটা call কে একটা সময়সীমা দেয়। Bulkhead প্রতিটা dependency কে একটা জায়গার সীমা দেয়। Circuit breaker বারবার ব্যর্থ dependency কে কিছুক্ষণ বিশ্রাম দেয়। আর fallback ঠিক করে, তখন user কী দেখবে। চারটা মিলে একটা ধীর dependency এর ক্ষতি তার নিজের ঘরেই আটকে রাখে।
০৭ Rate limiting কেন লাগে, আর fixed window ও sliding window কীভাবে গোনে?
এতক্ষণ আমরা নিজেদের বাঁচিয়েছি একটা অসুস্থ dependency থেকে। এবার বিপদ আসছে উল্টো দিক থেকে। Sale শুরুর পাঁচ মিনিট পর শাওন দেখলো, একটা account থেকে flash deal এর API তে সেকেন্ডে ৫০০টা request আসছে। এটা কোনো মানুষ নয়, একটা bot। সে সস্তা phone গুলো সবার আগে কিনে নিতে চায়। একটা bot একাই হাজার জন সাধারণ user এর সমান load তৈরি করছে।
এর প্রতিকার rate limiting: একজন client একটা নির্দিষ্ট সময়ে সর্বোচ্চ কয়টা request পাঠাতে পারবে, তার একটা সীমা বেঁধে দেওয়া। সীমা পার হলে বাড়তি request গুলো ফিরিয়ে দেওয়া হয়। সেশন ১৩ তে দেখেছেন, এই কাজটা সাধারণত API gateway তে হয়, যাতে বাড়তি request ভেতরের service পর্যন্ত পৌঁছাতেই না পারে।
“Client” কে চেনা হবে কীভাবে, সেটাও ঠিক করতে হয়। Login করা user এর বেলায় user ID দিয়ে। Login ছাড়া request এ IP address দিয়ে। কিন্তু বাংলাদেশের মোবাইল নেটওয়ার্কে হাজার হাজার user প্রায়ই একই public IP এর পেছনে থাকে। তাই শুধু IP দিয়ে কড়া limit বসালে নিরীহ user রাও আটকে যেতে পারে।
এবার আসল প্রশ্ন: request গুলো গুনবো কীভাবে? চারটা প্রচলিত algorithm আছে। প্রতিটার জন্য আলাদা ছবি দেখবো। ধরে নিন limit হলো প্রতি user এর জন্য মিনিটে ১০০টা request।
Fixed window counter
সবচেয়ে সহজ পদ্ধতি। সময়কে এক মিনিটের টুকরোয় ভাগ করুন: ১২:০০, ১২:০১, ১২:০২। প্রতিটা টুকরোকে বলে একটা window। প্রতিটা user এর প্রতিটা window এর জন্য একটা counter রাখুন। Request এলে counter এক বাড়ান। Counter ১০০ পার হলে request ফিরিয়ে দিন। নতুন মিনিট শুরু হলে counter আবার শূন্য থেকে গোনা শুরু করে। এই পদ্ধতির নাম fixed window।
Memory লাগে খুব কম, user প্রতি একটা সংখ্যা। কিন্তু সীমানায় একটা ফাঁক থাকে। Bot টা ১২:০০:৫৯ এ ১০০টা আর ১২:০১:০০ এ আরো ১০০টা request পাঠালো। দুটো আলাদা window, তাই দুটোই পাস করলো। অথচ মাত্র ২ সেকেন্ডে গেলো ২০০টা request, limit এর দ্বিগুণ।
Sliding window
এই ফাঁক বন্ধ করে sliding window। এখানে “এই মিনিট” নয়, সবসময় “ঠিক শেষ ৬০ সেকেন্ড” গোনা হয়। Window টা সময়ের সাথে সাথে পিছলে এগোয়, তাই এই নাম।
সবচেয়ে নিখুঁত উপায় হলো প্রতিটা request এর সময় আলাদা করে লিখে রাখা, আর প্রতিবার শেষ ৬০ সেকেন্ডের request গুনে দেখা। একে বলে sliding window log। কিন্তু user প্রতি ১০০টা timestamp রাখতে অনেক memory লাগে। তাই বেশিরভাগ system একটা কাছাকাছি হিসাব ব্যবহার করে, যার নাম sliding window counter:
এখন ১২:০১:১৫। আগের window এ ৮০টা request এসেছিলো, এই window এ এখন পর্যন্ত ৩০টা। শেষ ৬০ সেকেন্ডের মধ্যে আগের window এর শেষ ৪৫ সেকেন্ড পড়েছে, মানে তার ৪৫/৬০ অংশ। ধরে নেওয়া হয়, আগের window এর request গুলো সমানভাবে ছড়ানো ছিলো। তাহলে আগের window থেকে ধরা হয় ৮০ এর ০.৭৫ অংশ, মানে ৬০টা। তার সাথে এই window এর ৩০টা যোগ করলে মোট হয় ৯০। এটা ১০০ এর নিচে, তাই request টা পাস করে।
এবার bot এর কৌশলটা আবার চালান। ১২:০১:০০ এ আগের window এর পুরোটা শেষ ৬০ সেকেন্ডে পড়ে। তাই আগের window এর পুরো ১০০টাই গোনায় ধরা হয়। প্রথম নতুন request টাই limit পার করে, তাই ফিরে যায়। মাত্র দুটো counter রেখে সীমানার ফাঁক প্রায় পুরোটাই বন্ধ হলো। Cloudflare এর মতো বড় কোম্পানিও এই পদ্ধতি ব্যবহার করে।
০৮ Token bucket আর leaky bucket এর পার্থক্য কী?
Window এর পদ্ধতিগুলো প্রশ্ন করে, “শেষ এক মিনিটে কয়টা request এলো?” Bucket এর পদ্ধতিগুলো অন্যভাবে ভাবে। এদের ভেতরে একটা কাল্পনিক বালতি থাকে।
Token bucket
প্রতিটা user এর একটা বালতি আছে, যাতে সর্বোচ্চ ১০টা token ধরে। প্রতি সেকেন্ডে বালতিতে ২টা করে নতুন token পড়ে। বালতি ভরা থাকলে নতুন token উপচে পড়ে নষ্ট হয়। প্রতিটা request ঢোকার আগে বালতি থেকে একটা token নেয়। Token থাকলে request পাস। বালতি খালি হলে request ফিরে যায়। এই পদ্ধতির নাম token bucket।
এর সবচেয়ে বড় গুণ হলো burst সামলানো। Burst মানে অল্প সময়ে হঠাৎ অনেকগুলো request। একজন সাধারণ user অনেকক্ষণ চুপ ছিলো, তারপর app খুলতেই একসাথে ১০টা request গেলো: product list, cart, offer, profile। বালতিতে ১০টা token জমা ছিলো, তাই সবগুলো পাস করলো। কিন্তু bot টা টানা সেকেন্ডে ৫০০টা পাঠালে প্রথম ১০টার পর সে সেকেন্ডে মাত্র ২টা করে পায়। মানুষের স্বাভাবিক আচরণ বাধা পায় না, অথচ টানা অত্যাচার আটকে যায়।
এজন্য public API তে token bucket সবচেয়ে জনপ্রিয়। Stripe তাদের API তে এটা ব্যবহার করে, AWS API Gateway এর throttling ও token bucket দিয়ে চলে।
Leaky bucket
এবার শাওনের অন্য দিকের সমস্যাটা দেখুন। SMS provider এর সাথে চুক্তি আছে, শাওনরা সেকেন্ডে সর্বোচ্চ ৫০০টা SMS পাঠাতে পারবে। কিন্তু sale শুরুর সেকেন্ডে order আসে ৩,০০০টা, পরের সেকেন্ডে হয়তো অনেক কম। Provider এর কাছে এই ঝাঁকুনি পাঠালে সে নিজেই 429 ফেরত দেবে।
এখানে কাজে লাগে leaky bucket। এমন একটা বালতি কল্পনা করুন, যার তলায় একটা ছোট ফুটো। উপর থেকে যত জোরেই পানি ঢালুন, ফুটো দিয়ে পানি বের হয় একই গতিতে। বালতি ভরে গেলে বাড়তি পানি উপচে পড়ে যায়।
Software এ বালতিটা আসলে একটা queue। Request গুলো queue তে ঢোকে, আর একটা নির্দিষ্ট গতিতে (এখানে সেকেন্ডে ঠিক ৫০০টা) বের হয়। Queue ভরা থাকলে নতুন request ফিরিয়ে দেওয়া হয়। NGINX এর rate limiting এই পদ্ধতিতে চলে।
চারটা পাশাপাশি
| Algorithm | কী গোনে | Burst | Memory | কোথায় মানায় |
|---|---|---|---|---|
| Fixed window | প্রতি মিনিটের টুকরোয় কয়টা এলো | সীমানায় দ্বিগুণ পর্যন্ত চলে যায় | খুব কম | সহজ internal limit |
| Sliding window | ঠিক শেষ ৬০ সেকেন্ডে কয়টা এলো | প্রায় ঠেকায় | কম (counter) বা বেশি (log) | নিখুঁত per-user limit |
| Token bucket | হাতে কয়টা token জমা আছে | capacity পর্যন্ত allow করে | কম | Public API, user এর request |
| Leaky bucket | Queue থেকে স্থির গতিতে বের করে | বাইরে যাওয়ার গতি সবসময় সমান | queue এর সমান | Third-party এর limit মানা |
এক কথায় পার্থক্যটা এরকম। Token bucket ঠিক করে কতটা ঢুকতে পারবে, আর ছোটখাটো burst মেনে নেয়। Leaky bucket ঠিক করে কত গতিতে বের হবে, আর বাইরের দিকে কোনো burst যেতে দেয় না।
অনেকগুলো server মিলে একটা limit কীভাবে মানবে, মানে Redis এ counter রাখা আর race condition সামলানো, সেটা দেখবো সেশন ২৪ এ।
০৯ Limit পার হলে client কে কী বলবো, আর পুরো server চাপে পড়লে কী করবো?
429 আর Retry-After
Request ফিরিয়ে দেওয়ার সময় চুপচাপ connection কেটে দেবেন না। পরিষ্কার করে জানান, কী হয়েছে আর কখন আবার আসা যাবে। এর জন্য HTTP তে একটা নির্দিষ্ট code আছে: 429 Too Many Requests। সেশন ০৮ এ এর সাথে এক লাইনে পরিচয় হয়েছিলো।
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Content-Type: application/json
{"error": "rate_limited", "message": "একটু বেশি তাড়াতাড়ি হয়ে গেছে, 30 সেকেন্ড পর আবার চেষ্টা করুন"}
- Retry-After: কত সেকেন্ড পরে আবার চেষ্টা করা যাবে। একটা নির্দিষ্ট তারিখ আর সময়ও দেওয়া যায়।
- X-RateLimit-Limit আর X-RateLimit-Remaining: মোট limit কত, আর এই window এ আর কয়টা বাকি আছে। এগুলো standard নয়, কিন্তু GitHub এর মতো অনেক API এগুলো দেয়। ভালো client এগুলো দেখে নিজের গতি কমায়।
ভালো client এর দায়িত্বও আছে। 429 পেলে সে Retry-After এ বলা সময় পর্যন্ত অপেক্ষা করে। তার আগে আবার চেষ্টা করলে সে retry storm এর নতুন উৎস হয়ে ওঠে। শাওনদের mobile app এ এখন ঠিক এই নিয়ম বসানো হয়েছে।
Load shedding: বাংলাদেশের মানুষ এটা আগে থেকেই চেনে
Rate limiting একজন client এর বাড়াবাড়ি ঠেকায়। কিন্তু সবাই নিয়ম মেনে চললেও মোট traffic server এর ক্ষমতার চেয়ে বেশি হতে পারে। রাত বারোটার sale এ চল্লিশ লাখ মানুষের প্রত্যেকে হয়তো নিজের limit এর মধ্যেই আছে। তবু সব মিলিয়ে server আর পারছে না।
গরমের সন্ধ্যায় বিদ্যুতের চাহিদা যখন উৎপাদনের চেয়ে বেশি হয়, তখন কী হয়? বিদ্যুৎ বিভাগ কিছু এলাকায় কিছুক্ষণের জন্য লাইন বন্ধ রাখে। না রাখলে পুরো grid বসে যেতো, আর সবাই অন্ধকারে থাকতো। এটাই লোডশেডিং। Software এর load shedding ঠিক একই চিন্তা। সেশন ০৮ এ এক লাইনে দেখেছিলেন: চাপ খুব বেশি হলে কম জরুরি কাজ ইচ্ছা করে বাদ দেওয়া।
শাওনদের checkout service এখন নিজের অবস্থা মাপে: কয়টা request লাইনে অপেক্ষা করছে, আর তারা কতক্ষণ ধরে অপেক্ষা করছে। চাপ একটা সীমা পার হলে সে request গুলোকে গুরুত্ব অনুযায়ী আলাদা করে:
- সবার আগে বাদ: “আপনার পছন্দ হতে পারে” section আর analytics event। এগুলো না থাকলে কেউ টেরও পায় না।
- তারপর: Product এর review আর ছবির gallery। User একটু কম তথ্য পায়, কিন্তু কেনাকাটা চলে।
- শেষ পর্যন্ত রক্ষা: Cart, checkout আর payment। এগুলোই কোম্পানির আয়।
বাদ পড়া request গুলো সাথে সাথে 503 পায়। ভালো করে বাদ দেওয়ার মূল কথা হলো, তাড়াতাড়ি বাদ দেওয়া। একটা request ২ সেকেন্ড কাজ করিয়ে তারপর ফেলে দিলে সেই ২ সেকেন্ডের CPU পুরোটাই নষ্ট।
| Rate limiting | Load shedding | |
|---|---|---|
| কাকে থামায় | যে নির্দিষ্ট client নিজের সীমা পার করেছে | সব client মিলে যখন server এর ক্ষমতা পার হয় |
| কখন চালু থাকে | সবসময় | শুধু চাপের সময় |
| HTTP code | 429 | 503 |
| প্রশ্নটা | “এই user কি বেশি চাইছে?” | “এই মুহূর্তে আমি কতটা পারবো?” |
পুরো ছবি
সেই রাতের পরের সপ্তাহে শাওনের টিম সব কিছু ঢেলে সাজালো। প্রতিটা প্রতিকার এখন নিজের জায়গায় বসে আছে:
পরের বছর 11.11 এর রাতে SMS provider আবার ধীর হয়ে গেলো। এবার breaker ৩ সেকেন্ডের মধ্যে open হলো। SMS গুলো queue তে জমলো। Checkout একবারের জন্যও থামলো না। চল্লিশ মিনিট পর provider সেরে উঠলো, আর জমে থাকা সব SMS পরের দশ মিনিটে চলে গেলো। শাওন সেই রাতে এক কাপ চা নিয়ে শান্তিতে বসে থাকতে পারলো।
Distributed system এ কিছু না কিছু সবসময় ধীর বা ভাঙা থাকবে। লক্ষ্যটা তাই সব ব্যর্থতা ঠেকানো নয়। লক্ষ্য হলো, একটা অংশের ব্যর্থতা যেন বাকি অংশে ছড়াতে না পারে। Timeout, retry budget, circuit breaker আর bulkhead ব্যর্থতাকে ভেতর থেকে আটকায়। Rate limiting আর load shedding অতিরিক্ত চাপকে দরজাতেই আটকায়।
এই কৌশলগুলো সত্যিই কাজ করে কিনা, সেটা আসল ঘটনার আগেই ইচ্ছা করে failure ঘটিয়ে পরখ করা যায়। সেটাই সেশন ২২ এর বিষয়।
১০ মগজে প্রেশার দিন
আগে নিজে ভাবুন। তারপর উত্তর খুলে দেখুন: বেশিরভাগ মানুষ কীভাবে ভাবে, সেই চিন্তায় কী বাদ পড়ে, আর আসল কথাটা কোথায়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“বেশি retry মানে বেশি reliability। Network এ মাঝে মাঝে গোলমাল হয়, তাই প্রতিটা layer এ ৩ বার retry রাখলে user কম error দেখবে।”
সাময়িক গোলমালের বেলায় কথাটা ঠিক। কিন্তু dependency চাপে পড়লে কী হয়, সেটা এখানে বাদ পড়েছে।
যে জায়গাটা যোগ করা দরকার
Dependency টা চাপের কারণে ধীর হলে প্রতিটা retry সেই চাপ আরো বাড়ায়। প্রতিটা layer এর retry গুণ হয়ে যায়: দুটো layer এ ৩ বার করে retry মানে ১৬ গুণ load। অসুস্থ service তখন আর সেরে ওঠার সুযোগ পায় না। এটাই retry storm।
তাই retry রাখতে হলে শুধু একটা layer এ রাখুন। Exponential backoff আর jitter দিন। শুধু সাময়িক error এ retry করুন। পুরো service এর জন্য একটা retry budget রাখুন। আর যে call retry হবে, সেটা idempotent কিনা নিশ্চিত করুন।
Retry একটা বাড়তি load, যেটা আপনি ঠিক সেই সময়ে পাঠান, যখন dependency সবচেয়ে দুর্বল। তাই retry কে সবসময় একটা সীমার মধ্যে রাখুন।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Circuit breaker তো ব্যর্থ dependency কে call করা বন্ধ করে দেয়। তাই timeout না দিলেও চলে, breaker নিজেই সামলে নেবে।”
শুনতে যুক্তিসংগত। কিন্তু breaker কীভাবে “ব্যর্থতা” চেনে, সেটা ভেবে দেখা হয়নি।
যে জায়গাটা যোগ করা দরকার
Breaker open হয় fail হওয়া call গুনে। Timeout না থাকলে ধীর call কখনো fail করে না, শুধু ঝুলে থাকে। শাওনদের SMS call ৩০ সেকেন্ড পরে হয়তো সফলও হতো। Breaker তখন কোনো fail ই দেখে না, তাই কখনো open হয় না। এদিকে সব thread আটকে যায়।
Timeout ই “ধীর” কে “fail” এ রূপান্তর করে। তখনই breaker সেটা গুনতে পারে। তাই দুটো একসাথে লাগে। Timeout একটা call কে বাঁচায়, আর breaker পরের হাজারটা call কে বাঁচায়।
Timeout ছাড়া circuit breaker হলো এমন fuse, যেটা short circuit চিনতেই পারে না।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“প্রতি IP থেকে মিনিটে ৫টা login চেষ্টা। Fixed window counter দিয়ে গুনবো, বেশি হলে request reject করবো।”
সংখ্যাটা যুক্তিসংগত। কিন্তু তিনটা জায়গায় এই উত্তর দুর্বল।
যে জায়গাটা যোগ করা দরকার
প্রথমত, শুধু IP দিয়ে গুনলে দুই দিক থেকেই ভুল হয়। মোবাইল নেটওয়ার্কে হাজার জন user একই IP তে থাকতে পারে, তারা অকারণে আটকে যাবে। আবার bot অনেকগুলো IP ব্যবহার করলে প্রতিটা IP limit এর নিচেই থাকবে। তাই IP এর পাশাপাশি যে account এ login এর চেষ্টা হচ্ছে, সেটা ধরেও গুনুন।
দ্বিতীয়ত, fixed window এর সীমানায় দ্বিগুণ চেষ্টা ঢুকে যেতে পারে। Login এর মতো সংবেদনশীল জায়গায় sliding window বা token bucket ভালো।
তৃতীয়ত, শুধু reject করলেই কাজ শেষ নয়। 429 আর Retry-After দিয়ে পরিষ্কার উত্তর দিন। আর বারবার ভুল password এর পর অপেক্ষার সময় বাড়াতে থাকুন। অনেকগুলো server এ একটা limit কীভাবে মানা হবে, সেই প্রশ্ন এলে বলুন: counter গুলো Redis এর মতো একটা shared জায়গায় রাখতে হবে (সেশন ২৪)।
Rate limiting এর উত্তরে চারটা জিনিস বলুন: কাকে ধরে গুনবেন (key), কোন algorithm, limit পার হলে client কী পাবে, আর অনেক server এ counter কোথায় থাকবে।
১১ যে টার্মগুলো শিখলাম
| টার্ম | মানে |
|---|---|
| Cascading failure | একটা service এর সমস্যা তাকে call করা service গুলোতে একে একে ছড়িয়ে পড়া। (সেশন ১২) |
| Request-level timeout | প্রতিটা network call এর জন্য একটা সময়সীমা, যার পর call টা ছেড়ে দেওয়া হয়। |
| Little’s Law | একসাথে ব্যস্ত কাজের সংখ্যা, প্রতি সেকেন্ডে আসা কাজ আর প্রতিটা কাজের সময়ের গুণফলের সমান। |
| Retry storm | সবার retry মিলে এত load তৈরি হওয়া যে অসুস্থ dependency আর সেরে উঠতে পারে না। |
| Exponential backoff | প্রতিবার retry এর আগে আগের চেয়ে দ্বিগুণ সময় অপেক্ষা করা। (সেশন ০৯) |
| Jitter | Retry এর অপেক্ষার সাথে random সময় যোগ করা, যাতে সবাই একসাথে ফিরে না আসে। (সেশন ০৯) |
| Retry budget | পুরো service এর retry মূল request এর একটা নির্দিষ্ট অংশের (যেমন ১০%) বেশি হতে না দেওয়া। |
| Circuit breaker | বারবার fail করা dependency কে কিছুক্ষণের জন্য call করা বন্ধ রাখার ব্যবস্থা। |
| Closed | Breaker এর স্বাভাবিক state: সব call যায়, আর fail গোনা হয়। |
| Open | Breaker এর যে state এ কোনো call যায় না, সবাই সাথে সাথে fallback পায়। |
| Half-open | Open এর পর পরখ করার state: অল্প কয়েকটা trial call যায়, ফল দেখে breaker closed বা open হয়। |
| Fallback | আসল call না হলে user কে দেওয়ার জন্য আগে থেকে ঠিক করা বিকল্প উত্তর। |
| Bulkhead | প্রতিটা dependency এর জন্য আলাদা thread বা connection pool, যাতে একজনের সমস্যা অন্যদের জায়গা দখল না করে। |
| Rate limiting | একজন client একটা নির্দিষ্ট সময়ে কয়টা request পাঠাতে পারবে, তার সীমা। |
| Fixed window | সময়কে নির্দিষ্ট টুকরোয় ভাগ করে প্রতিটা টুকরোর request গোনা। সীমানায় burst ঢুকে যায়। |
| Sliding window | সবসময় ঠিক শেষ নির্দিষ্ট সময়ের (যেমন ৬০ সেকেন্ড) request গোনা। |
| Token bucket | নির্দিষ্ট গতিতে token জমে, প্রতিটা request একটা token খরচ করে। Capacity পর্যন্ত burst মেনে নেয়। |
| Leaky bucket | Request একটা queue তে জমে আর স্থির গতিতে বের হয়। ভরে গেলে নতুন request ফিরে যায়। |
| Burst | অল্প সময়ের মধ্যে হঠাৎ অনেকগুলো request আসা। |
| 429 Too Many Requests | Client তার rate limit পার করেছে, এটা জানানোর HTTP code। |
| Retry-After | কত সময় পর আবার চেষ্টা করা যাবে, তা জানানোর HTTP header। |
| Load shedding | Server এর মোট চাপ সীমা পার হলে কম জরুরি request ইচ্ছা করে তাড়াতাড়ি বাদ দেওয়া। (সেশন ০৮) |
১২ শর্ট সামারি
- একটা dependency fail করার চেয়ে ধীর হওয়া বেশি বিপজ্জনক, কারণ ধীর call আপনার thread ধরে রাখে আর cascading failure শুরু করে। প্রতিটা network call এ মেপে ঠিক করা timeout দিন।
- নিয়ন্ত্রণ ছাড়া retry চাপের সময় load বহুগুণ বাড়িয়ে retry storm তৈরি করে। শুধু একটা layer এ retry রাখুন, আর exponential backoff, jitter আর retry budget মেনে চলুন।
- Circuit breaker বারবার fail করা dependency কে কিছুক্ষণ call করা বন্ধ রাখে (closed, open, half-open)। Fallback ঠিক করে তখন user কী পাবে, আর bulkhead ক্ষতিকে একটা pool এর মধ্যে আটকে রাখে।
- Rate limiting একজন client এর বাড়াবাড়ি ঠেকায়। এর চারটা algorithm হলো fixed window, sliding window, token bucket আর leaky bucket, আর limit পার হলে client পায় 429 আর Retry-After। সবার মিলিত চাপ ক্ষমতা পার করলে load shedding কম জরুরি কাজ আগে বাদ দেয়।
১৩ মনে রাখুন
- ধীর dependency, down dependency এর চেয়ে বেশি বিপজ্জনক। ধীর call thread ধরে রাখে, আর thread ফুরালে পুরো service থেমে যায়।
- Network এর ওপারে যাওয়া প্রতিটা call এ timeout দিন। Timeout ঠিক করুন স্বাভাবিক p99 latency মেপে, অনুমান করে নয়।
- Retry একটা layer এ রাখুন, backoff, jitter আর budget সহ। প্রতিটা layer এ retry দিলে load গুণ হতে থাকে।
- Timeout আর circuit breaker একসাথে কাজ করে। Timeout ধীর call কে fail বানায়, breaker সেই fail গুনে dependency কে বিশ্রাম দেয়।
- প্রতিটা dependency এর fallback আগে থেকে ঠিক করুন। কোনো fallback না থাকলে অন্তত দ্রুত আর পরিষ্কার একটা error দিন।
- Public API তে token bucket, third-party এর limit মানতে leaky bucket। প্রথমটা burst মেনে নেয়, দ্বিতীয়টা বাইরের গতি সমান রাখে।
- Rate limiting একজনকে থামায়, load shedding সবার মিলিত চাপ সামলায়। দুটোই request তাড়াতাড়ি ফিরিয়ে দেয়, কাজ করিয়ে পরে নয়।