Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ৯/১৯অ্যাডভান্সড৫৫ মিনিট

Paxos Intuition — consensus problem-এর প্রথম উত্তর

Paxos Intuition

Naive consensus attempt দুটো (first-wins, trust-the-leader) কংক্রিট scenario-তে ভেঙে দেখায় কেন majority-respect আর prior-discovery বাধ্যতামূলক — সেই দুই invariant থেকেই Basic Paxos-এর Prepare/Promise আর Accept/Accepted phase বেরিয়ে আসে, quorum-intersection যার safety-প্রমাণ।

এই লেসন শেষে আপনি পারবেন

  • Consensus problem-টা leader election থেকে ঠিক কোথায় আলাদা — কে সিদ্ধান্ত নেবে বনাম কোন value-তে একমত হতে হবে — এই পার্থক্য কংক্রিট উদাহরণ দিয়ে ব্যাখ্যা করতে পারবেন
  • First-value-wins আর trust-the-leader — এই দুই naive consensus attempt কংক্রিট scenario-তে ঠিক কোথায় ভাঙে তা trace করে, সেখান থেকে Paxos-এর দুইটা core invariant নিজে derive করতে পারবেন
  • Basic Paxos-এর Prepare/Promise (Phase 1) আর Accept/Accepted (Phase 2) ধাপ দুটো ballot number-সহ নির্ভুলভাবে বর্ণনা করতে পারবেন, আর কেন proposer-কে majority-র মধ্যে সর্বোচ্চ-ballot accepted value adopt করতে বাধ্য করা হয় তা ব্যাখ্যা করতে পারবেন
  • Majority quorum-এর intersection argument দিয়ে বলতে পারবেন কেন দুইটা ভিন্ন value কখনো একসাথে decided হতে পারে না, আর acceptor-এর stable storage হারালে ঠিক কোন safety guarantee ভাঙে তা identify করতে পারবেন
  • Dueling-proposer race হাতে-কলমে trace করে দেখাতে পারবেন কীভাবে পরে-আসা higher-ballot proposer আগের majority-accepted value discover করে নিজের value বাদ দিয়ে সেটাই adopt করতে বাধ্য হয়, আর Multi-Paxos কোন দিকে এগোয় (Raft-এর ভিত্তি) তা সংক্ষেপে বলতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসন শেষ হয়েছিল ঠিক এই প্রশ্নে দাঁড়িয়ে: election ঠিক করে কে সিদ্ধান্ত নেবে, কোন value-তে একমত হতে হবে তা নয়। ধরুন majority-elected নতুন leader — epoch 7, লিখতে প্রস্তুত। কিন্তু ঠিক কী লিখবে? পুরনো leader (epoch 6) হয়তো তার শেষ write half-propagated রেখে গেছে — কিছু acceptor দেখেছে, কিছু দেখেনি। নতুন leader-এর নিজের client-queue-তে বসে আছে আরেকটা, সম্পূর্ণ ভিন্ন value। দুইটার মধ্যে কোনটা “সঠিক”? এইখানেই election শেষ হয়, consensus শুরু হয়।

সংজ্ঞাটা সংক্ষিপ্ত: কয়েকজন process, প্রত্যেকে একটা value propose করে; সবাইকে ঠিক একটা value-তে, majority-র মাধ্যমে, স্থায়ীভাবে একমত হতে হবে — process crash করলেও, network message হারালে বা দেরি করলেও। এর ঐতিহাসিক উত্তরের নাম Paxos (Leslie Lamport, খসড়া ১৯৮৯, প্রকাশ ১৯৯৮) — আর protocol-টার reputation বিচিত্র: “সবাই বোঝে consensus দরকার; কেউ বোঝে না Paxos কেন কাজ করে।”

আজ আমরা উল্টো পথে হাঁটব — algorithm মুখস্থ না করে। দুইটা স্বাভাবিক কিন্তু ভুল সমাধান দিয়ে শুরু করব, দেখব ঠিক কোথায় ভাঙে — আর সেই ভাঙনের জায়গা থেকেই Paxos-এর নিয়মগুলো নিজে বেরিয়ে আসবে।

মূল ধারণা

Naive attempt ১ — “প্রথম যেটা পৌঁছায়, সেটাই জেতে”

সবচেয়ে সরল idea: প্রতিটা acceptor নিজের কাছে প্রথম যে value আসে সেটাতে lock করে ফেলে; পরে অন্য কেউ ভিন্ন value পাঠালে reject করে। যে value majority acceptor-এর lock পায়, সেটাই decided।

N=5 acceptor (B0..B4)। তিনজন proposer একসাথে race করছে — P1 চায় “X”, P2 চায় “Y”, P3 চায় “Z”। নেটওয়ার্ক jitter-এ কার message কার কাছে আগে পৌঁছায় তার কোনো guarantee নেই:

B0, B1  -> প্রথমে P1-এর message পায়  -> lock("X")
B2, B3  -> প্রথমে P2-এর message পায়  -> lock("Y")
B4      -> প্রথমে P3-এর message পায়  -> lock("Z")

ফলাফল: 2 vote X, 2 vote Y, 1 vote Z — কেউই majority (৩) পায় না।

সবচেয়ে খারাপ অংশ: “first-wins” মানে acceptor আর কখনো নিজের lock বদলাবে না। Retry করলেও প্রতিটা acceptor তার প্রথম-দেখা value-তেই আটকে থাকে; নতুন করে চেষ্টা করলে network-jitter হয়তো ভিন্ন split দেবে, কিন্তু কখনো ৩-এর ভাগ মিলবে তার কোনো নিশ্চয়তা নেই। এটা liveness নষ্ট করে — decision চিরকালের জন্য আটকে যেতে পারে। মূল সমস্যা: acceptor-রা independently সিদ্ধান্ত নিচ্ছে, কোনো global coordination নেই যা বলবে majority কোন দিকে ঝুঁকছে।

Naive attempt ২ — “সবসময় বর্তমান leader-এর value বিশ্বাস করো”

Leader election লেসন মনে করুন: elected leader-ই একমাত্র legit proposer, acceptor-রা leader থেকে যা আসে তা-ই accept করে — শুধু “current leader” trust, কোনো ballot-tracking ছাড়াই। সরল শোনায়: leader তো majority vote পেয়েই leader হয়েছে।

কিন্তু failure-models আর leader-election লেসনের সেই পুরনো ভূত ফিরে আসে — failure detector ভুল করতে পারে:

leader = L1 (epoch 6), acceptor B0..B4 সবাই L1-কে trust করছে
t1  GC pause -> কিছু acceptor L1-কে "dead" ভাবল -> নতুন election
    -> L2 জিতল (epoch 7)
t2  L1 আসলে জীবিত, নিজেকে এখনো leader ভাবছে -> propose("X")
    পাঠাল B0,B1,B2-কে
t3  L2 propose("Y") পাঠাল B2,B3,B4-কে (নিজের majority ভেবে)
t4  B2 দুটোই পেল -> কাকে বিশ্বাস করবে? "current leader" শব্দটার
    কোনো global অর্থ নেই — দুজনেই নিজেকে current ভাবছে

ফলাফল ঠিক leader-election লেসনের dueling-leaders — শুধু এবার “কে leader” নয়, “কোন value” প্রশ্নটাও দ্বিধাবিভক্ত। Epoch-fencing দিয়ে পুরনো leader-এর write আটকানো যায় (আগের লেসন), কিন্তু সেটাও যথেষ্ট না: এমনকি একজনমাত্র live leader থাকলেও, তার আগের round-এ কিছু acceptor আগে থেকেই অন্য value accept করে ফেলে থাকতে পারে — সেই আধা-লেখা history উপেক্ষা করাটাও একটা নিয়ম-ভাঙা।

দুই ব্যর্থতা থেকে দুই invariant

দুই attempt-ই একই root cause-এ ভুগছে: কোনো round (বা leader) জানে না অন্য কোনো round ইতিমধ্যে কিছু “প্রায়-decide” করে ফেলেছে কি না, আর জানলেও সেটা respect করার কোনো নিয়ম নেই। এখান থেকেই দুইটা invariant বেরিয়ে আসে — Paxos-এর পুরো ভিত্তি এই দুই লাইনে:

Invariant I  : কোনো round-এ majority acceptor একবার value V accept
               করে ফেললে, তার পরের প্রতিটা round যদি সেটা জানতে
               পারে, তাকে V-ই propose করতে বাধ্য থাকতে হবে —
               নিজের value ভুলে যেতে হবে।
Invariant II : কোনো round free-এ propose করার আগে অবশ্যই একটা
               majority-কে জিজ্ঞেস করে জেনে নিতে হবে — "তোমাদের
               মধ্যে কেউ কি আগে থেকেই কিছু accept করে ফেলেছ?"

Invariant II আসলে Invariant I বাস্তবায়নের যন্ত্র — না জেনে মানা যায় না। দুইটা একসাথে attempt ১-এর সমস্যা মেটায় (independent lock নয়, majority-জ্ঞান বাধ্যতামূলক ভ্রমণ করে) আর attempt ২-র সমস্যাও (শুধু leader-trust যথেষ্ট না, প্রতিটা round-কে নিজে discover করতে হয় আগে কী হয়েছে)।

Basic Paxos — দুই phase, precise

Invariant II বাস্তবায়ন করে Phase 1; Invariant I বাস্তবায়ন করে Phase 2-র adopt-rule।

প্রতিটা proposal-এর একটা ballot number n থাকে — প্রতিটা proposer নিজের আগের ব্যবহারের তুলনায় strictly বড়, আর দুই proposer কখনো একই n বাছবে না (globally unique)। সাধারণ trick: n = round_counter * proposer_count + proposer_id — round বাড়লে n বাড়ে, আর proposer-id-র কারণে দুইজনের n কখনো সংঘর্ষ করে না।

Phase 1 — Prepare / Promise (discovery):

proposer  : নিজের ব্যবহৃত যেকোনো আগের ballot-এর চেয়ে বড় n বাছে,
            majority acceptor-কে Prepare(n) পাঠায়
acceptor  : n যদি এ পর্যন্ত promise-করা সর্বোচ্চ ballot-এর চেয়ে
            বড় হয় -> promise করে (আর কখনো n-এর চেয়ে ছোট কিছু
            accept করবে না), ফেরত পাঠায় তার এখন পর্যন্ত accept-করা
            সর্বোচ্চ-ballot (n_a, v_a) — কিছু accept না করে থাকলে null
            n বড় না হলে -> reject/ignore (stale proposer)

Phase 2 — Accept / Accepted (commitment):

proposer  : majority-র promise পেলে —
            কোনো promise-ই আগের accepted value রিপোর্ট করেনি
              -> নিজের value v প্রস্তাব করতে স্বাধীন
            অন্তত একটা promise আগের accepted value রিপোর্ট করেছে
              -> সেই promise-গুলোর মধ্যে সর্বোচ্চ-ballot value-টাই
                 (নিজেরটা না!) adopt করতে বাধ্য — এটাই safety-র
                 মূল নিয়ম
            adopt/own value নিয়ে Accept(n, v) পাঠায় majority-কে
acceptor  : n তার promise-করা সর্বোচ্চ ballot-এর চেয়ে ছোট না হলে
            accept করে, (n, v) নিজের accepted-state হিসেবে persist
            করে, Accepted(n, v) ফেরত পাঠায়
            majority accept করলে v decided

