Part 03 · High Level Design
Consensus, Leader Election and Distributed Locks
অনেক machine জুড়ে concurrency: clock কেন মিথ্যা বলে, leader election, Raft দিয়ে consensus, আর fencing token দিয়ে নিরাপদ distributed lock।
⏱ 3-4 ঘণ্টা · Station 28 / 80
যে job দুইবার চলল
একটা payroll service চলে তিনটা server এ। মাসে একবার, রাত ১২টায়, ঠিক একটা server কে bank এ বেতন পাঠাতে হয়। একটাই যেন পাঠায় সেজন্য team Redis এ একটা lock ব্যবহার করে: যে আগে payroll-lock key টা set করবে সে job চালাবে, আর server crash করলে আটকে না থাকে সেজন্য key টা 30 সেকেন্ডে expire হয়।
এক মাসে server A lock নিয়ে শুরু করল। মাঝপথে A এর Java process একটা garbage-collection pause এ 40 সেকেন্ড থেমে থাকল। Lock expire হয়ে গেল। Server B দেখল lock ফাঁকা, নিয়ে নিল, আর একই job শুরু করল। তারপর A জেগে উঠল, এখনো ভাবছে lock তার কাছে, আর কাজ চালিয়ে গেল। কিছু কর্মী দুইবার বেতন পেল।
কিছুই crash করেনি। প্রতিটা অংশ যেভাবে বানানো সেভাবেই কাজ করেছে। Bug টা একটা চুপচাপ ধরে নেওয়া কথায়: একটা process জানে lock এখনো তার কাছে আছে কিনা, আর এক machine এর সময় আরেক machine এর সময়ের সমান। Distributed system এ এর কোনোটাই সত্যি না। এই chapter হলো machine গুলো তবুও কীভাবে একমত হয়: clock কেন মিথ্যা বলে, একটা দল কীভাবে একজন leader বাছে (Raft), consensus কী দেয়, আর fencing token দিয়ে lock কীভাবে নিরাপদ করা যায়।
সমস্যা: যে machine রা সময়কে বা একে অপরকে ভরসা করতে পারে না
| কী হয় | কেন |
|---|---|
| পুরনো write নতুনটাকে overwrite করে | ভিন্ন ভিন্ন clock এর machine এর দেয়ালঘড়ির timestamp দিয়ে "সবশেষ" ঠিক করা |
| দুইটা server ই ভাবে সে leader | Network এর একটা ঝাঁকুনিতে পুরনো leader কে মৃত মনে হয়েছে, অথচ সে বেঁচে ছিল |
| Lock থাকার পরও job দুইবার চলে | Pause এর সময় lock expire হয়েছে; পুরনো holder জানতেই পারেনি |
| Cluster একদমই এগোতে পারে না | Majority বানানোর মতো যথেষ্ট node বেঁচে নেই |
| Election বারবার হতেই থাকে | Node গুলো একই মুহূর্তে timeout হয়ে vote ভাগ করে ফেলে |
| "Highly available" lock service failover এ lock হারায় | সে asynchronously replicate করত; নতুন primary lock টা কখনো দেখেনি |
চেয়ারম্যান ছাড়া একটা কমিটির কথা ভাবো
পাঁচজন কমিটি সদস্য আলাদা আলাদা শহরে থাকেন, কথা বলেন শুধু চিঠিতে। চিঠি দেরিতে আসতে পারে, হারাতে পারে, উল্টো ক্রমে পৌঁছাতে পারে। তাদের একসাথে সিদ্ধান্ত নিতে হয়: অফিসের চাবি কার কাছে থাকবে, কোন প্রস্তাব পাস হলো।
তারা দুইটা নিয়মে রাজি হন। প্রথমত, একটা সিদ্ধান্ত গোনা হবে শুধু যদি majority (5 এর 3) রাজি থাকে। দুইটা আলাদা majority তে সবসময় অন্তত একজন সদস্য মিলবেন, তাই দুইটা উল্টো সিদ্ধান্ত একসাথে পাস হতে পারে না। দ্বিতীয়ত, তারা একটা নম্বর দেওয়া term এর জন্য একজন চেয়ারম্যান বাছেন, আর শুধু চেয়ারম্যান সিদ্ধান্ত প্রস্তাব করেন। কিছুদিন চেয়ারম্যানের কোনো খবর না পেলে কেউ একজন পরের term এর জন্য নিজের নাম প্রস্তাব করে vote চান। প্রত্যেক সদস্য প্রতি term এ একবার vote দেন।
আর অফিসের চাবি ধার দেওয়ার সময় প্রতিটা ধারের একটা নম্বর থাকে যেটা শুধু বাড়ে: ধার #33, ধার #34। অফিসের দরজা একটা চাবি মানে শুধু যদি তার নম্বর এ পর্যন্ত দেখা সবচেয়ে বড় নম্বরের সমান বা বেশি হয়। ধার #33 এর মালিক এক মাস গায়েব থেকে ফিরে এলে, দরজা আগেই #34 চেনে, তাই তাকে ঢুকতে দেয় না। এটাই fencing token।
তবে একটা জায়গায় মেলে না। এক মাস পর ফেরা কমিটি সদস্য অন্তত জানেন যে এক মাস পেরিয়েছে। থেমে থাকা একটা process জানে না: তার কাছে কোনো সময়ই পার হয়নি। এজন্যই সুরক্ষা টা থাকতে হয় resource এর কাছে (দরজা, storage), holder এর নিজের সময়ের ধারণায় না।
একটা cluster এ 5টা node, majority voting চলে। কয়টা node fail করলেও cluster leader বাছতে আর write নিতে পারবে?
একমত হওয়ার পেছনের নিয়মগুলো
- Clock সরে যায় আর লাফ দেয়। প্রতিটা machine এর clock একটু দ্রুত বা ধীরে চলে, আর NTP সেটা ঠিক করে, কখনো পেছনে লাফিয়ে। Machine জুড়ে দেয়ালঘড়ির সময় কয়েক millisecond, খারাপ দিনে কয়েক সেকেন্ডও আলাদা হতে পারে। ভিন্ন machine এর দেয়ালঘড়ির timestamp মিলিয়ে কখনো "কোন write নতুন" ঠিক করবে না, আর ধরে নেবে না যে এক machine এর timeout আরেকটার সমান।
- Logical clock event সাজায়। Lamport clock একটা counter: প্রতিটা event এ বাড়াও, প্রতিটা message এর সাথে পাঠাও, আর message পেলে নিজেরটা বানাও max(নিজের, পাওয়া) + 1। A যদি B এর আগে ঘটে থাকে (A থেকে B হয়েছে), A এর সংখ্যা ছোট। Version vector আরেক ধাপ এগিয়ে ধরে ফেলে কখন দুইটা write একসাথে ঘটেছে।
- Consensus মানে একদল node একটা value (বা value এর একটা ক্রম, একটা replicated log) নিয়ে এমনভাবে একমত হওয়া, যাতে কিছু node crash করলে বা message হারালেও সেটা একমতই থাকে। এগোতে majority লাগে, আর কখনো দুইটা আলাদা জিনিস ঠিক করে না।
- সংক্ষেপে Raft। Node রা follower, candidate বা leader। Leader heartbeat পাঠায়। কোনো follower একটা random election timeout (ধরো 150 থেকে 300 ms) ধরে কিছু না শুনলে candidate হয়ে যায়, term বাড়ায়, নিজেকে vote দেয় আর বাকিদের কাছে চায়। প্রতিটা node প্রতি term এ একবার vote দেয়, আর শুধু এমন candidate কে যার log অন্তত তার মতো update। Majority পেলে leader। Random timeout split vote কে বিরল আর ছোট রাখে।
- Leader একটা log replicate করে। Client write পাঠায় leader এর কাছে; সে নিজের log এ যোগ করে follower দের পাঠায়; majority একটা entry রেখে দিলে সেটা committed হয় আর apply হয়। নতুন leader এর কাছে সবসময় প্রতিটা committed entry থাকে, তাই committed data কখনো হারায় না।
- Consensus system ব্যবহার করো, নিজে লিখো না। etcd, ZooKeeper আর Consul (ভেতরে Raft বা Zab) দেয় leader election, distributed lock, configuration আর service membership। CockroachDB, TiDB আর Spanner এর মতো database প্রতিটা data range এ consensus ব্যবহার করে। নিজে লেখা সূক্ষ্ম bug এর পুরনো উৎস।
- Lease হলো expiry সহ একটা lock। Crash করা client এর কাছে lock চিরকাল আটকে থাকা এড়ায়। কিন্তু থেমে থাকা বা slow client বুঝতে পারে না তার lease শেষ, তাই শুধু lease দিয়ে পুরনো holder কে কাজ করা থেকে থামানো যায় না।
- Fencing token lock কে নিরাপদ বানায়। প্রতিবার lock দেওয়ার সময় lock service একটা সংখ্যা দেয় যেটা শুধু বাড়ে (etcd revision, ZooKeeper zxid)। Client প্রতিটা write এর সাথে সেটা পাঠায়; resource এ পর্যন্ত দেখা সবচেয়ে বড় token মনে রাখে আর ছোট কিছু এলে ফিরিয়ে দেয়। পুরনো holder দরজাতেই আটকায়।
- পারলে lock এর দরকারই রাখো না। Idempotent কাজ, unique constraint, compare-and-set ("version = 7 হলেই শুধু update করো") আর প্রতি partition এ একজন মালিক প্রায়ই distributed lock এর প্রয়োজন পুরোপুরি সরিয়ে দেয়, আর এগুলো ঠিকমতো করা সহজ।
ধাপে ধাপে একটা leader failover
5 node এর Raft cluster এর leader মরলে কী হয়
ধাপ 1/5 N1 term 1 এর leader
প্রতি 50 ms এ heartbeat পাঠায় আর প্রতিটা write follower দের কাছে replicate করে। 5 এর 3 জনের কাছে পৌঁছালে write committed।
- term
- 1
- leader
- N1
- committed index
- 1,204
ধাপে ধাপে machine জুড়ে সমন্বয়
- আগে জিজ্ঞেস করো সমন্বয় আদৌ লাগবে কিনা। কাজটা কি idempotent করা যায়, unique constraint দিয়ে পাহারা দেওয়া যায়, বা compare-and-set বানানো যায়? একটা partition কি সবসময় একজন worker এর হাতে রাখা যায়?
- একটা node কেই একা কাজ করতে হলে (একটা scheduler, payroll job), etcd, ZooKeeper, Consul বা তোমার database এর leader election ব্যবহার করো, ঘরে বানানো timer না।
- Lock লাগলে consensus এর উপর দাঁড়ানো service থেকে lease সহ নাও, আর তার fencing token প্রতিটা resource এ পাঠাও যেখানে লেখো।
- Resource কে token check করাও (একটা version column, একটা conditional write), কারণ পুরনো holder কে থামাতে পারে শুধু resource।
- ক্রম যখন সঠিকতার জন্য জরুরি, event সাজাও logical version দিয়ে, দেয়ালঘড়ি দিয়ে না।
- Failure এর হিসাব করে cluster এর size ঠিক করো: 3টা node 1টা failure সহ্য করে, 5টা 2টা। Zone জুড়ে ছড়িয়ে দাও।
- Majority না থাকলে কী হবে ঠিক করো: system এর উচিত write ফিরিয়ে দেওয়া, আন্দাজ করা না।
নিজে বলো: সমন্বয়টা নিরাপদ নাকি না?
এটা কি দুইটা worker কে ধাক্কা খাওয়া থেকে বাঁচাবে?
থেমে থাকা worker জেগে উঠল; পুরনো token সহ তার write ফেরত
চালিয়ে দেখার মতো clock, vote আর fence
তিনটা experiment। 2 সেকেন্ডের clock পার্থক্যের দুইটা server ঠিক করে কোন দামের write নতুন, একবার দেয়ালঘড়ি দিয়ে, একবার Lamport clock দিয়ে। N1 মরার পর পাঁচটা node leader বাছে: term 2 এ split vote, তারপর term 3 এ একজন জেতে। আর 5 সেকেন্ডের lease এর একটা lock, যার holder 10 সেকেন্ড থেমে থাকে, storage এ fencing check সহ আর ছাড়া। ছয়টা version একই নিয়ম মানে, একই লাইন print করে।
package main
import (
"fmt"
"strings"
)
// 1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
// then writes price=100. The later write is A's.
func clocks() {
type write struct {
server string
price int
wall int // what the server's own clock said, in ms
lamport int
}
// Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
b := write{"B", 90, 500 + 2000, 1}
// A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
// max(own 0, seen 1) + 1 = 2.
a := write{"A", 100, 1000, max(0, b.lamport) + 1}
pick := func(by func(w write) int) write {
if by(a) > by(b) {
return a
}
return b
}
byWall := pick(func(w write) int { return w.wall })
byLamport := pick(func(w write) int { return w.lamport })
fmt.Println("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast")
fmt.Printf(" last write wins by wall clock: price=%d (B's stamp %d ms beats A's %d ms)\n", byWall.price, b.wall, a.wall)
fmt.Printf(" last write wins by Lamport clock: price=%d (A's counter %d beats B's %d)\n", byLamport.price, a.lamport, b.lamport)
}
// 2. Leader election, Raft style. Each node votes at most once per term, for the first
// candidate whose request reaches it. A candidate needs a majority of all 5 nodes.
func election(term int, candidates []string, arrival map[string][]string) (string, bool) {
nodes := []string{"N1", "N2", "N3", "N4", "N5"}
votes := map[string]int{}
votedFor := map[string]string{}
for _, c := range candidates { // a candidate votes for itself
votedFor[c] = c
votes[c]++
}
for _, n := range nodes {
if n == "N1" || votedFor[n] != "" { // N1 is down
continue
}
votedFor[n] = arrival[n][0] // the first request to arrive wins this node's vote
votes[arrival[n][0]]++
}
var tally []string
winner := ""
for _, c := range candidates {
tally = append(tally, fmt.Sprintf("%s %d", c, votes[c]))
if votes[c] >= 3 {
winner = c
}
}
fmt.Printf(" term %d: candidates %s, votes %s -> ", term, strings.Join(candidates, ", "), strings.Join(tally, ", "))
if winner == "" {
fmt.Println("no majority, wait a random time and try again")
return "", false
}
fmt.Printf("%s leads with %d of 5\n", winner, votes[winner])
return winner, true
}
// 3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause).
func lockDemo(fencing bool) string {
token := 32
highest := 0 // the storage remembers the highest token it has accepted
file := ""
write := func(who string, tok int) {
if fencing && tok < highest {
return // a stale lock holder: reject
}
highest = max(highest, tok)
file = who
}
token++
aToken := token // t=0: A gets the lock
token++
bToken := token // t=6 s: the lease has expired, B gets the lock
write("B", bToken)
write("A", aToken) // t=10 s: A wakes up, still believing it holds the lock
return file
}
func main() {
clocks()
fmt.Println("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5")
if _, ok := election(2, []string{"N3", "N5"}, map[string][]string{"N2": {"N3", "N5"}, "N4": {"N5", "N3"}}); !ok {
election(3, []string{"N5"}, map[string][]string{"N2": {"N5"}, "N3": {"N5"}, "N4": {"N5"}})
}
fmt.Println("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes")
fmt.Printf(" without fencing: the file says %s (B's write is lost)\n", lockDemo(false))
fmt.Printf(" with fencing: the file says %s (token 33 rejected, 34 already seen)\n", lockDemo(true))
}from typing import Callable, NamedTuple
class Write(NamedTuple):
server: str
price: int
wall: int # what the server's own clock said, in ms
lamport: int
def clocks() -> None:
"""1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
then writes price=100. The later write is A's."""
# Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
b = Write("B", 90, 500 + 2000, 1)
# A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
# max(own 0, seen 1) + 1 = 2.
a = Write("A", 100, 1000, max(0, b.lamport) + 1)
def pick(by: Callable[[Write], int]) -> Write:
return a if by(a) > by(b) else b
by_wall = pick(lambda w: w.wall)
by_lamport = pick(lambda w: w.lamport)
print("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast")
print(f" last write wins by wall clock: price={by_wall.price} (B's stamp {b.wall} ms beats A's {a.wall} ms)")
print(f" last write wins by Lamport clock: price={by_lamport.price} (A's counter {a.lamport} beats B's {b.lamport})")
def election(term: int, candidates: list[str], arrival: dict[str, list[str]]) -> bool:
"""2. Leader election, Raft style. Each node votes at most once per term, for the first
candidate whose request reaches it. A candidate needs a majority of all 5 nodes."""
votes: dict[str, int] = {}
voted_for: dict[str, str] = {}
for c in candidates: # a candidate votes for itself
voted_for[c] = c
votes[c] = votes.get(c, 0) + 1
for n in ["N1", "N2", "N3", "N4", "N5"]:
if n == "N1" or n in voted_for: # N1 is down
continue
voted_for[n] = arrival[n][0] # the first request to arrive wins this node's vote
votes[arrival[n][0]] = votes.get(arrival[n][0], 0) + 1
tally = ", ".join(f"{c} {votes[c]}" for c in candidates)
winner = next((c for c in reversed(candidates) if votes[c] >= 3), "")
line = f" term {term}: candidates {', '.join(candidates)}, votes {tally} -> "
if not winner:
print(line + "no majority, wait a random time and try again")
return False
print(line + f"{winner} leads with {votes[winner]} of 5")
return True
def lock_demo(fencing: bool) -> str:
"""3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause)."""
state = {"highest": 0, "file": ""} # the storage remembers the highest token it has accepted
def write(who: str, tok: int) -> None:
if fencing and tok < state["highest"]:
return # a stale lock holder: reject
state["highest"] = max(state["highest"], tok)
state["file"] = who
token = 32
token += 1
a_token = token # t=0: A gets the lock
token += 1
b_token = token # t=6 s: the lease has expired, B gets the lock
write("B", b_token)
write("A", a_token) # t=10 s: A wakes up, still believing it holds the lock
return str(state["file"])
clocks()
print("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5")
if not election(2, ["N3", "N5"], {"N2": ["N3", "N5"], "N4": ["N5", "N3"]}):
election(3, ["N5"], {"N2": ["N5"], "N3": ["N5"], "N4": ["N5"]})
print("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes")
print(f" without fencing: the file says {lock_demo(False)} (B's write is lost)")
print(f" with fencing: the file says {lock_demo(True)} (token 33 rejected, 34 already seen)")type Write = { server: string; price: number; wall: number; lamport: number }; // wall: the server's own clock, in ms
/** 1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
* then writes price=100. The later write is A's. */
function clocks() {
// Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
const b: Write = { server: "B", price: 90, wall: 500 + 2000, lamport: 1 };
// A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
// max(own 0, seen 1) + 1 = 2.
const a: Write = { server: "A", price: 100, wall: 1000, lamport: Math.max(0, b.lamport) + 1 };
const pick = (by: (w: Write) => number) => (by(a) > by(b) ? a : b);
const byWall = pick((w) => w.wall);
const byLamport = pick((w) => w.lamport);
console.log("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast");
console.log(` last write wins by wall clock: price=${byWall.price} (B's stamp ${b.wall} ms beats A's ${a.wall} ms)`);
console.log(` last write wins by Lamport clock: price=${byLamport.price} (A's counter ${a.lamport} beats B's ${b.lamport})`);
}
/** 2. Leader election, Raft style. Each node votes at most once per term, for the first
* candidate whose request reaches it. A candidate needs a majority of all 5 nodes. */
function election(term: number, candidates: string[], arrival: Record<string, string[]>): boolean {
const votes = new Map<string, number>();
const votedFor = new Map<string, string>();
for (const c of candidates) {
// a candidate votes for itself
votedFor.set(c, c);
votes.set(c, (votes.get(c) ?? 0) + 1);
}
for (const n of ["N1", "N2", "N3", "N4", "N5"]) {
if (n === "N1" || votedFor.has(n)) continue; // N1 is down
const first = arrival[n]![0]!; // the first request to arrive wins this node's vote
votedFor.set(n, first);
votes.set(first, (votes.get(first) ?? 0) + 1);
}
const tally = candidates.map((c) => `${c} ${votes.get(c)}`).join(", ");
const winner = [...candidates].reverse().find((c) => votes.get(c)! >= 3) ?? "";
const line = ` term ${term}: candidates ${candidates.join(", ")}, votes ${tally} -> `;
if (winner === "") {
console.log(line + "no majority, wait a random time and try again");
return false;
}
console.log(line + `${winner} leads with ${votes.get(winner)} of 5`);
return true;
}
/** 3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause). */
function lockDemo(fencing: boolean): string {
let highest = 0; // the storage remembers the highest token it has accepted
let file = "";
const write = (who: string, tok: number) => {
if (fencing && tok < highest) return; // a stale lock holder: reject
highest = Math.max(highest, tok);
file = who;
};
let token = 32;
const aToken = ++token; // t=0: A gets the lock
const bToken = ++token; // t=6 s: the lease has expired, B gets the lock
write("B", bToken);
write("A", aToken); // t=10 s: A wakes up, still believing it holds the lock
return file;
}
clocks();
console.log("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5");
if (!election(2, ["N3", "N5"], { N2: ["N3", "N5"], N4: ["N5", "N3"] })) {
election(3, ["N5"], { N2: ["N5"], N3: ["N5"], N4: ["N5"] });
}
console.log("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes");
console.log(` without fencing: the file says ${lockDemo(false)} (B's write is lost)`);
console.log(` with fencing: the file says ${lockDemo(true)} (token 33 rejected, 34 already seen)`);use std::collections::HashMap;
struct Write {
price: i32,
wall: i32, // what the server's own clock said, in ms
lamport: i32,
}
/// 1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
/// then writes price=100. The later write is A's.
fn clocks() {
// Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
let b = Write { price: 90, wall: 500 + 2000, lamport: 1 };
// A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
// max(own 0, seen 1) + 1 = 2.
let a = Write { price: 100, wall: 1000, lamport: 0.max(b.lamport) + 1 };
let pick = |by: fn(&Write) -> i32| if by(&a) > by(&b) { &a } else { &b };
let by_wall = pick(|w| w.wall);
let by_lamport = pick(|w| w.lamport);
println!("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast");
println!(" last write wins by wall clock: price={} (B's stamp {} ms beats A's {} ms)", by_wall.price, b.wall, a.wall);
println!(" last write wins by Lamport clock: price={} (A's counter {} beats B's {})", by_lamport.price, a.lamport, b.lamport);
}
/// 2. Leader election, Raft style. Each node votes at most once per term, for the first
/// candidate whose request reaches it. A candidate needs a majority of all 5 nodes.
fn election(term: i32, candidates: &[&str], arrival: &HashMap<&str, Vec<&str>>) -> bool {
let mut votes: HashMap<&str, i32> = HashMap::new();
let mut voted_for: HashMap<&str, &str> = HashMap::new();
for &c in candidates {
// a candidate votes for itself
voted_for.insert(c, c);
*votes.entry(c).or_default() += 1;
}
for n in ["N1", "N2", "N3", "N4", "N5"] {
if n == "N1" || voted_for.contains_key(n) {
continue; // N1 is down
}
let first = arrival[n][0]; // the first request to arrive wins this node's vote
voted_for.insert(n, first);
*votes.entry(first).or_default() += 1;
}
let tally: Vec<String> = candidates.iter().map(|c| format!("{c} {}", votes[c])).collect();
let winner = candidates.iter().rev().find(|c| votes[*c] >= 3);
let line = format!(" term {term}: candidates {}, votes {} -> ", candidates.join(", "), tally.join(", "));
match winner {
None => {
println!("{line}no majority, wait a random time and try again");
false
}
Some(w) => {
println!("{line}{w} leads with {} of 5", votes[w]);
true
}
}
}
/// 3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause).
fn lock_demo(fencing: bool) -> String {
let mut highest = 0; // the storage remembers the highest token it has accepted
let mut file = String::new();
let mut write = |who: &str, tok: i32| {
if fencing && tok < highest {
return; // a stale lock holder: reject
}
highest = highest.max(tok);
file = who.to_string();
};
let mut token = 32;
token += 1;
let a_token = token; // t=0: A gets the lock
token += 1;
let b_token = token; // t=6 s: the lease has expired, B gets the lock
write("B", b_token);
write("A", a_token); // t=10 s: A wakes up, still believing it holds the lock
file
}
fn main() {
clocks();
println!("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5");
let first = HashMap::from([("N2", vec!["N3", "N5"]), ("N4", vec!["N5", "N3"])]);
if !election(2, &["N3", "N5"], &first) {
let second = HashMap::from([("N2", vec!["N5"]), ("N3", vec!["N5"]), ("N4", vec!["N5"])]);
election(3, &["N5"], &second);
}
println!("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes");
println!(" without fencing: the file says {} (B's write is lost)", lock_demo(false));
println!(" with fencing: the file says {} (token 33 rejected, 34 already seen)", lock_demo(true));
}import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.ToIntFunction;
public class Main {
record Write(String server, int price, int wall, int lamport) {} // wall: the server's own clock, in ms
/** 1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
* then writes price=100. The later write is A's. */
static void clocks() {
// Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
Write b = new Write("B", 90, 500 + 2000, 1);
// A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
// max(own 0, seen 1) + 1 = 2.
Write a = new Write("A", 100, 1000, Math.max(0, b.lamport()) + 1);
ToIntFunction<Write> wall = Write::wall, lamport = Write::lamport;
Write byWall = wall.applyAsInt(a) > wall.applyAsInt(b) ? a : b;
Write byLamport = lamport.applyAsInt(a) > lamport.applyAsInt(b) ? a : b;
System.out.println("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast");
System.out.printf(" last write wins by wall clock: price=%d (B's stamp %d ms beats A's %d ms)%n", byWall.price(), b.wall(), a.wall());
System.out.printf(" last write wins by Lamport clock: price=%d (A's counter %d beats B's %d)%n", byLamport.price(), a.lamport(), b.lamport());
}
/** 2. Leader election, Raft style. Each node votes at most once per term, for the first
* candidate whose request reaches it. A candidate needs a majority of all 5 nodes. */
static boolean election(int term, List<String> candidates, Map<String, List<String>> arrival) {
Map<String, Integer> votes = new HashMap<>();
Map<String, String> votedFor = new HashMap<>();
for (String c : candidates) { // a candidate votes for itself
votedFor.put(c, c);
votes.merge(c, 1, Integer::sum);
}
for (String n : List.of("N1", "N2", "N3", "N4", "N5")) {
if (n.equals("N1") || votedFor.containsKey(n)) continue; // N1 is down
String first = arrival.get(n).get(0); // the first request to arrive wins this node's vote
votedFor.put(n, first);
votes.merge(first, 1, Integer::sum);
}
List<String> tally = new ArrayList<>();
String winner = "";
for (String c : candidates) {
tally.add(c + " " + votes.get(c));
if (votes.get(c) >= 3) winner = c;
}
String line = " term " + term + ": candidates " + String.join(", ", candidates) + ", votes " + String.join(", ", tally) + " -> ";
if (winner.isEmpty()) {
System.out.println(line + "no majority, wait a random time and try again");
return false;
}
System.out.println(line + winner + " leads with " + votes.get(winner) + " of 5");
return true;
}
/** 3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause). */
static String lockDemo(boolean fencing) {
int[] highest = {0}; // the storage remembers the highest token it has accepted
String[] file = {""};
interface Storage { void write(String who, int tok); }
Storage storage = (who, tok) -> {
if (fencing && tok < highest[0]) return; // a stale lock holder: reject
highest[0] = Math.max(highest[0], tok);
file[0] = who;
};
int token = 32;
int aToken = ++token; // t=0: A gets the lock
int bToken = ++token; // t=6 s: the lease has expired, B gets the lock
storage.write("B", bToken);
storage.write("A", aToken); // t=10 s: A wakes up, still believing it holds the lock
return file[0];
}
public static void main(String[] args) {
clocks();
System.out.println("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5");
if (!election(2, List.of("N3", "N5"), Map.of("N2", List.of("N3", "N5"), "N4", List.of("N5", "N3")))) {
election(3, List.of("N5"), Map.of("N2", List.of("N5"), "N3", List.of("N5"), "N4", List.of("N5")));
}
System.out.println("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes");
System.out.printf(" without fencing: the file says %s (B's write is lost)%n", lockDemo(false));
System.out.printf(" with fencing: the file says %s (token 33 rejected, 34 already seen)%n", lockDemo(true));
}
}#include <algorithm>
#include <cstdio>
#include <functional>
#include <map>
#include <string>
#include <vector>
struct Write {
const char* server;
int price;
int wall; // what the server's own clock said, in ms
int lamport;
};
std::string join(const std::vector<std::string>& xs, const std::string& sep) {
std::string out;
for (size_t i = 0; i < xs.size(); i++) out += (i ? sep : "") + xs[i];
return out;
}
// 1. Clocks. Server B's clock runs 2 seconds fast. B writes price=90 first; A reads it and
// then writes price=100. The later write is A's.
void clocks() {
// Real times: B at 500 ms, A at 1000 ms. B's clock adds 2000 ms.
Write b{"B", 90, 500 + 2000, 1};
// A received B's write (carrying Lamport time 1) before writing, so A's counter becomes
// max(own 0, seen 1) + 1 = 2.
Write a{"A", 100, 1000, std::max(0, b.lamport) + 1};
auto pick = [&](const std::function<int(const Write&)>& by) { return by(a) > by(b) ? a : b; };
Write byWall = pick([](const Write& w) { return w.wall; });
Write byLamport = pick([](const Write& w) { return w.lamport; });
std::printf("clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast\n");
std::printf(" last write wins by wall clock: price=%d (B's stamp %d ms beats A's %d ms)\n", byWall.price, b.wall, a.wall);
std::printf(" last write wins by Lamport clock: price=%d (A's counter %d beats B's %d)\n", byLamport.price, a.lamport, b.lamport);
}
// 2. Leader election, Raft style. Each node votes at most once per term, for the first
// candidate whose request reaches it. A candidate needs a majority of all 5 nodes.
bool election(int term, const std::vector<std::string>& candidates, std::map<std::string, std::vector<std::string>> arrival) {
std::map<std::string, int> votes;
std::map<std::string, std::string> votedFor;
for (const auto& c : candidates) { // a candidate votes for itself
votedFor[c] = c;
votes[c]++;
}
for (const std::string n : {"N1", "N2", "N3", "N4", "N5"}) {
if (n == "N1" || votedFor.count(n)) continue; // N1 is down
std::string first = arrival[n][0]; // the first request to arrive wins this node's vote
votedFor[n] = first;
votes[first]++;
}
std::vector<std::string> tally;
std::string winner;
for (const auto& c : candidates) {
tally.push_back(c + " " + std::to_string(votes[c]));
if (votes[c] >= 3) winner = c;
}
std::string line = " term " + std::to_string(term) + ": candidates " + join(candidates, ", ") + ", votes " + join(tally, ", ") + " -> ";
if (winner.empty()) {
std::printf("%sno majority, wait a random time and try again\n", line.c_str());
return false;
}
std::printf("%s%s leads with %d of 5\n", line.c_str(), winner.c_str(), votes[winner]);
return true;
}
// 3. A lock with a 5 s lease. Client A gets it, then freezes for 10 s (a long GC pause).
std::string lockDemo(bool fencing) {
int highest = 0; // the storage remembers the highest token it has accepted
std::string file;
auto write = [&](const std::string& who, int tok) {
if (fencing && tok < highest) return; // a stale lock holder: reject
highest = std::max(highest, tok);
file = who;
};
int token = 32;
int aToken = ++token; // t=0: A gets the lock
int bToken = ++token; // t=6 s: the lease has expired, B gets the lock
write("B", bToken);
write("A", aToken); // t=10 s: A wakes up, still believing it holds the lock
return file;
}
int main() {
clocks();
std::printf("election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5\n");
if (!election(2, {"N3", "N5"}, {{"N2", {"N3", "N5"}}, {"N4", {"N5", "N3"}}})) {
election(3, {"N5"}, {{"N2", {"N5"}}, {"N3", {"N5"}}, {"N4", {"N5"}}});
}
std::printf("lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes\n");
std::printf(" without fencing: the file says %s (B's write is lost)\n", lockDemo(false).c_str());
std::printf(" with fencing: the file says %s (token 33 rejected, 34 already seen)\n", lockDemo(true).c_str());
}ছয়টাই হুবহু এটা print করে:
clocks: B writes price=90, A sees it and writes price=100; B's clock is 2 s fast
last write wins by wall clock: price=90 (B's stamp 2500 ms beats A's 1000 ms)
last write wins by Lamport clock: price=100 (A's counter 2 beats B's 1)
election: leader N1 (term 1) dies; 4 nodes left, a majority is 3 of 5
term 2: candidates N3, N5, votes N3 2, N5 2 -> no majority, wait a random time and try again
term 3: candidates N5, votes N5 4 -> N5 leads with 4 of 5
lock with a 5 s lease: A gets token 33, pauses 10 s; B gets token 34 at 6 s and writes; A wakes and writes
without fencing: the file says A (B's write is lost)
with fencing: the file says B (token 33 rejected, 34 already seen)
Output টা পড়ো:
- wall clock: B এর write আগে হয়েছিল (আসল সময় 500 ms), কিন্তু B এর দ্রুত clock তাতে 2500 ms লিখেছে, তাই A এর পরের write কে হারিয়ে দেয়। ভুল দাম, 90, জেতে।
- Lamport clock: A B এর write দেখেছিল, তাই A এর counter বড় (2 বনাম 1)। কারণ-ফলের দিক থেকে পরের write, 100, জেতে, clock যাই বলুক।
- term 2: N3 আর N5 একসাথে দাঁড়িয়ে 2টা করে vote পেল। কারো 5 এর 3 নেই, তাই কেউ leader না, যেটা নিরাপদ।
- term 3: একটা random অপেক্ষার পর শুধু N5 দাঁড়াল, পেল 4টা vote। একজন leader, majority দিয়ে বাছা।
- without fencing: lease চলে যাওয়ার পরে A জেগে উঠে B এর data overwrite করল। File এ লেখা A।
- with fencing: storage আগেই token 34 দেখেছিল, তাই 33 সহ A এর write ফেরত গেল। File এ এখনো B।
- কোন language এ কী বদলালো: বাইরের variable বদলানো closure। Go, Rust, TypeScript আর C++ এর closure সরাসরি পারে (Rust এ
let mut writeলাগে)। Java র lambda শুধু effectively final variable ধরতে পারে, তাই fence এর state রাখা হয়েছে এক উপাদানের array তে; Python একই কারণে একটা ছোট dict ব্যবহার করে।
আসল project এ একমত হওয়া
- etcd (Kubernetes এর উপর চলে), ZooKeeper (Kafka বহু বছর ব্যবহার করেছে) আর Consul হলো চেনা consensus service। ওদের election আর lock এর recipe ব্যবহার করো, আর lease আর revision নিয়ে ওদের docs পড়ো।
- 3 বা 5টা node চালাও, availability zone জুড়ে ছড়ানো। বেশি voter মানে slow write; read বাড়াতে non-voting replica ব্যবহার করো।
- Timeout: election timeout হতে হবে স্বাভাবিক network delay এর চেয়ে অনেক বড়, আর leader ছাড়া যতক্ষণ চলতে পারো তার চেয়ে ছোট। Leader বদলানো দেখো; ঘনঘন বদলালে মানে node overloaded বা network নড়বড়ে।
- বাস্তবে fencing: etcd এর lease আর mod revision, ZooKeeper এর zxid, বা lock এর সাথে রাখা শুধু বাড়তে থাকা একটা
epoch। Resource এটা check করে conditional write দিয়ে (WHERE epoch <= :token)। - Redis lock: কাজের দক্ষতার জন্য ঠিক আছে (যেখানে দুইবার হলে ক্ষতি নেই সেখানে দুইবার কাজ এড়ানো), সঠিকতার জন্য না, যদি না resource fence করা থাকে। "Redlock" এর দাবি নিয়ে সাবধান; ভরসা করার আগে ওটা নিয়ে বিতর্কটা পড়ে নাও।
- Clock: সব জায়গায় NTP বা chrony চালাও আর drift এ alert দাও, তবু এমনভাবে design করো যাতে সঠিকতা clock এর মিলের উপর নির্ভর না করে। Spanner এর মতো system জানা ভুলের সীমা সহ বিশেষ hardware clock ব্যবহার করে আর সেই সময়টা অপেক্ষা করে নেয়।
যে design গুলো পরে problem create করে
সমন্বয়ের ভুলটা ধরো
network ভাগ হলো: প্রতিটা দিক নিজের leader বেছে নিল
একটু থামো, ভাবো
তোমার team একটা availability zone এ 3 node এর একটা etcd cluster চালায়। কেউ প্রস্তাব দিল "বাড়তি security এর জন্য" ৪ নম্বর একটা node যোগ করতে। এটা কি বেশি নিরাপদ? এর বদলে তুমি কী করবে?
উত্তর দেখো
আসলে না। 4টা node এ majority হলো 3, তাই cluster এখনো মাত্র 1টা failure সহ্য করে, 3টা node এর মতোই, অথচ প্রতিটা write এখন 2 এর বদলে 3টা acknowledgement এর জন্য অপেক্ষা করে, আর fail করার মতো আরেকটা machine বাড়ল। জোড় সংখ্যা fault tolerance না বাড়িয়ে শুধু খরচ বাড়ায়। এখানে আসল ঝুঁকি হলো 3টা node ই একটা availability zone এ: zone এর একটা outage পুরো cluster ফেলে দেয়। ভালো হয়: 3টা node রাখো আর 3টা zone এ ছড়িয়ে দাও, বা 2টা failure সহ্য করতে হলে 3টা zone জুড়ে 5টা node নাও।
ছোট্ট quiz
1. ভিন্ন server এর দেয়ালঘড়ির timestamp দিয়ে write সাজানো কেন ঠিক না?
উত্তর
Server এর clock সরে যায় আর ঠিক করা হয় (কখনো পেছনে), তাই ভিন্ন machine এর timestamp তুলনা করা যায় না। পরে ঘটা একটা write আগের timestamp নিয়ে এসে হেরে যেতে পারে।
2. Raft এ election timeout কেন random?
উত্তর
যাতে follower রা খুব কমই ঠিক একই মুহূর্তে timeout হয়। হলে কয়েকজন candidate vote ভাগ করে ফেলত আর কেউ জিতত না; random হলে সাধারণত একটা node আগে গিয়ে majority জোগাড় করে।
3. শুধু lease যেটা আটকায় না, fencing token সেটা কী থেকে বাঁচায়?
উত্তর
এমন একজন holder থেকে, যে না জেনেই lease হারিয়েছে (long pause, slow network) আর তারপর কাজ করে। Resource তার write ফিরিয়ে দেয়, কারণ আরো বড় একটা token আগেই ব্যবহার হয়ে গেছে।
4. 5 node এর majority ভিত্তিক cluster কয়টা node failure সহ্য করতে পারে?
উত্তর
দুইটা। তিনটা node থাকে, যেটা এখনো পাঁচের majority। তৃতীয় failure এ write বন্ধ হয়, জেনেবুঝেই।
তোমার challenge: যখন majority নেই
Election কে সবসময় N1 বাদ না দিয়ে down node এর একটা set নিতে দাও। আগের দুইটা call রেখে দাও (শুধু N1 down), তারপর তৃতীয় একটা call যোগ করো: term 4, candidate N5, N1, N2 আর N4 down, আর N3 vote দেয় N5 কে।
- Down node vote দেয় না, কিন্তু majority এখনো 5 এর 3: cluster এর size বদলায়নি।
- চালানোর আগে ঠিক করো: N5 কি leader হতে পারবে?
তুমি পেরেছো যখন নতুন line টা হবে term 4: candidates N5, votes N5 2 -> no majority, wait a random time and try again আর বাকি line গুলো বদলাবে না। 5 এর 3টা node down থাকলে বাকি 2টা যত চেষ্টাই করুক, কখনো leader বাছতে পারবে না। এটা bug না: যখন system নিশ্চিত হতে পারছে না যে আর কেউ কাজ করছে না, তখন সে কাজ করতে অস্বীকার করছে।
এক লাইনে
Machine রা clock কে ভরসা করতে পারে না, আর জানে না তারা থেমে ছিল কিনা, তাই একমত হওয়া আসে majority থেকে: একটা consensus system কে (etcd, ZooKeeper, Consul এর Raft) প্রতি term এ একজন leader বাছতে আর log commit করতে দাও, event সাজাও logical clock দিয়ে, lock কে দাও একটা lease আর এমন fencing token যেটা resource check করে, zone জুড়ে বেজোড় (odd) সংখ্যার node চালাও, আর যেখানে idempotency বা compare-and-set দিয়েই চলে সেখানে lock এর দরকারই রেখো না।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →