Distributed Lock ও তার বিপদ — Redlock বিতর্কসহ
Distributed Locks and Their Perils — the Redlock Debate
Shared memory না থাকায় আর pause-মৃত্যু আলাদা করা যায় না বলে distributed mutual exclusion mutex-এর চেয়ে মৌলিকভাবে কঠিন; lease TTL crash সামলায় কিন্তু pause-এ ভাঙে — fencing token-ই ঢাল।
আগে এটা বুঝি
আপনি operating-systems module-এ mutex লিখেছেন — আচ্ছাদিত আরামের
একটা জগৎ। pthread_mutex_lock()-এর নিচে kernel একটা atomic
instruction চালায় shared memory word-এ; নিরাপত্তা দাঁড়িয়ে একটা
ধর্মে: সব participant একই memory দেখে।
Production-এর এক রাত: billing-cron-এর দুই instance ঘুম থেকে
উঠেছে (deploy-এ পুরনোটা মরতে দেরি হয়েছিল), দুজনেই একই customer-
এর invoice তৈরি করেছে, দুটো charge গেছে। Team বলল — lock ব্যবহার
করব; Redis-এ SET key val NX PX 30000 লিখে দিল। সমস্যা শেষ?
না — আর এখান থেকেই আজকের লেসন। Network-জুড়ে ছড়ানো lock
mutex-এর মতো শোনায়, কিন্তু physics সম্পূর্ণ আলাদা: shared memory
নেই — সব খবর message হয়ে যায় (message-queues-লেসন); আর কেউ
নিশ্চিত জানে না অন্য party মরেছে নাকি শুধু দেরি করেছে —
failure-models-লেসনের crash-recovery model মনে করুন: বাইরে থেকে
pause আর মৃত্যু অপারেবল।
দুইটা প্রশ্ন আজকের spine: (১) lock service কাকে lock দিল, holder মরলে সেটা ফেরত আসে কীভাবে? (২) lease মরার পরেও পুরনো holder জেগে উঠে লেখা চালিয়ে গেলে (GC pause!) শেষ রক্ষা কে? উত্তরটার নাম fencing token — এই লেসনের হৃদয়।
মূল ধারণা
Mutex-এর ধর্ম কোথায় ভাঙে
| অনুমান | Single-process mutex | Distributed lock |
|---|---|---|
| Shared state | একটা memory word | কোথাও নেই — network message |
| Atomic primitive | CPU test-and-set | server-side request handling |
| Participant মরলে | OS জানে, cleanup করে | জানার উপায় নেই — timeout-ই ইঙ্গিত |
| সময়ের ভূমিকা | নগণ্য | TTL, timeout, clock — ঝুঁকির উৎস |
Lease — মেয়াদসহ ভাড়া
এই মেয়াদ-চুক্তির নাম lease: lock মানে
মালিকানা না, সময়সীমাসহ ভাড়া — acquire-এ server টোকেন দেয়
TTL মেয়াদে, কাজ বড় হলে renewal, শেষে release। চিরতরে-অপেক্ষা-
নীতির বদলে মেয়াদ — কারণ participant মরলে server-এর কাছে মৃত্যু
আর ধীরগতি একরকম: উত্তরই আসে না। আর জাদুটা release-ধাপে:
release না-ও করলেও চলে — holder crash করলে TTL ফুরোতেই lease
নিজে নিজে মরে, zombie-holder-চিরদখলের সমাধান। এই একটা বৈশিষ্ট্যের
জন্যই lease-based lock শিল্পের default।
টাইমার-বোমাটা — process pause
Crash solved। আসল দানব থেমে-যাওয়া:
t=0 Alice acquire (TTL 30 s), DB read: stock=42
t=1 Alice-এর JVM full GC — stop-the-world... ৩০ সেকেন্ড,
এক log-লাইন নেই, কারণ সব থেমে
t=31 Lease মরেছে। Bob acquire করে, stock=41 লেখে, শেষ
t=52 GC শেষ। Alice জাগল — তার হিসেবে ২ সেকেন্ড!
সে ভাবে তার lock বেঁচে; stock=40 লিখে দিল...Alice কোনো ভুল করেনি — retry করেনি, lock ভাঙেনি। কিন্তু তার ঘড়ির সত্য আর lock-server-এর সত্য আলাদা। শিক্ষণীয় asymmetry: client কখনো জানতে পারে না lock বেঁচে আছে কি না — জানে শুধু resource, যখন প্রথম write reject হয়।
Fencing token — resource-এর হাতে ঢাল
সমাধান: lock দিলে server fencing token-ও দেয় — প্রতিবার acquire-এ monotonically বাড়তি সংখ্যা (33, 34, 35…)। আর resource (storage/API — যা-ই রক্ষা হচ্ছে) মানে নিয়ম: সর্বোচ্চ seen-token-এর চেয়ে ছোট token এলে reject।
Alice acquire → token 33
Bob acquire → token 34 (Alice-এর lease মরার পরে)
Bob storage.write(token=34) → accepted, last_seen=34
Alice (GC থেকে জেগে) write(token=33) → REJECTED: 33 ≤ 34কেন কাজ করে? এটা timing-assumption-কে ordering-assumption-এ বদলায় — Alice-এর ঘড়ি, pause-দৈর্ঘ্য, TTL-হিসাব কিছুই আর গুরুত্বপূর্ণ না; একমাত্র সংখ্যার ক্রম, যা server একমুখীভাবে দেয়।
Redlock — পাঁচ Redis-এ ভোট
এক Redis node-এর failover-ফাঁকে lock হারায় (async replication — primary মরলে replica lock-এর খবর না-ও পায়)। antirez-এর Redlock উত্তর দেয় ভোটে:
১. নিজের ঘড়িতে T1 নাও
২. পাঁচটা স্বাধীন master-এ SET lock NX PX=ttl
৩. T2 নাও; validity = ttl − (T2−T1) − clock-drift margin
৪. ≥৩ node-এ set হলো আর validity > 0 → acquired
৫. unlock = সব node-এ Lua-script check-and-deleteবিতর্কটা — Kleppmann বনাম antirez, ন্যায্য সারাংশ
2016-এ Kleppmann-এর “How to do distributed locking” আক্রমণ, antirez- এর জবাব — দুজনেই ঠিক, ভিন্ন ধর্মে:
| প্রশ্ন | Kleppmann-পক্ষ | antirez-পক্ষ |
|---|---|---|
| System-model | Asynchronous — pause/delay-র bound নেই | Semi-synchronous — drift ছোট, measurable |
| GC-pause | Validity client-পাশে হিসাব — pause-এ ভাঙে; fencing ছাড়া unsafe | Edge-case; fencing-সহ Redlock practical |
| Clock jump | NTP-step/VM-migration-এ দুই holder সম্ভব | বিরল; drift-margin + monitoring |
| দর্শন | Safety চাইলে consensus-store + fencing | Progress ও সরলতার সৎ trade-off |
সারকথা: Redlock-এর safety দাঁড়িয়ে আছে সময়ের আচরণে; fencing token-ই সেই নির্ভরতা কাটে — আর fencing লাগলে multi-node ভোটের complexity অনেক ক্ষেত্রেই অপ্রয়োজনীয়। Decision, dogma নয়।
ভেতরে কী ঘটছে
Failure detector — মৃত্যু কেন নিশ্চিত জানা যায় না
Lock-server আসলে একটা failure detector: timeout ফুরোলে ঘোষণা holder সম্ভবত মৃত। দুই গুণ — completeness (মৃতকে শেষে ধরা) আর accuracy (জীবিতকে ভুল ঘোষণা না করা) — asynchronous network-এ দুটোই একসাথে perfect অসম্ভব: GC-pause accuracy ভাঙে, infinite delay completeness। Lease-এর নীতি এই অসম্ভবতার সমঝোতা: ভুল ঘোষণা হতে দাও, তবে ঘোষণার ফল ordering দিয়ে সারাও।
ZooKeeper recipes — ephemeral sequential znodes
/locks/order-processor/
lock-0000000041 ← node A (ephemeral)
lock-0000000042 ← node B (ephemeral)
lock-0000000043 ← node C (ephemeral)
নিয়ম: সবচেয়ে ছোট sequence-ই holder;
বাকিরা ঠিক আগের sibling-এ watch — সে মরলেই জানায়।তিন ভাষ্য। Ephemeral = znode owner-session-এর জীবনে বাঁধা; session মরলে (heartbeat বন্ধ) znode মুছে যায় — ZooKeeper-এর lease। Sequential monotonically বাড়তি নম্বর দেয় — fencing-token-এর raw material (enforce করে ZK না, client-কেই resource-এ পৌঁছে দিতে হয়)। Watch-predecessor herd effect ঠেকায় — release-এ একজনই জাগে। আর session-timeout-এর পেছনে সেই পুরনো সত্য: ZK-ও GC-pause- এ client-কে ভুলে মৃত ঘোষণা করতে পারে — তাই ZK-lock-ও fencing ছাড়া correctness-দাবি করে না।
- Lock serviceGrant/renew/revoke + monotonic token issue
- Lease (TTL)Holder-crash থেকে পুনরুদ্ধার — liveness-এর চুক্তি
- Fencing tokenSafety-র চুক্তি — stale writer ordering দিয়ে হারানো
- Resource guardশেষ প্রাচীর — token check, reject stale write
Database advisory lock — lock আর data এক ঘরে
PostgreSQL-এর pg_advisory_lock(key) প্রায়ই সবচেয়ে সঠিক উত্তর:
lock-অবস্থা connection-এর জীবনে বাঁধা — connection মরলে (crash,
network drop, timeout) server নিজেই ছেড়ে দেয়; আলাদা TTL, renewal,
fencing-যন্ত্র কিছুই লাগে না। Fate-sharing যুক্তি: যে data রক্ষা
হচ্ছে সেটা যদি এক database-এই থাকে, lock-ও সেখানেই — lock-service
আর storage একসাথে পড়লে partial-failure-দুঃস্বপ্ন নেই। Batch-job dedup
থেকে worker claim — এক-database জগতে advisory lock সস্তা-সঠিক;
multi-storage জগতেই আলাদা coordination service লাগে।
উদাহরণ
Payment-worker-এর এক রাত — token ছাড়া আর token-সহ
দুই worker, এক lock service, এক ledger-table; task: invoice process — balance read, fee যোগ, write; TTL ৩০ s। Fencing ছাড়া:
| সময় | Worker A | Worker B | Ledger |
|---|---|---|---|
| 0 s | acquire, read bal=500 | 500 | |
| 1 s | GC pause শুরু | 500 | |
| 32 s | (ঘুমন্ত) | acquire, read bal=500 | 500 |
| 33 s | write bal=550, release | 550 | |
| 54 s | জেগে (নিজের হিসেবে ৩ s) write bal=560 | 560 ✕ |
B-র fee হারাল — lost update: A পুরনো snapshot-এ গণনা করে overwrite
করেছে। Fencing-সহ: একই ঘটনায় A-র token 17, B-র 18; t=54-এ
A-র write guard-এ আটকায় (17 \< 18) — ledger থাকে 550, সঠিক।
A-র দুই বৈধ পথ: কাজ বাদ (lock ছিল efficiency-র জন্য), বা fresh
acquire + recompute (correctness দরকারে)। আর লক্ষ্য করুন — A-কে
lease-মৃত্যুর খবর কেউ বলেনি; খবরটা পৌঁছায় rejected-write-এ। এই
asymmetry-ই distributed lock-এর core mental model।
ছবিতে
LOCK SERVICE WORKER A STORAGE (guard)
┌─────────────┐ acquire ┌──────────┐ ┌──────────────┐
│ │◄──────────│ │ │ last_seen=? │
│ token=17 │ │ read │───read────────►│ │
│ ttl=30s │ │ bal=500 │ │ │
│ [LEASE A] │ │ ▒▒▒▒▒▒▒ │ │ │
└─────┬───────┘ │ ▒GC▒▒▒▒ │ ← ৫৪ সেকেন্ড │ │
│ ttl expires │ ▒pause▒ │ এক ঝলকে │ │
▼ │ ▒▒▒▒▒▒▒ │ │ │
┌─────────────┐ acquire │ ▒▒▒▒▒▒▒ │ │ │
│ token=18 │◄──────────────────────│ B ─────────────│──write(18)──►│
│ [LEASE B] │ │ │ │ accept, 18 │
└─────────────┘ │ │ ├──────────────┤
│ জেগে উঠে │──write(17)───►│ 17 ≤ 18 │
│ bal=560 │ │ ★ REJECTED ★ │
└──────────┘ └──────────────┘A-র সব সিদ্ধান্ত তার নিজের বিশ্বাস থেকে (লক আমার); শেষ সত্য-
পরীক্ষা storage-এর এক line-এ — last_seen-এর সাথে তুলনা।
নিজে চালিয়ে দেখুন
pg_advisory_lock-এর session-fate, আর thread-based GC-pause simulation-এ fencing-এর পার্থক্য
ধাপ ১ — দুই session, এক advisory lock:
# Terminal 1 — holder:
psql -d postgres -c 'SELECT pg_advisory_lock(424242);' # ধরে রাখে
# Terminal 2 — চেষ্টাকারী (প্রথমবার):
psql -d postgres -c 'SELECT pg_try_advisory_lock(424242);' # → f (deny)
# Terminal 1-এর session খুন করুন:
psql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
WHERE datname='postgres' AND pid <> pg_backend_pid();"
# Terminal 2 — আবার:
psql -d postgres -c 'SELECT pg_try_advisory_lock(424242);' # → t (মুক্ত!)কোনো TTL সেট করেননি, unlock চালাননি — connection-মৃত্যুই release- event। এটাই lease-semantics-এর পরিচ্ছন্নতম বাস্তবায়ন; DB-advisory lock-এ GC-pause-দুশ্চিন্তাও ছোট (জীবিত connection ধরে রাখে, মরলেই সত্যিই মুক্ত)।
ধাপ ২ — GC-pause simulation, fencing on/off:
"""gc_pause_fencing.py — python3 gc_pause_fencing.py fenced|unfenced"""
import sys, threading, time
TTL, PAUSE = 0.30, 0.60 # pause ইচ্ছা করে TTL ছাড়িয়ে যায়
FENCED = (sys.argv[1] if len(sys.argv) > 1 else 'unfenced') == 'fenced'
tok, expires, cv = [0], [0.0], threading.Condition()
def acquire():
with cv:
now = time.monotonic()
if expires[0] > now:
return None # live lease আছে
tok[0] += 1 # fencing token: একমুখী বৃদ্ধি
expires[0] = now + TTL
return tok[0]
balance = {'v': 500} # shared ledger
last_seen, glock, trail = [0], threading.Lock(), []
def guarded_write(writer, new_value, my_tok):
if FENCED:
with glock:
if my_tok <= last_seen[0]:
return False # ★ stale token — reject
last_seen[0] = my_tok
balance['v'] = new_value
trail.append((writer, new_value))
return True
def worker_a():
my_tok = acquire() # token 1, তারপর read —
snap = balance['v'] # B শুরুর আগেই হয়ে যায়
print(f'A: tok={my_tok}, read={snap}, pause {PAUSE}s...')
time.sleep(PAUSE) # ← THE PAUSE (lease মরে যায়)
planned = snap - 60 # পুরনো snapshot-এ হিসাব!
ok = guarded_write('A', planned, my_tok)
print(f'A: woke, write {planned} tok={my_tok} → {"ACCEPTED" if ok else "REJECTED"}')
def worker_b():
while True:
my_tok = acquire()
if my_tok: break
time.sleep(0.05) # lease ফুরোলেই পাবে
ok = guarded_write('B', balance['v'] - 50, my_tok)
print(f'B: tok={my_tok} → {"ACCEPTED" if ok else "REJECTED"}')
a = threading.Thread(target=worker_a); a.start()
b = threading.Thread(target=worker_b); b.start()
a.join(); b.join()
print(f'final = {balance["v"]} (expected: 450)')
assert balance['v'] == 450, 'LOST UPDATE — stale write overwrote fresh!'
print(f'PASS [{sys.argv[1] if len(sys.argv)>1 else "unfenced"}] — {trail}')$ python3 gc_pause_fencing.py unfenced
A: tok=1, read=500, pause 0.6s...
B: tok=2 → ACCEPTED
A: woke, write 440 tok=1 → ACCEPTED
final = 440 (expected: 450)
AssertionError: LOST UPDATE
$ python3 gc_pause_fencing.py fenced
B: tok=2 → ACCEPTED
A: woke, write 440 tok=1 → REJECTED
final = 450 (expected: 450)
PASS [fenced]তিন পাঠ: unfenced run-এ A কোনো exception পায়নি — silent corruption-ই এই বিপদের স্বভাব; reject এলো storage-guard থেকে, lock-service থেকে না — theory-র হুবহু প্রতিধ্বনি; আর বাস্তব PAUSE হতো ৫০ ms GC, TTL ৩০ s — ratio আলাদা, গঠন একই।
Session মরলে pg_advisory_lock নিজে নিজে মুক্ত হয় — lock-জীবন connection-জীবনে বাঁধা; আর GC-pause simulation-এ fencing ছাড়া stale holder সত্যিই lost-update লেখে, fencing চালু হলে সেই write সত্যিই reject হয়।
নিজে বানান
LeaseLock — lease-granting server, injectable pause, fencing guard, chaos test
- SimClock — virtual clock যা tick(ms) দিয়ে এগোয়, chaos test deterministic
- LockServer.acquire — live lease থাকলে deny, নাহলে monotonic token সহ TTL lease
- LockServer.release — শুধু নিজের token দিয়েই release করা যায়
- GuardedResource.write — fencing চালু হলে stale token reject, বন্ধ থাকলে সব accept
- chaos_run — holder-এর মাঝপথে TTL-ছাড়িয়ে pause inject করে property যাচাই
"""leaselock.py — simulated-clock chaos lab for lease locks"""
class SimClock:
def __init__(self):
self.now = 0 # ms
def tick(self, ms):
self.now += ms
class LockServer:
def __init__(self, clock, ttl_ms=100):
self.clock, self.ttl = clock, ttl_ms
self.leases = {} # res -> [holder, token, expiry]
self.last_token = {} # res -> monotonic counter
def acquire(self, res, client):
h = self.leases.get(res)
if h and h[2] > self.clock.now: # live lease — deny
return None
tok = self.last_token.get(res, 0) + 1
self.last_token[res] = tok # একমুখী, কখনো reset না
self.leases[res] = [client, tok, self.clock.now + self.ttl]
return tok # ← fencing token
def release(self, res, client, tok):
h = self.leases.get(res)
if h and h[:2] == [client, tok]:
del self.leases[res]
class GuardedResource:
"""storage — fencing চালু হলে stale token ফেরত"""
def __init__(self, fenced):
self.fenced, self.seen, self.writes = fenced, 0, []
def write(self, holder, token):
if self.fenced and token <= self.seen:
return False # ★ stale writer blocked
self.seen = max(self.seen, token)
self.writes.append(holder)
return True
def chaos_run(fenced):
clock = SimClock()
srv = LockServer(clock, ttl_ms=100)
res = GuardedResource(fenced)
tok_a = srv.acquire('stock', 'A') # A-র lease: token 1
clock.tick(40) # A কাজ করছে...
clock.tick(90) # ...GC pause! (130 > TTL 100)
tok_b = srv.acquire('stock', 'B') # lease মরা — B পায় token 2
ok_b = res.write('B', tok_b)
ok_a = res.write('A', tok_a) # A জেগে লেখার চেষ্টা
srv.release('stock', 'B', tok_b) # B ছাড়ল — crash-safe handoff
assert srv.acquire('stock', 'C') == 3
return ok_b, ok_a, res.writes
if __name__ == '__main__':
b1, a1, w1 = chaos_run(fenced=False)
print(f'unfenced: B={b1}, stale-A={a1}, trail={w1}')
assert (b1, a1) == (True, True), 'দুই writer-ই ঢুকেছে!'
b2, a2, w2 = chaos_run(fenced=True)
print(f'fenced : B={b2}, stale-A={a2}, trail={w2}')
assert (b2, a2) == (True, False), 'fencing stale-A আটকাতেই হবে!'
print('PASS — no-two-successful-writers টিকেছে শুধু fencing-এ')নমুনা output:
unfenced: B=True, stale-A=True, trail=['B', 'A']
fenced : B=True, stale-A=False, trail=['B']
PASS — no-two-successful-writers টিকেছে শুধু fencing-এতিন পাঠ: SimClock-ই সস্তা chaos-engineering — wall-clock-race নয়,
সময় হাতে ধরে exact দুর্ঘটনা replay; last_token কখনো reset হয় না
(reset করলে fencing-ক্রম ভাঙে); আর property-টা ছোট — successful
writers ≤ 1, যা টেকে শুধু guard-সহ অবস্থায়।
নিজে বাড়ান:
- Background renewal-thread যোগ করুন — renewal miss-এর পরিণতি chaos-test-এ দেখান
clock.jump_back(-50)injection — fencing কেন clock-jump-এও অটুট, প্রমাণ করুন- Guard-বিহীন resource simulate করুন — lock-shudhu-efficiency দর্শন সেখানেই সৎ
বাস্তব সিস্টেমে
বাস্তবে distributed lock
Redis SET NX PX + Redlock। সবচেয়ে বহুল ব্যবহৃত stack —
single-instance lock efficiency-র কাজে দুর্দান্ত (cache-fill dedup,
cron-leader pick); correctness-critical লেখায় fencing-ছাড়া ব্যবহার
না-করার দিকেই শিল্প-consensus ঝুঁকেছে।
Kubernetes Lease objects। controller-manager, scheduler — K8s-
এর নিজের component-রা leader election করে Lease API-তে: holder
identity + renewTime; মেয়াদ ফুরোলে অন্যরা প্রতিদ্বন্দ্বী —
cloud-native জগতের ZooKeeper-session-উত্তরাধিকার, operator-লেখকদের
জন্যও একই pattern।
etcd ও ZooKeeper। Consensus-backed coordination stores — etcd- র concurrency package সরাসরি session+lock recipe দেয়, revision number ready-made fencing source; heavy-duty correctness এদের ডাকে।
Postgres advisory locks। Oban (Elixir), GoodJob (Ruby), pg-boss (Node) — আধুনিক job-queue framework-এর পুরো ধারা advisory-lock-এই: job dedup, cron-leader, worker claim — fate-sharing যুক্তির শিল্প- বিজয়। আর Google-এর Chubby (2006) lock+membership service GFS/Bigtable- কে ভিত দিয়েছিল; ZooKeeper তার খোলা-উৎস প্রতিধ্বনি — lock সস্তা, তার প্রতিশ্রুতির সীমা জানাই engineering।
যে ভুলগুলো সবাই করে
“Redis-এ SET NX PX করলেই mutual exclusion নিশ্চিত — lock তো পেয়েছি।”
Acquire-confirmation মানে ভবিষ্যতের একচেটিয়া দখল নয় — TTL ফুরোলে server আপনার অজান্তেই অন্যকে দিতে পারে, আর pause থেকে জেগে আপনি একই ভাবে চলতে থাকেন। Lock পাওয়া momentary fact, continuing guarantee না; guarantee আসে resource-side fencing check থেকে।
“TTL বড় দিলেই সমস্যা মিটে যায় — ১০ মিনিট দাও, pause ঢাকা পড়বে।”
TTL বাড়া মানে crash-recovery দেরি বাড়া — মৃত holder-এর lock সবার কাজ TTL পর্যন্ত বন্ধ রাখে। আর pause-এর upper-bound নেই: swap, checkpoint, VM-stun যে-সংখ্যাই ছাড়িয়ে যেতে পারে। TTL liveness-tuning knob, safety-mechanism না; safety-র ঢাল আলাদা — fencing।
“Fencing token ব্যবহার করলে lock আর দরকার নেই — token-ই সব করে।”
Token নিজে কিছুই করে না — মূল্য তখনই, যখন resource check করে। Check-না-করা storage-এ (S3 PUT, third-party API) token সাজানো সংখ্যা মাত্র। উল্টোটাও সত্য: token-checking storage থাকলে lock-এর কাজ মূলত token-issue — তখন সস্তা single-node lock-ই যথেষ্ট, Redlock- বাহুল্য লাগে না।
“ZooKeeper/etcd-consensus আছে বলে ওদের lock সবসময় সঠিক — fencing-দুশ্চিন্তা সেখানে নেই।”
Consensus নিশ্চিত করে lock-service-এর নিজের অবস্থা consistent — client-GC-pause-এর বিরুদ্ধে নয়। Session-timeout ফুরোলে ephemeral znode মুছে অন্য client lock পেতে পারে, অথচ পুরনো client জেগে কাজ চালিয়ে যেতেই পারে — হুবহু Alice-দৃশ্য। ZK-recipe-ও sequence- number-as-fencing-token পর্যন্তই পূর্ণ; consensus store বেসলাইন শক্ত, জাদুর বাঁশ নয়।
বুঝেছেন কি না দেখুন
1Mutex-এর কোন দুটো ভিত্তি-অনুমান distributed lock-এ ভাঙে, আর প্রতিটা
ভাঙন কোন concrete ব্যর্থতা জন্ম দেয়?
যুক্তি
এক: shared memory নেই — lock-state পৌঁছায় message-এ, আর message দেরিতে/হারায়, তাই client-এর জ্ঞান সম্ভাব্য-পুরনো (stale belief); থেকে যায় ghost-write। দুই: মৃত্যু সরাসরি শনাক্ত অসম্ভব — pause আর crash বাইরে থেকে একরকম; থেকে যায় মৃত holder-এর lock-চিরদখল বা জীবিতকে ভুল বহিষ্কার। প্রথমের ঔষধ fencing, দ্বিতীয়টার ঔষধ lease।
2Service token 41 A-কে, তারপর 42 B-কে দিয়েছে; storage last_seen=42।
এখন আসলো A(token 41), B(token 42 আবার), C(token 39 — পুরনো cache)।
কে ঢুকবে, কেন?
প্রয়োগ
A: 41 ≤ 42 → reject। C: 39 ≤ 42 → reject — সবচেয়ে পুরনো। B:
strict-rule-এ 42 ≤ 42 → reject — duplicate delivery idempotency-
guard-এ ধরা পড়ে; চাইলে equal-token আলাদা handle, কিন্তু stale-নয়।
মূল নিয়ম এক: accept শুধু strictly-greater token; একমুখী ক্রম কখনো
ভাঙে না।
3Kleppmann-এর Redlock-আপত্তি এক বাক্যে কী, antirez-এর শক্ততম জবাব
কোনটা? মতভেদটা ঠিক কোন অনুমানের উপর?
যুক্তি
Kleppmann: Redlock-এর safety timing-assumption-এ দাঁড়িয়ে — bound ভাঙলে দুই holder সম্ভব, তাই fencing অমিটযোগ্য, আর fencing লাগলে Redlock-complexity-র দরকারই নেই। antirez: বাস্তব semi-synchronous — drift মাপা যায়, margin দেওয়া যায়; fencing-সহ Redlock progress-সহ বৈধ choice। কেন্দ্র: system-model — asynchronous না semi-synchronous।
4Redlock N=5, majority=3। Node-3 স্থায়ীভাবে মরেছে — acquire এখনো
সম্ভব? Validity থেকে drift-margin কেন বিয়োগ হয়; margin=0 রাখলে
ঠিক কোন দুর্ঘটনা?
প্রয়োগ
সম্ভব — বাকি ৪ node-এ চেষ্টা, ≥3 পেলেই acquired; এক-মৃত্যু tolerated (দুই-মৃত্যুতে majority নেই)। Margin কাটা হয় কারণ acquire-এর সময়খরচ (RTT×5) TTL খায় আর node-ঘড়ি আলাদাভাবে এগোয়: safe validity = ttl − elapsed − drift। Margin=0-তে slow-acquire/এগিয়ে-ঘড়িতে validity কাগজে বেঁচে, বাস্তবে ফুরোয় — client বিশ্বাস করে lock বেঁচে, অথচ অন্যের কাছে সব lease শেষ।
5Nightly-report job দিনে একবার, দুই replica deploy (দুটোই cron পায়),
লেখে শুধু report-file — shared DB নেই। Lock-strategy কী, fencing
লাগবে? উল্টো: একই job billing-ledger-এ লেখত?
ডিজাইন
Report-file: lock এখানে efficiency — duplicate হলে double compute,
correctness নয় (deterministic overwrite)। সস্তা Redis lock বা leader-
lease যথেষ্ট, fencing-অপ্রয়োজনীয় — শর্ত: idempotent/atomic-rename
write। Billing-ledger: correctness — double charge; তখন ledger-এ
fencing-column সহ conditional write (WHERE token > last) বা
transaction-ভেতরে dedup-unique-constraint; lock-কে কখনো একাই safety-
র ভরসা করা যাবে না।
এরপর কী
এরপর কী
Lock ছিল coordination-এর সবচেয়ে সরাসরি হাতিয়ার — একজন একসময়ে,
বাকিরা অপেক্ষায়। পরের লেসনে দিক উল্টে যাবে: coordination-কেই বাদ
দিয়ে convergence-এর গণিত। Gossip protocol — coordinator ছাড়া খবর
O(log N) round-এ cluster ছড়ায় কীভাবে — আর CRDT:
commutative+associative+idempotent merge-এ প্রতিটা replica শেষে
হুবহু এক state-এ মেলে। Redlock-বিতর্ক বলল safety-র জন্য coordination
লাগে — CRDT সেই প্রশ্নের AP-পাশের উত্তর: coordination বাদ, গণিত সামনে।
আরও পড়ুন
- Designing Data-Intensive Applications, Chapter 8 — Troubles with Distributed Systems — Martin Kleppmann · Process pause, unreliable clock আর lease-এর বিপদ — fencing token-ধারণার প্রধান উৎস
- Designing Data-Intensive Applications, Chapter 9 — Consistency and Consensus — Martin Kleppmann · Linearizable lock-এর প্রতিশ্রুতি, failure detector-এর সীমা, ZooKeeper/etcd-জাতীয় coordination service
- Highly Available Transactions: Virtues and Limitations (VLDB 2013) — Peter Bailis et al. · Coordination ছাড়া কোন guarantee সম্ভব আর কোনটার জন্য lock/consensus অমিটযোগ্য — সেই boundary
- Dynamo: Amazons Highly Available Key-value Store (SOSP 2007) — Giuseppe DeCandia et al. · Production-scale system কীভাবে long-lived distributed lock এড়িয়ে চলে — lock-free alternative-এর প্রামাণ্য উদাহরণ