“সর্বোচ্চ-ballot value” শর্তটা এড়ানো যায় না — proposer যদি শুধু কোনো একটা accepted value adopt করত (সবচেয়ে বড় ballot-এরটা না), তাহলে দুইটা ভিন্ন majority দুইটা ভিন্ন value-তে decide করে ফেলতে পারত। “সর্বোচ্চ” শব্দটাই Invariant I-কে গণিতে বাঁধে।

ভেতরে কী ঘটছে

Quorum intersection — কেন ঠিক এইটাই safety দেয়

N acceptor-এর মধ্যে যেকোনো দুই majority set (আকার > N/2) নিশ্চিতভাবে অন্তত একটা সদস্যে ছেদ করে — pigeonhole, quorum-replication লেসনের সেই একই যুক্তি। ধরুন round n1-এ majority M1 value V accept করেছে; পরের round n2 > n1-এ কোনো নতুন proposer Phase 1 চালালে সে যে majority M2-কে জিজ্ঞেস করবে, M1 ∩ M2 অন্তত একজন — আর সেই ছেদ-বিন্দুর acceptor তার Promise-এ অবশ্যই (n1, V) রিপোর্ট করবে, কারণ সে V ইতিমধ্যে accept করে রেখেছে। তাই নতুন proposer বাধ্যতামূলকভাবে V আবিষ্কার করবে — miss করার কোনো উপায় নেই। Invariant II মানেই “discovery guaranteed” — আর guarantee-টা আসে quorum-overlap থেকে, কোনো central registry থেকে না।

Stable storage — কেন শুধু memory-তে রাখলেই চলত না

Acceptor-এর promised ballot আর accepted (n, v) — এই দুইটা crash-এ হারিয়ে গিয়ে acceptor “কিছুই মনে নেই” অবস্থায় ফিরে এলে, সে আবার পুরনো, ইতিমধ্যে-superseded ballot-এ promise/accept দিতে পারে — আর সেটা সরাসরি quorum-intersection argument-টাই ভেঙে দেয় (ছেদ-বিন্দুর acceptor আর নির্ভরযোগ্য সাক্ষী থাকে না)। এজন্যই acceptor-এর promise/accept ডিস্কে persist করা বাধ্যতামূলক, প্রতিটা reply পাঠানোর আগে — এটা liveness-এর সমস্যা না (শুধু ধীর হওয়া), সরাসরি safety-র সমস্যা: দুই ভিন্ন value একসাথে decide হয়ে যাওয়া।

Round lifecycle — এক নজরে

Paxos round lifecycle — Prepare থেকে Decided পর্যন্ত
  1. ballot pickproposer নিজের আগের ব্যবহারের চেয়ে বড় n বাছে (globally unique scheme)
  2. phase 1 broadcastmajority acceptor-কে Prepare(n); persist-এর পরেই Promise reply
  3. discovery checkকোনো promise-এ আগের accepted value থাকলে সর্বোচ্চ-ballot-টা বাধ্যতামূলক adopt
  4. phase 2 broadcastmajority-কে Accept(n,v); persist-এর পরেই Accepted reply
  5. decidedmajority Accepted পেলে v decided — stale n থাকলে acceptor silently reject করে

খরচের হিসাব

ধাপmessage (প্রতি round)round-trip
Phase 12 × majority-size (Prepare + Promise)১ RTT
Phase 22 × majority-size (Accept + Accepted)১ RTT
dueling roundপ্রতি নতুন higher-ballot round-এ আবার দুই phase লাগেRTT বাড়ে, decision দেরি হয় — কিন্তু কখনো ভুল হয় না

শেষ row-টাই Paxos-এর কুখ্যাত liveness-দুর্বলতা: দুই proposer যদি বারবার একে অপরকে higher ballot দিয়ে interrupt করতে থাকে, decision চিরকাল পিছিয়ে যেতে পারে (safety টিকে থাকে, শুধু কেউ কখনো জেতে না)। বাস্তবে এটা এড়াতে একজন stable proposer (leader)-এর ধারণা লাগে — realworld অংশে ফিরে আসছি।

উদাহরণ

Full scenario — dueling proposers, হাতে-কলমে trace

৩টা acceptor (A0, A1, A2), majority = ২। Proposer P1 চায় value “X”, proposer P2 (P1-এর কথা না জেনেই) চায় value “Y”। Ballot: P1-এর n=11, P2-এর n=22 (ইচ্ছাকৃতভাবে বড়)।

tইভেন্টফলাফল
t1P1: Prepare(11) → A0, A1, A2সবাই promise করে (কারো কাছে prior accepted value নেই)
t2P1 majority-promise পেল, prior value কোথাও নেইনিজের “X” propose করার স্বাধীনতা
t3P1: Accept(11,"X") → শুধু A0-এ পৌঁছাল (A1,A2-এর message নেটওয়ার্কে in-flight)A0 accepted (11,"X")
t4P2 (P1-কে না জেনে): Prepare(22) → A0, A1, A2A0 রিপোর্ট করে (n=11,v="X"); A1,A2 রিপোর্ট করে কিছুই নেই
t5P2 majority-promise পেল; A0-এর রিপোর্টই সর্বোচ্চ-ballot accepted valueP2 বাধ্য — নিজের “Y” ফেলে “X” adopt করে
t6P2: Accept(22,"X") → A0, A1, A2 সবাই accept করে (22 তাদের promise-এর সমান-বড়)সবাই accepted (22,"X") — DECIDED “X”
t7P1-এর দেরি-হওয়া Accept(11,"X") অবশেষে A1, A2-এ পৌঁছাল11 < promised(22) → REJECTED, কোনো ক্ষতি নেই (মান তো “X”-ই ছিল)

