এই পর্বে যা শিখবেন
- Load balancer কী কী সমস্যা সমাধান করে, আর পথের কোন কোন জায়গায় সে বসে
- L4 আর L7 load balancer কী দেখে, কী পারে, কী দামে — আর HTTP/2-gRPC-র অসম ভারের কারণ ও প্রতিকার
- Round robin থেকে power of two choices — প্রতিটি algorithm-এর ধর্ম, simulator-এর ফল আর পেছনের গণিত
- Health check-এর হিসাব, slow start, connection draining আর rolling deploy
- LB-এর নিজের high availability, DSR আর global load balancing; একটি HAProxy-ধাঁচের config পড়া
১ · Load balancer কেন — চারটি কাজ
পর্ব ১৩-এ দেখেছেন, একটি বড় সার্ভারের বদলে অনেকগুলো ছোট, একই রকম, stateless সার্ভার রাখাই বড় সেবার পথ। কিন্তু তখনই একটি প্রশ্ন ওঠে: ব্যবহারকারীর ফোন কোন সার্ভারে যাবে? ফোন তো দশটি ঠিকানা মনে রাখতে পারে না, আর কোনটি এই মুহূর্তে বিকল, তাও জানে না। মাঝখানে যে বসে এই সিদ্ধান্ত নেয়, সে load balancer (সংক্ষেপে LB).
ঈদের আগে কমলাপুর রেলস্টেশনের দশটি টিকিট কাউন্টার, আর প্রবেশপথে একজন লাইনম্যান। যাত্রী জানেন শুধু প্রবেশপথটি; কোন কাউন্টারে দাঁড়াবেন, লাইনম্যান বলে দেন। কোনো কাউন্টারের কম্পিউটার বসে গেলে সেখানে আর কাউকে পাঠান না; নতুন কাউন্টার খুললে প্রথমে অল্প কয়েকজনকে পাঠান; আর কাউন্টার বন্ধের আগে নতুন যাত্রী পাঠানো থামান, কিন্তু লাইনে থাকা যাত্রীদের টিকিট দেওয়া শেষ হতে দেন। এই পর্বের প্রায় প্রতিটি ধারণা এই লাইনম্যানের কোনো না কোনো সিদ্ধান্ত।
১. ভার ভাগ করা
কেউ উপচে না পড়ে, কেউ বসে না থাকে — ভাগের নিয়ম algorithm (অংশ ৪–৫)।
throughput আর latency২. বিকলতা লুকানো
Health check দিয়ে অসুস্থ সার্ভার বাদ, সেরে উঠলে ফেরত — ব্যবহারকারী টের পান না (অংশ ৬)।
availability৩. একটিমাত্র প্রবেশপথ
Client জানে একটি নাম আর একটি virtual IP; পেছনে সার্ভার বদলালেও তার কিছু বদলায় না — autoscaling-এর ভিত্তি।
decoupling৪. সেবা না থামিয়ে deploy
একটি একটি সার্ভার সরিয়ে (draining), বদলে, ফেরানো — rolling deployment (অংশ ৭)।
zero downtimeL7 load balancer প্রায়ই TLS termination, compression আর routing-ও করে — এগুলো reverse proxy-র কাজ (পর্ব ১১)। এই পর্বের প্রশ্ন সংকীর্ণ কিন্তু গভীর: কোন অনুরোধ কোন সার্ভারে যাবে, আর কোন সার্ভার এখন অনুরোধ নেওয়ার যোগ্য? Network-এর দিক — ECMP আর packet-স্তরের কাজ — Computer Networks কোর্সের Data center নেটওয়ার্ক পর্বে।
Load balancer ক্ষমতা তৈরি করে না, থাকা ক্ষমতা কাজে লাগায়। মোট ক্ষমতা চাহিদার কম হলে লাগে বাড়তি ক্ষমতা বা load shedding (পর্ব ১৮)। আবার ক্ষমতা যথেষ্ট থাকলেও ভুল ভাগে কিছু সার্ভার উপচে পড়ে, আর tail latency আকাশে ওঠে (অংশ ৫)।
২ · Load balancer কোথায় বসে — DNS থেকে client-এর ভেতর পর্যন্ত
“Load balancer” কোনো একটি বাক্স নয়; একটি অনুরোধের পথে কয়েক জায়গায় ভাগের সিদ্ধান্ত হয়, প্রতিটি আলাদা মাপে:
| স্তর | কী ভাগ করে | কীভাবে | সীমাবদ্ধতা |
|---|---|---|---|
| DNS | ব্যবহারকারীদের region-এ | Round-robin DNS, GeoDNS | TTL-এ বদল ধীরে ছড়ায়; সার্ভারের ভার জানে না (পর্ব ৭) |
| Edge L4 | TCP/UDP সংযোগ, L7 নোডগুলোর মধ্যে | VIP, 5-tuple-এর hash, anycast, ECMP | HTTP দেখে না; সংযোগপ্রতি একটি সিদ্ধান্ত |
| L7 | প্রতিটি HTTP অনুরোধ | host-path routing, least request, power of two | প্রতি অনুরোধে বেশি CPU; TLS-এর key এখানে |
| ভেতরের | সার্ভিস থেকে সার্ভিসে | internal LB, বা পাশের sidecar proxy | অনেক LB-instance, প্রত্যেকে আংশিক তথ্য দেখে |
| Client-side | client নিজেই তালিকা থেকে বাছে | client-side load balancing (পর্ব ৪৭) | প্রতিটি client-এ লাইব্রেরি আর হালনাগাদ তালিকা |
একটি সাধারণ বড় সেবায় সব স্তর একসাথে থাকে। নিচের ছবিটি এই পর্বের মানচিত্র — বাকি অংশগুলো এর এক একটি বাক্স খুলে দেখবে:
Client থেকে app server — দুই স্তরের load balancing
Clients → L4 pair on a VIP (VRRP) → L7 tier → app servers with health checks
দুই স্তর কেন? L4 দ্রুত আর সরল — একটি জোড়া লাখো সংযোগ সামলায়, ভাঙেও কম। L7 প্রতি অনুরোধে বেশি CPU খরচ করে, তাই তাদের অনেকগুলো লাগে, আর তাদের মধ্যে ভাগ করে L4. ছোট সেবায় একটি managed L7 LB বা দুটি Nginx-ই যথেষ্ট। ভেতরের call-এ client বা পাশের sidecar নিজেই বাছতে পারে — service mesh-এর একটি মূল কাজ; backend-তালিকা কোথা থেকে আসে, তা পর্ব ৪৭ (Monolith বনাম microservices)-এর বিষয়।
৩ · L4 বনাম L7 — কে কী দেখে, কী পারে, কী দামে
ডাকঘরে চিঠি বাছাইয়ের দুজন কর্মী। প্রথমজন খাম খোলেন না — শুধু ঠিকানা আর পোস্ট কোড দেখে বস্তায় ফেলেন; খুব দ্রুত। দ্বিতীয়জন খাম খুলে চিঠি পড়েন — “অভিযোগ”, না “বিল” — আর ঠিক বিভাগে পাঠান। প্রথমজন L4 load balancer, দ্বিতীয়জন L7 load balancer; খাম খোলার চাবি হলো TLS-এর private key.
| প্রশ্ন | L4 (transport স্তর) | L7 (application স্তর) |
|---|---|---|
| কী দেখে | source ও destination IP, port, protocol — 5-tuple; ভেতরের বাইট দেখে না | পুরো HTTP অনুরোধ: Host, path, method, header, cookie; gRPC হলে service আর method-এর নাম |
| সিদ্ধান্তের একক | একটি সংযোগ — প্রথম packet-এ বাছাই, তারপর সব packet একই backend-এ (connection-level) | প্রতিটি অনুরোধ বা HTTP/2 stream — একই সংযোগের দুটি অনুরোধ দুই backend-এ যেতে পারে (request-level) |
| TLS | সাধারণত খোলে না (passthrough); certificate থাকে backend-এ; বড়জোর SNI দেখে ভাগ | সাধারণত এখানেই শেষ হয় (termination); চাইলে ভেতরে আবার এনক্রিপ্ট (mTLS) |
| যা পারে | যেকোনো TCP/UDP protocol — database, MQTT, game, DNS; খুব বেশি সংযোগ, কম খরচে | path ধরে routing, retry, timeout, header যোগ, cookie-ভিত্তিক affinity, canary, অনুরোধ-স্তরের metric |
| দাম | প্রতি packet-এ সামান্য কাজ; kernel, eBPF বা hardware-এ চলে | HTTP parse আর TLS-এর CPU; প্রতি অনুরোধে বাড়তি সময় সাধারণত এক ms-এর নিচে, নতুন TLS handshake হলে বেশি |
| উদাহরণ | Linux IPVS, cloud-এর network LB, HAProxy-র mode tcp | Nginx, HAProxy-র mode http, Envoy, cloud-এর application LB |
“L4” আর “L7” নাম OSI স্তর থেকে, তবে পণ্যভেদে অর্থ একটু আলাদা — একই HAProxy একটি port-এ L4, আরেকটিতে L7. আসল প্রশ্ন: সিদ্ধান্তের সময় LB কতটুকু দেখে, আর কত ঘন ঘন সিদ্ধান্ত নেয়? TLS খোলার লাভ (certificate এক জায়গায়, handshake-এর CPU backend থেকে সরে) আর দাম (key-সহ LB নিরাপত্তার কেন্দ্র; ভেতরে আবার এনক্রিপ্ট না করলে ভেতরের নেটওয়ার্ককে বিশ্বাস) পর্ব ১১-এ দেখেছেন।
একটি অনুরোধ L4 আর L7-এর ভেতর দিয়ে
ছবি ১৪.১-এর পথটি এবার ধাপে ধাপে দেখুন। প্রতিটি ধাপে দুটি সারি পাশাপাশি — L4 কী দেখছে, আর L7 কী দেখছে। “পরে” চাপুন:
- Client
198.51.100.7HTTPS - L4-A
VIP :443সংযোগ - L7-2
TLS শেষHTTP পড়ে - pool cart
/api/cart৩টি সুস্থ - cart-3
10.0.3.13বাছাই
| L4 যা দেখে | |
| L7 যা দেখে | |
| এই ধাপের সিদ্ধান্ত |
- Client সংযোগ খোলে। DNS থেকে পাওয়া
shop.example-এর ঠিকানাটি একটি virtual IP, কোনো একক সার্ভারের নয় (পর্ব ৭)। প্রথম packet একটি TCP SYN. - L4-A সংযোগটি একবারই বাছে। সে 5-tuple (দুই দিকের IP ও port, আর protocol) hash করে L7-2 বাছে আর connection table-এ লিখে রাখে। এরপর এই সংযোগের প্রতিটি packet অন্ধভাবে L7-2-তে যাবে; ভেতরে কী আছে, L4 কখনো জানবে না।
- L7-2 খাম খোলে। TLS handshake এখানে শেষ। এখন সে পরিষ্কার HTTP দেখছে: HTTP/2-এর stream 1-এ
GET /api/cart, সাথে Host আর session cookie. - Route আর backend বাছাই। Path মেলে
cartpool-এর সাথে; cart-2 health check-এ DOWN, তাই বাছাই বাকি তিনটি থেকে। LB এলোমেলোভাবে দুটি দেখে — cart-1-এ ৫টি চলমান, cart-3-এ ২টি — আর cart-3 বাছে। এই নিয়মই অংশ ৫-এর power of two choices. - Backend-এ পাঠানো। L7-2 নতুন সংযোগ খোলে না; খোলা keep-alive সংযোগের pool থেকে একটি নেয়, X-Forwarded-For আর request ID যোগ করে, আর ২ সেকেন্ডের timeout গোনে। এই সংযোগ L4 দিয়ে যায় না।
- উত্তর, আর পরের অনুরোধ। cart-3 ৩৮ ms-এ
200দিল; L7 এই latency মনে রাখে। একই client-সংযোগের stream 3-এরGET /api/offersগেল সম্পূর্ণ আলাদা pool-এর offers-2-তে (request-level); অথচ L4-এর চোখে এটি এখনো একই সংযোগ, তাই এখনো L7-2 (connection-level). এই পার্থক্যটিই পরের সমস্যার মূল।
দীর্ঘজীবী সংযোগের ফাঁদ — HTTP/2 আর gRPC-তে অসম ভার
HTTP/1.1-এ অনেক client-এর অনেক ছোট সংযোগ — connection-level ভাগেই ভার মোটামুটি সমান হতো। HTTP/2 এক সংযোগে অনেক অনুরোধ একসাথে চালায় (multiplexing), আর gRPC (পর্ব ৯)-এ একটি client সাধারণত একটি সংযোগ ঘণ্টার পর ঘণ্টা রাখে, সব RPC তার আলাদা stream-এ। ফলে L4-এর “একটি সংযোগ = একটি সিদ্ধান্ত” হয়ে যায় “একটি client-এর সব অনুরোধ = একটি backend”.
পাইকারি আড়তের গেটের দারোয়ান প্রতিটি ট্রাককে একটি গুদাম-দরজায় পাঠান। আগে আসত শত শত ছোট ভ্যান; এখন আসে তিনটি বিশাল ট্রাক। দারোয়ান ট্রাক গোনেন, বস্তা নয় — তাই তিনটি দরজায় পাহাড়, বাকিগুলো ফাঁকা। সমাধান: গেটেই মাল নামিয়ে প্রতিটি বস্তা আলাদা দরজায় পাঠানো (L7), নয়তো ট্রাকগুলোকে মাঝে মাঝে ফিরে গিয়ে আবার ঢুকতে বলা।
L4 (connection-level): ৩টি backend-এ ১,০০০ RPS করে, ৩টি অলস · L7 (request-level): ৬টিতেই ৫০০ RPS করে
Autoscaling-এ আরও খারাপ: backend ৬ থেকে ১২ হলেও পুরোনো সংযোগ পুরোনো backend-এই বাঁধা, নতুন ৬টি প্রায় কিছুই পায় না। Kubernetes-এর সাধারণ Service (kube-proxy) সংযোগ-স্তরে ভাগ করে বলে gRPC-তে এই সমস্যা সেখানে বহুল আলোচিত। প্রতিকার:
- HTTP/2-বোঝা L7 proxy (Envoy, Nginx-এর gRPC proxy, cloud-এর application LB) প্রতিটি stream আলাদা ভাগ করে; দাম আরও একটি hop আর CPU.
- Client-side load balancing: gRPC লাইব্রেরি সব backend-এর সাথে সংযোগ রেখে RPC পালা করে পাঠায় (যেমন
round_robinpolicy); দাম হালনাগাদ backend-তালিকা (পর্ব ৪৭)। - সংযোগের সর্বোচ্চ বয়স: সার্ভার কয়েক মিনিট পরপর, এলোমেলো দেরিসহ, HTTP/2-এর GOAWAY পাঠায়; client আবার জোড়ে, নতুন backend-ও ভাগ পায়। gRPC-তে এটি max connection age ধাঁচের সেটিং; WebSocket-এও একই কৌশল (পর্ব ১০)।
উল্টো দিকটিও মনে রাখুন: L7 proxy নিজেও backend-এর দিকে অল্প কয়েকটি দীর্ঘজীবী HTTP/2 সংযোগ রাখে। তাই L7-এর algorithm-কে সংযোগ নয়, চলমান অনুরোধ গুনতে হয়। Stream আর frame-এর গঠন Computer Networks কোর্সের HTTP/2 আর HTTP/3 পর্বে।
৪ · Algorithm — round robin থেকে power of two choices
প্রতিটি নতুন অনুরোধে (L4-এ নতুন সংযোগে) প্রশ্ন একটাই: সুস্থ backend-গুলোর মধ্যে কে? প্রতিটি algorithm-এ দেখুন — কী তথ্য লাগে, তা কত টাটকা, আর ভুল হলে কী ঘটে।
Round robin — পালা করে
১, ২, ৩, … ১০, আবার ১. শুধু একটি counter লাগে। সব সার্ভার একই রকম আর অনুরোধ প্রায় সমান হলে চমৎকার। দুর্বলতা: কে ধীর, কোন অনুরোধ ভারী, কে এখন ব্যস্ত — কিছুই জানে না; দুই সেকেন্ডের রিপোর্ট-অনুরোধে আটকে থাকা সার্ভারও পালা এলে নতুন কাজ পায়।
Weighted round robin — বড় সার্ভারের বেশি পালা
যন্ত্র মেশানো থাকলে প্রতিটির একটি ওজন — দ্বিগুণ শক্তিশালী সার্ভার দ্বিগুণ পালা। সরল উপায়ে ৫:১:১ মানে a-কে পরপর ৫টি, তারপর b, c; ওজন ২০:২০:১ হলে একজন পরপর ২০টি। Nginx-এর upstream module একটি smooth weighted round robin পদ্ধতি ব্যবহার করে (প্রকাশিত commit অনুযায়ী ২০১২ সালের দিকে যুক্ত), যা একই অনুপাত রেখে পালা ছড়িয়ে দেয়:
- প্রতিটি সার্ভারের একটি “চলতি ওজন” (current) আছে, শুরুতে ০।
- প্রতিটি বাছাইয়ে সবার current-এ নিজের ওজন যোগ হয়।
- যার current সবচেয়ে বেশি, সে জেতে (সমান হলে তালিকায় আগেরজন)।
- বিজয়ীর current থেকে সবার ওজনের যোগফল বাদ যায়।
প্রতি বাছাইয়ে মোট যোগ আর বিয়োগ সমান, তাই current-গুলোর যোগফল সবসময় ০; মোট ওজনের সমান বাছাইয়ে প্রত্যেকে ঠিক নিজের ওজনের সমান বার জেতে। নিচের কোড ৫:১:১-এর প্রথম ১৪টি বাছাই ছাপে আর পরিচিত ক্রমের সাথে মেলায়:
// Smooth weighted round robin (the method Nginx's upstream module uses for weights)
// versus naive WRR that sends `weight` requests in a row to each server.
function smooth(weights, picks, show) {
const cur = weights.map(() => 0), total = weights.reduce((a, b) => a + b), out = [];
for (let k = 0; k < picks; k++) {
let best = 0;
for (let i = 0; i < weights.length; i++) {
cur[i] += weights[i]; // every server gains its weight
if (cur[i] > cur[best]) best = i; // highest current weight wins (first one on ties)
}
const before = cur.join(",");
cur[best] -= total; // the winner pays back the total
out.push("abc"[best]);
if (show && k < 7) console.log("pick " + (k + 1) + ": [" + before + "] -> " + out[k] + " -> [" + cur.join(",") + "]");
}
return out;
}
function naive(weights, picks) {
const cycle = weights.flatMap((w, i) => Array(w).fill("abc"[i]));
return Array.from({ length: picks }, (_, k) => cycle[k % cycle.length]);
}
function longestRun(s) { // most picks of the same server in a row
let best = 1, run = 1;
for (let i = 1; i < s.length; i++) { run = s[i] === s[i - 1] ? run + 1 : 1; best = Math.max(best, run); }
return best;
}
const s = smooth([5, 1, 1], 14, true), nv = naive([5, 1, 1], 14);
console.log("smooth 5:1:1 " + s.join(" ") + " longest run " + longestRun(s));
console.log("naive 5:1:1 " + nv.join(" ") + " longest run " + longestRun(nv));
console.log("first 7 equal a a b a c a a? " + (s.slice(0, 7).join(" ") === "a a b a c a a"));
const s2 = smooth([20, 20, 1], 41, false), n2 = naive([20, 20, 1], 41);
console.log("weights 20:20:1 over 41 picks: longest run smooth " + longestRun(s2) + ", naive " + longestRun(n2) +
"; smooth counts a/b/c = " + ["a", "b", "c"].map(x => s2.filter(y => y === x).length).join("/"));আউটপুট:
pick 1: [5,1,1] -> a -> [-2,1,1]
pick 2: [3,2,2] -> a -> [-4,2,2]
pick 3: [1,3,3] -> b -> [1,-4,3]
pick 4: [6,-3,4] -> a -> [-1,-3,4]
pick 5: [4,-2,5] -> c -> [4,-2,-2]
pick 6: [9,-1,-1] -> a -> [2,-1,-1]
pick 7: [7,0,0] -> a -> [0,0,0]
smooth 5:1:1 a a b a c a a a a b a c a a longest run 4
naive 5:1:1 a a a a a b c a a a a a b c longest run 5
first 7 equal a a b a c a a? true
weights 20:20:1 over 41 picks: longest run smooth 1, naive 20; smooth counts a/b/c = 20/20/1
বাছাই ১-এ সবাই ওজন পেল — [5,1,1]; a জিতে ৭ ফেরত দিল, হলো −২। বাছাই ৩-এ b আর c দুজনেরই ৩, তাই b (তালিকায় আগে)। বাছাই ৭-এর পর সব আবার [0,0,0] — চক্র পূর্ণ। ক্রম a a b a c a a, যা Nginx-এর প্রকাশিত commit-বার্তার ক্রমের সাথে মেলে (true). ৫:১:১-এ লাভ সামান্য (পরপর ৪টি বনাম ৫টি a), কারণ সাতটির পাঁচটিই a. আসল পার্থক্য শেষ লাইনে: ২০:২০:১-এ সরল পদ্ধতি একজনকে পরপর ২০টি দেয়, smooth পদ্ধতি কাউকে পরপর দুবারও নয় — অথচ ভাগ ঠিক ২০/২০/১.
Random — এলোমেলো
প্রতিটি অনুরোধে একটি এলোমেলো সুস্থ সার্ভার। কোনো ভাগ করা অবস্থা লাগে না, তাই একশোটি LB-instance সমন্বয় ছাড়াই চলে। দীর্ঘ মেয়াদে ভাগ সমান, কিন্তু স্বল্প মেয়াদে এলোমেলো ঢেউ থাকে।
Least connections আর least outstanding requests
যার হাতে এই মুহূর্তে সবচেয়ে কম চলমান কাজ, নতুন কাজ তাকে। সবচেয়ে সুন্দর ধর্ম: সে নিজে থেকেই গতির সাথে মানিয়ে নেয়। পর্ব ৪-এর Little-এর সূত্র L = λ × W — একই হারে কাজ পেলে ধীর সার্ভারের W বড়, তাই চলমান কাজ L-ও বেশি, আর সে স্বাভাবিকভাবেই কম নতুন কাজ পায়। L7-এ keep-alive আর HTTP/2-এর কারণে সংযোগ গোনা অর্থহীন, তাই গোনা হয় চলমান অনুরোধ (Envoy-তে “least request”). দুর্বলতা দুটি: অনেক LB পুরোনো তথ্য দেখলে সবাই একই “সবচেয়ে ফাঁকা” সার্ভারে ঝাঁপায়, আর দ্রুত ত্রুটি ফেরানো সার্ভার সবচেয়ে “ফাঁকা” দেখায় (ভাবনার প্রশ্ন ০২)।
Least response time আর EWMA
সাম্প্রতিক latency-ও দেখা — যেমন latency-র EWMA (নতুন মাপকে বেশি গুরুত্ব দেওয়া চলমান গড়) কম যার, তাকে। ধীর হতে থাকা সার্ভার দ্রুত কম ভার পায়। ঝুঁকি: পুরোনো মাপে দোল খাওয়া, আর দ্রুত ত্রুটিকে “দ্রুত” ভাবা — তাই সাধারণত ত্রুটির হার আর power of two choices-এর সাথে মিলিয়ে ব্যবহার হয়।
Power of two random choices — দুটি দেখে ভালোটি
এলোমেলোভাবে মাত্র দুটি সার্ভার দেখে কম ব্যস্তটিকে দেওয়া। প্রায় random-এর মতো সরল, ফল প্রায় least connections-এর কাছাকাছি, আর পুরোনো তথ্যে তার চেয়েও টেকসই; খরচ প্রতি অনুরোধে O(1). Envoy-এর least request সমান ওজনে এভাবেই চলে (ডিফল্টে দুটি বাছাই); Nginx-এ random two least_conn, HAProxy-তে balance random(2) ধাঁচের বিকল্প আছে (সংস্করণভেদে আলাদা)।
Hash — একই key, একই সার্ভার
IP hash, user ID বা cache key-র hash থেকে সার্ভার বাছা — লক্ষ্য একই জিনিস একই জায়গায়, যাতে cache কাজে লাগে। দাম: জনপ্রিয় key বা NAT-CGNAT-এর পেছনের অনেক ব্যবহারকারী (পর্ব ৭) একটি সার্ভার গরম করে; আর hash mod N-এ N বদলালে প্রায় সব key ঘর বদলায়। প্রতিকার consistent hashing (পর্ব ১৬); session-এর sticky session পর্ব ১৫-এ।
| Algorithm | যে তথ্য লাগে | ভালো যখন | সাবধান যখন |
|---|---|---|---|
| Round robin | একটি counter | একই রকম সার্ভার, সমান অনুরোধ | বিষম গতি বা অনুরোধ |
| Weighted (smooth) RR | স্থির ওজন | ক্ষমতা জানা ও স্থির | পুরোনো ওজন; চলমান ভার জানে না |
| Random | কিছুই না | অনেক স্বাধীন LB | স্বল্প মেয়াদের ঢেউ, বিষম গতি |
| Least conn / least request | চলমান কাজের সংখ্যা | একটি LB, বিষম অনুরোধ, দীর্ঘ সংযোগ | পুরোনো তথ্য; দ্রুত-ত্রুটির সার্ভার; নতুন সার্ভারে ঝাঁক |
| Least response time / EWMA | সাম্প্রতিক latency | গতি সময়ে সময়ে বদলায় | দোল খাওয়া; দ্রুত ত্রুটি |
| Power of two choices | দুটি এলোমেলোর চলমান কাজ | বড় fleet, অনেক LB, কিছুটা পুরোনো তথ্য | ২–৩টি সার্ভারে প্রায় least conn-এর মতোই |
| Hash / consistent hash | অনুরোধের key | cache-এর locality | hot key, NAT-এর পেছনের ভিড় |
৫ · সংখ্যায় তুলনা — বিষম গতির সার্ভার, পুরোনো তথ্য আর দুটি বাছাইয়ের গণিত
ধরা যাক ১০টি সার্ভার, গতি আলাদা: দুটি ১.৫, দুটি ১.২৫, চারটি ১, দুটি পুরোনো ০.৭৫ — মোট ক্ষমতা ১১ একক কাজ প্রতি ms. প্রতিটি সার্ভার একবারে একটি অনুরোধে কাজ করে, বাকিরা সারিতে অপেক্ষা করে। অনুরোধ আসে এলোমেলোভাবে (Poisson), ক্ষমতার ৬৫% চাপে; ৯০% ছোট (গড় ৬ একক), ১০% বড় (গড় ৪৬)। সব algorithm একই অনুরোধ-ধারা পায়; শেষ দুটি সারিতে LB সারির দৈর্ঘ্য দেখে ৫০ ms পুরোনো অবস্থায় — অনেক LB-instance থাকলে যা স্বাভাবিক।
// 10 servers with different speeds, one FIFO worker each; the SAME request trace for every algorithm.
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 speed = [1.5, 1.5, 1.25, 1.25, 1, 1, 1, 1, 0.75, 0.75]; // work units per ms
const n = speed.length, cap = speed.reduce((a, b) => a + b), rho = 0.65, N = 50000;
const rnd = mulberry32(14), exp = m => -m * Math.log(1 - rnd());
const lambda = rho * cap / 10; // requests per ms; mean job = 10 units
const trace = []; let t = 0;
for (let i = 0; i < N; i++) { // Poisson arrivals; 90% small jobs, 10% big
t += exp(1 / lambda); trace.push([t, rnd() < 0.9 ? exp(6) : exp(46)]);
}
function run(name, pick, staleMs = 0) { // staleMs: the LB sees queue lengths this old
const r = mulberry32(7), fin = speed.map(() => []), ptr = speed.map(() => 0), lat = [];
let maxQ = 0, view = null, next = 0;
trace.forEach(([at, size], i) => {
const q = fin.map((f, s) => { while (ptr[s] < f.length && f[ptr[s]] <= at) ptr[s]++; return f.length - ptr[s]; });
if (at >= next) { view = q.slice(); next = at + staleMs; }
const s = pick(i, view, r), last = fin[s].length ? fin[s][fin[s].length - 1] : 0;
const done = Math.max(at, last) + size / speed[s]; // wait behind the queue, then get served
fin[s].push(done); lat.push(done - at); maxQ = Math.max(maxQ, q[s] + 1);
});
lat.sort((a, b) => a - b);
const mean = lat.reduce((a, b) => a + b) / N, p99 = lat[Math.ceil(0.99 * N) - 1]; // nearest rank
console.log(name.padEnd(27) + mean.toFixed(1).padStart(7) + p99.toFixed(1).padStart(9) + String(maxQ).padStart(6));
}
const LC = (i, q) => { let b = i % n; for (let k = 1; k < n; k++) { const s = (i + k) % n; if (q[s] < q[b]) b = s; } return b; };
const P2 = (i, q, r) => { const a = Math.floor(r() * n); let b = Math.floor(r() * (n - 1)); if (b >= a) b++; return q[b] < q[a] ? b : a; };
console.log("capacity " + cap + " units/ms, offered load " + rho * 100 + "%, " + N + " requests");
console.log("algorithm mean ms p99 ms maxQ");
run("Round robin", i => i % n);
run("Random", (i, q, r) => Math.floor(r() * n));
run("Least connections", LC);
run("Power of two choices", P2);
run("Least conn, 50 ms stale", LC, 50);
run("Power of two, 50 ms stale", P2, 50);আউটপুট:
capacity 11 units/ms, offered load 65%, 50000 requests
algorithm mean ms p99 ms maxQ
Round robin 206.1 1944.5 166
Random 135.2 1377.4 165
Least connections 11.9 124.1 6
Power of two choices 22.0 187.4 10
Least conn, 50 ms stale 53.6 364.3 45
Power of two, 50 ms stale 31.7 232.1 17
Round robin আর random গতি-অন্ধ। প্রত্যেকে পায় মোট ভারের ১০% — প্রতি ms-এ ০.৭১৫ একক কাজ: দ্রুত সার্ভারের (১.৫) কাছে ক্ষমতার ৪৭.৭%, ধীরটির (০.৭৫) কাছে ৯৫.৩%. প্রায়-পূর্ণ ধীর সার্ভারের সারি ১৬৬-তে ওঠে, p99 প্রায় ২ সেকেন্ড — fleet মাত্র ৬৫% ব্যস্ত হলেও (পর্ব ৪: utilization ১-এর কাছে গেলে অপেক্ষা লাফিয়ে বাড়ে)। Random এখানে কম খারাপ এসেছে, কিন্তু trace-এর seed বদলালে কোনো ধারায় উল্টোও হয় — দুজনের মূল সমস্যা এক। Least connections: গড় ১১.৯ ms, p99 ১২৪ ms, সারি সর্বোচ্চ ৬ — ওজন না জেনেই দ্রুত সার্ভারকে বেশি দিয়েছে। Power of two: ২২.০ আর ১৮৭ ms — মাত্র দুটি দেখে p99 round robin-এর দশ গুণের বেশি ভালো। শেষ দুটি সারি: তথ্য ৫০ ms পুরোনো হলে least connections-এর p99 ৩৬৪ ms, সারি ৪৫ — ৫০ ms-এ আসা প্রায় ৩৬টি অনুরোধ একই পুরোনো ছবি দেখে একই এক-দুটি “সবচেয়ে ফাঁকা” সার্ভারে ঝাঁপায় (herd behavior); power of two-র p99 ২৩২ ms, সারি ১৭. মডেলের সীমা: একটি worker, network-এর সময় নেই — সংখ্যা বাস্তবে আলাদা, তুলনার দিকটি টেকসই।
দুটি বাছাই ধাপে ধাপে
ছয়টি সার্ভার, প্রতিটির পাশে এই মুহূর্তে চলমান অনুরোধের সংখ্যা। তিনটি নতুন অনুরোধ আসছে। লাল চিহ্ন দেওয়া দুটি এলোমেলোভাবে বাছা — “পরে” চাপুন:
| চলমান (S1…S6) | |
| Least conn হলে | |
| দেখার খরচ |
- শুরুর অবস্থা। S1 থেকে S6-এর হাতে ৩, ১, ৪, ২, ৫ আর ২টি চলমান অনুরোধ। নতুন অনুরোধ r1 এল। Least connections হলে LB ছয়টিই দেখে S2 বাছত।
- দুটি এলোমেলো বাছাই। LB তুলল S2 আর S5 — বাকি চারটির খবর এই সিদ্ধান্তে লাগবেই না।
- কম ব্যস্তটি জেতে। S2-তে ১টি, S5-এ ৫টি, তাই r1 গেল S2-তে; S2 এখন ২টি।
- সেরা নয়, কিন্তু খারাপও নয়। r2-এর জোড়া S1 (৩) আর S3 (৪); r2 গেল S1-এ, অথচ সবচেয়ে ফাঁকা ছিল S2, S4, S6 (২টি করে)। Power of two সেরাটি খোঁজে না, শুধু জোড়ার খারাপটিকে এড়ায় — এই ছাড়টুকুই অনেক LB-তে সবাইকে একই সার্ভারে ঝাঁপানো থেকে বাঁচায়।
- ভারী সার্ভার রক্ষা পায়। r3-এর জোড়ায় S5 (৫) আর S6 (২); r3 গেল S6-এ। সবচেয়ে ব্যস্ত S5 দুবার জোড়ায় এসেও একবারও জেতেনি।
- সম্ভাবনার হিসাব। সবার ভার আলাদা হলে, ভারে r-তম (১ = সবচেয়ে ফাঁকা) সার্ভারের নতুন অনুরোধ পাওয়ার সম্ভাবনা ২(n − r) / (n(n − ১))। n = ৬-এ সবচেয়ে ফাঁকাটি ১০/৩০ = ১/৩, সবচেয়ে ব্যস্তটি ০ — random দিত সবাইকে ১/৬।
কেন দুটি বাছাই এত শক্তিশালী — একটি সরল যুক্তি
প্রশ্নটির গাণিতিক রূপ “balls into bins” (বল আর বাক্স): n টি বল (অনুরোধ) n টি বাক্সে (সার্ভার), কেউ বেরোয় না। সবচেয়ে ভরা বাক্সে কতগুলো?
- একটি বাছাই (random): একটি বাক্সে অন্তত k টি বল পড়ার সম্ভাবনা ≤ C(n, k) × (১/n)k ≤ ১/k!, তাই যেকোনো বাক্সে ≤ n/k!, যা ১-এর নিচে নামে যখন k! > n. এখান থেকেই পরিচিত ফল: সর্বোচ্চ প্রায় ln n / ln ln n (উচ্চ সম্ভাবনায়)।
- দুটি বাছাই: βi = অন্তত i টি বল আছে এমন বাক্সের ভাগ। একটি বল (i + ১) উচ্চতায় বসে শুধু যখন দুটি বাছাই-ই অন্তত i উচ্চতায় পড়ে — সম্ভাবনা প্রায় βi২, তাই মোটামুটি βi+1 ≲ βi২। বল মোট n টি, তাই β২ ≤ ১/২। প্রতি ধাপে বর্গ: βi ≤ (১/২)২^(i−২) — দ্বিগুণ-সূচকীয় পতন, ১/n-এর নিচে যখন i ≈ log₂ log₂ n + ২।
এই যুক্তির নিখুঁত প্রমাণ দেন Azar, Broder, Karlin ও Upfal (“Balanced Allocations”, ১৯৯৪): d টি বাছাইয়ে সর্বোচ্চ প্রায় ln ln n / ln d + O(১)। ১ থেকে ২-এ উন্নতি সূচকীয়, ২ থেকে ৩-এ শুধু একটি ধ্রুবক গুণ। Michael Mitzenmacher তাঁর PhD গবেষণায় (UC Berkeley, ১৯৯৬) একে চলমান সারির মডেলে নেন (“supermarket model”): বড় n-এ অন্তত i টি কাজ আছে এমন সারির ভাগ random-এ প্রায় λi, দুটি বাছাইয়ে প্রায় λ(২^i − ১) (λ = utilization; প্রায় একই সময়ে Vvedenskaya, Dobrushin ও Karpelevich-ও স্বাধীনভাবে এমন ফল পান)। λ = ০.৯, i = ৫: random-এ প্রায় ০.৫৯, দুটি বাছাইয়ে প্রায় ০.০৩৮ — সীমায় পাওয়া মান, ছোট fleet-এ সংখ্যা আলাদা।
তত্ত্বটি নিজে মিলিয়ে দেখুন — একসাথে n টি অনুরোধ n টি সার্ভারে, d = ১, ২, ৩:
// Balls into bins: n requests go to n servers, all at once (no request finishes).
// Each request looks at d random servers and joins the least loaded (d = 1: plain random).
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; }; }
console.log("n max load d=1 d=2 d=3 | ln n/ln ln n log2(ln n)");
for (const n of [1000, 100000, 1000000]) {
const maxLoad = [];
for (const d of [1, 2, 3]) {
const r = mulberry32(2026 + d), bins = new Uint32Array(n);
for (let i = 0; i < n; i++) {
let best = Math.floor(r() * n);
for (let k = 1; k < d; k++) { const c = Math.floor(r() * n); if (bins[c] < bins[best]) best = c; }
bins[best]++;
}
let mx = 0;
for (let i = 0; i < n; i++) if (bins[i] > mx) mx = bins[i];
maxLoad.push(mx);
}
const L = Math.log(n), LL = Math.log(L);
console.log(String(n).padEnd(11) + String(maxLoad[0]).padStart(13) + String(maxLoad[1]).padStart(5) +
String(maxLoad[2]).padStart(5) + " |" + (L / LL).toFixed(2).padStart(13) + (LL / Math.LN2).toFixed(2).padStart(12));
}আউটপুট:
n max load d=1 d=2 d=3 | ln n/ln ln n log2(ln n)
1000 5 3 3 | 3.57 2.79
100000 7 3 3 | 4.71 3.53
1000000 9 4 3 | 5.26 3.79
n হাজার গুণ (এক হাজার থেকে দশ লাখ) হলে random-এর সবচেয়ে ভরা সার্ভার ৫ থেকে ৯-এ ওঠে, দুটি বাছাইয়ে মাত্র ৩ থেকে ৪-এ, তিনটিতে ৩-এই থাকে। ডানের কলাম দুটি তত্ত্বের প্রধান পদ — ln n / ln ln n আর log₂(ln n) = ln ln n / ln ২; ছোট পদ আর ধ্রুবক বাদ বলে মাপা মান একটু বড়। আসল কথা বৃদ্ধির হার: একটি বাছাইয়ে সর্বোচ্চ ভার চোখে পড়ার মতো বাড়ে, দুটিতে প্রায় নড়ে না, আর ২ থেকে ৩-এ লাভ সামান্য — তাই বাস্তবে সবাই দুটিতেই থামে। সম্ভাবনার যন্ত্রপাতি DSA কোর্সের Randomized algorithm পর্বে।
৬ · Health check — কখন বাদ, কখন ফেরত, আর কত অনুরোধের দামে
Algorithm বাছে শুধু “সুস্থ” সার্ভারের মধ্যে। কে সুস্থ, তা LB জানে দুই উপায়ে:
Active check
LB নির্দিষ্ট বিরতিতে (interval) probe পাঠায় — TCP সংযোগ, বা GET /healthz-এ 200. পরপর fall বার ব্যর্থে DOWN, পরপর rise বার সফলে UP; traffic না থাকলেও কাজ করে।
Passive check
আসল traffic-এ সংযোগ ব্যর্থ, timeout বা পরপর কয়েকটি 5xx হলে কিছুক্ষণ বাদ। ব্যস্ত সার্ভারে খুব দ্রুত, traffic না থাকলে অন্ধ। উন্নত রূপ outlier ejection — পর্ব ৪৮।
আলাদা fall আর rise একরকম hysteresis: একবারের হোঁচটে বাদ নয়, একবারের সাফল্যে ফেরতও নয় — নইলে সীমানার সার্ভার বারবার আসা-যাওয়া করত (flapping). দাম সময়: সার্ভার ঝুলে গেলে প্রতিটি probe timeout পর্যন্ত অপেক্ষা করে, আর বিকলতা দুটি probe-এর মাঝে যেকোনো মুহূর্তে শুরু হতে পারে:
গড়: ≈ interval/২ + (fall − ১) × interval + timeout
ব্যর্থ অনুরোধ ≈ (মোট QPS ÷ সার্ভার সংখ্যা) × ধরা পড়ার সময়
শেষ সূত্রটি ধরে নেয় ভার সমান ভাগ আর কোনো retry নেই। এখন সংখ্যা বসিয়ে দেখুন — তিনটি active setting, তারপর passive check, আর শেষে check-এর নিজের খরচ:
// Health-check timing. Model: a check starts every `inter` seconds; against a HUNG server
// each check waits `timeout` before it counts as failed; `fall` failures in a row = DOWN,
// `rise` successes in a row = UP again. Failure time is uniform between two checks.
function detect(c) {
const worst = c.fall * c.inter + c.timeout; // failed just after a check had started
const avg = c.inter / 2 + (c.fall - 1) * c.inter + c.timeout;
return { worst, avg, back: c.rise * c.inter }; // back UP at most rise x inter after the fix
}
const QPS = 5000, servers = 10, each = QPS / servers; // even spread across servers
console.log("total " + QPS + " QPS on " + servers + " servers = " + each + " QPS each");
console.log("active check setting detect avg worst | failed avg worst | UP after");
const settings = [
["inter 2s, timeout 1s, fall 3, rise 2", { inter: 2, timeout: 1, fall: 3, rise: 2 }],
["inter 5s, timeout 2s, fall 3, rise 2", { inter: 5, timeout: 2, fall: 3, rise: 2 }],
["inter 1s, timeout 0.5s, fall 2, rise 3", { inter: 1, timeout: 0.5, fall: 2, rise: 3 }],
];
for (const [name, c] of settings) {
const d = detect(c);
console.log(name.padEnd(39) + (d.avg + " s").padStart(7) + (d.worst + " s").padStart(8) + " |" +
String(d.avg * each).padStart(8) + String(d.worst * each).padStart(7) + " |" + (d.back + " s").padStart(6));
}
const reqTimeout = 1; // the LB's own per-request timeout (s)
for (const k of [5, 20]) { // passive: k bad answers in a row in real traffic
const fast = k / each, hung = reqTimeout + k / each;
console.log("passive, " + String(k).padStart(2) + " errors in a row: fast errors " + (fast * 1000) + " ms (~" + k +
" failed); hung server " + hung + " s (~" + Math.round(hung * each) + " failed)");
}
const lbNodes = 20, fleet = 500; // cost side: every LB node checks every server
console.log("check load: " + lbNodes + " LB nodes x " + fleet + " servers / 2 s = " + lbNodes * fleet / 2 + " checks/s");আউটপুট:
total 5000 QPS on 10 servers = 500 QPS each
active check setting detect avg worst | failed avg worst | UP after
inter 2s, timeout 1s, fall 3, rise 2 6 s 7 s | 3000 3500 | 4 s
inter 5s, timeout 2s, fall 3, rise 2 14.5 s 17 s | 7250 8500 | 10 s
inter 1s, timeout 0.5s, fall 2, rise 3 2 s 2.5 s | 1000 1250 | 3 s
passive, 5 errors in a row: fast errors 10 ms (~5 failed); hung server 1.01 s (~505 failed)
passive, 20 errors in a row: fast errors 40 ms (~20 failed); hung server 1.04 s (~520 failed)
check load: 20 LB nodes x 500 servers / 2 s = 5000 checks/s
প্রথম সারি: গড়ে ১ + ৪ + ১ = ৬ সেকেন্ড, সবচেয়ে খারাপে ৩ × ২ + ১ = ৭; প্রতি সার্ভারে ৫০০ QPS, তাই বাদ পড়ার আগেই গড়ে ৩,০০০টি অনুরোধ ব্যর্থ। ঢিলেঢালা ৫ সেকেন্ডে ৭,২৫০; ১ সেকেন্ড আর fall ২-এ ১,০০০. Passive check অন্য মাপের: দ্রুত ত্রুটিতে ৫টি জমতে ১০ ms, ক্ষতি প্রায় ৫টি; ঝুলে থাকলে প্রথম timeout পর্যন্ত অপেক্ষা — প্রায় ৫০৫টি, তবুও ৩,০০০-এর চেয়ে অনেক কম। শেষ লাইন খরচ: ২০টি LB নোড × ৫০০টি সার্ভার ÷ ২ সেকেন্ড = সেকেন্ডে ৫,০০০টি check, শুধু “বেঁচে আছেন?” জানতে — তাই বড় fleet-এ প্রতিটি LB একটি উপসেট দেখে (subsetting). বাস্তবে retry (শুধু idempotent অনুরোধে, পর্ব ৪৮) এই ব্যর্থতার অনেকটা লুকায়।
ফেরার সময়ের ফাঁদ — slow start
সদ্য চালু সার্ভারের cache আর connection pool খালি, JIT চলছে — warm-up. অথচ least connections-এর চোখে সে-ই সবচেয়ে আকর্ষণীয় (০টি কাজ!), তাই প্রথম মুহূর্তেই প্রায় সব নতুন অনুরোধ তার ঘাড়ে — একটি ছোট thundering herd. Slow start-এ ফেরা সার্ভারের ওজন কয়েক সেকেন্ড ধরে শূন্যের কাছ থেকে পূর্ণ মানে তোলা হয়। HAProxy-র slowstart শুধু আগে বিকল দেখা সার্ভারে প্রযোজ্য, LB নিজে চালু হওয়ার সময় নয়; Envoy-তেও এমন বিকল্প আছে (সংস্করণভেদে আলাদা)।
ধরা যাক: মোট ২,০০০ RPS, চারটি সার্ভারে ৫০০ করে; check প্রতি ২ সেকেন্ডে, timeout ১ সেকেন্ড, fall ৩, rise ২; slow start ৩০ সেকেন্ড, ওজন সরলরেখায় বাড়ে।
| সময় | |
| S3-এর check | |
| মোট ব্যর্থ অনুরোধ |
- স্বাভাবিক অবস্থা। চারটি সার্ভার, প্রতিটি ২৫% — ৫০০ RPS. LB ২ সেকেন্ড পরপর
GET /healthzপাঠায়, অপেক্ষা সর্বোচ্চ ১ সেকেন্ড। - t = ০: S3 ঝুলে গেল। ধরা যাক deadlock-এ সব thread আটকে: process চলছে, port খোলা, উত্তর নেই। শেষ check সফল ছিল, তাই LB এখনো S3-কে ২৫% দিচ্ছে — প্রতিটি অনুরোধ timeout-এ ব্যর্থ হবে।
- প্রথম দুটি check ব্যর্থ। প্রথম check ০.৮ সেকেন্ডে শুরু, ১.৮-এ ব্যর্থ (১/৩); দ্বিতীয়টি ২.৮-এ শুরু, ৩.৮-এ ব্যর্থ (২/৩)। ততক্ষণে প্রায় ৩.৮ × ৫০০ ≈ ১,৯০০টি অনুরোধ ব্যর্থ।
- t = ৫.৮ সেকেন্ড: DOWN. তৃতীয় check ৫.৮-এ ব্যর্থ (৩/৩) — S3 বাদ। ধরা পড়তে লাগল ০.৮ + ২ × ২ + ১ = ৫.৮ সেকেন্ড, সূত্রের গড় (৬) আর সবচেয়ে খারাপের (৭) মধ্যে। মোট ক্ষতি ≈ ২,৯০০টি অনুরোধ (retry না থাকলে)।
- বাদ থাকা অবস্থায়। S1, S2, S4 প্রতিটি ≈ ৬৬৭ RPS. তিনজনের ৩৩% বাড়তি ভার নেওয়ার জায়গা — N+1 আর headroom (পর্ব ৫) — না থাকলে তারাও ধসে পড়ত। LB S3-কে check করেই চলেছে।
- t = ৬০ সেকেন্ড: সেরে উঠল। Restart-এর পর ৬০.৮ আর ৬২.৮ সেকেন্ডের check সফল (২/২) — S3 আবার UP, কিন্তু slow start-এ, ওজন প্রায় শূন্য।
- t = ৭৭.৮ সেকেন্ড: অর্ধেক পথ। ওজন অন্যদের অর্ধেক: ভাগ = ০.৫ ÷ (১ + ১ + ১ + ০.৫) = ১৪.৩% ≈ ২৮৬ RPS; বাকিরা ১ ÷ ৩.৫ = ২৮.৬% ≈ ৫৭১ RPS.
- t = ৯২.৮ সেকেন্ড: পূর্ণ ওজন। চারটিই আবার ২৫%. ক্ষতি ছিল শুধু প্রথম ৫.৮ সেকেন্ডে; কমানোর উপায় passive check, অন্য সার্ভারে retry আর ছোট interval — খুব ছোট হলে অবশ্য দীর্ঘ garbage collection-কেও মৃত্যু ভেবে ভুল হয়।
ভালো health check কী যাচাই করে
- অগভীর বনাম গভীর: “process বেঁচে আছে” প্রায় কিছুই বলে না; সব dependency যাচাই করা deep health check বিপজ্জনক — database একটু ধীর হলেই সব সার্ভার একসাথে ফেল, LB সবাইকে বাদ দেয় (correlated failure). ভালো মাঝামাঝি: সার্ভার নিজের কাজে প্রস্তুত কি না — thread pool ফাঁকা, config লোড হয়েছে।
- Liveness আর readiness আলাদা: Kubernetes-এ liveness probe ব্যর্থ হলে container restart, readiness probe ব্যর্থ হলে শুধু traffic থেকে সরানো; dependency-র সমস্যায় restart-এ লাভ নেই, তাই liveness অগভীর।
- সবাই অসুস্থ হলে? অনেক LB fail open করে — health না মেনে সবার মধ্যে ভাগ, কারণ “সবাই বিকল” সাধারণত check-এরই ভুল। Envoy-তে সুস্থের হার একটি সীমার (লেখার সময়ে ডিফল্ট ৫০%) নিচে নামলে এই “panic” মোড চালু হয়।
- Check-এর পথ আর আসল পথ এক: একই port, TLS আর proxy-স্তর — নইলে check সফল, ব্যবহারকারী ব্যর্থ। Failure detection-এর তত্ত্ব পর্ব ৪৪ (Membership আর failure detection)-এ।
৭ · Deploy আর connection draining — কেউ টের না পেয়ে সার্ভার বদলানো
সরাসরি process বন্ধ করলে হাতের প্রতিটি অনুরোধ মাঝপথে ভাঙে। Connection draining (কিছু পণ্যে “deregistration delay”) এর প্রতিকার — বন্ধ হতে যাওয়া কাউন্টারে “আর নতুন লাইন নয়” বোর্ড, কিন্তু লাইনের যাত্রীদের টিকিট দেওয়া শেষ:
- Drain চিহ্ন: LB-কে বলা হয় সার্ভারটিকে আর নতুন অনুরোধ না দিতে (HAProxy-র runtime API-তে
set server be_cart/cart2 state drainধাঁচের কমান্ড; Kubernetes-এ pod terminating হলে readiness হারিয়ে endpoint তালিকা থেকে সরে)। - অপেক্ষা: চলমান অনুরোধ শেষ হওয়া পর্যন্ত, তবে একটি সর্বোচ্চ সময়সীমা পর্যন্ত (ধরা যাক ৩০ সেকেন্ড) — দীর্ঘ upload বা স্থায়ী সংযোগের জন্য যা যথেষ্ট নাও হতে পারে।
- ভদ্রভাবে বন্ধ: সার্ভার নতুন সংযোগ নেওয়া থামায়, keep-alive সংযোগে “Connection: close” বা HTTP/2-এর GOAWAY পাঠায়, হাতের কাজ শেষ করে বেরোয়।
- নতুন সংস্করণ, check, slow start: চালু হয়ে health check-এ পরপর rise বার সফল, তারপর ধীরে পূর্ণ ভারে; তারপর পরের সার্ভার।
বণ্টিত ব্যবস্থায় “সরানো” সঙ্গে সঙ্গে ঘটে না। Kubernetes-এ pod বন্ধের নির্দেশ আর সব endpoint তালিকা থেকে তার নাম মোছা দুটি সমান্তরাল প্রক্রিয়া; process তাড়াতাড়ি বেরোলে মাঝের অল্প সময়ের অনুরোধ ভাঙে। প্রচলিত অভ্যাস: বন্ধের আগে কয়েক সেকেন্ড অপেক্ষা (preStop hook), আর মোট সময়সীমা (terminationGracePeriodSeconds, ডিফল্ট ৩০ সেকেন্ড) তার চেয়ে বড়। স্থায়ী WebSocket-এর draining পর্ব ১০-এ।
Rolling deploy-এর অঙ্ক — কতগুলো একসাথে সরানো যায়
N টি সার্ভার u ভাগ ব্যস্ত থাকলে k টি সরালে বাকিদের ব্যস্ততা u × N ÷ (N − k). ধরা যাক ১০টি, ৭০% ব্যস্ত: একটি সরালে ≈ ৭৭.৮%, দুটি সরালে ৮৭.৫% — queueing-এর বিপজ্জনক এলাকার কাছে। তাই deploy কম ভিড়ে, অল্প কয়েকটি করে, বা আগে নতুন কপি যোগ করে (Kubernetes-এর maxSurge আর maxUnavailable, ডিফল্ট ২৫% করে)। Blue-green আর canary পর্ব ৫৩ (Deployment আর release)-এ; canary routing পর্ব ১১-এ।
সব একসাথে — একটি HAProxy-ধাঁচের config
নিচের config পড়ার জন্য — একটি উদাহরণ। HAProxy-র directive আর ডিফল্ট সংস্করণভেদে আলাদা (যেমন http-check send নতুন সংস্করণের লেখার ধরন), তাই বাস্তবে নিজের সংস্করণের অফিসিয়াল ডকুমেন্টেশনে মিলিয়ে নিন।
# Illustrative only: a simplified L7 setup for the "cart" service.
# Directive names and defaults differ between HAProxy versions - check the docs.
defaults
mode http
timeout connect 2s
timeout client 30s
timeout server 5s
timeout check 1s # extra read timeout for health checks
retries 2
option redispatch # a retry may go to a different server
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs/shop.pem alpn h2,http/1.1
default_backend be_cart
backend be_cart
balance leastconn # or: roundrobin, random(2), source
option httpchk
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host shop.example
http-check expect status 200
default-server check inter 2s fall 3 rise 2 slowstart 30s
server cart1 10.0.3.11:8080 weight 100
server cart2 10.0.3.12:8080 weight 100
server cart3 10.0.3.13:8080 weight 50 # older, smaller machine
server cart4 10.0.3.14:8080 weight 100| লাইন | কী বোঝায় | সতর্কতা |
|---|---|---|
bind :443 ssl … alpn h2,http/1.1 | TLS এখানে শেষ; HTTP/2 বা HTTP/1.1 | certificate আর key এখন LB-এর দায়িত্ব |
balance leastconn | চলমান সংযোগ কম যার, তাকে | অনেক instance-এ প্রত্যেকে নিজের গোনা দেখে (অংশ ৫) |
option httpchk, http-check … | active check: GET /healthz-এ 200 | ভাগ করা dependency-তে যেন না আটকায় |
inter 2s fall 3 rise 2 | HAProxy-র পরিচিত ডিফল্টও এটাই | সবচেয়ে খারাপে ৩ × ২ + ১ = ৭ সেকেন্ড |
slowstart 30s | ফেরা সার্ভারের ওজন ৩০ সেকেন্ডে পূর্ণ | LB নিজে চালু হওয়ার সময় প্রযোজ্য নয় |
weight 50 | cart3 অর্ধেক ভাগ পাবে | যন্ত্র বদলালে ওজনও বদলাতে হয় |
retries 2, option redispatch | সংযোগ-ব্যর্থতায় আবার চেষ্টা, প্রয়োজনে অন্য সার্ভারে | ভিড়ে retry storm-এর ঝুঁকি (পর্ব ৪৮) |
৮ · Load balancer নিজে পড়ে গেলে — VRRP, anycast, DSR আর global load balancing
সব traffic যে দরজা দিয়ে যায়, সেটি নিজেই single point of failure — একটি LB-যন্ত্রের পেছনে একশোটি সার্ভার রাখলেও availability ওই একটি যন্ত্রের সমান। দুটি প্রচলিত নকশা:
Active-passive জোড়া — VRRP আর virtual IP
দুটি LB, একটি সক্রিয়, অন্যটি প্রস্তুত (active-passive); VIP থাকে সক্রিয়টির কাছে। VRRP (RFC 2338, ১৯৯৮; এখনকার v3 — RFC 5798, ২০১০)-এ সক্রিয় যন্ত্র নিয়মিত ঘোষণা পাঠায়, ডিফল্টে প্রতি সেকেন্ডে। পরপর প্রায় তিনটি না এলে standby master হয়ে VIP নেয়, আর gratuitous ARP পাঠিয়ে switch-কে জানায় — কয়েক সেকেন্ডে failover; Linux-এ keepalived পরিচিত বাস্তবায়ন। দুর্বলতা: standby বসে থাকে, সক্রিয়টির ক্ষমতাই সীমা; connection table-এর তথ্য কপি না হলে চলমান সংযোগ ভাঙে; আর মাঝের link কাটলে দুজনেই VIP দাবি করতে পারে — split-brain (পর্ব ৪৩)।
Active-active — anycast আর ECMP
অনেকগুলো L4 নোড একসাথে একই VIP BGP দিয়ে router-কে ঘোষণা করে (active-active); router ECMP-এ প্রতিটি flow-এর 5-tuple hash করে কোনো একটি নোডে পাঠায়। নোড পড়লে তার ঘোষণা উঠে যায়, flow-গুলো বাকিদের মধ্যে ভাগ হয় — standby নেই, ক্ষমতা বাড়াতে শুধু নোড যোগ। সমস্যা: নতুন নোড আগের সংযোগ চেনে না। প্রতিকার — সব নোড একই consistent hashing টেবিল থেকে backend বাছে, তাই প্রায় সব সংযোগ একই backend পায়; Google-এর Maglev (NSDI ২০১৬) এর প্রকাশিত উদাহরণ (Data center নেটওয়ার্ক; পর্ব ১৬)।
Direct server return — উত্তর LB দিয়ে না ফেরানো
অনুরোধ ছোট, উত্তর বড়। DSR-এ L4 LB শুধু অনুরোধ পৌঁছায়; backend loopback-এ VIP রেখে উত্তর সরাসরি client-কে পাঠায়, তাই LB বয় traffic-এর ছোট অংশ। দাম: LB উত্তর দেখে না — L7-কাজ বা TLS termination অসম্ভব, সেটআপ জটিল।
Global load balancing — region-এর মধ্যে ভাগ
একাধিক region থাকলে প্রথম ভাগ DNS বা anycast-এ — GeoDNS কাছের region দেয়, health-aware DNS বিকল region সরায়; TTL কেন failover ধীর করে, পর্ব ৭-এ দেখেছেন। নিয়ম: global স্তর মোটা দাগে (region), ভেতরের L4–L7 সূক্ষ্ম দাগে (সার্ভার)। পুরো region হারানো পর্ব ৫১ (Failover আর disaster recovery)-এর বিষয়; স্থির content edge-এই থামে (পর্ব ১৭)।
আরও গভীরে: Google-এর প্রকাশিত Site Reliability Engineering বইয়ের (২০১৬) load balancing অধ্যায়ে subsetting (প্রতিটি client শুধু একটি উপসেটের সাথে সংযোগ রাখে) আর lame duck অবস্থা (বন্ধ হতে যাওয়া backend জানায় “আমাকে আর নতুন কাজ দেবেন না” — draining-এর client-side রূপ) বর্ণিত। Finagle আর Linkerd-এর প্রকাশিত ডকুমেন্টেশন অনুযায়ী তারা power of two choices-এর সাথে latency-র EWMA মেলায়। আর Mitzenmacher-এর “How Useful Is Old Information?” (IEEE TPDS, ২০০০) দেখায়, পুরোনো তথ্যে “সবচেয়ে কম ব্যস্তটিকে বাছাই” ভিড় জমায় — অংশ ৫-এর শেষ দুটি সারির মতো।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- L4 আর L7-এর পার্থক্য? — L4 IP-port দেখে সংযোগ ভাগ করে, TLS খোলে না; L7 HTTP পড়ে প্রতিটি অনুরোধ ভাগ করে, path দিয়ে routing পারে, CPU বেশি।
- interval ২ s, timeout ১ s, fall ৩ — সবচেয়ে খারাপে কত সেকেন্ডে ধরা পড়ে? — ৩ × ২ + ১ = ৭; প্রতি সার্ভারে ৫০০ QPS-এ প্রায় ৩,৫০০টি অনুরোধ ব্যর্থ।
- Least connections থাকতে power of two কেন? — O(১) খরচ, পুরোনো তথ্যে ভিড় জমায় না; সর্বোচ্চ ভার log n / log log n থেকে log log n-এর ধাঁচে।
- gRPC সার্ভিসে নতুন pod ভার পাচ্ছে না — কেন? — দীর্ঘজীবী HTTP/2 সংযোগ, আর L4 শুধু সংযোগ ভাগ করে; প্রতিকার L7 proxy, client-side LB বা সংযোগের সর্বোচ্চ বয়স।
- LB নিজেই SPOF হলে? — VRRP-তে active-passive, বা anycast-ECMP-তে active-active, সাথে consistent hashing.
৯ · AI-এর যুগে load balancing-নকশা যাচাই
“Scalable” নকশা চাইলে AI প্রায়ই লেখে “একটি load balancer বসান, round robin দিন”, সাথে Kafka, Redis আর microservices. LB-র প্রশ্নে তার সাধারণ ভুল:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| “একটি load balancer বসান” — একটিই | LB নিজেই SPOF | জিজ্ঞেস করুন: LB পড়লে কী হয়, কত সেকেন্ডে? |
| সব জায়গায় round robin, বা “least connections সবসময় সেরা” | বিষম গতিতে RR ধীরদের ডোবায়; least conn পুরোনো তথ্যে ভিড় জমায়, দ্রুত-ত্রুটির সার্ভারে traffic টানে | অংশ ৫-এর simulator-এ নিজের সংখ্যা বসান |
| gRPC সার্ভিস L4 LB-এর পেছনে, “ভার সমান হবে” | connection-level ভাগ | প্রতি backend-এর RPS দেখুন |
| “২ সেকেন্ড interval মানে ২ সেকেন্ডে ধরা পড়ে” | fall আর timeout বাদ গেছে | অংশ ৬-এর calculator |
| Health check-এ সব dependency যাচাই | ভাগ করা dependency ধীর হলে সবাই একসাথে বাদ | failure-mode review: “database ২ সেকেন্ড ধীর হলে?” |
| পণ্যের ডিফল্ট আর সীমা আত্মবিশ্বাসের সাথে লেখা | সংস্করণ আর সময়ভেদে বদলায়; AI প্রায়ই পুরোনো বা বানানো সংখ্যা দেয় | অফিসিয়াল ডকুমেন্টেশন |
| ছোট সেবায় GSLB, anycast, service mesh — সব একসাথে | over-engineering | প্রতিটি স্তরের পক্ষে নির্দিষ্ট সমস্যা আর সংখ্যা চান |
AI-কে নকশার সমালোচক হিসেবে কাজে লাগান — সংখ্যা আর সীমাবদ্ধতা নিজে দিন, হিসাব আর ঝুঁকি আলাদা করে চান:
AI-এর দেওয়া load balancing-নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে বাক্স-আঁকিয়ে থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Least connections তো সব সার্ভার দেখে সবচেয়ে ফাঁকাটি বাছে — তাহলে বড় সিস্টেম কেন মাত্র দুটি এলোমেলো সার্ভার দেখার নিয়ম বেছে নেয়?
কারণ বাস্তবে “সব সার্ভারের এই মুহূর্তের অবস্থা” কেউ জানে না। একটি বড় সেবায় অনেকগুলো LB-instance থাকে — L7 নোড, প্রতিটি সার্ভিসের পাশের sidecar, client-side লাইব্রেরি। প্রত্যেকে শুধু নিজের পাঠানো অনুরোধ গোনে, বা কিছুক্ষণ পরপর আসা রিপোর্ট দেখে। ফলে প্রত্যেকের হাতে আংশিক আর পুরোনো তথ্য।
এই অবস্থায় least connections একটি ফাঁদে পড়ে। ধরুন সব LB-ই দেখল S7 সবচেয়ে ফাঁকা। পরের কয়েক মিলিসেকেন্ডে সবাই একসাথে S7-কে কাজ দিল — এক মুহূর্তে S7 সবচেয়ে ব্যস্ত। তথ্য হালনাগাদ হলে সবাই আবার আরেকজনের দিকে ছুটল। একে বলে herd behavior — ঈদের আগে বাস কাউন্টারে “ওই লাইনটা ছোট!” শুনে সবাই একসাথে সেদিকে দৌড়ালে যা হয়। অংশ ৫-এর simulator-এ তথ্য ৫০ ms পুরোনো হতেই least connections-এর p99 ১২৪ থেকে ৩৬৪ ms-এ উঠেছিল।
এর তাত্ত্বিক ভিত্তি পুরোনো। ১৯৯৪ সালে Azar, Broder, Karlin ও Upfal প্রমাণ করেন, n টি কাজ n টি সার্ভারে এলোমেলো দিলে সবচেয়ে ভরাটিতে প্রায় ln n / ln ln n টি কাজ, কিন্তু প্রতিটি কাজ দুটি এলোমেলো সার্ভার দেখে কম ভরাটিতে গেলে মাত্র প্রায় ln ln n / ln ২ — সূচকীয় উন্নতি। Michael Mitzenmacher ১৯৯৬ সালের PhD গবেষণায় একে চলমান সারির মডেলে নেন, আর ২০০০ সালের “How Useful Is Old Information?” প্রবন্ধে দেখান, পুরোনো তথ্যে “সবচেয়ে কম ভরাকে বাছাই” খারাপ করে, কিন্তু অল্প কয়েকটি এলোমেলো বাছাই অনেক টেকসই থাকে। এলোমেলোতাই এখানে সমন্বয়ের কাজ করে: দুটি LB একই সার্ভার-জোড়া দেখার সম্ভাবনা কম, তাই তারা একই জায়গায় ঝাঁপায় না।
বাস্তবেও তাই: Envoy-এর least request, Nginx-এর random two least_conn, HAProxy-র random(2); খরচ প্রতি অনুরোধে O(১)।
মূল উপলব্ধি: বণ্টিত ব্যবস্থায় “সর্বোত্তম” সিদ্ধান্তের জন্য নিখুঁত, টাটকা তথ্য লাগে, যা প্রায় কখনো থাকে না। যে নিয়ম অসম্পূর্ণ তথ্যেও ভালো থাকে, সেটিই প্রায়ই জেতে — নিখুঁত নিয়ম নয়।
প্র ০২একটি সার্ভার অসুস্থ হয়ে সব অনুরোধে সঙ্গে সঙ্গে 500 ফেরাচ্ছে। Least connections তাকে কম, না বেশি traffic দেবে? কেন?
বেশি — আর এটিই least connections-এর সবচেয়ে বিপজ্জনক দিক। সুস্থ সার্ভার একটি অনুরোধে ধরা যাক ৫০ ms খাটে; অসুস্থটি ১ ms-এ “পারব না” বলে দেয়। Little-এর সূত্রে (L = λ × W) একই হারে কাজ পেলে অসুস্থটির হাতে চলমান কাজ সুস্থের প্রায় পঞ্চাশ ভাগের এক ভাগ। Least connections দেখে: “এ তো সবচেয়ে ফাঁকা!” — আর নতুন অনুরোধ তার দিকে ঠেলে দেয়। যত বেশি পাঠায়, তত বেশি ব্যর্থ হয়, কিন্তু সে তবুও ফাঁকাই দেখায়। একটি অসুস্থ সার্ভার এভাবে মোট traffic-এর বড় অংশ গিলে ফেলতে পারে। Google-এর প্রকাশিত Site Reliability Engineering বইয়ে (২০১৬) এই ঘটনাকে বলা হয়েছে sinkholing — অসুস্থ সার্ভার গর্তের মতো traffic টেনে নেয়।
Least response time-এ আরও স্পষ্ট: ত্রুটি সবচেয়ে দ্রুত, তাই সে “সবচেয়ে ভালো”। অন্ধ round robin এখানে কম ক্ষতিকর — অসুস্থটিও ঠিক ১/n পায়।
প্রতিকারগুলো একসাথে লাগে:
- Passive check আর outlier ejection: পরপর কয়েকটি 5xx দেখলেই কিছুক্ষণ বাদ — অংশ ৬-এর হিসাবে ব্যস্ত সার্ভারে মাত্র কয়েক মিলিসেকেন্ডে। Envoy-এর outlier detection এর পরিচিত উদাহরণ; বিস্তারিত পর্ব ৪৮।
- ত্রুটিকে ভারের মতো গোনা: latency-ভিত্তিক নিয়মে ব্যর্থ অনুরোধকে কৃত্রিমভাবে বড় latency ধরা, যাতে ত্রুটি “দ্রুত” না দেখায়।
- বাদ দেওয়ার সর্বোচ্চ সীমা: একসাথে খুব বেশি সার্ভার বাদ না দেওয়া, যাতে একটি ভাগ করা সমস্যায় পুরো fleet খালি না হয়।
শিক্ষাটা গভীরে: প্রতিটি “স্মার্ট” algorithm কোনো একটি সংকেত (চলমান কাজ, latency) বিশ্বাস করে। সেই সংকেত যখন মিথ্যা বলে — যেমন ত্রুটি দ্রুত বলে কম ব্যস্ত দেখায় — তখন স্মার্ট algorithm অন্ধ algorithm-এর চেয়েও খারাপ করে।
মূল উপলব্ধি: ভার মাপার যে সংকেত আপনি ব্যবহার করছেন, তা কীভাবে ভুল হতে পারে — এই প্রশ্ন না করে কোনো algorithm বেছে নেবেন না। আর load balancer-কে শুধু ভার নয়, ফলাফলও (সাফল্য বনাম ত্রুটি) দেখতে হয়।
প্র ০৩HTTP/1.1-এর যুগে যে L4 load balancer নিখুঁত চলত, HTTP/2 আর gRPC আসার পর সে হঠাৎ “ভুল” কাজ করতে শুরু করল কেন? প্রতিকারগুলোর দাম কী?
Load balancer বদলায়নি, বদলেছে সংযোগের ধরন। HTTP/1.1-এ একটি সংযোগে একবারে একটিই অনুরোধ চলত; ব্রাউজার আর client-রা তাই অনেকগুলো ছোট, প্রায়ই স্বল্পজীবী সংযোগ খুলত। হাজার হাজার সংযোগ এলোমেলো ভাগ হলে বড় সংখ্যার নিয়মে ভার মোটামুটি সমান হয় — connection-level ভাগই যথেষ্ট ছিল।
HTTP/2 (RFC 7540, ২০১৫; এখনকার মান RFC 9113, ২০২২) এক সংযোগে অনেক stream একসাথে চালায়, আর gRPC (Google ২০১৫ সালে open source করে) এই সুবিধা পুরো কাজে লাগায়: একটি client একটি সংযোগ খুলে ঘণ্টার পর ঘণ্টা রাখে, প্রতি সেকেন্ডে হাজারো RPC সেখান দিয়ে যায়। L4-এর চোখে এখন সংযোগ মাত্র কয়েকটি, কিন্তু প্রতিটি বিশাল। পাঁচটি client আর বিশটি backend হলে বড়জোর পাঁচটি backend কাজ পায়। Autoscaling নতুন backend যোগ করলেও পুরোনো সংযোগ নড়ে না, তাই নতুনরা প্রায় বসে থাকে — dashboard-এ ক্ষমতা বেড়েছে, ব্যবহারকারীর অভিজ্ঞতায় নয়। Kubernetes-এর সাধারণ Service সংযোগ-স্তরে ভাগ করে বলে সেখানে এই সমস্যা বহুল আলোচিত; ২০১৮ সালে Kubernetes-এর blog-এ এ নিয়ে একটি পরিচিত লেখাও প্রকাশিত হয়।
প্রতিকার আর দাম:
- L7 proxy (Envoy, Nginx-এর gRPC proxy, cloud-এর application LB): প্রতিটি stream আলাদা ভাগ। দাম: প্রতিটি অনুরোধে বাড়তি hop, CPU আর পরিচালনার আরও একটি স্তর।
- Client-side load balancing: client নিজেই সব backend-এর সাথে সংযোগ রেখে পালা করে পাঠায়। দাম: প্রতিটি ভাষার লাইব্রেরিতে একই যুক্তি, আর হালনাগাদ backend-তালিকা — service discovery (পর্ব ৪৭)।
- সংযোগের সর্বোচ্চ বয়স: সার্ভার কয়েক মিনিট পরপর এলোমেলো দেরিসহ GOAWAY পাঠায়; client নতুন করে জোড়ে। দাম: মাঝে মাঝে নতুন handshake, আর ভারসাম্য আসে ধীরে, তাৎক্ষণিক নয়।
মূল উপলব্ধি: একটি স্তরের পরিবর্তন (protocol) অন্য স্তরের অনুমান (প্রতিটি সংযোগ ছোট ও সমান) চুপচাপ ভেঙে দিতে পারে। নকশা পর্যালোচনায় তাই জিজ্ঞেস করুন: আমাদের load balancer আসলে কী গুনছে — সংযোগ, না অনুরোধ?
প্র ০৪Health check যত ঘন ঘন আর যত গভীর, তত ভালো — এই কথাটি কোথায় ভুল?
দুটি দিকেই সীমা আছে, আর দুটিই বাস্তবে বড় বিপর্যয়ের কারণ হয়েছে।
ঘনত্বের দাম — তিনটি। প্রথম, ভুল সন্দেহ: interval ২০০ ms আর fall ২ মানে আধা সেকেন্ডের একটি garbage collection বিরতিতেই সুস্থ সার্ভার বাদ। অনেক সার্ভার একসাথে এমন বিরতিতে পড়লে (একই ভিড়, একই কোড) একসাথে বাদ, বাকিদের ঘাড়ে বাড়তি ভার, তারাও ধীর — cascading failure. দ্বিতীয়, check-এর নিজের ভার: অংশ ৬-এর হিসাবে ২০টি LB নোড × ৫০০টি সার্ভার ÷ ২ সেকেন্ড = সেকেন্ডে ৫,০০০টি check; interval ২০০ ms করলে ৫০,০০০ — শুধু “বেঁচে আছেন?” প্রশ্নে। Google-এর SRE বইয়ে বর্ণিত subsetting এর একটি উত্তর: প্রতিটি client সব backend নয়, একটি উপসেট দেখে। তৃতীয়, flapping: সীমানায় থাকা সার্ভার বারবার আসা-যাওয়া করলে cache আর connection pool বারবার ঠান্ডা হয়। Fall আর rise আলাদা রাখা, আর slow start, এজন্যই।
গভীরতার দাম — ভাগ করা নির্ভরতা। /healthz যদি database-এ একটি query চালায়, তবে database কয়েক সেকেন্ড ধীর হলেই সব app server একসাথে check-এ ফেল। LB সবাইকে বাদ দেয় — অথচ অনেক অনুরোধ (cache থেকে পড়া, স্থির পাতা) database ছাড়াই চলতে পারত। আংশিক সমস্যা পূর্ণ বিপর্যয়। এজন্য অনেক LB সবাই বিকল দেখলে fail open করে (Envoy-এর panic threshold), আর ভালো নকশায় health check শুধু নিজের প্রস্তুতি দেখে; dependency-র সমস্যা সামলায় timeout, circuit breaker (পর্ব ৪৮) আর graceful degradation (পর্ব ১৮)।
তাহলে সঠিক মাত্রা? কয়েকটি প্রশ্ন: কত সেকেন্ডের ব্যর্থতা আমরা সহ্য করি (SLO, পর্ব ৫০)? সার্ভারের স্বাভাবিক সবচেয়ে লম্বা বিরতি কত (GC, deploy)? Passive check কতটা ঢাকে? সাধারণত মাঝারি active check (কয়েক সেকেন্ড), দ্রুত passive check আর retry মিলিয়ে ভালো ফল আসে।
মূল উপলব্ধি: Failure detection সবসময় দুই ভুলের মধ্যে আপস — দেরিতে ধরা আর ভুল করে ধরা। এই আপস এড়ানো যায় না; শুধু সচেতনভাবে বেছে নেওয়া যায়।
প্র ০৫দুটি load balancer রাখলেই কি SPOF সমস্যা মিটে যায়? VRRP থেকে Maglev — এই বিবর্তন কী শেখায়?
অঙ্কে প্রায় মিটে যায়, বাস্তবে নয়। দুটি স্বাধীন LB, প্রতিটি ৯৯.৯% সময় চালু হলে দুটি একসাথে বন্ধ থাকার সম্ভাবনা ০.০০১ × ০.০০১ — availability তত্ত্বে ৯৯.৯৯৯৯% (পর্ব ৫)। কিন্তু এই হিসাব তিনটি জিনিস ধরে নেয়, যার কোনোটিই নিশ্চিত নয়।
- Failover নিজে নিখুঁত: VRRP (প্রথম RFC ১৯৯৮ সালে, এখনকার v3 ২০১০ সালে) সক্রিয় যন্ত্রের নীরবতা টের পেতে কয়েক সেকেন্ড নেয়; ততক্ষণ সেবা বন্ধ। আর চলমান সংযোগের তথ্য নতুন master না জানলে সেগুলো ভাঙে।
- দুটির ব্যর্থতা স্বাধীন: একই config ভুল দুটিতেই একসাথে গেলে, বা একই software bug একই ধরনের অনুরোধে দুটিকেই ফেলে দিলে redundancy কোনো কাজে আসে না — correlated failure.
- দুজনের সম্পর্ক পরিষ্কার: মাঝের link কাটলে দুজনেই নিজেকে master ভাবতে পারে — split-brain, একই IP দুই জায়গায়।
বড় প্রতিষ্ঠানগুলো তাই active-passive জোড়া থেকে active-active-এর দিকে গেছে। Google ২০১৬ সালে NSDI-তে Maglev-এর নকশা প্রকাশ করে: সাধারণ সার্ভারে চলা অনেকগুলো software L4 LB, router ECMP দিয়ে তাদের মধ্যে ভাগ করে, আর প্রতিটি LB একই consistent-hash টেবিল থেকে backend বাছে। ফলে কোনো LB পড়লে তার সংযোগগুলো অন্য LB-তে গেলেও বেশিরভাগ একই backend পায় — standby বসে থাকে না, ক্ষমতা বাড়াতে শুধু যন্ত্র যোগ, আর একটি LB হারানো সাধারণ ঘটনা, বিপর্যয় নয়। একই চিন্তা anycast-এ বহু শহরে ছড়ালে CDN আর public DNS-এর মতো সেবা দাঁড়ায়।
সাথে config-এর সতর্কতা: নিয়ম আগে কয়েকটি নোডে, তারপর বাকিদের।
মূল উপলব্ধি: Redundancy মানে শুধু কপি রাখা নয় — কপিগুলোর ব্যর্থতা যেন স্বাধীন হয়, failover যেন দ্রুত ও নিয়মিত পরীক্ষিত হয়, আর একটি অংশ হারানো যেন “স্বাভাবিক দিন” হয়। নকশায় “দুটি LB” লিখেই থামবেন না; জিজ্ঞেস করুন, কোন ঘটনায় দুটিই একসাথে পড়তে পারে?
অনুশীলন
আগে নিজে খাতায় লিখুন, তারপর উত্তর মিলিয়ে নিন। হিসাবগুলো অংশ ৫ আর ৬-এর simulator-এ বসিয়ে যাচাইও করা যায়।
-
Algorithm বাছাই: তিনটি কাল্পনিক সেবার জন্য L7-এর algorithm বেছে কারণ লিখুন। (ক) একটি ছোট REST API — ৬টি হুবহু একই সার্ভার, প্রায় সব অনুরোধ ৫–১৫ ms, সামনে একটিমাত্র Nginx. (খ) একটি রিপোর্টিং সেবা — ২০টি সার্ভারের ৫টি পুরোনো (প্রায় অর্ধেক গতি), অনুরোধ ২০ ms থেকে ৩ সেকেন্ড, সামনে ৮টি L7 নোড স্বাধীনভাবে চলে। (গ) একটি ছবি-thumbnail সেবা — প্রতিটি সার্ভার নিজের memory-তে thumbnail cache রাখে, একই ছবি বারবার চাওয়া হয়।
(ক) Round robin, বা least request — সার্ভার একই রকম, অনুরোধ প্রায় সমান, তাই round robin-এর অন্ধতার দাম সামান্য; একটিমাত্র LB, তাই least request-এর তথ্যও টাটকা। Power of two-র বাড়তি সুবিধা এখানে প্রায় নেই।
(খ) Power of two choices (least request-এর সাথে), ওজন আর slow start — গতি ও অনুরোধ বিষম, আর ৮টি L7 নোড আংশিক তথ্য দেখে। Round robin ধীরদের ডোবাবে (অংশ ৫-এ p99 প্রায় ১,৯৪৫ ms); least connections অনেক LB-তে ভিড় জমাতে পারে (পুরোনো তথ্যে ৩৬৪ বনাম ২৩২ ms). পুরোনো সার্ভারে অর্ধেক ওজন, আর passive check, যাতে দ্রুত-ত্রুটির সার্ভার traffic না টানে।
(গ) Consistent hashing (ছবির ID দিয়ে) — একই ছবি একই সার্ভারে গেলে cache-এ মেলে; অন্য নিয়মে প্রতিটি ছবি সব cache-এ ছড়াবে।
hash mod Nনয়, কারণ সার্ভার যোগ-বাদে প্রায় সব cache অকেজো; consistent hashing-এ মোটামুটি ১/N সরে (পর্ব ১৬)। জনপ্রিয় ছবির জন্য ভারের সীমা বা আলাদা cache স্তর (পর্ব ৩০)। -
ধরা পড়ার সময় আর ক্ষতির হিসাব: একটি সেবা ১২টি সার্ভারে মোট ৬,০০০ QPS (সমান ভাগ) সামলায়। Active check: interval ৫ সেকেন্ড, timeout ২ সেকেন্ড, fall ৩, rise ২. একটি সার্ভার ঝুলে গেল। (ক) গড় আর সবচেয়ে খারাপ ক্ষেত্রে কত সেকেন্ডে ধরা পড়বে, আর ততক্ষণে মোটামুটি কতগুলো অনুরোধ ব্যর্থ? (খ) setting বদলে interval ১ সেকেন্ড, timeout ০.৫ সেকেন্ড, fall ২ করলে? (গ) পরপর ৫টি ত্রুটিতে বাদ দেওয়া passive check যোগ করলে কী বদলায়, যদি সার্ভারটি ঝুলে না থেকে সঙ্গে সঙ্গে
503ফেরায়?প্রতি সার্ভারে ৬,০০০ ÷ ১২ = ৫০০ QPS.
(ক) গড় = interval/২ + (fall − ১) × interval + timeout = ২.৫ + ২ × ৫ + ২ = ১৪.৫ সেকেন্ড; সবচেয়ে খারাপ = ৩ × ৫ + ২ = ১৭ সেকেন্ড। ব্যর্থ অনুরোধ: গড়ে ৫০০ × ১৪.৫ = ৭,২৫০, সবচেয়ে খারাপে ৫০০ × ১৭ = ৮,৫০০ (retry না থাকলে)। অংশ ৬-এর calculator-এর দ্বিতীয় সারির সাথে মেলে, কারণ সেখানেও প্রতি সার্ভারে ৫০০ QPS. সেরে ওঠার পর ফেরত আসতে সর্বোচ্চ rise × interval = ২ × ৫ = ১০ সেকেন্ড।
(খ) গড় = ০.৫ + ১ × ১ + ০.৫ = ২ সেকেন্ড, সবচেয়ে খারাপ = ২ × ১ + ০.৫ = ২.৫ সেকেন্ড; ব্যর্থ অনুরোধ গড়ে ১,০০০, সবচেয়ে খারাপে ১,২৫০ — প্রায় সাত গুণ কম। দাম: প্রতি সেকেন্ডে check পাঁচ গুণ বেশি, আর ১ সেকেন্ডের বেশি GC-বিরতিতে সুস্থ সার্ভারও বাদ পড়তে পারে (fall মাত্র ২)।
(গ) সার্ভার সঙ্গে সঙ্গে ত্রুটি ফেরালে ৫টি ত্রুটি জমতে লাগে ৫ ÷ ৫০০ = ০.০১ সেকেন্ড = ১০ ms, ক্ষতি প্রায় ৫টি অনুরোধ — active check-এর হাজারের তুলনায় নগণ্য। তাই বাস্তবে দুটোই থাকে: passive দ্রুত ধরে (traffic থাকলে), active ধরে নীরব সার্ভার আর ঠিক করে কখন ফেরত আনা নিরাপদ।
-
নকশার সমালোচনা — gRPC আর autoscaling: একটি কাল্পনিক ride-sharing অ্যাপের “trip” সার্ভিসকে ৪টি “dispatch” pod gRPC-তে ডাকে, Kubernetes-এর সাধারণ (L4) Service দিয়ে। trip-এর ৮টি pod ছিল; ভিড়ে autoscaler সেটি ১৬ করল, কিন্তু latency কমল না, আর নতুন ৮টি pod-এর CPU প্রায় শূন্য। কী ঘটছে, আর আপনি কী বদলাবেন?
কারণ: প্রতিটি dispatch pod সাধারণত একটি দীর্ঘজীবী HTTP/2 সংযোগে সব RPC পাঠায়। L4 Service ভাগ করে সংযোগ — তাই ৪টি সংযোগে বড়জোর ৪টি trip pod ব্যস্ত (দুটি একই pod-এ পড়লে আরও কম)। ১৬ হলেও পুরোনো সংযোগ পুরোনো pod-এই বাঁধা; ক্ষমতা বেড়েছে, কাজে লাগেনি।
যা বদলাব: (১) মাঝে HTTP/2-বোঝা L7 proxy বা mesh-এর sidecar, যাতে প্রতিটি RPC আলাদা ভাগ হয়; অথবা (২) gRPC-র client-side load balancing — সব trip pod-এর ঠিকানা জেনে (headless Service বা service discovery, পর্ব ৪৭)
round_robin-ধাঁচের policy; আর (৩) trip সার্ভারে সংযোগের সর্বোচ্চ বয়স, যাতে নতুন pod-ও ভাগ পায়। Metric-ও বদলাব: প্রতি pod-এর RPS আর in-flight RPC, শুধু গড় CPU নয় — নইলে সমস্যা dashboard-এ লুকিয়ে থাকে। -
ফল আগে বলুন, তারপর চালান: অংশ ৫-এর
lb_compare.js-এspeedলাইনটি বদলে দিন:[1, 1, 1, 1, 1, 1, 1, 1, 1, 0.25]— নয়টি সাধারণ সার্ভার আর একটি চার গুণ ধীর। চাপ আগের মতোই মোট ক্ষমতার ৬৫%. চালানোর আগে লিখুন: কোন algorithm-গুলোর p99 বিস্ফোরিত হবে, কেন, আর least connections ও power of two-র ফল মোটামুটি কেমন থাকবে?পূর্বাভাস: মোট ক্ষমতা ৯.২৫; ৬৫% চাপে প্রতি ms-এ ৬.০১ একক কাজ। Round robin আর random প্রত্যেককে ১০% দেয় — ০.৬০১: সাধারণ সার্ভারের ৬০%, কিন্তু ধীরটির (০.২৫) ২৪০%. তার সারি পুরো সময় বাড়তেই থাকবে, আর তার ১০% অনুরোধই p99 দখল করবে। Least connections আর power of two লম্বা সারি দেখে তাকে এড়াবে।
আসল ফল:
capacity 9.25 units/ms, offered load 65%, 50000 requests algorithm mean ms p99 ms maxQ Round robin 6415.9 115260.1 2978 Random 5122.0 97698.7 2691 Least connections 14.6 146.8 6 Power of two choices 27.8 239.0 11 Least conn, 50 ms stale 56.6 376.8 43 Power of two, 50 ms stale 38.2 297.7 15Round robin-এর p99 ১১৫ সেকেন্ড, অথচ পুরো সিমুলেশন প্রায় ৮৩ সেকেন্ডের (৫০,০০০টি অনুরোধ ÷ প্রতি ms-এ ০.৬০১টি অনুরোধ)! ধীর সার্ভার পেয়েছে ৫,০০০টি অনুরোধ (কাজ প্রায় ৫,০০০ × ১০ ÷ ০.২৫ = ২,০০,০০০ ms), ৮৩ সেকেন্ডে শেষ করতে পারে প্রায় ২,০৮০টি — বাকি প্রায় ২,৯০০টি সারিতে, maxQ ২,৯৭৮-এর সাথে মেলে। Least connections (১৪৭ ms) আর power of two (২৩৯ ms) প্রায় অক্ষত; পুরোনো তথ্যে আবারও power of two (২৯৮) least connections-কে (৩৭৭) হারায়। শিক্ষা: একটি খারাপ সার্ভার গতি-অন্ধ algorithm-এ পুরো সেবার tail নষ্ট করে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমLoad balancer-এর পেছনে একটি সার্ভারকে নতুন সংস্করণে বদলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- LB-তে সার্ভারটিকে draining চিহ্ন দেওয়া — নতুন অনুরোধ বন্ধ
- চলমান অনুরোধ শেষ হওয়ার অপেক্ষা, সর্বোচ্চ সময়সীমা পর্যন্ত
- পুরোনো process ভদ্রভাবে বন্ধ, নতুন সংস্করণ চালু
- Health check পরপর rise বার সফল — সার্ভার আবার UP
- Slow start-এ ওজন ধীরে ধীরে পূর্ণ মানে তোলা
- পরের সার্ভারে একই প্রক্রিয়া শুরু
একটি load balancer শুধু IP ঠিকানা আর TCP port দেখে, TLS খোলে না। নিচের কোনটি সে করতে পারবে না?
- TCP সংযোগগুলো কয়েকটি backend-এ ভাগ করা
/api/আর/images/path আলাদা backend-এ পাঠানো- TCP health check দিয়ে বিকল backend বাদ দেওয়া
- HTTP নয় এমন protocol (যেমন database সংযোগ) ভাগ করা
Active health check: interval ২ সেকেন্ড, timeout ১ সেকেন্ড, পরপর ৩ বার ব্যর্থ হলে DOWN. একটি সার্ভার ঝুলে গেল (উত্তরই দেয় না)। সবচেয়ে খারাপ ক্ষেত্রে কত সেকেন্ডে সে বাদ পড়বে?
- প্রায় ৩ সেকেন্ড
- প্রায় ৬ সেকেন্ড
- প্রায় ৭ সেকেন্ড
- প্রায় ৯ সেকেন্ড
তিনটি gRPC client প্রত্যেকে একটি করে HTTP/2 সংযোগ খুলে সেকেন্ডে ১,০০০টি RPC পাঠায়। মাঝে একটি L4 load balancer, পেছনে ৬টি backend. দেখা গেল ৩টি backend প্রায় অলস। মূল কারণ কী?
- L4 load balancer-এর health check ভুল করে তিনটি backend বাদ দিয়েছে
- HTTP/2 একটি সংযোগে একসাথে একটির বেশি অনুরোধ পাঠাতে পারে না
- gRPC UDP-তে চলে, তাই L4 load balancer তাকে ভাগ করতে পারে না
- প্রতিটি client-এর সব RPC একটি দীর্ঘজীবী সংযোগে যায়, আর L4 সংযোগ ভাগ করে, অনুরোধ নয়
n টি অনুরোধ একসাথে n টি সার্ভারে যাচ্ছে (কোনোটি শেষ হচ্ছে না)। প্রতিটি অনুরোধ দুটি এলোমেলো সার্ভার দেখে কম ভরাটিতে যায়। n খুব বড় হলে সবচেয়ে ভরা সার্ভারের ভার মোটামুটি কোন হারে বাড়ে?
- প্রায় n/২
- প্রায় log n / log log n — একটি বাছাইয়ের মতোই
- প্রায় log log n
- সবসময় ঠিক ১
Smooth weighted round robin-এ তিনটি সার্ভার a, b, c-এর ওজন ৫:১:১. নিয়ম: প্রতি বাছাইয়ে সবার “চলতি ওজনে” নিজের ওজন যোগ হয়, সবচেয়ে বেশি যার সে জেতে (সমান হলে তালিকায় আগেরজন), আর বিজয়ীর চলতি ওজন থেকে মোট ওজন (৭) বাদ যায়। শুরুতে সব চলতি ওজন ০। প্রথম সাতটি বাছাই কী?
- a a b a c a a
- a a a a a b c
- a b a c a a a
- a b c a a a a
Least connections চলছে। একটি backend অসুস্থ হয়ে প্রতিটি অনুরোধে ১ ms-এর মধ্যে 500 ফেরাচ্ছে, অথচ সুস্থরা একটি অনুরোধে প্রায় ৫০ ms নেয়। কী ঘটার সম্ভাবনা বেশি, আর প্রতিকার কী?
- সে কম traffic পাবে, কারণ least connections ত্রুটির হার গোনে
- সে ঠিক ১/n ভাগ পাবে, অন্যদের মতোই
- সে বেশি traffic টানবে; প্রতিকার passive check বা outlier ejection
- Active check প্রতিটি অনুরোধের আগে চলে বলে LB তাকে সঙ্গে সঙ্গে বাদ দেবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- চার কাজ: ভার ভাগ, বিকলতা লুকানো, একটি প্রবেশপথ, সেবা না থামিয়ে deploy; LB ক্ষমতা বানায় না।
- L4 সংযোগ ভাগ করে, L7 প্রতিটি অনুরোধ; দীর্ঘ HTTP/2-gRPC সংযোগে L4 ভার অসমান করে।
- বিষম গতিতে round robin ধীরদের ডোবায়; least request মানিয়ে নেয়; পুরোনো তথ্যে power of two choices সবচেয়ে টেকসই।
- ধরা পড়ার সময় ≈ fall × interval + timeout; ক্ষতি ≈ প্রতি সার্ভারের QPS × সেই সময়; ফেরার পর slow start.
- Deploy-এ draining আর u × N ÷ (N − k); LB নিজে redundant — VRRP বা anycast-ECMP.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৫ · Stateless বনাম stateful — session, sticky session আর tokenপরবর্তী পর্বSession কোথায় রাখবেন, sticky session-এর দাম আর token-ভিত্তিক নকশা।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১৬ · Consistent hashing — ring, virtual node আর rendezvous hashingএই মডিউলেHash-ভিত্তিক ভাগ, সার্ভার বদলালে কত key সরে, আর ভারের সীমা।
- Computer Networks · Data center নেটওয়ার্ক — leaf-spine আর load balancerপাশের ট্র্যাকECMP, Maglev, DSR আর packet-স্তরে load balancing.
- DSA · Randomized algorithmগভীরে যানPower of two choices-এর পেছনের সম্ভাবনার যন্ত্রপাতি।