এই পর্বে যা শিখবেন
- Consensus-এর তিন শর্ত, আর এটি কোথায় লাগে
- Majority quorum: ২f + ১-এর হিসাব আর দুই majority মেলার ছোট প্রমাণ
- Raft-এর election, log replication, commit-এর নিয়ম আর পাঁচটি safety property — simulator-এ চালিয়ে
- Client, linearizable read, snapshot আর membership বদলের মূল ভাবনা
- Paxos-এর দুই ধাপ, Raft–Paxos–ZAB–VR-এর তুলনা, আর cluster-এর আকার ও latency-র হিসাব
১ · Consensus সমস্যা — কয়েকটি যন্ত্র, একটিই সিদ্ধান্ত
পর্ব ২২-এ আমরা leader-follower replication দেখেছি। সেখানে একটি প্রশ্ন খোলা ছিল: leader মরলে নতুন leader কে ঠিক করবে? বাইরের একটি monitor ঠিক করলে সে নিজেই single point of failure. আবার network ভাগ হলে দুই দিকে দুজন নিজেকে leader ভাবতে পারে — split-brain (পর্ব ২২)। এই পর্বের বিষয় সেই প্রশ্নের নীতিগত উত্তর: সার্ভারগুলো নিজেরাই, নিয়ম মেনে, একটি সিদ্ধান্তে একমত হবে।
একটি মুদি দোকানের কথা ভাবুন। পাঁচ ভাই পালা করে বসেন, আর প্রত্যেকের কাছে বাকির খাতার একটি করে কপি। নিয়ম: প্রতিটি লেনদেন সব কপিতে একই ক্রমে উঠবে। একজন অসুস্থ হলেও বাকিরা দোকান চালাবেন; তিনি ফিরে এসে বাদ পড়া লাইনগুলো টুকে নেবেন। ক্রম ঠিক করেন একজন “ম্যানেজার”। তিনি হঠাৎ উধাও হলে বাকিরা ভোটে নতুন ম্যানেজার বাছেন — কিন্তু কোনো মুহূর্তেই দুজন ম্যানেজার নয়, আর কারও খাতা থেকে একবার পাকা হওয়া লাইন মুছবে না। এই নিয়মের বইটির নামই consensus protocol.
তিনটি শর্ত
ধরা যাক কয়েকটি process, প্রত্যেকে একটি মান প্রস্তাব করতে পারে। Consensus প্রোটোকলের কাছে তিনটি জিনিস চাওয়া হয়:
- Agreement: সচল কোনো দুটি process ভিন্ন মানে সিদ্ধান্ত নেবে না।
- Validity: সিদ্ধান্ত হওয়া মানটি কেউ না কেউ প্রস্তাব করেছিল — প্রোটোকল নিজে মান বানায় না।
- Termination: যে process crash করেনি, সে শেষ পর্যন্ত একটি সিদ্ধান্তে পৌঁছাবে।
প্রথম দুটি safety — “খারাপ কিছু কখনো ঘটবে না”। তৃতীয়টি liveness — “ভালো কিছু শেষে ঘটবেই”। পর্ব ৩৮-এ FLP ফল দেখেছেন: বার্তার দেরির কোনো সীমা না থাকলে, একটি process-ও crash করতে পারলে, কোনো deterministic প্রোটোকল তিনটিই সব সময় দিতে পারে না। তাই Raft আর Paxos-এর মতো প্রোটোকলের কৌশল একই: safety কখনো ছাড়ে না, আর liveness দেয় যখন network মোটামুটি ভালো আচরণ করে — এই ধারণাকে বলা হয় partial synchrony.
ব্যর্থতার মডেল: সার্ভার crash করে পরে ডিস্ক নিয়ে ফিরতে পারে; বার্তা হারাতে, দেরিতে, দুবার বা উল্টো ক্রমে আসতে পারে। কিন্তু কেউ মিথ্যা বলে না — crash fault মডেল। মিথ্যাবাদী বা ভাঙা সার্ভারের Byzantine fault আলাদা, আরও দামি সমস্যা (ভাবনার প্রশ্ন ৪)।
কোথায় কোথায় লাগে
👑 Leader election
“এখন কে leader” — এই একটি মানে সবার একমত হওয়া, যাতে একসাথে দুজন লেখা না নেয়।
split-brain প্রতিরোধ📒 Replicated state machine
একই deterministic আদেশ একই ক্রমে চালালে সব কপির অবস্থা এক হয়। তাই প্রতিটি log slot-এ একটি করে consensus — কোন আদেশটি কত নম্বরে।
ডেটাবেস, key-value store📣 Atomic broadcast
সব বার্তা সব সচল সার্ভারে একই ক্রমে পৌঁছানো — total order. Crash-fault মডেলে এটি আর consensus সমান শক্তির সমস্যা।
পর্ব ৩৪-এর total order🗂️ Configuration আর metadata
কোন shard কোন node-এ, কে lock ধরে আছে — ছোট কিন্তু জরুরি তথ্য, যা রাখে coordination service (পর্ব ৪৩)।
etcd, ZooKeeperদ্বিতীয় কার্ডটিই এই পর্বের কেন্দ্র — replicated state machine: প্রতিটি সার্ভারে একটি consensus module, একটি log আর একটি state machine (যেমন key-value টেবিল)। পর্ব ৩৩-এর log-ভিত্তিক streaming-এর পেছনেও এই চিন্তা (পর্ব ৩৩)।
Replicated state machine — একটি সার্ভারের ভেতরে
Every server runs the same log through the same deterministic state machine
- Client
SET x=5আদেশ পাঠায় - Consensus module
Raftঅন্যদের সাথে একমত - Log
x=1 · y=2 · x=5সব সার্ভারে একই ক্রম - State machine
x=5, y=2সবার একই ফল
২ · Majority quorum — ২f + ১ কেন, আর দুই majority কেন মেলে
সিদ্ধান্ত পাকা করতে কতজনের সম্মতি লাগবে? দুই চরম পথই খারাপ:
- সবার সম্মতি: পর্ব ২৫-এর two-phase commit-এর মতো। একজন বিকল হলেই সব থামে (পর্ব ২৫)।
- যেকোনো একজন: Network দুই ভাগ হলে দুই দিকে দুটি ভিন্ন সিদ্ধান্ত — agreement ভাঙে।
মাঝের পথটি quorum: N সদস্যের মধ্যে অর্ধেকের বেশি, অর্থাৎ Q = ⌊N/২⌋ + ১. তাহলে একসাথে f = N − Q = ⌊(N − ১)/২⌋ জন crash করলেও বাকিরা Q পূরণ করতে পারে। উল্টো দিক থেকে: f-টি ব্যর্থতা সহ্য করতে কমপক্ষে N = ২f + ১ সদস্য লাগে।
N = ৩ → Q = ২, f = ১ · N = ৪ → Q = ৩, f = ১ · N = ৫ → Q = ৩, f = ২
প্রমাণ: যেকোনো দুটি majority মেলে
ধরুন A আর B দুটি majority. প্রতিটির আকার অন্তত ⌊N/২⌋ + ১, তাই |A| + |B| ≥ ২⌊N/২⌋ + ২ ≥ (N − ১) + ২ = N + ১. কিন্তু মোট সদস্য মাত্র N জন। কবুতরের খোপের নীতিতে (pigeonhole) অন্তত একজন সদস্যকে দুই দলেই থাকতে হবে। ∎
এই ছোট সত্য থেকেই দুটি বড় ফল:
- Network ভাগ হলে বড়জোর এক ভাগ এগোয়। দুই ভাগই majority হতে পারে না। ছোট ভাগ লেখা নেয় না — নিরাপদ, কিন্তু সেখানে সেবা বন্ধ। পর্ব ৪০-এর ভাষায় এটি consistency-র পক্ষে বাছাই (পর্ব ৪০)।
- পুরোনো সিদ্ধান্ত হারায় না। একটি entry majority-তে পৌঁছে পাকা হলো। পরে যে majority নতুন leader বাছবে, তার অন্তত একজন সদস্যের কাছে entry-টি আছে। Raft-এর ভোটের নিয়ম সেই সদস্যকে দিয়েই পাহারা বসায় (অংশ ৭)।
মুদি দোকানের পাঁচ ভাইয়ের যেকোনো তিনজন একটি দল। সোমবারের দলে বড়, মেজ আর সেজ; বুধবারের দলে সেজ আর দুই ছোট। পাঁচজনে তিন আর তিন মিলিয়ে ছয়টি জায়গা, তাই কাউকে দুবার আসতেই হয় — এখানে সেজ ভাই, যিনি সোমবারের কথা বুধবারের দলকে মনে করিয়ে দেবেন।
পর্ব ৪১-এর quorum-এর সাথে সম্পর্ক: leaderless replication-এর R + W > N শর্তও একই intersection-এর যুক্তি (পর্ব ৪১)। কিন্তু শুধু overlap দিয়ে consensus হয় না — একসাথে দুটি লেখা এলে কোনটি আগে, সে ক্রমে কেউ একমত হয় না। Raft আর Paxos overlap-এর সাথে যোগ করে term বা proposal number, আর একটি log-এর ক্রম।
৩ · Raft-এর মানচিত্র — তিন ভূমিকা, term আর দুটি RPC
২০১৪ সালে Stanford-এর Diego Ongaro আর John Ousterhout USENIX ATC সম্মেলনে প্রকাশ করেন “In Search of an Understandable Consensus Algorithm” — Raft. তাঁদের ঘোষিত লক্ষ্য ছিল দক্ষতার চেয়ে বোঝার সুবিধা: সমস্যাটিকে leader election, log replication আর safety — এই তিন টুকরোয় ভাগ করা, আর সব ক্ষমতা একজন শক্তিশালী leader-এর হাতে রাখা। একই বছর Ongaro-র PhD গবেষণাপত্রে membership বদল, snapshot আর client-এর খুঁটিনাটি আরও বিস্তারে আসে।
তিন ভূমিকা
- Follower: নিষ্ক্রিয়; শুধু leader আর candidate-এর বার্তার উত্তর দেয়। শুরুতে সবাই follower.
- Candidate: leader-এর খবর না পেয়ে যে নিজেকে প্রার্থী ঘোষণা করে ভোট চাইছে।
- Leader: সব client অনুরোধ নেয়, log-এ entry যোগ করে follower-দের পাঠায়, আর নিয়মিত heartbeat দিয়ে জানায় “আমি আছি”। Log শুধু leader থেকে follower-এর দিকে যায়, উল্টো দিকে কখনো নয়।
Term — সময়ের বদলে ক্রমিক যুগ
Raft সময়কে ভাগ করে term-এ: ১, ২, ৩ … প্রতিটি term শুরু হয় একটি নির্বাচন দিয়ে। কেউ জিতলে বাকি term সে leader; ভোট ভাগ হয়ে কেউ না জিতলে term-টি leader ছাড়াই শেষ, আর নতুন term-এ আবার নির্বাচন। প্রতিটি সার্ভার নিজের currentTerm রাখে, আর প্রতিটি বার্তায় term লেখা থাকে। দুটি নিয়ম:
- নিজের চেয়ে বড় term দেখলে সঙ্গে সঙ্গে সেটি গ্রহণ, আর leader বা candidate হলে follower-এ নেমে আসা।
- ছোট term-এর অনুরোধ প্রত্যাখ্যান — পুরোনো leader ফিরে এসে লেখাতে পারবে না।
অর্থাৎ term একটি logical clock (পর্ব ৩৯), আর একই সাথে fencing token: পর্ব ২২-এর epoch-এর মতো, বড় নম্বর পুরোনো নেতাকে থামায়।
Raft-এর তিন অবস্থা আর বদলের নিয়ম
Server states: follower → candidate → leader, and the events that move a server between them
কী ডিস্কে, কী মেমরিতে
| অবস্থা | কোথায় | অর্থ |
|---|---|---|
currentTerm | ডিস্ক, উত্তর দেওয়ার আগে fsync | সর্বশেষ দেখা term |
votedFor | ডিস্ক | এই term-এ কাকে ভোট; restart-এর পর দ্বিতীয় ভোট আটকায় |
log[] | ডিস্ক | প্রতিটি entry: index, term আর আদেশ |
commitIndex, lastApplied | মেমরি | কত পর্যন্ত পাকা, আর কত পর্যন্ত চালানো হয়েছে |
nextIndex[], matchIndex[] | শুধু leader-এর মেমরি | প্রতিটি follower-কে পরের কোন index পাঠাতে হবে, আর কত পর্যন্ত মিল নিশ্চিত |
পুরো প্রোটোকল চলে দুটি RPC দিয়ে: RequestVote (candidate ভোট চায়) আর AppendEntries (leader entry পাঠায়; ফাঁকা হলে এটিই heartbeat). পরে snapshot-এর জন্য তৃতীয় একটি — InstallSnapshot (অংশ ৮)।
৪ · Leader election — randomized timeout, RequestVote আর up-to-date যাচাই
প্রতিটি follower একটি টাইমার চালায় — election timeout. Leader-এর heartbeat এলে, বা কোনো candidate-কে ভোট দিলে, টাইমার নতুন করে শুরু হয়। কিছুই না এলে সময় ফুরায়, আর follower candidate হয়:
currentTermএক বাড়ায়, নিজেকে ভোট দেয়, দুটোই ডিস্কে লেখে।- সবাইকে RequestVote পাঠায়: নিজের term, নিজের id, আর নিজের log-এর শেষ entry-র index ও term —
lastLogIndex,lastLogTerm. - তিনটির একটি ঘটা পর্যন্ত অপেক্ষা: (ক) majority ভোট পেলে leader, আর সঙ্গে সঙ্গে heartbeat; (খ) সমান বা বড় term-এর একজন leader-এর AppendEntries পেলে follower; (গ) আবার timeout হলে নতুন term-এ নতুন নির্বাচন।
কে ভোট পাবে — দুটি শর্ত
- এক term-এ এক ভোট: যে আগে চাইল, সে পায় (first-come-first-served);
votedForডিস্কে থাকে বলে restart-এর পরেও দ্বিতীয় ভোট নেই। - Up-to-date যাচাই: candidate-এর log ভোটদাতার log-এর অন্তত সমান হালনাগাদ হতে হবে। তুলনা: শেষ entry-র term যার বড়, সে এগিয়ে; শেষ term সমান হলে যার log লম্বা, সে এগিয়ে।
প্রথম শর্ত আর majority মিলে দেয় Election Safety: এক term-এ দুজন majority পেতে হলে অন্তত একজনকে দুবার ভোট দিতে হতো — যা নিয়মে নিষেধ। দ্বিতীয় শর্তটি অংশ ৬ আর ৭-এর মূল চাবি।
Timeout কেন random
সবার টাইমার এক হলে সবাই একসাথে candidate হয়ে নিজেকে ভোট দেবে — কেউ majority পাবে না: split vote, চিরকাল চলতে পারে। Raft-এর সমাধান সরল: প্রতিবার timeout একটি সীমার মধ্যে random — গবেষণাপত্রের উদাহরণে ১৫০ থেকে ৩০০ ms. সাধারণত একজনের টাইমার আগে ফুরায়, আর অন্যদের ফুরানোর আগেই সে ভোট জোগাড় করে ফেলে; ভোট ভাগ হলেও পরের term-এ নতুন random সময়।
গবেষণাপত্রের শর্ত: broadcastTime ≪ electionTimeout ≪ MTBF. অর্থাৎ round trip timeout-এর চেয়ে অনেক ছোট (নইলে অকারণ নির্বাচন), আর timeout বিকল হওয়ার গড় ব্যবধানের চেয়ে অনেক ছোট (নইলে leader-হীন সময় বেশি)। লেখার সময়ের etcd documentation-এ ডিফল্ট heartbeat ১০০ ms আর election timeout ১,০০০ ms — সংস্করণভেদে আলাদা হতে পারে, আর region পেরোনো cluster-এ এগুলো বাড়াতে হয়।
এবার একটি নির্বাচন ধাপে ধাপে। পাঁচটি সার্ভার; term ১-এর leader S1 হঠাৎ crash করল। বাকি চারজনের random টাইমার: S2 = ২৪৪, S3 = ১৫০, S4 = ২২৯, S5 = ২৯৭ ms. বার্তা পৌঁছাতে ৫ থেকে ২০ ms. (সংখ্যাগুলো নিচের simulator-এর seed ১-এর হুবহু আউটপুট।) “পরে” চাপুন:
| ঘড়ি | |
| S1 | |
| S2 | |
| S3 | |
| S4 | |
| S5 | |
| S3-এর ভোট |
- t = ০ — leader নেই। S1 crash করেছে, কিন্তু কেউ জানে না। চারজনই term ১-এর follower, প্রত্যেকের টাইমার আলাদা random সংখ্যায়।
- t = ১৫০ ms — S3-এর টাইমার আগে ফুরাল। S3 term ২-এ candidate, নিজেকে ভোট (১ / ৫), আর সবাইকে RequestVote(term ২, lastLogIndex, lastLogTerm). না জিতলে পরের timeout ৪৪৫ ms-এ — নতুন random সংখ্যা।
- t = ১৬১–১৬৬ ms — তিনটি ভোট। S5, S2 আর S4 অনুরোধ পেল, বড় term ২ গ্রহণ করল। এই term-এ কাউকে ভোট দেয়নি, আর S3-এর log পিছিয়ে নয় — তাই ভোট দিল আর নিজেদের টাইমার নতুন করে চালু করল।
- t = ১৭৩ ms — majority. দুটি ভোট পৌঁছাতেই নিজেরটিসহ ৩ / ৫. S3 term ২-এর leader; এই term-এ আর কেউ হতে পারবে না। সে সবাইকে ফাঁকা AppendEntries — heartbeat — পাঠাল।
- t = ১৭৯–১৮৫ ms — সবাই জানল। S4, S5, S2 heartbeat পেয়ে টাইমার আবার শুরু করল। Leader-হীন সময় প্রায় ১৭৩ ms, বেশিরভাগই timeout-এর অপেক্ষা। S1 ফিরলে term ২ দেখে follower হবে।
এবার নিজে চালান। পাঁচ সার্ভারের ছোট একটি event-ভিত্তিক মডেল: timeout ১৫০–২৯৯ ms, বার্তার দেরি ৫–২০ ms, seed-সহ PRNG (mulberry32), তাই প্রতিবার একই ফল। দ্বিতীয় seed-এ দুজন প্রায় একসাথে candidate হয়:
// Raft leader election: 5 servers, old leader S1 (term 1) crashes at t=0. Seeded, times in ms.
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; }; }
function elect(seed) {
const rnd = mulberry32(seed), N = 5, MAJ = 3, ev = [];
const timeout = () => 150 + Math.floor(rnd() * 150); // randomized election timeout: 150..299
const delay = () => 5 + Math.floor(rnd() * 16); // one-way message delay: 5..20
const s = [1, 2, 3, 4, 5].map(i => ({ id: "S" + i, term: 1, voted: "S1", role: "follower", votes: 0, due: 0 }));
s[0].role = "crashed";
const arm = (x, t) => { x.due = t + timeout(); ev.push({ t: x.due, k: "timeout", to: x, due: x.due }); };
const send = (t, from, k) => s.forEach(y => { if (y !== from) ev.push({ t: t + delay(), k, from, to: y, term: from.term }); });
s.slice(1).forEach(x => arm(x, 0));
console.log(`--- seed ${seed}: timers ` + s.slice(1).map(x => x.id + "=" + x.due).join(" "));
let end = Infinity;
while (ev.length) {
ev.sort((a, b) => a.t - b.t); const e = ev.shift(), x = e.to;
if (e.t > end) break;
if (x.role === "crashed" || (e.k === "timeout" && (e.due !== x.due || x.role === "leader"))) continue;
if (e.term > x.term) { x.term = e.term; x.voted = null; x.role = "follower"; }
if (e.k === "timeout") {
if (x.role === "candidate") console.log(`t=${e.t} ${x.id}: term ${x.term} ended with only ${x.votes} votes (split vote)`);
x.term++; x.role = "candidate"; x.voted = x.id; x.votes = 1; arm(x, e.t);
console.log(`t=${e.t} ${x.id}: timeout -> candidate, term ${x.term}, votes for itself, next timeout t=${x.due}`); send(e.t, x, "ask");
} else if (e.k === "ask") {
const ok = e.term === x.term && (x.voted === null || x.voted === e.from.id);
if (ok) { x.voted = e.from.id; arm(x, e.t); console.log(`t=${e.t} ${x.id}: votes for ${e.from.id} in term ${x.term}`); }
ev.push({ t: e.t + delay(), k: "vote", from: x, to: e.from, term: x.term, ok });
} else if (e.k === "vote" && x.role === "candidate" && e.ok && e.term === x.term && ++x.votes === MAJ) {
x.role = "leader"; end = e.t + 25;
console.log(`t=${e.t} ${x.id}: LEADER of term ${x.term} (${MAJ} of ${N} votes), sends heartbeats`); send(e.t, x, "beat");
} else if (e.k === "beat" && e.term === x.term) { x.role = "follower"; arm(x, e.t); console.log(`t=${e.t} ${x.id}: heartbeat from ${e.from.id}, follower in term ${x.term}`); }
}
}
elect(1); // a clean election
elect(23); // two candidates at once: a split voteআউটপুট:
--- seed 1: timers S2=244 S3=150 S4=229 S5=297
t=150 S3: timeout -> candidate, term 2, votes for itself, next timeout t=445
t=161 S5: votes for S3 in term 2
t=164 S2: votes for S3 in term 2
t=166 S4: votes for S3 in term 2
t=173 S3: LEADER of term 2 (3 of 5 votes), sends heartbeats
t=179 S4: heartbeat from S3, follower in term 2
t=184 S5: heartbeat from S3, follower in term 2
t=185 S2: heartbeat from S3, follower in term 2
--- seed 23: timers S2=163 S3=160 S4=244 S5=195
t=160 S3: timeout -> candidate, term 2, votes for itself, next timeout t=358
t=163 S2: timeout -> candidate, term 2, votes for itself, next timeout t=314
t=169 S4: votes for S2 in term 2
t=178 S5: votes for S3 in term 2
t=314 S2: term 2 ended with only 2 votes (split vote)
t=314 S2: timeout -> candidate, term 3, votes for itself, next timeout t=590
t=321 S5: votes for S2 in term 3
t=322 S3: votes for S2 in term 3
t=331 S4: votes for S2 in term 3
t=342 S2: LEADER of term 3 (3 of 5 votes), sends heartbeats
t=357 S3: heartbeat from S2, follower in term 3
t=358 S5: heartbeat from S2, follower in term 3
t=359 S4: heartbeat from S2, follower in term 3
Seed ১ হুবহু ওপরের stepper. Seed ২৩ আসল শিক্ষা: S3 (১৬০) আর S2 (১৬৩) মাত্র ৩ ms ব্যবধানে candidate; অনুরোধ পৌঁছানোর আগেই দুজন নিজেকে ভোট দিয়ে ফেলেছে। S4 ভোট দিল S2-কে, S5 দিল S3-কে — 2 votes করে, কেউ ৩ নয়। Term ২ leader ছাড়াই শেষ। তারপর random-এর গুণ: S2-এর পরের timeout ৩১৪, S3-এর ৩৫৮ — ৪৪ ms ব্যবধান। বড় term দেখে S3-ও candidate থেকে নেমে S2-কে ভোট দিল (t=322 S3: votes for S2 in term 3), আর ৩৪২ ms-এ S2 জিতল। Split vote নিরাপত্তা ভাঙে না, শুধু সময় নেয় — এখানে leader পেতে লাগল ৩৪২ ms, seed ১-এর ১৭৩ ms-এর প্রায় দ্বিগুণ।
৫ · Log replication — AppendEntries, consistency check আর commit index
Leader পাওয়া গেল; এখন আসল কাজ। Client একটি আদেশ পাঠালে leader সেটি নিজের log-এ নতুন entry হিসেবে যোগ করে — index, নিজের term আর আদেশ। তারপর সব follower-কে সমান্তরালে AppendEntries পাঠায়:
AppendEntries {
term: 3, // leader's current term
leaderId: "S1",
prevLogIndex: 5, // index of the entry just before the new ones
prevLogTerm: 3, // term of that entry
entries: [ { index: 6, term: 3, cmd: "SET A3=rumana" } ], // empty = heartbeat
leaderCommit: 5 // leader's commitIndex
}
reply { term: 3, success: true } // false if the follower lacks (prevLogIndex, prevLogTerm)Consistency check — একটি entry দিয়ে পুরো log মেলানো
Follower নতুন entry নেয় কেবল যদি তার log-এ prevLogIndex জায়গায় ঠিক prevLogTerm-এর entry থাকে; না থাকলে success: false. এই consistency check থেকে induction-এ আসে Log Matching: দুটি log-এ একই index আর term-এর entry থাকলে সেই index পর্যন্ত দুটি log পুরোপুরি এক — কারণ এক term-এ একজন leader এক index-এ একটিই entry বানায়, আর প্রতিটি যোগের আগে ঠিক আগের entry মেলানো হয়।
প্রত্যাখ্যান হলে leader সেই follower-এর nextIndex এক কমিয়ে আবার পাঠায়। পেছাতে পেছাতে যেখানে দুই log মেলে, সেখান থেকে বাকি সব entry যায়; follower-এর বিরোধী entry মুছে leader-এরটি বসে। অনেক বাস্তবায়নে follower বিরোধী term-এর প্রথম index জানিয়ে দেয়, তাই এক ঘর এক ঘর না পিছিয়ে কয়েক ধাপেই মিল মেলে।
Commit index — কখন পাকা
Leader প্রতিটি follower-এর matchIndex জানে। কোনো index N majority-র কাছে থাকলে, আর N-এর entry leader-এর বর্তমান term-এর হলে, leader commitIndex = N করে। তখন N পর্যন্ত সব entry committed: leader সেগুলো state machine-এ চালায় আর client-কে উত্তর দেয়। পরের AppendEntries-এর leaderCommit দেখে follower-রাও নিজেদের state machine-এ চালায়। মোটা অক্ষরের শর্তের কারণ অংশ ৬-এ।
ধাপে ধাপে একটি entry. পাঁচ সার্ভার, leader S1 (term ৩); সবার log-এ index ১–৫ আছে আর commitIndex ৫. ধরা যাক একমুখী পথ ০.৫ ms, একটি fsync ১ ms; S3-এর ডিস্ক ব্যস্ত (fsync ৩ ms) আর S5 দূরের zone-এ (একমুখী ২ ms). একটি বাস-টিকিট সেবা (কাল্পনিক) সিট A3 রুমানার নামে লিখতে চায়:
| ঘড়ি | |
| S1 · leader | |
| S2, S4 | |
| S3 | |
| S5 | |
| টেকসই কপি (leader-এর হিসাবে) | |
| Client |
- t = ০ — leader-এর log-এ যোগ। আদেশ এল (client id c7, ক্রমিক নম্বর ১২ — কেন, অংশ ৮-এ)। S1 এটিকে index ৬, term ৩-এ যোগ করে fsync শুরু করল, আর একই সাথে চার follower-কে AppendEntries(prevLogIndex ৫, prevLogTerm ৩, entries [#৬], leaderCommit ৫) পাঠাল।
- t = ০.৫ ms — consistency check. S2, S3, S4-এর index ৫-এ term ৩-এর entry আছে — মিলল; তারা #৬ যোগ করে fsync শুরু করল। S5-এর বার্তা এখনো পথে।
- t = ১.৫ ms — প্রথম দুটি সাফল্য। Leader-এর নিজের fsync ১.০ ms-এ শেষ — একটি টেকসই কপি। S2 আর S4-এর fsync শেষ; তারা success পাঠাল। ডিস্কে লেখার আগে success বলা নিষেধ, নইলে crash-এ কপি হারিয়ে majority-র হিসাব মিথ্যা হবে।
- t = ২.০ ms — commit. দুটি success পৌঁছাল: S1 + S2 + S4 = ৩ / ৫, আর entry-টি বর্তমান term ৩-এর — commitIndex ৬. S1 আদেশটি চালাল, client OK পেল। সময় = একটি round trip (১ ms) + follower-এর fsync (১ ms); leader-এর fsync সমান্তরালে। ধীর S3 আর S5 majority-র বাইরে, তাই latency বাড়ায়নি।
- t = ৫.০ ms, তারপর — সবাই মেলে। S3-এর success ৪.০ ms-এ, S5-এর ৫.০ ms-এ। Follower-রা commit জানে পরের AppendEntries-এর leaderCommit = ৬ থেকে, আর তখন চালায়। সাড়া না দেওয়া follower-কে leader বারবার পাঠাতে থাকে।
এবার কঠিন অবস্থা: অনেকক্ষণ বন্ধ থাকা একটি follower, আর একটু পিছিয়ে থাকা আরেকজন। S1 সদ্য term ৩-এর leader হয়েছে; নতুন leader সবার nextIndex ধরে নেয় নিজের শেষ index + ১ — আশাবাদী অনুমান। S3-এর log-এ মাত্র ৩টি entry, আর S5-এর ৪টি:
// Raft log replication with the consistency check. Entry = "term:command"; index = position + 1.
const TERM = 3, MAJ = 3, base = ["1:x=1", "1:y=2", "2:x=5", "2:z=7", "2:y=9"];
const L = { S1: [...base], S2: [...base], S3: base.slice(0, 3), S4: [...base], S5: base.slice(0, 4) };
const up = { S2: true, S3: false, S4: true, S5: true }; // S3 crashed long ago, still down
const next = {}, match = {}; let commit = 3; // S1 knows 1..3 are committed
for (const f in up) { next[f] = L.S1.length + 1; match[f] = 0; } // a new leader starts optimistic
const term = (log, i) => (i === 0 ? 0 : +log[i - 1].split(":")[0]);
function appendEntries(f) {
const prev = next[f] - 1, log = L[f], pt = term(L.S1, prev);
if (prev > log.length || term(log, prev) !== pt) { // consistency check fails
console.log(` AE->${f} prevLogIndex=${prev} prevLogTerm=${pt}: REJECT (${f} has ${log.length} entries)`);
next[f]--; return false;
}
const entries = L.S1.slice(prev);
entries.forEach((e, k) => { const i = prev + k; // drop a conflicting suffix, add missing
if (i < log.length && term(log, i + 1) !== +e.split(":")[0]) log.length = i;
if (i >= log.length) log.push(e); });
match[f] = prev + entries.length; next[f] = match[f] + 1;
console.log(` AE->${f} prevLogIndex=${prev} prevLogTerm=${pt}: OK, sent ${entries.length}, matchIndex=${match[f]}`);
return true;
}
function round(title) {
console.log(title);
for (const f in up) if (up[f]) while (!appendEntries(f));
for (let n = L.S1.length; n > commit; n--) { // highest n stored on a majority...
const copies = 1 + Object.values(match).filter(m => m >= n).length;
if (copies >= MAJ && term(L.S1, n) === TERM) { commit = n; break; } // ...and from the CURRENT term
}
console.log(` copies of index 5: ${1 + Object.values(match).filter(m => m >= 5).length}, leader commitIndex=${commit}`);
}
round("round 1: S1 just won term 3; S3 is down");
L.S1.push("3:x=8"); round("round 2: client sends x=8 -> index 6, term 3");
up.S3 = true; round("round 3: S3 restarts with 3 entries");
for (const s in L) console.log(`${s}: ${L[s].join(" ")}`);আউটপুট:
round 1: S1 just won term 3; S3 is down
AE->S2 prevLogIndex=5 prevLogTerm=2: OK, sent 0, matchIndex=5
AE->S4 prevLogIndex=5 prevLogTerm=2: OK, sent 0, matchIndex=5
AE->S5 prevLogIndex=5 prevLogTerm=2: REJECT (S5 has 4 entries)
AE->S5 prevLogIndex=4 prevLogTerm=2: OK, sent 1, matchIndex=5
copies of index 5: 4, leader commitIndex=3
round 2: client sends x=8 -> index 6, term 3
AE->S2 prevLogIndex=5 prevLogTerm=2: OK, sent 1, matchIndex=6
AE->S4 prevLogIndex=5 prevLogTerm=2: OK, sent 1, matchIndex=6
AE->S5 prevLogIndex=5 prevLogTerm=2: OK, sent 1, matchIndex=6
copies of index 5: 4, leader commitIndex=6
round 3: S3 restarts with 3 entries
AE->S2 prevLogIndex=6 prevLogTerm=3: OK, sent 0, matchIndex=6
AE->S3 prevLogIndex=5 prevLogTerm=2: REJECT (S3 has 3 entries)
AE->S3 prevLogIndex=4 prevLogTerm=2: REJECT (S3 has 3 entries)
AE->S3 prevLogIndex=3 prevLogTerm=2: OK, sent 3, matchIndex=6
AE->S4 prevLogIndex=6 prevLogTerm=3: OK, sent 0, matchIndex=6
AE->S5 prevLogIndex=6 prevLogTerm=3: OK, sent 0, matchIndex=6
copies of index 5: 5, leader commitIndex=6
S1: 1:x=1 1:y=2 2:x=5 2:z=7 2:y=9 3:x=8
S2: 1:x=1 1:y=2 2:x=5 2:z=7 2:y=9 3:x=8
S3: 1:x=1 1:y=2 2:x=5 2:z=7 2:y=9 3:x=8
S4: 1:x=1 1:y=2 2:x=5 2:z=7 2:y=9 3:x=8
S5: 1:x=1 1:y=2 2:x=5 2:z=7 2:y=9 3:x=8
Round ১: S2 আর S4-এর কাছে ফাঁকা AppendEntries (sent 0) — heartbeat. S5-এর কাছে প্রথমবার REJECT (S5 has 4 entries); leader এক ঘর পিছিয়ে prevLogIndex ৪ দিয়ে মিলিয়ে বাকি একটি entry পাঠাল। এখন index ৫ চারটি কপিতে — তবু leader commitIndex=3! কারণ index ৪ আর ৫ term ২-এর entry, আর নতুন leader পুরোনো term-এর entry কপি গুনে commit করে না। Round ২: client-এর x=8 এল index ৬, term ৩-এ; তিন follower-এ পৌঁছাতেই commitIndex এক লাফে ৬ — আর Log Matching-এর জোরে তার আগের ৪ আর ৫-ও সাথে সাথে committed. Round ৩: S3 ফিরল। দুবার REJECT (prevLogIndex ৫, তারপর ৪), তৃতীয়বার index ৩-এ term ২ মিলল, আর একবারে ৩টি entry গেল। শেষে পাঁচটি log অক্ষরে অক্ষরে এক।
বাস্তবের গতি: leader উত্তরের অপেক্ষা না করে পরপর পাঠায় (pipelining), আর অনেক অনুরোধ একটি AppendEntries ও একটি fsync-এ জমায় (batching). তাই প্রতিটি commit-এ round trip লাগলেও সেকেন্ডে হাজার হাজার commit সম্ভব।
৬ · Leader বদল, log মেরামত আর “শুধু নিজের term” নিয়ম
গোলমাল বাধে leader মরলে: কারও কাছে কিছু entry নেই, কারও কাছে এমন entry, যা কখনো commit হয়নি। Raft-এর উত্তর কড়া কিন্তু সরল — নতুন leader-এর log-ই সত্য। Follower-দের তার সাথে মেলাতে হবে; বিরোধী entry মুছে leader-এরটি বসবে। Leader নিজে কখনো নিজের entry মোছে না — Leader Append-Only. এটি নিরাপদ, কারণ ভোটের up-to-date নিয়ম নিশ্চিত করে যে নতুন leader-এর কাছে সব committed entry আছেই।
নিচের stepper-এ প্রতিটি log দেখানো হয়েছে index ধরে প্রতিটি entry-র term দিয়ে — যেমন “১ ১ ২ ২” মানে index ১ আর ২-এ term ১-এর entry, ৩ আর ৪-এ term ২-এর:
| S1 | |
| S2 | |
| S3 | |
| S4 | |
| S5 | |
| commitIndex |
- শুরু। S1 term ২-এর leader. Index ১–৩ চারজনের কাছে — committed. Index ৪ (term ২) শুধু S1 আর S2-তে, majority নয়; এর client উত্তরের অপেক্ষায়।
- S1 crash; S3 candidate. S3-এর টাইমার আগে ফুরাল: term ৩, RequestVote(lastLogIndex ৩, lastLogTerm ২)।
- ভোট — up-to-date যাচাই। S2-এর শেষ entry (৪, term ২) S3-এর (৩, term ২)-এর চেয়ে এগিয়ে, তাই S2 ভোট দিল না (তবে term ৩ গ্রহণ করল)। S4-এর শেষ (৩, term ২) সমান — ভোট দিল। S5-এর শেষ (২, term ১) পিছিয়ে — ভোট দিল। নিজেরটিসহ ৩ / ৫: S3 leader. S5 কখনো জিততে পারত না — তার কাছে committed index ৩ নেই, তাই কেউ তাকে ভোট দিত না; S2 আগে জাগলে সে-ও জিততে পারত।
- মেরামত। S3 term শুরুতেই একটি ফাঁকা no-op entry যোগ করল: index ৪, term ৩. S2-এর কাছে prev (৩, term ২) মিলল, কিন্তু index ৪-এ S2-এর term ২ বনাম leader-এর term ৩ — বিরোধ; S2 নিজের #৪ মুছে leader-এরটি বসাল। S4 সরাসরি নিল। S5-এ prev (৩, term ২) নেই — একবার REJECT, তারপর prev (২, term ১) মিলিয়ে দুটি entry নিল।
- Commit আর S1-এর ফেরা। Index ৪ (term ৩) এখন S3, S2, S4, S5-এ — majority, আর বর্তমান term-এর: commitIndex ৪, সাথে ১–৩-ও নিশ্চিত। S1 ফিরে term ৩-এর AppendEntries দেখে follower হলো, তার term ২-এর #৪ মুছে term ৩-এরটি বসল। পুরোনো #৪-এর আদেশ হারাল — তবে তা কখনো commit হয়নি; client একই id আর ক্রমিক নম্বরে আবার পাঠাবে (অংশ ৮)।
কেন পুরোনো term-এর entry কপি গুনে commit করা যায় না
অংশ ৫-এর simulator-এ index ৫ চার কপিতে থেকেও commit হয়নি। কেন এত সাবধানতা? Raft গবেষণাপত্রের একটি বিখ্যাত উদাহরণ (সেখানে Figure 8), পাঁচ সার্ভারে, শুধু index ২ নিয়ে:
| ধাপ | কী ঘটে | Index ২ কার কাছে কোন term-এ |
|---|---|---|
| ক | S1 term ২-এর leader; index ২ (term ২) শুধু S1 আর S2-তে পাঠিয়ে crash | S1: ২, S2: ২ |
| খ | S5 term ৩-এ S3, S4-এর ভোটে জেতে (সবার শেষ term ১); নিজের কাছে index ২ (term ৩) লিখে crash | S1: ২, S2: ২, S5: ৩ |
| গ | S1 ফিরে term ৪-এ জেতে, পুরোনো index ২ (term ২) S3-তেও পাঠায় — এখন তিন কপি, majority | S1, S2, S3: ২ · S5: ৩ |
| ঘ — বিপদ | S1 কপি গুনে index ২ commit করে crash করলে: S5 term ৫-এ দাঁড়ায়, শেষ term ৩ বড় বলে S2, S3, S4 ভোট দেয়; S5 সবার index ২ নিজের term ৩-এর entry দিয়ে বদলায় | সবাই: ৩ — “committed” entry উধাও! |
| ঙ — Raft-এর নিয়ম | S1 আগে নিজের term ৪-এর entry (index ৩) majority-তে পাঠায়; সেটি commit হলে index ২-ও committed, আর majority-র শেষ term ৪ বলে S5 আর জেতে না | S1, S2, S3: ২ ৪ — নিরাপদ |
তাই নিয়ম: leader কপি গুনে commit করে শুধু নিজের term-এর entry; আগের entry-গুলো তার সাথে পরোক্ষভাবে commit হয়। নতুন leader term-এর শুরুতে no-op যোগ করে এজন্যই — নতুন অনুরোধের অপেক্ষা ছাড়াই আগের entry পাকা হয়, আর leader জানে commitIndex কত।
৭ · Safety — পাঁচটি নিশ্চয়তা আর একটি ছোট প্রমাণ
Raft গবেষণাপত্র পাঁচটি property প্রমাণ করে। এগুলো মিলেই “কোনো দুটি সার্ভার কখনো একই index-এ ভিন্ন আদেশ চালাবে না”:
| Property | অর্থ | কীভাবে নিশ্চিত হয় |
|---|---|---|
| Election Safety | এক term-এ বড়জোর একজন leader | এক term-এ এক ভোট, ডিস্কে লেখা; জিততে majority |
| Leader Append-Only | Leader নিজের log-এ শুধু যোগ করে, মোছে বা বদলায় না | প্রোটোকলের নিয়ম |
| Log Matching | একই index আর term-এর entry থাকলে সেখান পর্যন্ত দুই log অভিন্ন | এক term-এ এক leader এক index-এ এক entry; প্রতিটি AppendEntries-এ consistency check |
| Leader Completeness | কোনো term-এ committed entry পরের সব term-এর leader-এর log-এ থাকে | Majority-র overlap + up-to-date ভোট + শুধু নিজের term-এর entry গুনে commit |
| State Machine Safety | কেউ একটি index-এ একটি entry চালালে অন্য কেউ সেই index-এ ভিন্ন entry চালাবে না | ওপরের চারটির ফল; চালানো হয় শুধু committed entry, ক্রমানুসারে |
Leader Completeness — প্রমাণের ধারণা
ধরুন term T-এর leader একটি entry e commit করেছে। তাহলে e আছে একটি majority M₁-এর কাছে। এখন T-এর পরের প্রথম কোনো term U-র leader L, যার log-এ e নেই — এমন ধরে নিই, আর দেখি অসংগতি আসে কি না।
- L জিতেছে একটি majority M₂-এর ভোটে। M₁ আর M₂ মেলে (অংশ ২) — একজন ভোটদাতা v, যার কাছে e আছে, আর যে L-কে ভোট দিয়েছে।
- v ভোট দিয়েছে, তাই L-এর log v-এর log-এর অন্তত সমান হালনাগাদ। দুই সম্ভাবনা:
- L-এর শেষ term v-এর শেষ term-এর সমান: তাহলে L-এর log লম্বা বা সমান; Log Matching অনুযায়ী v-এর পুরো log L-এর ভেতরে আছে — e-সহ। অসংগতি।
- L-এর শেষ term বড়: সেই শেষ entry বানিয়েছিল আগের কোনো leader, যার term T-এর চেয়ে বড় (কারণ v-এর শেষ term অন্তত T) আর U-র চেয়ে ছোট। ধরে নেওয়া অনুযায়ী সেই leader-এর log-এ e ছিল (U-ই প্রথম e-হীন leader). Log Matching অনুযায়ী L-এর log-এ সেই entry পর্যন্ত সব আছে — e-সহ। আবার অসংগতি। ∎
কোথায় “শুধু নিজের term” নিয়মটি লাগল? “Commit” মানে ধরে নেওয়া হয়েছে, e যে term-এ commit হলো, সেই term-এর leader-ই কপি গুনেছে। অংশ ৬-এর ধাপ “ঘ”-তে এই শর্ত ভেঙেছিল — term ৪-এর leader term ২-এর entry গুনেছিল।
পরীক্ষা ও ইন্টারভিউতে সাধারণত যা আসে:
- ৫টি node-এর Raft cluster কতগুলো ব্যর্থতা সহ্য করে? ২টি crash; majority ৩, আর f = ⌊(৫ − ১)/২⌋ = ২.
- Randomized election timeout কেন? সবাই একসাথে candidate হয়ে split vote না করে; সাধারণত একজন আগে জেগে জেতে।
- Entry কখন committed? বর্তমান term-এর leader সেটি majority-তে পৌঁছেছে দেখলে; আগের term-এর entry এর সাথে পরোক্ষভাবে।
- Partition-এ দুই leader হতে পারে কি? ছোট ভাগের পুরোনো leader নিজেকে leader ভাবতে পারে, কিন্তু majority নেই বলে কিছু commit করতে পারে না; এক term-এ একজনই leader.
- Raft আর 2PC-এর পার্থক্য? 2PC সবার YES চায় আর coordinator মরলে আটকায়; Raft majority চায় আর leader মরলে নতুন leader বাছে।
৮ · Client, read, snapshot আর membership বদল
Client — leader খোঁজা আর একবারের বেশি নয়
Client যেকোনো সার্ভারে পাঠাতে পারে; follower হলে সে leader-এর ঠিকানা জানিয়ে ফিরিয়ে দেয় (redirect). সূক্ষ্ম সমস্যা অন্যখানে: leader entry commit করে চালাল, কিন্তু উত্তর দেওয়ার আগেই crash করল। Client নতুন leader-কে আবার পাঠালে আদেশ দুবার চলবে — যেমন wallet থেকে ৫০০ টাকা কাটা। Raft গবেষণাপত্রের সমাধান: client-এর id আর প্রতিটি অনুরোধের ক্রমিক নম্বর; state machine প্রতিটি client-এর শেষ নম্বর আর উত্তর মনে রাখে, পুরোনো নম্বর এলে আগের উত্তর দেয়, আবার চালায় না। এটি পর্ব ০৮-এর idempotency key-এর একই চিন্তা (পর্ব ০৮, পর্ব ৩৪)।
Linearizable read — leader-এর কাছ থেকে পড়লেও কেন যথেষ্ট নয়
লেখা log দিয়ে যায়; পড়া কি সরাসরি leader-এর state machine থেকে দেওয়া যায়? বিপদ: partition-এ S1 ছোট ভাগে, বড় ভাগে নতুন leader নতুন লেখা commit করছে, আর S1 জানে না সে আর leader নয় — পুরোনো মান পড়াবে। Linearizability (পর্ব ৪০) রাখতে তিনটি পথ:
- পড়াটিও log-এ: সরল, কিন্তু প্রতিটি পড়ায় একটি লেখার দাম।
- ReadIndex: leader বর্তমান commitIndex মনে রাখে (term-এ নিজের একটি entry আগে commit হতে হবে — no-op-এর আরেক কাজ), একটি heartbeat-এ majority-র সাড়া নিয়ে নিশ্চিত হয় সে এখনো leader, আর state machine ততদূর চালানো হলে পড়ায় — ডিস্ক নয়, একটি round trip. Follower-ও leader-এর কাছে read index জেনে পড়াতে পারে।
- Leader lease: heartbeat-এ majority সাড়া দিলে leader ধরে নেয়, election timeout-এর চেয়ে কম একটি সময় পর্যন্ত আর কেউ leader হবে না, আর সেই সময়ে round trip ছাড়াই পড়ায়। দ্রুততম, কিন্তু নিরাপত্তা নির্ভর করে ঘড়ির গতির পার্থক্য সীমিত থাকার ওপর — দীর্ঘ GC pause বা ঘড়ির ভুলে পুরোনো মান পড়ানো সম্ভব (পর্ব ৩৯, পর্ব ৪৩)।
লেখার সময়ের documentation অনুযায়ী etcd ডিফল্টে linearizable read দেয়, আর চাইলে “serializable” read দেয় — স্থানীয় কপি থেকে, দ্রুত, কিন্তু পুরোনো মান হতে পারে। অর্থাৎ পড়ার নিশ্চয়তাও একটি বাছাই।
Snapshot আর log compaction
Log চিরকাল বাড়লে ডিস্ক ভরবে, আর restart-এ পুরো log চালাতে ঘণ্টা লাগবে। তাই প্রতিটি সার্ভার মাঝে মাঝে নিজেই state machine-এর একটি snapshot নেয় — কোন index (lastIncludedIndex) আর কোন term (lastIncludedTerm) পর্যন্ত — তারপর সেই পর্যন্ত log মুছে ফেলে। শেষ index আর term রাখা হয়, যাতে পরের AppendEntries-এর consistency check তখনো চলে। কোনো follower এত পিছিয়ে থাকে যে তার দরকারি entry leader আর রাখেনি, তখন leader পাঠায় পুরো snapshot — InstallSnapshot.
মুদি দোকানের খাতা বছর শেষে মোটা হলে প্রতিটি ক্রেতার মোট বাকি নতুন খাতার প্রথম পাতায় “৩১ ডিসেম্বর পর্যন্ত” লিখে রাখা হয়। ছয় মাস পরে ফেরা ভাইকে পুরোনো সব পাতা নয়, সেই মোট আর তার পরের লাইন দেখালেই চলে। Event sourcing-এর snapshot-ও একই কৌশল (পর্ব ৩৫)।
Membership বদল — সংক্ষেপে
৩টি সার্ভার থেকে ৫টিতে যেতে হলে সবাই একসাথে নতুন তালিকা জানবে না। মাঝের সময়ে পুরোনো তালিকার একটি majority আর নতুন তালিকার আরেকটি majority না মিলতে পারে — একই term-এ দুজন leader. Raft-এর দুটি সমাধান:
- Joint consensus (মূল গবেষণাপত্র): মাঝখানে একটি অন্তর্বর্তী configuration, যেখানে প্রতিটি সিদ্ধান্তে পুরোনো আর নতুন — দুই তালিকারই আলাদা majority লাগে। সেটি commit হলে নতুন তালিকায় যাওয়া।
- একবারে একটি সার্ভার (Ongaro-র PhD গবেষণাপত্র): শুধু একটি করে যোগ বা বাদ। তখন পুরোনো আর নতুন তালিকার যেকোনো majority সব সময় মেলে, তাই অন্তর্বর্তী ধাপ লাগে না। পরে এর একটি সূক্ষ্ম ত্রুটি আর তার সমাধান (নতুন leader আগে নিজের term-এর একটি entry commit করবে) প্রকাশিত হয় বলে জানা যায়; বাস্তবায়নের আগে সর্বশেষ বিবরণ মিলিয়ে নিন।
খালি নতুন সার্ভার majority-তে ঢুকলে commit আটকাতে পারে, তাই অনেক সিস্টেমে তাকে আগে ভোটহীন সদস্য (etcd-এ learner) করে log ধরানো হয়।
৯ · Paxos-এর ধারণা, আর Raft–Paxos–ZAB–VR পাশাপাশি
Paxos Leslie Lamport-এর, Raft-এর প্রায় পঁচিশ বছর আগের (ইতিহাস ভাবনার প্রশ্ন ১-এ)। একটি মানে একমত হওয়ার রূপটি single-decree Paxos. তিনটি ভূমিকা, একই যন্ত্র একাধিকটি নিতে পারে:
- Proposer: একটি মান প্রস্তাব করে।
- Acceptor: ভোটার; এদের majority যা গ্রহণ করে, সেটিই সিদ্ধান্ত।
- Learner: সিদ্ধান্ত জেনে নেয় আর কাজে লাগায়।
| ধাপ | বার্তা | নিয়ম |
|---|---|---|
| ১ক | Proposer → সবাই: Prepare(n); n একটি অনন্য, ক্রমশ বড় proposal number | n আগে ব্যবহৃত সব নম্বরের চেয়ে বড়; দুই proposer কখনো একই n নেয় না |
| ১খ | Acceptor → proposer: Promise(n, আগে গৃহীত সবচেয়ে বড় নম্বরের প্রস্তাব) | প্রতিশ্রুতি দেয়: n-এর চেয়ে ছোট নম্বরের কোনো প্রস্তাব আর গ্রহণ করবে না; আগে বড় নম্বরে প্রতিশ্রুতি দিয়ে থাকলে উপেক্ষা |
| ২ক | Majority-র promise পেলে proposer → সবাই: Accept(n, v) | v বাছাইয়ের নিয়ম: promise-গুলোর মধ্যে আগে গৃহীত সবচেয়ে বড় নম্বরের প্রস্তাবের মান; কেউ কিছু গ্রহণ না করে থাকলে নিজের মান |
| ২খ | Acceptor → learner: Accepted(n, v) | n-এর চেয়ে বড় নম্বরে প্রতিশ্রুতি না দিয়ে থাকলে গ্রহণ। Majority একই (n, v) গ্রহণ করলে মান “chosen” |
কেন নিরাপদ: কোনো মান majority-তে গৃহীত হলে পরের যেকোনো proposer-এর promise-দাতাদের অন্তত একজন সেই খবর আনবে (অংশ ২), আর ধাপ ২ক-এর নিয়মে সেই মানই প্রস্তাব করতে হবে। Raft-এর up-to-date ভোট আর Paxos-এর “সবচেয়ে বড় নম্বরের মান নাও” — একই intersection-এর দুটি রূপ। কেন আটকাতে পারে: দুজন proposer পালা করে বড় নম্বরে Prepare পাঠালে একে অপরের ধাপ ২ বাতিল করতে থাকে — livelock. সমাধান একজন প্রধান proposer, অর্থাৎ leader, আর random বিরতি — Raft-এর random timeout-এর মতোই।
Multi-Paxos: একটি log মানে অনেকগুলো slot, প্রতিটিতে একটি Paxos. একজন স্থির leader সব ভবিষ্যৎ slot-এর জন্য ধাপ ১ একবারে সেরে নেয়, তারপর প্রতিটি আদেশে শুধু ধাপ ২ — majority-র সাথে একটি round trip, Raft-এর AppendEntries-এর মতো। এর একক আনুষ্ঠানিক বর্ণনা নেই, তাই বাস্তবায়নভেদে খুঁটিনাটি আলাদা — Raft-এর জন্মের একটি কারণ।
| প্রোটোকল | এক লাইনে | কোথায় দেখা যায় (প্রকাশিত তথ্য অনুযায়ী) |
|---|---|---|
| Raft (২০১৪) | শক্তিশালী leader; সব committed entry যার কাছে, শুধু সে-ই জিততে পারে; log শুধু leader থেকে বাইরে যায় | etcd, Consul, CockroachDB, TiKV, Kafka-র KRaft, RabbitMQ-র quorum queue |
| (Multi-)Paxos (১৯৯০-এর দশক) | যেকোনো proposer; leader একটি optimization; নতুন leader ধাপ ১-এ অন্যদের কাছ থেকে বাদ পড়া মান জেনে নেয় | Google-এর Chubby (২০০৬) আর Spanner (২০১২)-এর গবেষণাপত্রে বর্ণিত |
| ZAB | ZooKeeper-এর atomic broadcast; epoch-সহ primary-backup; নতুন leader আগে সবাইকে মিলিয়ে নেয় | Apache ZooKeeper |
| Viewstamped Replication (১৯৮৮) | Oki ও Liskov-এর; primary আর “view change” — গঠনে Raft-এর খুব কাছাকাছি | মূলত গবেষণা ও শিক্ষায়; ২০১২ সালে Liskov ও Cowling-এর সংশোধিত বর্ণনা |
আরও গভীরে: Heidi Howard ও সহকর্মীদের Flexible Paxos (২০১৬) দেখায়, ধাপ ১ আর ধাপ ২-এর quorum-কে শুধু পরস্পরের সাথে মিলতে হয়, দুটিকেই majority হতে হয় না — প্রতিদিনের commit ছোট quorum-এ, বিরল leader বদল বড় quorum-এ। EPaxos (২০১৩) leader ছাড়াই, পরস্পর-নিরপেক্ষ আদেশ সমান্তরালে commit করে। Howard ও Mortier-এর ২০২০ সালের “Paxos vs Raft” লেখার উপসংহার: দুটি প্রায় একই, মূল পার্থক্য leader নির্বাচনের পদ্ধতিতে। আর Kafka-র ZooKeeper থেকে KRaft-এ যাওয়ার নকশা আছে KIP-500-এ (পর্ব ৩৩)।
১০ · দাম, cluster-এর আকার আর AI-এর যুগে যাচাই
একটি commit-এর দাম
Leader নিজের কপি নিজেই রাখে, তাই তার দরকার আরও Q − ১টি follower-এর সাফল্য। Commit-এর latency প্রায় = leader থেকে (Q − ১)-তম দ্রুততম follower-এর RTT + সেই follower-এর fsync (leader-এর fsync সমান্তরালে)। Leader-এর নিজের “RTT” ০ ধরে সবার RTT সাজালে, বিজোড় N-এ এটি ঠিক মাঝের মান — median. সবচেয়ে ধীরজন নয়, মাঝের জন। নিচের simulator প্রথমে N = ১ থেকে ৭-এর টেবিল দেখায়, তারপর ধরা যাক দুই ধরনের বসানোয় seed-সহ RTT নিয়ে commit-এর সময়:
// Majority quorums: size, failures tolerated, and what one commit costs in network time.
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 maj = (n) => Math.floor(n / 2) + 1;
console.log(" N majority failures-tolerated acks-the-leader-waits-for");
for (let n = 1; n <= 7; n++)
console.log(String(n).padStart(2) + String(maj(n)).padStart(9) + String(n - maj(n)).padStart(15) +
String(maj(n) - 1).padStart(20) + (n % 2 === 0 ? " (even: no gain over " + (n - 1) + ")" : ""));
const rnd = mulberry32(42);
const places = [["one region, 3 zones", 0.4, 2.0], ["spread over 3 regions", 40, 180]]; // assumed RTT ranges (ms)
for (const [name, lo, hi] of places) {
const rtt = Array.from({ length: 6 }, () => +(lo + rnd() * (hi - lo)).toFixed(1));
console.log(`--- ${name}: leader->follower RTTs (ms) = ${rtt.join(", ")}`);
for (const n of [3, 5, 7]) {
const all = [0, ...rtt.slice(0, n - 1)].sort((a, b) => a - b); // leader's own copy costs 0
const commit = all[maj(n) - 1]; // majority-th fastest = median
console.log(`N=${n}: sorted [${all.join(", ")}] -> commit waits ${commit} ms (fastest ${maj(n)} of ${n})`);
}
}আউটপুট:
N majority failures-tolerated acks-the-leader-waits-for
1 1 0 0
2 2 0 1 (even: no gain over 1)
3 2 1 1
4 3 1 2 (even: no gain over 3)
5 3 2 2
6 4 2 3 (even: no gain over 5)
7 4 3 3
--- one region, 3 zones: leader->follower RTTs (ms) = 1.4, 1.1, 1.8, 1.5, 0.7, 1.2
N=3: sorted [0, 1.1, 1.4] -> commit waits 1.1 ms (fastest 2 of 3)
N=5: sorted [0, 1.1, 1.4, 1.5, 1.8] -> commit waits 1.4 ms (fastest 3 of 5)
N=7: sorted [0, 0.7, 1.1, 1.2, 1.4, 1.5, 1.8] -> commit waits 1.2 ms (fastest 4 of 7)
--- spread over 3 regions: leader->follower RTTs (ms) = 78.3, 127.5, 161.2, 106.1, 75, 163.5
N=3: sorted [0, 78.3, 127.5] -> commit waits 78.3 ms (fastest 2 of 3)
N=5: sorted [0, 78.3, 106.1, 127.5, 161.2] -> commit waits 106.1 ms (fastest 3 of 5)
N=7: sorted [0, 75, 78.3, 106.1, 127.5, 161.2, 163.5] -> commit waits 106.1 ms (fastest 4 of 7)
টেবিল: ৪-এর majority ৩, তবু সহ্য করে ১টি — ৩-এর সমান; বাড়তি সার্ভার মানে বাড়তি অপেক্ষা আর বিকল হওয়ার বাড়তি সম্ভাবনা। এক region-এ সব সংখ্যা ১–২ ms; N = ৩-এ ১.১, ৫-এ ১.৪, আর ৭-এ ১.২ ms — সপ্তম সার্ভারটি (০.৭ ms) দ্রুত বলে মাঝের মান একটু নামল। Latency নির্ভর করে কে কোথায়, কতজন তার ওপর নয়। Region পেরোলে ছবি বদলায়: N = ৫ আর ৭ দুটিতেই ১০৬.১ ms, প্রতিটি লেখায়। Region-এর দূরত্বের হিসাব পর্ব ০৭-এ (পর্ব ০৭)।
কেন ৩ বা ৫, আর কোথায় বসাবেন
- ৩: একটি ব্যর্থতা সহ্য করে; একটি রক্ষণাবেক্ষণে থাকলে আরেকটি বিকল হলেই লেখা বন্ধ।
- ৫: দুটি সহ্য করে — একটি রক্ষণাবেক্ষণে আর একটি হঠাৎ বিকল হলেও চলে।
- ৭ বা বেশি: leader-এর বার্তা বাড়ে, majority বড় হয়; লাভ সামান্য। বেশি কপি লাগলে ভোটহীন read replica.
- বসানো: আলাদা failure domain-এ, যেমন তিনটি availability zone-এ; দুটি data centre-এ ভাগ করার ফাঁদ অনুশীলন ৩-এ।
- Leader-হীন সময়: নতুন leader না আসা পর্যন্ত লেখা বন্ধ — মোটামুটি একটি election timeout আর একটি নির্বাচন, যা availability-র হিসাবে ঢোকে (পর্ব ০৫)।
Throughput: সব লেখা একজন leader দিয়ে যায়, তাই একটি group-এর গতি একটি যন্ত্রের সীমায় বাঁধা। বড় ডেটাবেস তাই ডেটা টুকরোয় ভাগ করে (পর্ব ২৩) প্রতিটির জন্য আলাদা Raft group চালায় — Multi-Raft, প্রকাশিত নকশা অনুযায়ী CockroachDB আর TiKV-তে। আর অ্যাপের lock, lease বা configuration-এর জন্য নিজে Raft না লিখে etcd বা ZooKeeper-এর মতো coordination service ব্যবহার করুন; সেই নকশা পর্ব ৪৩-এ (পর্ব ৪৩)।
AI-এর যুগে consensus-এর নকশা যাচাই
“High availability-র জন্য কী করব?” — AI প্রায়ই বলে, “Raft, etcd, Kafka আর Redis যোগ করুন।” নামগুলো ঠিক, কিন্তু সংখ্যা আর ব্যর্থতার হিসাব প্রায়ই ভুল:
| AI-এর ভুল | কেমন দেখায় | কীভাবে যাচাই করবেন |
|---|---|---|
| Quorum-এর পাটিগণিতে ভুল | “২টি node, দুই data centre-এ”; “৪টি ৩-এর চেয়ে টেকসই” | Q = ⌊N/২⌋ + ১, f = N − Q; প্রতিটি failure domain হারালে কতজন থাকে |
| Commit-এর ভুল সংজ্ঞা | “leader ডিস্কে লিখলেই commit”; “সবার ack লাগে” | Majority, আর বর্তমান term-এর entry |
| Latency-র ভুল হিসাব | “commit = সবচেয়ে দূরের follower-এর RTT” | সব RTT সাজিয়ে Q-তম দ্রুততম (leader ০ ধরে), সাথে fsync |
| ACID আর CAP-এর consistency গোলানো | “Raft আছে, তাই isolation আছে”; “follower থেকে পড়লেও linearizable” | Consensus ক্রম দেয়, isolation আলাদা (পর্ব ২৪); পড়ার নিশ্চয়তা ReadIndex বা lease-এ |
| Failure mode উপেক্ষা | partition-এ পুরোনো leader, GC pause-এ lease, ধীর ডিস্ক — কিছুই নেই | প্রতিটি অংশে জিজ্ঞেস করুন: এখানে থামলে কী হয় |
| Byzantine আর crash গোলানো | “Raft হ্যাক হওয়া node-ও সামলায়” | Raft ও Paxos crash fault-এর জন্য; Byzantine-এ ৩f + ১ আর ভিন্ন প্রোটোকল |
| পণ্যের সীমা বানানো আর বাড়তি জটিলতা | etcd-এ “কয়েক TB ডেটা” বা “লাখো লেখা প্রতি সেকেন্ড”; সাধারণ cron-এর জন্য নিজে Raft লেখা | অফিশিয়াল documentation আর নিজের load test; আগে দেখুন একটি managed সেবাই যথেষ্ট কি না |
AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে cluster-এর আকার, বসানো আর ব্যর্থতার তালিকা লিখুন, তারপর এভাবে যাচাই করান:
AI-এর নকশা মানার আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Paxos পুরোনো আর প্রমাণিত — তবু ২০১৪ সালে নতুন একটি প্রোটোকল কেন লাগল? “বোঝা সহজ” কি সত্যিই প্রকৌশলের মানদণ্ড?
Leslie Lamport Paxos লেখেন ১৯৮০-এর দশকের শেষে, একটি কাল্পনিক গ্রিক দ্বীপ Paxos-এর খণ্ডকালীন সংসদের গল্প হিসেবে — আইনসভার সদস্যরা আসা-যাওয়া করেন, তবু আইনের খাতায় অসংগতি হয় না। “The Part-Time Parliament” জমা পড়ে ১৯৯০ সালে, কিন্তু রূপকের ভারে পর্যালোচকেরা আগ্রহ হারান; ছাপা হয় ১৯৯৮ সালে ACM-এর একটি জার্নালে। ২০০১ সালে Lamport লেখেন “Paxos Made Simple”, যার প্রথম বাক্যের মর্ম: সহজ ভাষায় বললে Paxos খুবই সরল। সমস্যা অন্য জায়গায় — একটি মানের Paxos সরল, কিন্তু একটি পূর্ণ log, leader, membership বদল আর snapshot-এর বিবরণ কোথাও একসাথে আনুষ্ঠানিকভাবে লেখা ছিল না।
২০০৭ সালে Google-এর প্রকৌশলীরা “Paxos Made Live”-এ লেখেন, Chubby-র জন্য Paxos বাস্তবায়নে তাঁদের অনেক নিজস্ব সিদ্ধান্ত নিতে হয়েছে, আর শেষ সিস্টেমটি একটি অপ্রমাণিত প্রোটোকলের ওপর দাঁড়িয়ে। Ongaro আর Ousterhout এই ফাঁক থেকেই শুরু করেন। তাঁরা সমস্যাটি ভাগ করেন, অবস্থার সংখ্যা কমান (যেমন log-এ ফাঁক থাকতে না দেওয়া, আর log শুধু leader থেকে বাইরে যাওয়া), আর random timeout-এর মতো সরল কৌশল বেছে নেন। দুই বিশ্ববিদ্যালয়ের ছাত্রছাত্রীদের নিয়ে একটি user study-তে তাঁরা দেখান, বেশিরভাগই Raft-এর প্রশ্নে Paxos-এর চেয়ে ভালো করেছেন। গবেষণাপত্রটি USENIX ATC ২০১৪-এর best paper পুরস্কার পায়, আর কয়েক বছরের মধ্যে বহু open-source বাস্তবায়ন আসে।
“বোঝা সহজ” প্রকৌশলের মানদণ্ড কেন? কারণ distributed system-এর বেশিরভাগ বড় বাগ প্রোটোকলে নয়, বাস্তবায়নে — যেখানে প্রকৌশলী নিয়মের একটি কোণ ভুল বুঝেছেন। যে নকশা দলের সবাই মাথায় রাখতে পারেন, তার review, testing আর on-call debugging সবই ভালো হয়।
মূল উপলব্ধি: একটি প্রমাণিত অ্যালগরিদম যথেষ্ট নয়; পূর্ণ সিস্টেমের স্পষ্ট বর্ণনা আর মানুষের বোঝার ক্ষমতাও নির্ভরযোগ্যতার অংশ।
প্র ০২FLP বলে asynchronous network-এ consensus সবসময় সম্ভব নয়। তাহলে etcd বা ZooKeeper রোজ কাজ করে কীভাবে?
১৯৮৫ সালে Fischer, Lynch আর Paterson প্রমাণ করেন: বার্তার দেরির কোনো সীমা নেই, আর একটি process-ও crash করতে পারে — এমন মডেলে কোনো deterministic প্রোটোকল নিশ্চিত করতে পারে না যে সবাই সব সময় সিদ্ধান্তে পৌঁছাবে। মূল কারণ: ধীর আর মৃত process আলাদা করা যায় না। অপেক্ষা করলে মৃতের জন্য চিরকাল অপেক্ষা; না করলে ধীরজনের সাথে ভিন্ন সিদ্ধান্তের ঝুঁকি। প্রমাণটি দেখায়, এক দুর্ভাগা ক্রমে বার্তা এলে প্রোটোকল চিরকাল “অনিশ্চিত” অবস্থায় ঘুরতে পারে।
বাস্তবের উত্তর তিনটি পথে। প্রথম, safety আর liveness আলাদা করা: Raft আর Paxos কখনো দুটি ভিন্ন সিদ্ধান্ত নেয় না, যত খারাপ network-ই হোক; শুধু অগ্রগতি থামতে পারে। দ্বিতীয়, partial synchrony: ১৯৮৮ সালে Dwork, Lynch আর Stockmeyer দেখান, network যদি শেষ পর্যন্ত কোনো এক সময় থেকে একটি অজানা সীমার ভেতরে আচরণ করে, তাহলে consensus সম্ভব। Raft-এর timeout ঠিক এই অনুমান: বেশিরভাগ সময় বার্তা timeout-এর চেয়ে অনেক দ্রুত আসে। তৃতীয়, random: FLP deterministic প্রোটোকলের কথা বলে; ১৯৮৩ সালে Ben-Or দেখান random মুদ্রা-নিক্ষেপে সম্ভাব্যতা ১ নিয়ে সিদ্ধান্ত হয়। Raft-এর random timeout পূর্ণ randomized consensus নয়, কিন্তু একই চিন্তা থেকে দুর্ভাগা ক্রমটি ভাঙে।
তাই বাস্তবে FLP-এর চেহারা হলো: network খুব খারাপ হলে etcd লেখা নেওয়া বন্ধ করে, বারবার নির্বাচন হয় — কিন্তু ডেটা ভুল হয় না। Operator-এর কাজ timeout ঠিক রাখা, ধীর ডিস্ক আর ভিড় network ধরা, যাতে অনুমানটি বেশিরভাগ সময় সত্য থাকে।
মূল উপলব্ধি: অসম্ভবতার ফল নকশা থামায় না, নকশার প্রতিশ্রুতি ঠিক করে দেয় — সব সময় সঠিক, আর network-এর সময় ভালো থাকলে দ্রুত ও নিয়মিত অগ্রগতি।
প্র ০৩Consensus এত নিরাপদ হলে সব ডেটা একটি বড় Raft cluster-এ রাখা হয় না কেন? Kafka-র মতো সিস্টেম ZooKeeper ছেড়ে নিজের Raft-এ গেল কেন?
একটি Raft group-এর তিনটি সীমা আছে। এক leader: সব লেখা একটি যন্ত্রের CPU, ডিস্ক আর network দিয়ে যায় — সার্ভার বাড়ালে throughput বাড়ে না, বরং leader-কে আরও কপি পাঠাতে হয়। প্রতিটি লেখায় round trip আর fsync: এক region-এ মিলিসেকেন্ড, region পেরোলে শত মিলিসেকেন্ড। পুরো state প্রতিটি সদস্যে: snapshot আর নতুন সদস্য ধরানোর খরচ ডেটার আকারের সাথে বাড়ে। এজন্য etcd-এর documentation নিজেই এটিকে ছোট, গুরুত্বপূর্ণ metadata-র জন্য বলে, আর storage-এর একটি সীমা রাখে — সংখ্যাটি সংস্করণভেদে দেখে নিতে হয়।
বড় ডেটাবেসের সমাধান তাই ভাগ করা: Google-এর Spanner-এর প্রকাশিত নকশায় ডেটা হাজারো টুকরোয় ভাগ, প্রতিটি টুকরোর নিজের Paxos group; CockroachDB আর TiKV একই কাজ Raft দিয়ে করে — Multi-Raft. তখন throughput টুকরোর সংখ্যার সাথে বাড়ে, কিন্তু কয়েক টুকরো জুড়ে লেনদেনে আবার 2PC লাগে — প্রতিটি participant নিজেই একটি consensus group, তাই coordinator মরলেও কেউ আটকে থাকে না (পর্ব ২৫)।
Kafka-র গল্প আরেক দিক দেখায়। আগে cluster-এর metadata থাকত আলাদা ZooKeeper cluster-এ — দুটি ভিন্ন সিস্টেম চালানো, দুই রকম নিরাপত্তা আর monitoring, আর বড় cluster-এ controller বদলালে সব metadata আবার পড়ার দেরি। KIP-500 প্রস্তাব করে metadata-কেই Kafka-র ভেতরে একটি Raft-ভিত্তিক log বানাতে (KRaft). প্রকাশিত তথ্য অনুযায়ী লেখার সময়ের সর্বশেষ বড় সংস্করণে ZooKeeper আর সমর্থিত নয়। শিক্ষাটি: consensus-এর জায়গা বদলায়নি, কিন্তু “কে চালায়, কতগুলো সিস্টেম” — সেই operations-এর হিসাব বদলেছে।
মূল উপলব্ধি: consensus একটি দামি যন্ত্র, ছোট আর জরুরি সত্যের জন্য; বড় ডেটায় তাকে ভাগ করে অনেক ছোট group-এ ছড়াতে হয়, আর কে সেই যন্ত্র চালাবে — সেটিও নকশার অংশ, কারণ প্রতিটি বাড়তি সিস্টেম মানে বাড়তি পাহারা আর বাড়তি বিভ্রাটের সুযোগ।
প্র ০৪Raft ধরে নেয় কোনো সার্ভার মিথ্যা বলে না। মিথ্যাবাদী বা হ্যাক হওয়া সার্ভার থাকলে কী লাগে, আর সেটি কেন এত দামি?
১৯৮২ সালে Lamport, Shostak আর Pease “Byzantine Generals Problem”-এ প্রশ্নটি তোলেন: কয়েকজন সেনাপতি একসাথে আক্রমণ বা পিছু হটার সিদ্ধান্ত নেবেন, কিন্তু কয়েকজন বিশ্বাসঘাতক ভিন্ন জনকে ভিন্ন কথা বলেন। তাঁদের ফল: সাধারণ বার্তায় f জন বিশ্বাসঘাতক সামলাতে কমপক্ষে ৩f + ১ সেনাপতি লাগে। Crash-এ লাগে ২f + ১ — কারণ crash হওয়া সার্ভার চুপ থাকে, মিথ্যা বলে না। Byzantine-এ একজন সার্ভার দুই দলকে দুই রকম ভোট দিতে পারে, তাই দুই quorum-এর মিলের জায়গায় অন্তত একজন সৎ সদস্য থাকতে হয় — এজন্য quorum বড়, প্রায় দুই-তৃতীয়াংশ।
১৯৯৯ সালে Castro আর Liskov-এর PBFT দেখায়, Byzantine fault সহ্য করেও বাস্তবে চলার মতো গতি সম্ভব। তবু দাম বড়: বেশি সার্ভার, প্রতিটি সিদ্ধান্তে সবার সাথে সবার বার্তা (মোটামুটি N²), digital signature বা MAC-এর হিসাব, আর একই bug সব কপিতে থাকলে লাভ নেই — তাই কখনো কখনো ভিন্ন দলের লেখা ভিন্ন বাস্তবায়ন লাগে। পরে blockchain-এর জগতে proof-of-work আর নানা BFT প্রোটোকল এই প্রশ্নের ভিন্ন ভিন্ন উত্তর দেয়, যেখানে অংশগ্রহণকারীরা একে অপরকে বিশ্বাস করে না।
একটি প্রতিষ্ঠানের নিজের data centre-এ সাধারণত crash fault মডেলই যুক্তিসংগত: সার্ভারগুলো এক দলের, এক নিয়ন্ত্রণে। মিথ্যা বার্তার বড় ঝুঁকি — নষ্ট ডিস্ক বা মেমরির ভুল বিট — checksum আর ECC দিয়ে আলাদাভাবে ধরা হয়, আর বাইরের আক্রমণ ঠেকানো হয় mTLS আর প্রবেশাধিকার নিয়ন্ত্রণে (পর্ব ৫২)। ভিন্ন প্রতিষ্ঠান, যারা একে অপরকে পুরো বিশ্বাস করে না, একটি যৌথ খাতা রাখতে চাইলে তখন BFT-র প্রশ্ন আসে।
মূল উপলব্ধি: fault model একটি নকশার সিদ্ধান্ত। ভুল মডেল বাছলে হয় অকারণে তিন গুণ দাম, নয়তো এমন ঝুঁকি, যা প্রোটোকল কখনো দেখার কথাই ভাবেনি।
প্র ০৫একটি সার্ভার শুধু leader-এর সাথে কথা বলতে পারছে না, বাকিদের সাথে পারছে। এমন “আংশিক” network ত্রুটিতে Raft কী সমস্যায় পড়ে, আর কীভাবে সামলানো হয়?
মৌলিক Raft ধরে নেয় network হয় চলে, নয়তো পুরো ভাগ হয়। বাস্তবে প্রায়ই দেখা যায় gray failure: একটি switch-এর একটি port-এ সমস্যা, বা একটি পথে packet হারাচ্ছে। ধরুন S4 leader-এর heartbeat পাচ্ছে না, কিন্তু বাকিদের সাথে কথা বলতে পারছে। তার timeout ফুরাবে, সে term বাড়িয়ে ভোট চাইবে। তার log হয়তো পিছিয়ে, তাই জিতবে না — কিন্তু বড় term দেখে সুস্থ leader নেমে যাবে! নতুন নির্বাচন, S4 আবার timeout, আবার বড় term — cluster বারবার leader হারাবে, লেখা থেমে থাকবে। Safety ঠিক থাকে, liveness হারায়।
Ongaro-র PhD গবেষণাপত্রে এর প্রতিকার আছে। PreVote: candidate হওয়ার আগে একটি “মহড়ার ভোট” — term না বাড়িয়ে জিজ্ঞেস করা, “আমি দাঁড়ালে কি ভোট পাব?” যারা সম্প্রতি leader-এর খবর পেয়েছে, তারা না বলে; ফলে বিচ্ছিন্ন সার্ভার term বাড়িয়ে গোলমাল করতে পারে না। CheckQuorum জাতীয় নিয়ম: leader নিজেই নির্দিষ্ট সময়ে majority-র সাড়া না পেলে নেমে যায়, যাতে ছোট ভাগে আটকে থাকা পুরোনো leader client-দের বিভ্রান্ত না করে। etcd-এর মতো বাস্তবায়নে এ ধরনের বিকল্প আছে; কোনটি ডিফল্টে চালু, তা সংস্করণভেদে দেখে নিতে হয়।
প্রকাশিত incident লেখায় এমন ঘটনার বর্ণনা আছে। যেমন Cloudflare-এর ২০২০ সালের নভেম্বরের একটি লেখা (“A Byzantine failure in the real world”) অনুযায়ী, একটি switch-এর আংশিক ত্রুটিতে etcd cluster-এ বারবার leader বদল হয়, আর তার ওপর নির্ভরশীল ডেটাবেসের failover ব্যবস্থাও অস্থির হয়ে পড়ে। শিক্ষাটি সাধারণ: failure detection (পর্ব ৪৪) আর consensus একসাথে পরীক্ষা করতে হয়, শুধু “পুরো বন্ধ” নয়, “আধা চালু” অবস্থাতেও।
মূল উপলব্ধি: প্রমাণ টেকে মডেলের ভেতরে; বাস্তবের network মডেলের বাইরে যায়। তাই liveness-এর জন্য বাড়তি সুরক্ষা আর আংশিক ত্রুটির মহড়া দরকার।
অনুশীলন
আগে কাগজে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।
-
Quorum আর latency-র হিসাব: ৫ সার্ভারের Raft cluster. Leader A1 region A-তে; follower-দের RTT: A2 (region A-র আরেক zone) ১ ms, B1 ৩৫ ms, B2 ৩৬ ms (দুটিই region B), C1 ৯০ ms (region C). fsync বাদ দিন। বের করুন: (ক) majority আর কতগুলো crash সহ্য করে; (খ) প্রতিটি commit-এর network সময়; (গ) region B পুরো বন্ধ হলে commit চলবে কি, আর সময় কত; (ঘ) কেউ প্রস্তাব দিল region A-তে আরেকটি সার্ভার A3 (RTT ১ ms) যোগ করতে — N = ৬. Majority, সহনশীলতা, commit-এর সময়, আর region A পুরো বন্ধ হলে কী হয়?
- (ক) Q = ⌊৫/২⌋ + ১ = ৩; f = ৫ − ৩ = ২টি crash.
- (খ) RTT সাজানো, leader ০ ধরে: [০, ১, ৩৫, ৩৬, ৯০]. তৃতীয় দ্রুততম = ৩৫ ms — leader, A2 আর B1. C1-এর ৯০ ms-এর অপেক্ষা লাগে না।
- (গ) B1, B2 বন্ধ: সচল A1, A2, C1 — ৩ / ৫, majority আছে, commit চলে। কিন্তু এখন C1-এর ack ছাড়া উপায় নেই: [০, ১, ৯০] → ৯০ ms. Availability থাকল, latency প্রায় আড়াই গুণ।
- (ঘ) N = ৬: Q = ৪, f = ২ — সহনশীলতা বাড়েনি। সাজানো [০, ১, ১, ৩৫, ৩৬, ৯০], চতুর্থ = ৩৫ ms — একই। কিন্তু region A-তে এখন ৩টি; A বন্ধ হলে বাকি ৩ / ৬ < ৪ — লেখা বন্ধ, অথচ ৫-সার্ভারের নকশায় বাকি ৩ / ৫ নতুন leader বেছে চলত। বাড়তি সার্ভার নকশাকে দুর্বল করল।
-
আউটপুট অনুমান করুন — কে জিততে পারে? পাঁচ সার্ভারের Raft cluster. Leader S1 (term ৩) crash করেছে। বাকিদের log, index ধরে প্রতিটি entry-র term: S2 = ১ ১ ২ ২ ৩; S3 = ১ ১ ২ ২; S4 = ১ ১ ২; S5 = ১ ১ ২ ২. (S1-এর log ছিল ১ ১ ২ ২ ৩.) S1 না ফিরলে (ক) S2, S3, S4, S5-এর মধ্যে কে কে leader হতে পারে? প্রত্যেকের জন্য কে কে ভোট দেবে, লিখুন। (খ) Index ৫ (term ৩) কি নিশ্চিতভাবে টিকবে? (গ) কোন index-গুলো কোনো অবস্থাতেই হারাবে না?
শেষ entry (index, term): S2 (৫, ৩), S3 (৪, ২), S4 (৩, ২), S5 (৪, ২)। জিততে ৩টি ভোট লাগে, নিজেরটিসহ; সচল চারজন।
S2: S3 yes, S4 yes, S5 yes -> 4 votes -> CAN win S3: S2 no, S4 yes, S5 yes -> 3 votes -> CAN win S5: S2 no, S3 yes, S4 yes -> 3 votes -> CAN win S4: S2 no, S3 no, S5 no -> 1 vote -> can NOT win(ক) S2, S3 আর S5 জিততে পারে; S4 পারে না — তার শেষ (৩, ২) সবার চেয়ে পিছিয়ে। S3 আর S5-এর log সমান, তাই পরস্পরকে ভোট দেয়; S2 দুজনকেই না বলে। (খ) না। Index ৫ মাত্র দুই কপিতে (S1, S2) — commit হয়নি। S3 বা S5 জিতলে তারা নিজেদের term ৪-এর entry index ৫-এ বসাবে, আর S2-এর term ৩-এর entry মুছে যাবে। এর client কখনো OK পায়নি, তাই কোনো প্রতিশ্রুতি ভাঙেনি। (গ) Index ১–৪: সম্ভাব্য তিন বিজয়ী S2, S3, S5 — তিনজনের log-এই আছে।
-
নকশা সমালোচনা: একটি দল (কাল্পনিক) etcd চালাচ্ছে দুটি data centre-এ, প্রতিটিতে একটি করে সার্ভার — “একটি data centre গেলে অন্যটি চালাবে”। পরে প্রস্তাব এল, প্রতিটিতে দুটি করে, মোট ৪টি। দুটি নকশার সমস্যা ব্যাখ্যা করুন, আর একটি ভালো নকশা প্রস্তাব করুন।
- ২টি সার্ভার: Q = ২, f = ০. যেকোনো একটি গেলে বা সংযোগ কাটলে লেখা বন্ধ — একটি সার্ভারের চেয়েও খারাপ, কারণ এখন দুটির যেকোনোটির বিভ্রাটেই সেবা বন্ধ।
- ২ + ২ = ৪টি: Q = ৩, f = ১. এক data centre হারালে বাকি ২ / ৪ < ৩ — বন্ধ; সংযোগ কাটলে দুই দিকেই ২ — কেউ এগোয় না। যে বিপদের জন্য নকশা, ঠিক সেটিতেই সেবা নেই।
- ভালো নকশা: তৃতীয় একটি স্বাধীন জায়গা (আরেকটি data centre, zone বা region) আর ২ + ২ + ১ = ৫, বা ১ + ১ + ১ = ৩ — তখন যেকোনো একটি জায়গা হারালেও majority থাকে। তৃতীয় জায়গার সার্ভার ছোট হলেও চলে; তবে দূরে হলে commit-এ কখনো তার ack লাগতে পারে — অনুশীলন ১-এর মতো হিসাব করুন।
- তৃতীয় জায়গা না থাকলে: একটি data centre-কে প্রধান ধরে অন্যটিতে asynchronous কপি আর নিয়মবদ্ধ, হাতে চালানো failover (পর্ব ৫১) — consensus এখানে জাদু জানে না।
-
ব্যাখ্যা করুন: একজন সহকর্মী commit-এর নিয়ম সহজ করতে চান: “যেকোনো entry majority-তে পৌঁছালেই commit — term দেখার দরকার কী?” অংশ ৬-এর পাঁচ সার্ভারের ঘটনাক্রম (ধাপ ক–ঙ) ধরে দেখান, এই নিয়মে কোন ধাপে কী ভাঙে, আর আসল নিয়মে S5 কেন আর জিততে পারে না।
কী ভাঙে: ধাপ গ-তে index ২ (term ২) S1, S2, S3-এ — সহকর্মীর নিয়মে তখনই commit; client OK পায়, S1 হয়তো state machine-এ চালিয়েও ফেলে। ধাপ ঘ-তে S1 crash করলে S5 (শেষ term ৩) S2, S3 (শেষ term ২) আর S4 (শেষ term ১)-এর ভোট পায়, আর সবার index ২ নিজের term ৩-এর entry দিয়ে বদলে দেয়। চালানো হয়ে যাওয়া entry উধাও — State Machine Safety ভাঙল।
আসল নিয়মে: S1 আগে নিজের term ৪-এর entry (যেমন no-op, index ৩) majority-তে পাঠায়। এখন সেই majority-র প্রত্যেকের শেষ term ৪, আর S5-এর শেষ term ৩ — up-to-date যাচাইয়ে তারা কেউ S5-কে ভোট দেবে না, আর তাদের বাদ দিয়ে S5 majority পায় না। মনে রাখার সূত্র: কপির সংখ্যা যথেষ্ট নয়, কোন term-এর leader গুনছে, সেটিও প্রমাণের অংশ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRaft-এ leader crash থেকে নতুন leader পর্যন্ত ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- একজন follower-এর random election timeout ফুরায়, কারণ heartbeat আসছে না
- সে term এক বাড়িয়ে candidate হয় আর নিজেকে ভোট দেয়
- সবাইকে RequestVote পাঠায়, সাথে শেষ entry-র index আর term
- অন্যরা term আর log যাচাই করে ভোট দেয়, নিজেদের টাইমার নতুন করে চালু করে
- Majority ভোট পেয়ে সে leader হয় আর heartbeat পাঠায়
- Leader term-এর শুরুতে একটি no-op entry commit করে
Raft-এ candidate C-এর log-এর শেষ entry: index ৭, term ৩। ভোটদাতা V-এর log-এর শেষ entry: index ৯, term ২। V এই term-এ এখনো কাউকে ভোট দেয়নি। V কি C-কে ভোট দেবে?
- হ্যাঁ, কারণ C-এর শেষ entry-র term বড়
- না, কারণ V-এর log লম্বা
- না, কারণ দুটির log সমান নয়
- নির্ভর করে random timeout-এর ওপর
Raft-এ term ৫-এর leader দেখল, term ৪-এর একটি entry এখন ৫টির মধ্যে ৩টি সার্ভারে আছে। Leader-এর কী করা উচিত?
- সঙ্গে সঙ্গে commit করে state machine-এ চালানো
- Entry-টি মুছে ফেলা, কারণ সেটি পুরোনো term-এর
- নিজের term ৫-এর একটি entry majority-তে পৌঁছানো পর্যন্ত অপেক্ষা করা
- সব ৫টি সার্ভারে পৌঁছানো পর্যন্ত অপেক্ষা করা
৫ সার্ভারের একটি Raft cluster-এ leader থেকে চার follower-এর RTT ২ ms, ৯ ms, ৩০ ms আর ৮৫ ms. fsync বাদ দিলে একটি commit-এ network-এর জন্য মোটামুটি কত সময় লাগে?
- ২ ms
- ৯ ms
- ৩০ ms
- ৮৫ ms
একজন Raft follower এমন AppendEntries পেল, যাতে prevLogIndex = ৭ আর prevLogTerm = ৪। Follower-এর log-এ index ৭-এ আছে term ৩-এর একটি entry. এরপর কী হবে?
- Follower নতুন entry-গুলো যোগ করবে, পুরোনোগুলো রেখেই
- Follower leader-কে নিজের index ৭ নিতে বলবে
- Follower candidate হয়ে নতুন নির্বাচন শুরু করবে
- Follower প্রত্যাখ্যান করবে, আর leader nextIndex কমিয়ে আবার পাঠাবে
Paxos-এ একজন acceptor আগে proposal number ৮-এ promise দিয়েছে। এখন সে Accept(n = ৫, v = "B") পেল। সে কী করবে?
- প্রত্যাখ্যান বা উপেক্ষা করবে, কারণ ৫ তার প্রতিশ্রুত ৮-এর চেয়ে ছোট
- গ্রহণ করবে, কারণ এখনো কোনো মান chosen হয়নি
- n = ৮ করে গ্রহণ করবে
- নিজে proposer হয়ে Prepare(৯) পাঠাবে
একটি Raft cluster-এর leader network partition-এ ছোট ভাগে পড়ে গেল; বড় ভাগে নতুন leader বাছা হয়ে লেখা চলছে। পুরোনো leader নিজের স্থানীয় state machine থেকে সরাসরি পড়া দিলে কী ঝুঁকি, আর কোন কৌশল তা ঠেকায়?
- সে নতুন লেখা commit করে ডেটা ভাগ করে ফেলবে; ঠেকায় fsync
- কোনো ঝুঁকি নেই, কারণ leader-এর কাছে সব committed entry থাকে; শুধু timeout বাড়ালেই হয়
- সে পুরোনো মান পড়াবে; ReadIndex-এ পড়ার আগে majority-র সাড়া নিয়ে leader-পদ নিশ্চিত করা ঠেকায়
- সে term বাড়িয়ে নতুন leader-কে সরিয়ে দেবে; ঠেকায় PreVote
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Consensus = agreement + validity + termination. Safety কখনো ছাড়া হয় না; liveness আসে network মোটামুটি ভালো থাকলে — FLP-এর বাস্তব উত্তর।
- Majority: Q = ⌊N/২⌋ + ১, f = ⌊(N − ১)/২⌋; দুই majority সব সময় মেলে। Cluster ৩ বা ৫, আলাদা failure domain-এ; জোড় সংখ্যায় লাভ নেই।
- Raft-এর নির্বাচন: random timeout, term এক বাড়ানো, এক term-এ এক ভোট, আর ভোট শুধু অন্তত সমান হালনাগাদ log-কে — তাই এক term-এ বড়জোর এক leader, আর তার কাছে সব committed entry.
- Log replication: prevLogIndex/prevLogTerm যাচাই, প্রত্যাখ্যানে nextIndex পেছানো, বিরোধী entry মুছে leader-এরটি; commit majority-তে, কিন্তু কপি গুনে শুধু নিজের term-এর entry.
- বাস্তবে: client id-তে একবারের বেশি নয়, ReadIndex বা সাবধানী lease-এ linearizable read, snapshot, একবারে একটি সদস্য বদল; প্রতিটি commit-এ majority-র round trip.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৩ · Coordination — distributed lock, lease আর fencing tokenপরবর্তী পর্বConsensus-এর ওপর lock, lease আর fencing token.
- পর্ব ২২ · Replication — failover-এর ভেতরেফিরে দেখুনFailover আর split-brain — এই পর্বের প্রশ্নের শুরু।
- পর্ব ২৫ · Distributed transaction — consensus-ভিত্তিক commitফিরে দেখুন2PC-এর blocking আর Paxos Commit.
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DBMS ও SQL · Consistency model, quorum আর consensusগভীরে যানডেটাবেসের দিক থেকে quorum, consensus আর distributed SQL.
- অপারেটিং সিস্টেম · Distributed OSপাশের ট্র্যাকOS-এর দিক থেকে কয়েক যন্ত্রের সমন্বয়।