লক্ষ্য করুন: P2 ballot-race জিতেছে, কিন্তু নিজের value চাপাতে পারেনি — Invariant I-এর জোরে A0-এর মধ্য দিয়ে P1-এর আধা-committed “X” প্রবাহিত হয়ে P2-এর decision-এও ঢুকে গেছে। t7-এ দেখুন: late message নিরাপদে reject হলো, কোনো conflicting accept ঘটল না — stale ballot-এর বিরুদ্ধে acceptor-এর নিজস্ব প্রতিরক্ষা।

ছবিতে

 proposer                       acceptors (majority)
    │--- Prepare(n) --------------->│  │  │
    │<-- Promise(n, n_a, v_a) ------│  │  │   (n_a,v_a = prior accepted, বা null)
    │
    │  [majority reached: কোনো promise-এ prior value থাকলে
    │   সর্বোচ্চ-ballot v_a adopt করো, নইলে নিজের v]
    │
    │--- Accept(n, v) -------------->│  │  │
    │<-- Accepted(n, v) -------------│  │  │
    │
    │  majority Accepted -> DECIDED v
Phase 1 → Phase 2 message flow — Prepare/Promise discovery-র পরে Accept/Accepted commitment; মাঝখানের সিদ্ধান্তই Invariant I বাস্তবায়ন করে।
      N = {A0, A1, A2, A3, A4}
    M1 (round n1, accepted V) = {A0, A1, A2}
    M2 (round n2>n1, Prepare) = {A2, A3, A4}
                                       ▲
                                       │ ছেদ = {A2}
     A2-ই M2-কে বাধ্য করে V আবিষ্কার করতে — pigeonhole guarantee,
     কাকতালীয় ঘটনা নয়; N যত বড়ই হোক, majority-র সংজ্ঞাই ছেদ কিনে দেয়
Quorum intersection — round n1-এ accept-করা majority M1 আর পরের round n2-এর Prepare-majority M2 অন্তত এক বিন্দুতে ছেদ করে; সেই বিন্দুই discovery-র গ্যারান্টি।

নিজে চালিয়ে দেখুন

EXPERIMENT

Dueling proposers — ৩ acceptor, ২ proposer, বেশি ballot কে জেতে কিন্তু কী adopt করে

macOS / Linux / Windows (Python 3)· ১৫ মিনিট
# paxos_duel.py -- Basic Paxos: dueling proposers race, 3 acceptors
class Acceptor:
    def __init__(self, name):
        self.name = name
        self.promised = 0
        self.accepted_n = 0
        self.accepted_v = None

    def prepare(self, n):
        if n > self.promised:
            self.promised = n
            return True, self.accepted_n, self.accepted_v
        return False, self.accepted_n, self.accepted_v

    def accept(self, n, v):
        if n >= self.promised:
            self.promised = n
            self.accepted_n = n
            self.accepted_v = v
            return True
        return False


A0, A1, A2 = Acceptor("A0"), Acceptor("A1"), Acceptor("A2")
acceptors = [A0, A1, A2]
MAJORITY = len(acceptors) // 2 + 1

print(f"acceptors={[a.name for a in acceptors]}  majority-size={MAJORITY}")

print("\n--- proposer P1: ballot n=11, value='X' ---")
promises = []
for a in acceptors:
    ok, an, av = a.prepare(11)
    print(f"  Prepare(11) -> {a.name}: PROMISE (had accepted n={an}, v={av!r})")
    promises.append((a, an, av))
best_n, best_v = max((p[1], p[2]) for p in promises)
if best_n > 0:
    chosen = best_v
    print(f"  majority promised ({len(promises)}/{len(acceptors)}); "
          f"highest prior accepted n={best_n} v={best_v!r} -> MUST adopt {chosen!r}")
else:
    chosen = "X"
    print(f"  majority promised ({len(promises)}/{len(acceptors)}); "
          f"no prior accepted value anywhere -> free to propose own {chosen!r}")

print("  Accept(11, 'X') -> A0: ", end="")
ok = A0.accept(11, chosen)
print("ACCEPTED" if ok else "REJECTED")
print("  Accept(11, 'X') -> A1, A2: IN-FLIGHT (network delay, not yet delivered)")

print("\n--- proposer P2: ballot n=22, value='Y' (unaware of P1) ---")
promises2 = []
for a in acceptors:
    ok, an, av = a.prepare(22)
    print(f"  Prepare(22) -> {a.name}: PROMISE (had accepted n={an}, v={av!r})")
    promises2.append((a, an, av))
best_n2, best_v2 = max((p[1], p[2]) for p in promises2)
if best_n2 > 0:
    chosen2 = best_v2
    print(f"  majority promised ({len(promises2)}/{len(acceptors)}); "
          f"highest prior accepted n={best_n2} v={best_v2!r} -> MUST adopt {chosen2!r} (own 'Y' discarded)")
else:
    chosen2 = "Y"
    print(f"  majority promised ({len(promises2)}/{len(acceptors)}); "
          f"no prior accepted value anywhere -> free to propose own {chosen2!r}")

accepted_by = []
for a in acceptors:
    ok = a.accept(22, chosen2)
    print(f"  Accept(22, {chosen2!r}) -> {a.name}: {'ACCEPTED' if ok else 'REJECTED'}")
    if ok:
        accepted_by.append(a.name)
print(f"  {len(accepted_by)}/{len(acceptors)} accepted -> "
      f"{'** DECIDED value=' + repr(chosen2) + ' at n=22 **' if len(accepted_by) >= MAJORITY else 'no decision'}")

print("\n--- P1's delayed Accept(11,'X') finally arrives at A1, A2 ---")
for a in (A1, A2):
    ok = a.accept(11, "X")
    print(f"  Accept(11,'X') -> {a.name}: {'ACCEPTED' if ok else 'REJECTED'} "
          f"(promised={a.promised})")

print(f"\nfinal state: " + ", ".join(f"{a.name}(n={a.accepted_n},v={a.accepted_v!r})" for a in acceptors))

প্রত্যাশিত output (deterministic):

acceptors=['A0', 'A1', 'A2']  majority-size=2

--- proposer P1: ballot n=11, value='X' ---
  Prepare(11) -> A0: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> A1: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> A2: PROMISE (had accepted n=0, v=None)
  majority promised (3/3); no prior accepted value anywhere -> free to propose own 'X'
  Accept(11, 'X') -> A0: ACCEPTED
  Accept(11, 'X') -> A1, A2: IN-FLIGHT (network delay, not yet delivered)

--- proposer P2: ballot n=22, value='Y' (unaware of P1) ---
  Prepare(22) -> A0: PROMISE (had accepted n=11, v='X')
  Prepare(22) -> A1: PROMISE (had accepted n=0, v=None)
  Prepare(22) -> A2: PROMISE (had accepted n=0, v=None)
  majority promised (3/3); highest prior accepted n=11 v='X' -> MUST adopt 'X' (own 'Y' discarded)
  Accept(22, 'X') -> A0: ACCEPTED
  Accept(22, 'X') -> A1: ACCEPTED
  Accept(22, 'X') -> A2: ACCEPTED
  3/3 accepted -> ** DECIDED value='X' at n=22 **

--- P1's delayed Accept(11,'X') finally arrives at A1, A2 ---
  Accept(11,'X') -> A1: REJECTED (promised=22)
  Accept(11,'X') -> A2: REJECTED (promised=22)

final state: A0(n=22,v='X'), A1(n=22,v='X'), A2(n=22,v='X')

পড়ার সময় তিনটা লক্ষ্য: P1-এর Phase 1 আর প্রথম Accept সফল হলো নীরবে; P2 উঁচু ballot নিয়ে ঢুকে A0-এর promise-এ P1-এর কাজ দেখে ফেলল আর নিজের “Y” বাদ দিয়ে “X”-ই পাঠাল; শেষে P1-এর দেরি-হওয়া message নিরাপদে বাতিল হলো — কারণ ততক্ষণে সব acceptor উঁচু ballot-এ promise দিয়ে ফেলেছে। কোনো মুহূর্তেই দুইটা ভিন্ন value কোথাও decide হয়নি।

এটা কী প্রমাণ করে

P2-র বেশি ballot P1-কে race-এ হারায়, কিন্তু P1-এর আগে-থেকে accept হওয়া value discover করে P2 নিজের value ফেলে সেটাই propose করতে বাধ্য হয় — Invariant I বাস্তবে কার্যকর হতে দেখায়; late/stale Accept message safely reject হয়, কোনো conflicting decision ঘটে না।

নিজে বানান

BUILD IT

PaxosCluster — N=5 acceptor, ৩ প্রতিযোগী proposer, বারবার duel সহ্য করেও এক decision-এ convergence

Python · ●●●●○
  1. Acceptor ক্লাস: promised/accepted_n/accepted_v state, prepare() আর accept() হ্যান্ডলার
  2. Proposer ক্লাস: phase-1 broadcast, majority-promise-check, adopt-highest-ballot rule, phase-2 broadcast
  3. Driver: P1 phase-2 আংশিক পৌঁছানো অবস্থায় P2 উঁচু ballot নিয়ে ঢোকায়, P2 সফল হওয়ার পরও P3 আরও উঁচু ballot নিয়ে ঢোকায়
  4. প্রতিটা Prepare/Accept-এর ফলাফল print করুন — কে কী discover করল, কী adopt করতে বাধ্য হলো
  5. শেষে assert করুন সব acceptor-এর accepted_v একই — safety হাতে-কলমে verify

লক্ষ্য: experiment-এর ৩-acceptor duel-কে বড় cluster-এ, তিনজন প্রতিযোগী proposer-সহ generalize করা — দেখানো যে প্রতিটা নতুন, উঁচু-ballot proposer বারবার একই আগের value discover করে সেটাই পুনরায় adopt করে, কখনো ভিন্ন কিছুতে decide হয় না।

# paxos_cluster.py -- PaxosCluster: N=5 acceptors, 3 competing proposers
class Acceptor:
    def __init__(self, name):
        self.name = name
        self.promised = 0
        self.accepted_n = 0
        self.accepted_v = None

    def prepare(self, n):
        if n > self.promised:
            self.promised = n
            return True, self.accepted_n, self.accepted_v
        return False, self.accepted_n, self.accepted_v

    def accept(self, n, v):
        if n >= self.promised:
            self.promised = n
            self.accepted_n = n
            self.accepted_v = v
            return True
        return False


class Proposer:
    def __init__(self, pid, acceptors, majority):
        self.pid = pid
        self.acceptors = acceptors
        self.majority = majority

    def propose(self, n, value, phase1_targets, phase2_targets):
        print(f"--- proposer P{self.pid}: ballot n={n}, own value={value!r} ---")
        promises = []
        for a in phase1_targets:
            ok, an, av = a.prepare(n)
            print(f"  Prepare({n}) -> {a.name}: PROMISE (had accepted n={an}, v={av!r})")
            promises.append((an, av))
        if len(promises) < self.majority:
            print(f"  only {len(promises)}/{self.majority} promises -> no majority, retry later")
            return None
        best_n, best_v = max(promises)
        if best_n > 0:
            chosen = best_v
            print(f"  {len(promises)} promises, majority reached; highest prior accepted "
                  f"n={best_n} v={best_v!r} -> MUST adopt {chosen!r} (own {value!r} discarded)")
        else:
            chosen = value
            print(f"  {len(promises)} promises, majority reached; no prior accepted value "
                  f"anywhere -> free to propose own {chosen!r}")
        accepted_by = 0
        for a in phase2_targets:
            ok = a.accept(n, chosen)
            print(f"  Accept({n},{chosen!r}) -> {a.name}: {'ACCEPTED' if ok else 'REJECTED'}")
            if ok:
                accepted_by += 1
        if accepted_by >= self.majority:
            print(f"  {accepted_by}/{len(phase2_targets)} accepted -> "
                  f"** DECIDED value={chosen!r} at n={n} **")
            return chosen
        print(f"  only {accepted_by}/{len(phase2_targets)} accepted -> no decision this round")
        return None


B = [Acceptor(f"B{i}") for i in range(5)]
MAJORITY = len(B) // 2 + 1
print(f"acceptors={[b.name for b in B]}  majority-size={MAJORITY}")
print()

p1 = Proposer(1, B, MAJORITY)
p2 = Proposer(2, B, MAJORITY)
p3 = Proposer(3, B, MAJORITY)

# Round 1: P1 phase1 reaches all 5, phase2 only reaches B0 (rest in-flight)
p1.propose(11, "X", phase1_targets=B, phase2_targets=[B[0]])
print()

# P2 races in with a higher ballot before P1's remaining Accept messages land
p2.propose(12, "Y", phase1_targets=B, phase2_targets=B)
print()

# P3 arrives even later, still tries its own value
p3.propose(13, "Z", phase1_targets=B, phase2_targets=B)
print()

print("--- P1's delayed Accept(11,'X') finally arrives at B1..B4 ---")
for a in B[1:]:
    ok = a.accept(11, "X")
    print(f"  Accept(11,'X') -> {a.name}: {'ACCEPTED' if ok else 'REJECTED'} (promised={a.promised})")

print()
print("final state: " + ", ".join(f"{a.name}(n={a.accepted_n},v={a.accepted_v!r})" for a in B))
values = {a.accepted_v for a in B}
print(f"single decided value across cluster: {values == {'X'}}")

প্রত্যাশিত output (deterministic):

acceptors=['B0', 'B1', 'B2', 'B3', 'B4']  majority-size=3

--- proposer P1: ballot n=11, own value='X' ---
  Prepare(11) -> B0: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> B1: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> B2: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> B3: PROMISE (had accepted n=0, v=None)
  Prepare(11) -> B4: PROMISE (had accepted n=0, v=None)
  5 promises, majority reached; no prior accepted value anywhere -> free to propose own 'X'
  Accept(11,'X') -> B0: ACCEPTED
  only 1/1 accepted -> no decision this round

--- proposer P2: ballot n=12, own value='Y' ---
  Prepare(12) -> B0: PROMISE (had accepted n=11, v='X')
  Prepare(12) -> B1: PROMISE (had accepted n=0, v=None)
  Prepare(12) -> B2: PROMISE (had accepted n=0, v=None)
  Prepare(12) -> B3: PROMISE (had accepted n=0, v=None)
  Prepare(12) -> B4: PROMISE (had accepted n=0, v=None)
  5 promises, majority reached; highest prior accepted n=11 v='X' -> MUST adopt 'X' (own 'Y' discarded)
  Accept(12,'X') -> B0: ACCEPTED
  Accept(12,'X') -> B1: ACCEPTED
  Accept(12,'X') -> B2: ACCEPTED
  Accept(12,'X') -> B3: ACCEPTED
  Accept(12,'X') -> B4: ACCEPTED
  5/5 accepted -> ** DECIDED value='X' at n=12 **

--- proposer P3: ballot n=13, own value='Z' ---
  Prepare(13) -> B0: PROMISE (had accepted n=12, v='X')
  Prepare(13) -> B1: PROMISE (had accepted n=12, v='X')
  Prepare(13) -> B2: PROMISE (had accepted n=12, v='X')
  Prepare(13) -> B3: PROMISE (had accepted n=12, v='X')
  Prepare(13) -> B4: PROMISE (had accepted n=12, v='X')
  5 promises, majority reached; highest prior accepted n=12 v='X' -> MUST adopt 'X' (own 'Z' discarded)
  Accept(13,'X') -> B0: ACCEPTED
  Accept(13,'X') -> B1: ACCEPTED
  Accept(13,'X') -> B2: ACCEPTED
  Accept(13,'X') -> B3: ACCEPTED
  Accept(13,'X') -> B4: ACCEPTED
  5/5 accepted -> ** DECIDED value='X' at n=13 **

--- P1's delayed Accept(11,'X') finally arrives at B1..B4 ---
  Accept(11,'X') -> B1: REJECTED (promised=13)
  Accept(11,'X') -> B2: REJECTED (promised=13)
  Accept(11,'X') -> B3: REJECTED (promised=13)
  Accept(11,'X') -> B4: REJECTED (promised=13)

final state: B0(n=13,v='X'), B1(n=13,v='X'), B2(n=13,v='X'), B3(n=13,v='X'), B4(n=13,v='X')
single decided value across cluster: True

তিনটা পাঠ: P1-এর প্রথম round শুধু ১টা acceptor-এ পৌঁছে থেমে গেল — majority ছাড়া কোনো decision নেই, এমনকি প্রস্তাবকারী নিজেও জিতল না। P2 উঁচু ballot নিয়ে ঢুকে B0-এর মধ্য দিয়ে “X” discover করল, নিজের “Y” বাদ দিয়ে সেটাই পুরো cluster-এ ছড়িয়ে দিল — এবং সেটাই decided হলো। P3 আরও পরে, আরও উঁচু ballot নিয়ে এসেও নতুন কিছু চাপাতে পারল না — এতক্ষণে সবাই “X” accept করে বসে আছে, তাই P3-ও বাধ্য হয়ে “X”-ই আবার propose করল। তিনজন প্রতিযোগী, তিনটা আলাদা value-র ইচ্ছা, তবু cluster-জুড়ে একটাই সত্য টিকে থাকল।

নিজে বাড়ান:

  1. একটা চতুর্থ proposer যোগ করুন যেটা সবার আগে শুরু করে কিন্তু phase-2 message সবচেয়ে দেরিতে পাঠায় — দেখুন race-এর ক্রম বদলালে decided value বদলায় কি না
  2. Stale-ballot Prepare-এর জন্য explicit REJECT branch যোগ করুন — এখন কোড শুধু condition-fail হলে চুপ থাকে, বাস্তব system-এ NACK পাঠানো হয় (দ্রুত retry-র জন্য)
  3. N=7 cluster-এ minority (৩টা) স্থায়ীভাবে বন্ধ করে দেখুন decision তবু হয় কি না; majority-র নিচে (৪-এর কম জীবিত) নামলে কী ঘটে

বাস্তব সিস্টেমে

Chubby (Google) — reputation-এর জন্মস্থান। Google-এর distributed lock service Chubby ছিল প্রথম বড় production Paxos deployment। Chubby-র architect Mike Burrows-এর মুখেই সেই বিখ্যাত পর্যবেক্ষণ — Paxos-এর কাগজে-লেখা বর্ণনা আর বাস্তব production system-এর চাহিদার মধ্যে বিস্তর ফারাক — যা সংক্ষেপে ছড়িয়েছে ঠিক এই লেসনের intro-র সেই উক্তিতে।

Paxos Made Live — engineering-এর দাম। Basic Paxos আজ আমরা দেখলাম একটা single value নিয়ে; production system-এর দরকার ধারাবাহিক log — হাজারো value পরপর decide করা (write-ahead-log-এর মতো)। প্রতিটা log-entry-র জন্য গোটা Phase 1 আবার চালালে প্রতি write-এ অতিরিক্ত round-trip, throughput তলানিতে। Google-এর টিম disk corruption, snapshotting, group-membership change, epoch-change সামলাতে গিয়ে দেখেছিল — মূল paper-এ এসবের কোনো রেসিপি নেই, নিজেদেরই বানাতে হয়েছিল।

Multi-Paxos — সংক্ষিপ্ত ইঙ্গিত, implement নয়। সমাধানের মূল ধারণা সহজ: একজন proposer যদি stable leader হয়ে যায় (যেমন leader-election লেসনের মতো elected, বা শুধু “সর্বশেষ successful ballot-holder”), তাহলে প্রতিটা নতুন value-র জন্য Phase 1 আর চালাতেই হয় না — একবার Phase 1 জিতলে, একই ballot-এর নিচে ধারাবাহিক Phase 2-গুলো (Accept-only) দিয়ে পুরো log ভরে ফেলা যায়। এটাই Multi-Paxos — consensus-কে replicated log-এ বদলে দেয়, আর leader না বদলালে প্রতি entry-তে মাত্র এক round-trip লাগে। কিন্তু এই লেসনে আমরা Multi-Paxos implement করছি না — leader-change, log-gap filling, snapshot-truncation-এর নিয়মগুলো নিজেই একটা আলাদা প্রোটোকলের মাপের জটিলতা, আর সেটাই ঠিক পরের লেসনের কাজ, একটা ভিন্ন, বেশি explainable প্রোটোকলের অধীনে।

যে ভুলগুলো সবাই করে

“Paxos মানেই একটা বিশাল জটিল algorithm — পুরোটা মুখস্থ করতে হবে।”

Basic Paxos আসলে মাত্র দুই invariant-এর যন্ত্রায়ন — majority-accepted value পরের round-কে বাধ্য করে, আর প্রতিটা round আগে discover করে নেয় কী accept হয়ে গেছে। যে জটিলতার ভয় মানুষ পায়, সেটা সাধারণত Multi-Paxos-এর production-ইঞ্জিনিয়ারিং (log-gap, leader-change, snapshot) — মূল দুই-phase algorithm নিজে ছোট।

“যে proposer ballot-race জেতে, সে-ই তার নিজের value চাপিয়ে দিতে পারে।”

Example-এর t5 ঠিক এই ভুল ভাঙে: P2 উঁচু ballot নিয়ে race জিতেছে, কিন্তু জেতাটা শুধু propose করার অনুমতি দেয়, কী propose করবে তা নয়। Majority-র কেউ আগে থেকে কিছু accept করে থাকলে, race-জয়ী proposer-কেও সেটাই মানতে হয় — নিজের ইচ্ছা প্রাসঙ্গিক না।

“Prepare(n)-এ majority সাড়া দিলেই মানে value decided হয়ে গেছে।”

Phase 1-এর majority promise শুধু “propose করার অনুমতি” দেয় — decision ঘটে কেবল Phase 2-এর majority accept-এ। দুইটা আলাদা majority-round লাগে; শুধু Prepare-এ সাড়া পাওয়া মানে এখনো কিছুই স্থায়ী হয়নি, Phase 2 ব্যর্থ হলে (build-এর P1-এর প্রথম round দেখুন) কোনো decision-ই ঘটে না।

“Acceptor-এর state শুধু memory-তে রাখলেই চলবে — দ্রুতও হবে।”

Crash-এ memory হারালে acceptor আবার পুরনো, ইতিমধ্যে-superseded ballot-এ promise/accept দিতে পারে — যেটা সরাসরি quorum-intersection argument ভেঙে দেয়। এটা কোনো performance-সমস্যা না, সরাসরি safety-সমস্যা: দুই ভিন্ন value একসাথে decide হয়ে যাওয়ার দরজা খুলে যায়। Stable storage তাই optional optimization না, invariant-এর পূর্বশর্ত।

বুঝেছেন কি না দেখুন

1

Consensus problem-এর সংজ্ঞা এক বাক্যে বলুন — leader election-এর সাথে পার্থক্য ঠিক কোথায়?

স্মরণ

Consensus: কয়েকজন process, প্রত্যেকে একটা value propose করে, সবাইকে majority-র মাধ্যমে ঠিক একটা value-তে স্থায়ীভাবে একমত হতে হবে, crash/message-loss সহ্য করেও। Election ঠিক করে কে decide করবে; consensus ঠিক করে কোন value-তে একমত হবে — সম্পূর্ণ আলাদা প্রশ্ন।

2

Naive attempt ১ (first-value-wins)-এ কেন liveness ভাঙে — শুধু একবারের bad luck, নাকি structural সমস্যা?

যুক্তি

Structural। Acceptor-রা independently lock করে, retry করলেও ভিন্ন network-jitter ভিন্ন split দিতে পারে — কোনো global mechanism নেই যা majority-র দিকে ঠেলে দেয়। তাই decision চিরকালের জন্য আটকে যাওয়া সম্ভব, কোনো একটা রাউন্ডের দুর্ভাগ্য না।

3

Example-এর t5-এ P2 কেন নিজের “Y” ফেলে “X” adopt করল — ঠিক কোন rule বাধ্য করল?

প্রয়োগ

Phase 2-র adopt rule। A0-এর promise-এ prior accepted (n=11,v="X") রিপোর্ট হয়েছিল — promise-গুলোর মধ্যে এটাই সর্বোচ্চ-ballot accepted value, তাই P2 বাধ্যতামূলকভাবে সেটাই adopt করে, নিজের value বিবেচনার বাইরে চলে যায়।

4

Quorum intersection argument দিয়ে বলুন কেন দুইটা ভিন্ন value কখনো একসাথে decide হতে পারে না।

যুক্তি

যেকোনো দুই majority set অন্তত এক acceptor-এ ছেদ করে (pigeonhole)। সেই ছেদ-বিন্দুর acceptor একই সময়ে দুই ভিন্ন value accept করে রাখতে পারে না — উঁচু ballot-এর promise আগের সব ছোট-ballot accept বন্ধ করে দেয়। তাই যেকোনো পরের round বাধ্য হয়ে আগের majority-accepted value-ই discover করবে ও পুনরায় propose করবে।

5

৭-acceptor cluster-এ কতজন একসাথে স্থায়ীভাবে crash সহ্য করা যায়, decision থেমে না গিয়ে? Reasoning দিন।

ডিজাইন

Majority দরকার ⌈7/2⌉ = 4, তাই বাকি 7 − 4 = 3 জন crash সহ্য করা যায় (N = 2f+1 প্যাটার্ন, f=3)। তার বেশি crash হলে ৪-জনের majority পাওয়াই অসম্ভব — liveness আটকে যায় (safety তবু ভাঙে না, শুধু কোনো decision হয় না)।

6

Multi-Paxos কীভাবে “প্রতি value-এ পূর্ণ Phase 1+2” খরচ কমায়, আর কোন নতুন জটিলতা আনে — যে কারণে এই লেসনে সেটা implement করা হয়নি?

যুক্তি

Stable leader একবার Phase 1 জিতলে, একই ballot-এর নিচে বাকি সব log-entry-তে শুধু Phase 2 (Accept-only) লাগে — প্রতি entry-তে ১ round-trip। নতুন জটিলতা: leader-change-এ নতুন Phase 1, log-এর gap-fill, snapshot-truncation — এগুলো নিজেই আলাদা প্রোটোকল-স্তরের কাজ, তাই পরের লেসনের বিষয়।

এরপর কী

এরপর কী

আজ invariant দুইটা দেখেছি, Basic Paxos-এর দুই phase হাতে-কলমে trace করেছি, আর দেখেছি dueling proposers-এও safety কীভাবে টিকে থাকে। কিন্তু সৎ স্বীকারোক্তি: protocol-টা কাজ করে ঠিকই, কিন্তু ব্যাখ্যা করা কঠিন — কোন state কোথায় কেন থাকে তা বোঝা, নতুন engineer-কে শেখানো, Multi-Paxos-এ log-gap আর leader-change সামলানো — সবকিছু বেশ ধোঁয়াশে থেকে যায়। ঠিক এই ফাঁক পূরণ করতেই Diego Ongaro আর John Ousterhout ২০১৪ সালে লিখেছিলেন Raft — শিরোনামেই লক্ষ্য ঘোষণা করে: “In Search of an Understandable Consensus Algorithm.”

Raft একই safety guarantee দেয় — একই majority-quorum invariant দুটো, একই CP trade-off — কিন্তু ইচ্ছাকৃতভাবে decompose করা: strong leader (একজনই লেখে, বাকিরা follow করে), randomized-timeout election (leader-election লেসনেরই বিস্তৃত রূপ), আর log-replication যা explicit log-consistency check দিয়ে Multi-Paxos-এর ধোঁয়াশে অংশগুলো transparent করে। পরের লেসনে ঠিক সেই decomposition-টাই দেখব — term, log-matching, election safety — একই গন্তব্য, ভিন্ন কিন্তু বোধগম্য রাস্তা।

আরও পড়ুন

  • The Part-Time Parliament — Leslie Lamport — ACM Transactions on Computer Systems 16(2), 1998 · Paxos-এর মূল paper — allegorical Greek-parliament framing-এর কারণে বহু বছর প্রায় কেউ পড়েইনি
  • Paxos Made Simple — Leslie Lamport — ACM SIGACT News 32(4), 2001 · একই protocol, allegory ছাড়া বলা — এই লেসনের Phase 1/Phase 2 বর্ণনার মূল উৎস
  • Paxos Made Live: An Engineering Perspective — Tushar Chandra, Robert Griesemer, Joshua Redstone — PODC 2007 · Google Chubby-তে Paxos production-এ বসাতে যে practical gap পেরোতে হয়েছিল — Multi-Paxos-এর ইঞ্জিনিয়ারিং কঠিনতার প্রমাণ, পরের লেসনের motivation