সরাসরি পর্বে যান
পর্ব ৪৩ · ৬৬-এর মধ্যে · মডিউল ৭

Coordination — distributed lock, lease আর fencing token

Coordination — Distributed Locks, Leases & Fencing Tokens

Taught in Bangla, this lesson separates efficiency locks from correctness locks, explains why every lock is a lease with a TTL and keep-alive, the core ideas of ZooKeeper, etcd and Chubby, lock and leader-election recipes with ephemeral-sequential nodes and lease revisions, the paused-holder danger and fencing tokens, Redis SET NX PX and both sides of the Redlock debate, choosing lease length, and lock-free alternatives such as CAS and single-writer designs.

এই পর্বে যা শিখবেন

  • কোন কাজে coordination লাগে, আর efficiency বনাম correctness lock-এর পার্থক্য
  • ZooKeeper, etcd আর Chubby-র মূল ধারণা — ephemeral আর sequential node, watch, lease, revision-ভিত্তিক transaction
  • Lock আর leader election-এর প্রচলিত রেসিপি, আর কেন প্রত্যেকে শুধু আগের জনকে দেখে
  • থেমে থাকা lock-ধারীর বিপদ, fencing token দিয়ে তার প্রতিকার, আর Redis lock ও Redlock বিতর্কের দুই পক্ষ
  • Lease-এর মেয়াদ বাছাইয়ের হিসাব, ঘড়ির অনুমান, আর lock ছাড়াই সমস্যা মেটানোর চারটি উপায়

১ · কেন coordination — একজনই, একবারই, একই তথ্য

একটি কাল্পনিক ride-sharing অ্যাপের কথা ভাবুন। প্রতি রাতে চালকদের দিনের আয় হিসাব করে তাঁদের wallet-এ টাকা পাঠানোর একটি job চলে। একটি মেশিন বিকল হলেও যাতে টাকা যায়, তাই তিনটি মেশিনে তিনটি scheduler. এখন উল্টো বিপদ: তিনজনই job ছাড়লে প্রত্যেক চালক তিনগুণ টাকা পাবেন। দরকার ঠিক একজন কাজটি করবে, আর সে মরলে আরেকজন দায়িত্ব নেবে। পর্ব ৩৭ — Background job, scheduler আর workflow engine-এ এই সমস্যা প্রথম দেখেছিলেন; এই পর্বে তার পূর্ণ তত্ত্ব।

এ ধরনের সমঝোতাকে বলে coordination: কয়েকটি স্বাধীন মেশিনকে একটি বিষয়ে একমত রাখা। চারটি চাহিদা বারবার আসে:

  • একজন নেতা: একটি scheduler, একটি database-এর primary (পর্ব ২২ — Replication), একটি partition-এর মালিক। এর নাম leader election.
  • একটি সম্পদে একজনের অধিকার: একটি ফাইল একজনই লিখবে, একটি বাইরের API-কে একসাথে একজনই ডাকবে। এটি mutual exclusion, আর অনেক মেশিন জুড়ে তার যন্ত্র distributed lock.
  • সবার কাছে একই config: কোন feature চালু, কোন shard কোন সার্ভারে (পর্ব ২৩ — Partitioning আর sharding) — বদল হলে সবাইকে জানানো।
  • কে কোথায় আছে: কোন সার্ভিসের কোন instance এখন জীবিত, কোন ঠিকানায় — service discovery. এর পূর্ণ নকশা পর্ব ৪৭ — Monolith বনাম microservices-এ; কে বেঁচে আছে তা জানার কৌশল পর্ব ৪৪ — Membership আর failure detection-এ।

ফেরিঘাটে একটিই র‍্যাম্প, আর সিরিয়াল-খাতা একটি থাকলে ঝগড়া নেই। কিন্তু তিনজন রক্ষক তিনটি খাতা নিয়ে, যোগাযোগ শুধু মাঝে মাঝে হারানো চিরকুটে — তাহলে দুটি ট্রাক একসাথে উঠতে পারে। Distributed coordination এই সমস্যা: তিন খাতা একটির মতো আচরণ করবে, অথচ একজন রক্ষক অসুস্থ হলেও ঘাট চলবে।

একটি মেশিনে সহজ, অনেক মেশিনে কঠিন — কেন

একটি মেশিনের ভেতরে lock পুরোনো, পরিচিত জিনিস: CPU-র atomic instruction দিয়ে বানানো mutex (OS কোর্সের Atomic instruction আর spinlock আর Mutex আর semaphore পর্ব), আর একে অপরের lock-এর অপেক্ষায় আটকে যাওয়ার বিপদ deadlock (Deadlock পর্ব)। সেখানে তিনটি সুবিধা আছে, যা অনেক মেশিনে নেই:

  1. একটি shared memory: lock-এর অবস্থা এক জায়গায়, সবাই একই মান দেখে। অনেক মেশিনে অবস্থাটি network-এর ওপাশে, আর বার্তা দেরিতে আসে, হারায়।
  2. মৃত্যুর নিশ্চিত খবর: একটি process মরলে OS জানে, তার lock ছেড়ে দিতে পারে। অনেক মেশিনে উত্তর না এলে বোঝার উপায় নেই — মেশিন মৃত, ধীর, না network কাটা (পর্ব ৩৮ — Distributed system-এর কঠিন সত্য)।
  3. একটি ঘড়ি: “৫ সেকেন্ড পরে” সবার কাছে একই। অনেক মেশিনে প্রত্যেকের ঘড়ি একটু আলাদা গতিতে চলে, আর কখনো লাফ দেয় (পর্ব ৩৯ — সময় আর ক্রম)।

এই তিনটি অভাবই পুরো পর্বের সব জটিলতার উৎস।

প্রথম প্রশ্ন: lock-টি কীসের জন্য — efficiency না correctness?

Martin Kleppmann ২০১৬ সালের একটি প্রকাশ্য লেখায় (“How to do distributed locking”) একটি সহজ কিন্তু শক্তিশালী ভাগের কথা বলেন, যা আজ প্রায় সব আলোচনার শুরু:

প্রশ্নEfficiency lockCorrectness lock
উদ্দেশ্যএকই কাজ দুবার না করা — খরচ বাঁচানোদুজন একসাথে করলে ফল ভুল হয় — তা ঠেকানো
উদাহরণCache-এর মান একজনই আবার হিসাব করবে (পর্ব ২৯), একটি রিপোর্ট একবার বানানোএকই স্টকের শেষ পণ্য দুজনকে বিক্রি না করা, একটি ফাইল একজনই লিখবে
Lock ব্যর্থ হলেমাঝে মাঝে বাড়তি কাজ, হয়তো একটি ইমেইল দুবারডেটা নষ্ট, টাকা হারানো বা দুবার কাটা
যথেষ্ট ব্যবস্থাএকটি সাধারণ lock (যেমন একটি Redis instance)Consensus-চালিত সেবা + fencing token, বা lock ছাড়া নকশা

উত্তর না জেনে lock বাছা মানে হয় অকারণে ভারী ব্যবস্থা, নয়তো অনিরাপদ হালকা ব্যবস্থা।

২ · Lock-এর মেয়াদ কেন — lease, TTL আর keep-alive

সবচেয়ে সরল distributed lock: database-এর একটি সারি locks(name, holder). A লিখল “nightly-payout-এর ধারক A”, কাজ শেষে মুছে দেবে। এখন A-র মেশিন কাজের মাঝে বন্ধ হয়ে গেল। সারিটি কে মুছবে? A আর নেই; অন্যরা জানে না A মৃত না ধীর। Lock চিরকাল ধরা থাকল, রাতের পেমেন্ট আর কখনো চলবে না — যতক্ষণ না কেউ হাতে মোছেন।

সমাধান: lock-কে চিরস্থায়ী না করে একটি মেয়াদ দেওয়া। এটিই lease — “এই অধিকার আগামী ১০ সেকেন্ডের জন্য এই client-এর” ধরনের প্রতিশ্রুতি। মেয়াদকে বলে TTL. ধারক বেঁচে থাকলে মেয়াদ ফুরানোর আগেই নবায়ন করে — keep-alive বা heartbeat; মরে গেলে নবায়ন থামে, lease নিজে ফুরায়, আর অন্য কেউ lock নিতে পারে। কেউ কাউকে “মৃত” ঘোষণা করে না — শুধু সময় পেরোয়।

হোটেলের রুমের card-চাবি চেক-আউটের সময় পেরোলে আর দরজা খোলে না। অতিথি থাকতে চাইলে রিসেপশনে গিয়ে মেয়াদ বাড়ান (keep-alive). অতিথি না জানিয়ে চলে গেলে হোটেলকে তাঁকে খুঁজতে হয় না; মেয়াদ শেষে রুম আপনাআপনি পরের জনের জন্য খালি।

Lease শব্দটি distributed systems-এ এসেছে Cary Gray আর David Cheriton-এর ১৯৮৯ সালের SOSP paper “Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency” থেকে। সেখানে প্রশ্ন ছিল file cache-এর মিল রাখা, কিন্তু মূল চিন্তা আজও একই: সময়সীমাসহ অধিকার দিলে বিকল ধারকের জন্য চিরকাল অপেক্ষা করতে হয় না — দাম হলো ঘড়ির ওপর কিছুটা নির্ভরতা।

Lease-এর তিনটি নিয়ম

  1. নবায়ন মেয়াদের অনেক আগে: প্রচলিত অভ্যাস প্রতি TTL/৩-এ একবার, যাতে একটি-দুটি নবায়ন হারালেও lease না ফুরায়।
  2. ধারক নিজেকে আগেভাগে থামায়: সে মেয়াদ গোনে অনুরোধ পাঠানোর আগের মুহূর্ত থেকে, সেবা গোনে অনুমতি দেওয়ার মুহূর্ত থেকে; তাই ধারকের হিসাবে lease আগে শেষ হয় — নিরাপদ দিকে ভুল (অংশ ৮)।
  3. মেয়াদ শেষে কাজ থামানো: নবায়ন ব্যর্থ হলে বা মেয়াদ ফুরালে ধারক lock-এর অধীন সব কাজ বন্ধ করবে।

তৃতীয় নিয়মটি শুনতে সহজ, কিন্তু এর ফাঁকেই এই পর্বের সবচেয়ে বড় বিপদ লুকানো: ধারক যদি এমনভাবে থেমে যায় যে সে নিজেই জানে না সময় পেরিয়েছে? সেই প্রশ্নে অংশ ৬-এ ফিরব। তার আগে দেখি lease কে দেয়।

৩ · Coordination service — ZooKeeper, etcd আর Chubby

Lock-এর অবস্থা যে মেশিনে থাকে, সেটি বিকল হলে সব lock-ও বিকল — নতুন একটি single point of failure. তাই lock-এর তথ্য কয়েকটি মেশিনে কপি করতে হয়, এমনভাবে যে কপিগুলো কখনো দুটি ভিন্ন উত্তর দেবে না। এটিই consensus-এর কাজ: সাধারণত ৩ বা ৫টি node, যার বেশিরভাগ (quorum) সচল থাকলে সিদ্ধান্ত চলে। ৩টিতে একটি, ৫টিতে দুটি node বিকল হলেও সেবা চলে। Raft কীভাবে leader বাছে আর log মেলায়, তা পর্ব ৪২ — Consensus-এ; এখানে আমরা সেই যন্ত্রের ওপর বানানো সেবা দেখব — coordination service.

এই সেবাগুলোর লেখা linearizable (পর্ব ৪০ — CAP, PACELC আর consistency model): একটি লেখা সফল হলে পরের সবাই সেটি দেখবে, যেন একটিই কপি। Lock-এর জন্য এটি অপরিহার্য — দুজন একসাথে “lock খালি” দেখলে সব শেষ। তিনটি প্রভাবশালী নকশা:

Chubby (Google)

Mike Burrows-এর OSDI ২০০৬ paper “The Chubby lock service for loosely-coupled distributed systems”-এ বর্ণিত। একটি Chubby cell-এ সাধারণত পাঁচটি replica, ভেতরে Paxos. ছোট ফাইলের মতো interface, যার ওপর lock. Paper অনুযায়ী GFS আর Bigtable master বাছাইয়ে এটি ব্যবহার করত; লক্ষ্য ঘণ্টা বা দিন ধরে রাখা lock.

প্রকাশিত paper · বন্ধ উৎস

ZooKeeper (Apache)

Yahoo!-তে শুরু; Hunt, Konar, Junqueira ও Reed-এর USENIX ATC ২০১০ paper “ZooKeeper: Wait-free coordination for Internet-scale systems”. ZooKeeper নিজে lock দেয় না — দেয় কয়েকটি primitive, যা দিয়ে client lock, election, queue বানায়। ভেতরে Zab নামের broadcast protocol.

open source · Java

etcd

CoreOS ২০১৩ সালে শুরু করে; ভেতরে Raft. Kubernetes তার পুরো cluster-এর অবস্থা etcd-এ রাখে। Key-value interface, প্রতিটি বদলে বাড়া একটি revision নম্বর, TTL-সহ lease আর শর্তসহ transaction.

open source · Go

পাঁচটি মূল ধারণা

নাম আলাদা, কিন্তু তিনটি সিস্টেমের ধারণাগুলো প্রায় এক। ZooKeeper-এর ভাষায় শুরু করি, তারপর etcd-এর সমতুল্য।

  • Znode আর key: ZooKeeper-এর ডেটা ফাইল সিস্টেমের মতো একটি গাছ — /lock/payout; প্রতিটি ঘর একটি znode, তাতে অল্প ডেটা (ডিফল্ট সর্বোচ্চ প্রায় ১ MB — সংস্করণভেদে যাচাই করুন)। etcd-এ সমতল key space, prefix দিয়ে একসাথে পড়া যায়।
  • Session আর ephemeral node: প্রতিটি ZooKeeper client-এর একটি session, যা client নিয়মিত heartbeat দিয়ে জীবিত রাখে। Session-এর মেয়াদ (session timeout) পেরোলে সেই client-এর বানানো সব ephemeral node সেবা নিজেই মুছে দেয়। অর্থাৎ ephemeral node হলো lease-এ বাঁধা তথ্য। etcd-এ একই কাজ করে lease: LeaseGrant দিয়ে TTL-সহ একটি lease, key-কে তার সাথে জোড়া, আর LeaseKeepAlive দিয়ে নবায়ন; lease ফুরালে জোড়া সব key মুছে যায়।
  • Sequential node: ZooKeeper-এ “sequential” পতাকা দিয়ে node বানালে নামের শেষে সেবা একটি বাড়তে থাকা ১০ অঙ্কের নম্বর বসায় — lock-0000000007. এই নম্বর বলে কে আগে এসেছে। etcd-এ প্রতিটি key-এর create_revision একই কাজ করে।
  • Watch: client বলতে পারে “এই node বদলালে বা মুছলে আমাকে জানানো হোক”। ZooKeeper-এর ক্লাসিক watch একবারের — একবার জানিয়ে শেষ, আবার চাইতে হয় (৩.৬ সংস্করণ থেকে স্থায়ী watch-ও আছে বলে documentation-এ বলা)। etcd-এ watch একটি চলমান stream, আর নির্দিষ্ট revision থেকে শুরু করা যায়, তাই মাঝের বদল হারায় না।
  • শর্তসহ লেখা: ZooKeeper-এ setData(path, data, version) শুধু তখনই সফল, যখন node-এর version এখনো সেটি। etcd-এ পূর্ণ transaction: If(compare) Then(ops) Else(ops), যেখানে compare হতে পারে একটি key-এর mod_revision, create_revision, version বা মান — একটি compare-and-set.
ধারণাZooKeeperetcdChubby (paper অনুযায়ী)
ভেতরের consensusZabRaftPaxos
জীবিত থাকার চুক্তিsession + heartbeatlease + keep-alivesession + KeepAlive
“আমি থাকলে থাকবে”ephemeral znodelease-এ জোড়া keyephemeral ফাইল
আগমনের ক্রমsequential znodecreate_revisionlock generation নম্বর
বদলের খবরwatch (মূলত একবারের)watch streamevent
শর্তসহ লেখাversion-সহ setData, multiTxn(If/Then/Else)lock-এর ওপর নির্ভর

কী রাখবেন, কী রাখবেন না

প্রতিটি লেখা leader হয়ে বেশিরভাগ node-এ টেকসইভাবে লেখা হয়, তাই লেখার গতি সীমিত। এই সেবা ছোট, অতি গুরুত্বপূর্ণ তথ্যের জন্য — কে leader, কোন lock কার, কোন partition কোথায়, কোন config চালু; ব্যবহারকারীর ডেটা বা সেকেন্ডে লাখো লেখা এখানে নয়। Kafka বহু বছর ZooKeeper-এ metadata রাখত, কিন্তু ২০২৫ সালের ৪.০ সংস্করণ থেকে নিজের Raft-ভিত্তিক KRaft ব্যবহার করে (লেখার সময়ের তথ্য)।

নিচের ছবিটি এই পর্বের মানচিত্র: ওপরে দুটি client, মাঝে তিন node-এর coordination service, নিচে সেই storage যাকে lock রক্ষা করে — আর storage নিজেই token যাচাই করছে।

Client, coordination service আর fencing token যাচাই করা storage

Clients take a lease and a fencing token from a 3-node ensemble; the storage remembers the highest token it has seen and rejects older ones

Distributed lock: client, coordination service আর storage-এর fencing যাচাই Client AClient B GC-তে থেমে · token 33এখন ধারক · token 34 lock · keep-alive lease + token Coordination service · ৩-node ensemble LeaderFollowerFollower লেখা নেয়কপিকপি Raft · Zab · Paxos — majority ২/৩ ephemeral node · lease · watch · revision Storage · database বা ফাইল সর্বোচ্চ দেখা token = 34 34 ≥ 34 → গৃহীত 33 < 34 → প্রত্যাখ্যাত বাঁয়ে A-এর লেখা · ডানে B-এর লেখা নীল: lock চাওয়া · বেগুনি: lease আর token সবুজ: নতুন token-এর লেখা লাল ড্যাশ: থেমে থাকা ধারকের পুরোনো লেখা
ছবি ৪৩.১ — Client lease আর একটি fencing token পায় coordination service থেকে, যার ভেতরে তিনটি node consensus দিয়ে একই অবস্থা রাখে (অংশ ৩)। কিন্তু শেষ পাহারা storage-এর: সে সর্বোচ্চ দেখা token মনে রাখে, আর থেমে থাকা A-এর পুরোনো token 33-এর লেখা ফিরিয়ে দেয় (অংশ ৬)।

৪ · Lock-এর দুটি রেসিপি — ephemeral-sequential সারি আর lease + revision

রেসিপি ১ · ZooKeeper-এর lock সারি

ZooKeeper-এর documentation-এর “Recipes” অংশে lock-এর একটি প্রচলিত পদ্ধতি আছে। ভাবুন ব্যাংকের কাউন্টারে টোকেন-যন্ত্র: প্রত্যেকে একটি ক্রমিক টোকেন নেন, সবচেয়ে ছোট টোকেন যাঁর হাতে, তিনি কাউন্টারে। ধাপগুলো:

  1. /lock-এর নিচে ephemeral আর sequential একটি node বানানো — যেমন /lock/lock-0000000003. Ephemeral, যাতে client মরলে node নিজে মোছে; sequential, যাতে আগমনের ক্রম থাকে।
  2. /lock-এর সব child পড়া (watch ছাড়া)।
  3. নিজের নম্বর সবচেয়ে ছোট হলে lock আপনার।
  4. নইলে ঠিক আগের নম্বরের node-টির ওপর watch রাখা — শুধু সেটির।
  5. Watch বাজলে সরাসরি lock ধরে নেওয়া নয় — আবার ধাপ ২. কারণ আগের জন হয়তো lock-ধারী ছিলেন না, অপেক্ষার মধ্যেই মরে গিয়েছিলেন।

ছাড়ার সময় ধারক নিজের node মোছেন; মরে গেলে session-এর মেয়াদ শেষে সেবা মুছে দেয়। দুই ক্ষেত্রেই পরের জনের watch বাজে।

কেন শুধু আগের জনকে? সবাই /lock-এর child-তালিকায় watch রাখলে প্রতিটি মোছায় অপেক্ষারত সবাই জাগবে, অথচ lock পাবে একজন। এটি thundering herd — ZooKeeper-এর documentation-এ “herd effect”. N জন client-এর পুরো সারি শেষ হতে মোট জাগানো (N−১) + (N−২) + … + ১ = N(N−১)/২, আর রেসিপিতে মাত্র N−১. নিচের simulator-এ পাঁচজন client, seed স্থির; মাঝের একজন অপেক্ষার সময়ে মরে যায়।

// ZooKeeper-style lock queue: ephemeral-sequential nodes, each waiter watches only its predecessor.
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 rnd = mulberry32(43), arrive = ["C1", "C2", "C3", "C4", "C5"];
for (let i = arrive.length - 1; i > 0; i--) {            // seeded arrival order
  const j = Math.floor(rnd() * (i + 1)); [arrive[i], arrive[j]] = [arrive[j], arrive[i]];
}
const q = arrive.map((c, i) => ({ c, name: "lock-" + String(i).padStart(10, "0") }));
const show = () => q.forEach((n, i) => console.log("  " + n.c + " " + n.name + "  " +
  (i === 0 ? "HOLDS the lock" : "watches " + q[i - 1].name)));
console.log("children of /lock (sorted):"); show();
let recipe = 0, naive = 0;
function remove(i, why) {                              // a node is deleted (release or session expiry)
  const gone = q.splice(i, 1)[0];
  naive += q.length;                                   // naive: everyone watching /lock wakes up
  console.log(`\n${gone.c} ${why}: ${gone.name} deleted`);
  if (i < q.length) {                                  // recipe: only the next node's watch fires
    recipe++;
    console.log(`  notify ${q[i].c} only -> re-reads children -> ` +
      (i === 0 ? "it is lowest: HOLDS the lock" : "not lowest: now watches " + q[i - 1].name));
  }
}
remove(2, "session expired while waiting");            // a waiter in the middle crashes
while (q.length) remove(0, "releases");
const tri = n => n * (n - 1) / 2;
console.log(`\nnotifications for these 5 clients: recipe ${recipe}, naive (all watch /lock children) ${naive}`);
for (const n of [100, 1000]) console.log(`${n} clients, no crash: recipe ${n - 1}, naive ${tri(n)}`);

আউটপুট:

children of /lock (sorted):
  C3 lock-0000000000  HOLDS the lock
  C1 lock-0000000001  watches lock-0000000000
  C4 lock-0000000002  watches lock-0000000001
  C2 lock-0000000003  watches lock-0000000002
  C5 lock-0000000004  watches lock-0000000003

C4 session expired while waiting: lock-0000000002 deleted
  notify C2 only -> re-reads children -> not lowest: now watches lock-0000000001

C3 releases: lock-0000000000 deleted
  notify C1 only -> re-reads children -> it is lowest: HOLDS the lock

C1 releases: lock-0000000001 deleted
  notify C2 only -> re-reads children -> it is lowest: HOLDS the lock

C2 releases: lock-0000000003 deleted
  notify C5 only -> re-reads children -> it is lowest: HOLDS the lock

C5 releases: lock-0000000004 deleted

notifications for these 5 clients: recipe 4, naive (all watch /lock children) 10
100 clients, no crash: recipe 99, naive 4950
1000 clients, no crash: recipe 999, naive 499500

Seed-এর ক্রমে C3 আগে এসেছে, তাই সে ধারক; বাকি চারজন শিকলের মতো প্রত্যেকে ঠিক আগের জনকে দেখছে। তারপর C4 অপেক্ষার মধ্যেই মরল। তার node মুছতেই শুধু C2-এর watch বাজল; C2 তালিকা আবার পড়ে দেখল সে এখনো সবচেয়ে ছোট নয়, তাই নতুন করে C1-এর node দেখতে শুরু করল — রেসিপির ধাপ ৫ ঠিক এই ক্ষেত্রের জন্য। এরপর প্রতিটি ছাড়ায় ঠিক একজন জাগে আর lock পায়।

মোট জাগানো: রেসিপিতে ৪টি, সবাই-দেখে পদ্ধতিতে ১০টি (৪ + ৩ + ২ + ১)। পাঁচজনে পার্থক্য ছোট, কিন্তু ১০০ জনে ৯৯ বনাম ৪,৯৫০, আর ১,০০০ জনে ৯৯৯ বনাম ৪,৯৯,৫০০ — প্রতিটি জাগা আবার ensemble-এ একটি পড়ার অনুরোধ। Quadratic বৃদ্ধিই herd effect-এর আসল দাম।

রেসিপি ২ · etcd-ধাঁচের lease + revision

etcd-এর Go client-এর concurrency package-এ একটি lock আছে, যার ধাঁচ একই, শুধু ভাষা আলাদা। Ephemeral-এর জায়গায় lease, sequential নম্বরের জায়গায় create_revision:

lease = LeaseGrant(ttl = 10s)                 // keep it alive every ~3 s
my    = "/lock/payout/" + lease.id
Txn( If:   CreateRevision(my) == 0            // my key does not exist yet
     Then: Put(my, "worker-A", lease)
     Else: Get(my) )
rev   = CreateRevision(my)                     // e.g. 9071: a global, ever-growing number
loop:
  older = keys under "/lock/payout/" with CreateRevision < rev
  if older is empty: I HOLD THE LOCK; fencing token = rev
  else: Watch(the one with the largest CreateRevision < rev) until it is deleted
release: Delete(my)  or  LeaseRevoke(lease)   // crash: the lease expires by itself

এখানেও প্রত্যেকে শুধু ঠিক আগের key দেখে — herd নেই। আর একটি বাড়তি উপহার: create_revision পুরো store-এ একটিই বাড়তে থাকা নম্বর, তাই পরের ধারকের নম্বর সবসময় বড়। অংশ ৬-এ দেখবেন, এই নম্বরটিই fencing token হিসেবে কাজে লাগে। etcd-এর command-line টুল etcdctl lock আর ZooKeeper-এর Curator library-র মতো সহায়ক এই রেসিপিগুলো তৈরি অবস্থায় দেয়; নিজে বানানোর আগে সেগুলো দেখুন।

৫ · Leader election — একই রেসিপি, অন্য নাম

Leader election আসলে একটি দীর্ঘ সময় ধরে রাখা lock, যার ধারক নিজের পরিচয় সবাইকে জানায়। ZooKeeper-এ প্রত্যেক প্রার্থী /election-এর নিচে ephemeral-sequential node বানায়; সবচেয়ে ছোট নম্বর leader, আর নিজের node-এ ঠিকানা লেখে। বাকিরা ঠিক আগের node দেখে — অংশ ৪-এর শিকল। etcd-এর concurrency package-এ এর নাম Election (Campaign, Proclaim, Resign, Observe). Kubernetes-এর controller-গুলো একই কাজ করে coordination.k8s.io-এর Lease object দিয়ে, যা শেষে etcd-এ লেখা হয়; লেখার সময়ে kube-controller-manager-এর ডিফল্ট lease প্রায় ১৫ সেকেন্ড (সংস্করণভেদে যাচাই করুন)।

সুস্থ অবস্থায় সব সরল। কঠিন অংশ হলো leader আর ensemble-এর মাঝের network কেটে যাওয়া। তখন দুই পক্ষ দুটি আলাদা ঘড়িতে দুটি আলাদা সিদ্ধান্ত নেয় — ensemble ঠিক করে session শেষ কি না, leader ঠিক করে নিজেকে থামাবে কি না। নিচের stepper-এ তিনটি scheduler, session timeout ৬ সেকেন্ড।

তিনটি scheduler S1, S2, S3 · session timeout ৬ s · node-এর নম্বরই epoch
ঘড়ি
/election-এর child
Ensemble-এর চোখে leader
S1
S2
S3
  1. t = ০: সুস্থ অবস্থা। তিনজন ephemeral-sequential node বানিয়েছে; S1-এর n_7 সবচেয়ে ছোট, তাই সে leader. S2 দেখছে n_7, S3 দেখছে n_8. Node-এর নম্বর ৭-কেই S1-এর epoch ধরা যায়।
  2. t = ১০: network কাটল। S1-এর heartbeat আর ensemble-এ পৌঁছাচ্ছে না। Ensemble শুধু জানে শেষ heartbeat এসেছিল t = ১০-এ; S1 জানে না যে কিছু হয়েছে।
  3. t = ১২: S1 নিজেকে থামাল। দুই সেকেন্ড উত্তর না পেয়ে S1-এর client library অবস্থা জানাল Disconnected. ভালো অভ্যাস: এই মুহূর্ত থেকেই leader-এর কাজ বন্ধ, কারণ S1 জানে না ensemble তার session এখনো রেখেছে কি না। Ensemble-এর হিসাবে session-এর এখনো ৪ সেকেন্ড বাকি।
  4. t = ১৬: session শেষ। শেষ heartbeat-এর ৬ সেকেন্ড পরে ensemble session বাতিল করল আর S1-এর ephemeral n_7 মুছে দিল। শুধু S2-এর watch বাজল; S3 কিছুই জানল না — herd নেই।
  5. t = ১৬.১: নতুন leader. S2 তালিকা পড়ে দেখল তার n_8 এখন সবচেয়ে ছোট — সে leader, epoch ৮, আর নিজের node-এ ঠিকানা লিখল। বিচ্ছিন্নতা থেকে নতুন leader পর্যন্ত প্রায় ৬ সেকেন্ড (t = ১০ থেকে ১৬.১), যার শেষ ৪ সেকেন্ড কেউ job ছাড়েনি — session timeout-এর দাম।
  6. t = ২০: পুরোনো leader ফিরল। Network জোড়া লাগতেই S1 শুনল Session expired — এখন নিশ্চিত, সে আর leader নয়। সে নতুন প্রার্থী হিসেবে n_10 বানিয়ে n_9 দেখতে শুরু করল। সতর্কতা: S1 যদি বিচ্ছিন্ন না হয়ে GC-তে থেমে থাকত, t = ১২-এর সতর্কতাও সে নিতে পারত না — সেই ক্ষেত্রের জন্য অংশ ৬।

দুই ঘড়ির দৌড়। Ensemble session শেষ করে নিজের ঘড়িতে শেষ heartbeat-এর ৬ সেকেন্ড পরে; leader নিজেকে থামায় নিজের ঘড়িতে শেষ উত্তরের কিছু পরে। নিরাপত্তার শর্ত: leader-এর থামা সবসময় ensemble-এর মুছে দেওয়ার আগে। শর্তটি টেকে কেবল তখনই, যখন দুটি ঘড়ি কাছাকাছি গতিতে চলে আর leader process নিজে সত্যিই চলছে।

৬ · আসল বিপদ — থেমে থাকা ধারক আর fencing token

এ পর্যন্ত ধরে নিয়েছি, ধারক নিজের lease-এর হিসাব রাখতে পারে। কিন্তু একটি process কোনো সতর্কতা ছাড়াই কয়েক সেকেন্ড, এমনকি কয়েক মিনিট থেমে যেতে পারে — পর্ব ৩৮-এ এর নাম process pause:

  • Garbage collection: “stop-the-world” ধাপে সব thread থামে; বড় heap-এ কয়েক সেকেন্ড পর্যন্ত।
  • Virtual machine-এর বিরতি, swap আর ধীর disk: hypervisor VM থামাতে পারে, page আনতে বা I/O-তে process আটকে থাকে।
  • Network-এ দেরি: process থামেনি, কিন্তু লেখার packet পৌঁছাল lease শেষ হওয়ার পরে — ফল একই।

স্বাভাবিক প্রতিক্রিয়া: “লেখার ঠিক আগে যাচাই করব lock এখনো আমার কি না।” কাজ হয় না, কারণ বিরতি যাচাই আর লেখার মাঝেও আসতে পারে:

if lease.stillMine():          // true at t = 9.9 s
    # ---- GC pause of 6 s happens exactly here ----
    storage.write(newValue)    // arrives at t = 15.9 s; lease ended at t = 10 s

যাচাই আর কাজের মাঝের এই ফাঁক একটি race condition; আর ধারক নিজের ভেতর থেকে কখনো এটি বন্ধ করতে পারবে না, কারণ থেমে থাকার সময় সে নিজে কিছুই চালাতে পারে না। নিচের simulator-এ একটি দোকানের স্টক ১০টি। A ১টি বিক্রি করবে, B ২টি; সঠিক শেষ ফল ৭. Lease ১০ সেকেন্ড; A lock নিয়ে স্টক পড়েই GC-তে থামে, t = ২ থেকে ১৫ পর্যন্ত। প্রথমবার storage কিছুই যাচাই করে না, দ্বিতীয়বার fencing token যাচাই করে।

// Lease lock without and with fencing tokens. A pauses past its lease; B takes the lock.
function run(fencing, bWriteAt) {
  const TTL = 10, lock = { until: 0, token: 32 };     // lease 10 s; last token given was 32
  const store = { stock: 10, maxToken: 0 };           // the storage: 10 items in stock
  const out = [], mine = {};
  const acquire = (c, t) => {                         // lock service: free or lease expired?
    if (t < lock.until) return false;
    lock.until = t + TTL; mine[c] = ++lock.token;
    out.push(`t=${t}  ${c} gets lock, token ${mine[c]}, lease until t=${lock.until}`); return true;
  };
  const read = (c, t) => { mine[c + "read"] = store.stock; out.push(`t=${t}  ${c} reads stock=${store.stock}`); };
  const write = (c, t, sell) => {                     // storage checks the token (if fencing)
    const v = mine[c + "read"] - sell, tok = mine[c];
    if (fencing && tok < store.maxToken) {
      out.push(`t=${t}  ${c} writes stock=${v} with token ${tok} -> REJECTED (seen ${store.maxToken})`);
      return false;
    }
    store.stock = v; store.maxToken = Math.max(store.maxToken, tok);
    out.push(`t=${t}  ${c} writes stock=${v} with token ${tok} -> ok`); return true;
  };
  const ev = [[0, () => acquire("A", 0)], [1, () => { read("A", 1); out.push("      A: GC pause from t=2 to t=15"); }],
    [11, () => acquire("B", 11)], [12, () => read("B", 12)],
    [bWriteAt, () => { write("B", bWriteAt, 2); lock.until = 0; out.push(`t=${bWriteAt}  B releases`); }],
    [15, () => { if (!write("A", 15, 1)) { acquire("A", 16); read("A", 16); write("A", 17, 1); } }]];
  ev.sort((x, y) => x[0] - y[0]).forEach(e => e[1]());
  console.log((fencing ? "WITH fencing" : "NO fencing") + ` (B writes at t=${bWriteAt})`);
  console.log(out.join("\n") + `\nfinal stock=${store.stock} (correct: 10 - 1 - 2 = 7)\n`);
}
run(false, 13);
run(true, 13);

আউটপুট:

NO fencing (B writes at t=13)
t=0  A gets lock, token 33, lease until t=10
t=1  A reads stock=10
      A: GC pause from t=2 to t=15
t=11  B gets lock, token 34, lease until t=21
t=12  B reads stock=10
t=13  B writes stock=8 with token 34 -> ok
t=13  B releases
t=15  A writes stock=9 with token 33 -> ok
final stock=9 (correct: 10 - 1 - 2 = 7)

WITH fencing (B writes at t=13)
t=0  A gets lock, token 33, lease until t=10
t=1  A reads stock=10
      A: GC pause from t=2 to t=15
t=11  B gets lock, token 34, lease until t=21
t=12  B reads stock=10
t=13  B writes stock=8 with token 34 -> ok
t=13  B releases
t=15  A writes stock=9 with token 33 -> REJECTED (seen 34)
t=16  A gets lock, token 35, lease until t=26
t=16  A reads stock=8
t=17  A writes stock=7 with token 35 -> ok
final stock=7 (correct: 10 - 1 - 2 = 7)

প্রথম অংশে lock service নিজের কাজ ঠিকই করেছে, তবু ফল ভুল। A জেগে উঠে তার পুরোনো হিসাব (১০ − ১ = ৯) লিখে দিল, আর B-এর বিক্রি (৮) মুছে গেল। শেষ স্টক ৯ — অথচ আসলে আছে ৭টি; দুটি পণ্য এমন গ্রাহককে বিক্রি হতে পারে, যা দোকানে নেই। এটি পর্ব ২৪-এর lost update, এবার lock থাকা সত্ত্বেও।

দ্বিতীয় অংশে storage প্রতিটি লেখার token দেখে সর্বোচ্চটি মনে রাখে। B-এর লেখায় সে ৩৪ দেখেছে; t = ১৫-এ A-এর ৩৩ এল — ৩৩ < ৩৪, তাই প্রত্যাখ্যান। A বুঝল তার lock আর নেই; আবার lock নিল (token ৩৫), নতুন করে পড়ল (৮), লিখল ৭ — সঠিক। Lock service ধারককে থামাতে পারে না, কিন্তু storage পুরোনো ধারকের লেখা আটকাতে পারে।

নিচের stepper-এ একই ঘটনা ধীরে ধীরে, সংখ্যাগুলো simulator-এর সাথে মেলানো। Token ৩৩ আর ৩৪ নম্বর দুটি Kleppmann-এর ২০১৬ সালের লেখার বিখ্যাত ছবির মতো রাখা হয়েছে।

Lease ১০ s · স্টক ১০ · A বিক্রি করবে ১, B বিক্রি করবে ২
ঘড়ি
Lock-এর ধারক · token
A
B
Storage-এর দেখা সর্বোচ্চ token
Storage-এ stock
  1. t = ০: A lock পেল, token ৩৩. Lock service প্রতিবার lock দেওয়ার সময় একটি নম্বর বাড়ায় আর দেয় — fencing token. আগের দেওয়া ছিল ৩২, তাই এবার ৩৩. Lease শেষ হবে t = ১০-এ।
  2. t = ১ → ২: A পড়ল, তারপর থামল। A স্টক ১০ পড়ল আর হিসাব করল ৯ লিখবে। ঠিক তখন একটি দীর্ঘ GC বিরতি শুরু — t = ১৫ পর্যন্ত A কিছুই চালাতে পারবে না, keep-alive-ও না।
  3. t = ১০ → ১১: lease ফুরাল, B lock পেল। Keep-alive না আসায় A-এর lease t = ১০-এ শেষ। t = ১১-এ B চাইল, পেল token ৩৪. A এখনো থেমে আছে আর নিজেকে ধারক ভাবছে — এখন দুজন “ধারক”, কিন্তু একজনই বৈধ।
  4. t = ১২ → ১৩: B লিখল token ৩৪-সহ। B ১০ পড়ে ৮ লিখল। Storage token ৩৪ দেখে মনে রাখল: এখন থেকে ৩৪-এর ছোট কোনো token-এর লেখা নয়। B lock ছেড়ে দিল।
  5. t = ১৫: A জেগে লিখতে চাইল token ৩৩-সহ। Storage মিলিয়ে দেখল ৩৩ ছোট ৩৪-এর চেয়ে — প্রত্যাখ্যান; স্টক ৮-ই থাকল। Fencing না থাকলে এখানেই স্টক ৯ হয়ে B-এর বিক্রি মুছে যেত।
  6. t = ১৬ → ১৭: A নতুন করে শুরু করল। প্রত্যাখ্যান থেকে A জানল lock আর তার নয়। আবার lock নিল (token ৩৫), নতুন করে পড়ল ৮, লিখল ৭. শেষ ফল সঠিক: ১০ − ১ − ২ = ৭. শিক্ষা: নিরাপত্তা আসে lock service থেকে নয়, storage-এর যাচাই থেকে।

Fencing token-এর তিনটি শর্ত

  1. একঘেয়েভাবে বাড়ে: পরের ধারকের নম্বর সবসময় বড় — ZooKeeper-এ sequential নম্বর বা zxid, etcd-এ create_revision, Raft-এ term, Chubby-তে “sequencer”. এলোমেলো UUID-তে “বড়-ছোট” নেই।
  2. Storage নিজে যাচাই করে: সর্বোচ্চ দেখা token মনে রাখা আর যাচাই-লেখা একসাথে, পারমাণবিকভাবে। Database-এ এটি একটি শর্তসহ update: UPDATE … SET stock = 8, fence = 34 WHERE id = 1 AND fence <= 34.
  3. Storage token আগে দেখে: নতুন ধারকের লেখার আগেই পুরোনোজনের লেখা পৌঁছালে storage তখনো ৩৪ দেখেনি — ৩৩ গৃহীত হবে। তাই নতুন ধারক শুরুতেই token নিবন্ধন করে (অনুশীলন ৩)।

“Fencing” শব্দটি high-availability cluster-এর পুরোনো জগৎ থেকে। Failover-এ পুরোনো node-কে shared disk-এ লেখা থেকে “বেড়া দিয়ে” আটকানো হতো — অনুমতি কেড়ে নিয়ে, বা বিদ্যুৎ বন্ধ করে, যার ডাকনাম STONITH (“Shoot The Other Node In The Head”). Fencing token একই চিন্তার ভদ্র রূপ: পুরোনো node-কে মারতে হয় না, শুধু তার লেখা অগ্রাহ্য হয়। পর্ব ২২-এর failover-এ যে epoch দেখেছিলেন, সেটিও এই পরিবারের।

যে storage token বোঝে না। বাইরের পেমেন্ট API, ইমেইল সেবা বা একটি সাধারণ ফাইল সিস্টেম আপনার token যাচাই করবে না। সেখানে fencing-এর বদলে idempotency key (পর্ব ৩৪ — Delivery semantics) বা সেবার নিজের শর্তসহ লেখা লাগে। Fencing ঠিক ততটাই শক্ত, যতটা পথের শেষ যাচাইটি।

৭ · Redis-এ lock — SET NX PX, আর Redlock বিতর্ক

বাস্তবে সবচেয়ে প্রচলিত distributed lock সম্ভবত একটি Redis instance-এর ওপর। Redis-এর documentation-এ বর্ণিত এক-instance ধাঁচটি পর্ব ২৯ — Eviction আর invalidation-এর single-flight lock-এ দেখেছিলেন:

# take: set only if absent (NX), expire after 5000 ms (PX); the value is a random token of the caller
SET lock:report a-71 NX PX 5000      # -> OK for one caller, (nil) for everyone else
# release: delete only if the value is still mine -- check and delete in one Lua script
#   if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end

SET NX PX একটি পারমাণবিক কমান্ডে দুটি কাজ করে: key না থাকলে বসানো, আর মেয়াদ দেওয়া — তাই lock আর lease একসাথে। পুরোনো ধাঁচে আলাদা SETNX আর EXPIRE দুটি কমান্ড ছিল; মাঝে crash হলে মেয়াদহীন lock থেকে যেত। ছাড়ার নিয়মটি কেন এত জরুরি, নিচের stepper-এ দেখুন — একটি রিপোর্ট job, lock-এর মেয়াদ ৫ সেকেন্ড, কিন্তু এক রাতে কাজে লাগল ৭ সেকেন্ড।

key lock:report · PX 5000 · A-এর কাজে লাগবে ~৭ s
ঘড়ি
lock:report-এর মান · মেয়াদ
A
B
C
কে নিজেকে ধারক ভাবছে
  1. t = ০: A lock নিল। মান হিসেবে নিজের একটি এলোমেলো token a-71, মেয়াদ ৫ সেকেন্ড। কাজটি সাধারণত ৩ সেকেন্ডে শেষ হয়, তাই মেয়াদ যথেষ্ট মনে হয়েছিল।
  2. t = ৫.০: মেয়াদ শেষ। আজ database ধীর, A-এর কাজ চলছে। Redis key মুছে দিল — Redis জানে না A বেঁচে আছে কি না, শুধু সময় গোনে।
  3. t = ৫.৫: B lock পেল। Key নেই, তাই B-এর SET … NX সফল, মান b-19. এখন A আর B দুজনেই একই রিপোর্টে কাজ করছে — lease-ভিত্তিক যেকোনো lock-এ এটি সম্ভব।
  4. t = ৭.০: A ভুলভাবে ছাড়ল। কাজ শেষে A সরাসরি DEL lock:report দিল, মান না দেখে। মুছে গেল B-এর lock! B কিছুই জানে না, কাজ চালিয়ে যাচ্ছে।
  5. t = ৭.২: C-ও lock পেল। Key খালি দেখে C-এর SET সফল। এখন B আর C দুজনই নিজেকে বৈধ ধারক ভাবছে — একটি ভুল ছাড়া পুরো সারির শৃঙ্খলা ভাঙল।
  6. নিরাপদ নকশায় একই মুহূর্ত। A ছাড়ে একটি Lua script দিয়ে, যা মান মিলিয়ে তবেই মোছে: মান এখন b-19, A-এর a-71 নয় — ফল ০, B-এর lock অক্ষত, আর t = ৭.২-এ C পেল (nil), অপেক্ষা করবে। তবু ৫.৫ থেকে ৭.০ পর্যন্ত A আর B একসাথে কাজ করেছে; correctness-এর জন্য তাই fencing token বা idempotent ফল লাগবেই।

এক instance-এর আরেকটি সীমা — failover

একটি Redis instance বিকল হলে lock-ও বিকল। Replica রাখলে নতুন সমস্যা: Redis-এর replication ডিফল্টে asynchronous, তাই A-এর lock primary-তে লেখা হলো কিন্তু replica-তে পৌঁছানোর আগেই primary মরল, replica primary হলো — সেখানে lock-টি নেই, B সহজেই পেল। Redis-এর documentation নিজেই এই ঝুঁকির কথা বলে। Cache-এর failover-এ কী হারায়, তার পূর্ণ আলোচনা পর্ব ৩০ — Distributed cache-এ।

Redlock — আর তাকে ঘিরে বিতর্ক

এই সমস্যার জন্য Redis-এর স্রষ্টা Salvatore Sanfilippo (antirez) Redlock প্রস্তাব করেন, যা Redis-এর documentation-এ বর্ণিত: ধরা যাক পাঁচটি সম্পূর্ণ স্বাধীন Redis master (একে অপরের replica নয়)। Client একই key আর এলোমেলো মান দিয়ে পাঁচটিতেই SET NX PX চেষ্টা করে; অন্তত তিনটিতে (majority) সফল হলে আর লাগা সময় মেয়াদের চেয়ে অনেক কম হলে lock তার, বৈধ সময় = মেয়াদ − লাগা সময় − ঘড়ির ছাড়। ব্যর্থ হলে সবগুলো থেকে ছাড়ে।

২০১৬ সালের ফেব্রুয়ারিতে এ নিয়ে একটি প্রকাশ্য, শিক্ষণীয় বিতর্ক হয়। দুটি লেখাই অনলাইনে আছে; নিচে দুই পক্ষের মূল যুক্তি যতটা নিরপেক্ষভাবে সম্ভব:

প্রশ্নKleppmann — “How to do distributed locking”antirez — “Is Redlock safe?”
Efficiency-র জন্যএকটি Redis instance-ই যথেষ্ট; পাঁচটি master বাড়তি খরচ আর জটিলতাRedlock বেশি টেকসই বিকল্প, যেখানে এক instance-এর বিভ্রাট গ্রহণযোগ্য নয়
Correctness-এর জন্যনিরাপদ নয়, কারণ নিরাপত্তা নির্ভর করে network delay, process pause আর ঘড়ির ত্রুটি সীমিত থাকার অনুমানেযুক্তিসংগত সিস্টেম-অনুমানে নিরাপদ; ঘড়ি মেলানো লাগে না, শুধু কাছাকাছি গতিতে চলা লাগে
ঘড়ির লাফএকটি node-এর ঘড়ি হঠাৎ এগোলে (যেমন NTP-এর ধাক্কায়) key আগেভাগে মুছে দুজন majority পেতে পারেOperation-এর দায়: ঘড়ি লাফাতে না দেওয়া, ধীরে মেলানো (slew), monotonic সময় ব্যবহার
Fencing tokenRedlock বাড়তে থাকা নম্বর দেয় না, তাই থেমে থাকা ধারককে storage আটকাতে পারে নাStorage যদি token যাচাই করতে পারে, তাহলে এলোমেলো অনন্য token দিয়েই শর্তসহ লেখা চলে — আর তখন distributed lock-এর প্রয়োজনই কমে
Process pauseLock পাওয়ার পরের বিরতি ধরা যায় নাপাওয়ার সময়ের দেরি Redlock মেপে ধরে; পাওয়ার পরের বিরতি সব lease-ভিত্তিক lock-এর সমান সমস্যা, ZooKeeper-এরও

লক্ষ করুন, দুই পক্ষ একটি জায়গায় একমত: lease-ভিত্তিক কোনো lock নিজে থেমে থাকা ধারককে থামাতে পারে না। মতভেদ মূলত সময়ের অনুমান বাস্তবে কতটা টেকে, আর Redlock এক instance-এর চেয়ে কতটা বাড়তি নিরাপত্তা দেয় — তা নিয়ে। প্রায় সবার মানা ব্যবহারিক শিক্ষা: efficiency lock-এ একটি Redis যথেষ্ট; correctness-এ consensus-চালিত সেবা আর fencing token, অথবা lock ছাড়া নকশা (অংশ ৯)। দুটি লেখা পড়ে নিজে বিচার করুন।

৮ · Lease কত লম্বা — TTL-এর trade-off আর ঘড়ির অনুমান

Lease-এর মেয়াদ একটি টানাটানি। ছোট TTL-এ ধারক মরলে দ্রুত আরেকজন দায়িত্ব নেয়; কিন্তু ছোট একটি বিরতি বা হারানো packet-এও lease ফুরায়, যদিও ধারক সুস্থ — এর নাম false expiry, আর প্রতিটি false expiry মানে অকারণ leader বদল বা দুজন ধারকের ঝুঁকি। বড় TTL-এ উল্টো: false expiry কম, কিন্তু মৃত ধারকের জন্য সবাই দীর্ঘ অপেক্ষা করে। ধরা যাক নবায়ন হয় প্রতি R = TTL/৩-এ, আর শেষ নবায়নের s সেকেন্ড পরে কিছু ঘটে (০ ≤ s < R):

crash-এর পরে recovery ≈ (TTL − s) + দখলের সময় · গড়ে TTL − R/2 + দখল · সবচেয়ে খারাপ ≈ TTL + দখল
false expiry ঘটে যদি বিরতি p > TTL − s · keep-alive-এর চাপ = ধারকের সংখ্যা ÷ R

নিচের simulator-এ ধারকের ১০,০০০টি বিরতি — বেশিরভাগ আধা সেকেন্ডের কম, কিছু ১–৫ সেকেন্ড, অল্প কয়েকটি ৫–৩০ সেকেন্ড (মিশ্রণটি ধরে নেওয়া, আপনার সিস্টেমে মেপে বসান) — আর ১,০০০টি crash; দখলের সময় ০.৩ সেকেন্ড ধরা হয়েছে। তিনটি TTL তুলনা করা হলো: ২, ১০ আর ৩০ সেকেন্ড।

// Lease TTL trade-off: recovery time after a holder crash vs false expiries caused by pauses.
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 rnd = mulberry32(4343);
const ACQ = 0.3;                        // a waiter needs ~0.3 s to see the expiry and take the lock (assumed)
function pauseLen() {                   // assumed mix of stalls: GC, VM steal, slow disk, lost packets
  const r = rnd();
  if (r < 0.95) return rnd() * 0.5;     // 95%: under half a second
  if (r < 0.995) return 1 + rnd() * 4;  // 4.5%: 1-5 s
  return 5 + rnd() * 25;                // 0.5%: 5-30 s
}
// 10,000 stalls of the holder; each starts at a random point inside its renewal interval
const stalls = Array.from({ length: 10000 }, () => ({ len: pauseLen(), phase: rnd() }));
const crashes = Array.from({ length: 1000 }, () => rnd());   // 1,000 crashes, random phase
console.log("TTL   renew   avg recovery   worst recovery   false expiries / 10k stalls");
for (const TTL of [2, 10, 30]) {
  const R = TTL / 3;                    // keep-alive every TTL/3
  // lease left when the stall starts = TTL - (time since last renewal)
  const falseExp = stalls.filter(s => s.len > TTL - s.phase * R).length;
  const rec = crashes.map(f => TTL - f * R + ACQ);  // lease runs out, then a waiter takes over
  const avg = rec.reduce((a, b) => a + b, 0) / rec.length, worst = Math.max(...rec);
  console.log(String(TTL).padStart(3) + "s" + R.toFixed(2).padStart(7) + "s" +
    avg.toFixed(1).padStart(12) + " s" + worst.toFixed(1).padStart(15) + " s" +
    String(falseExp).padStart(17));
}
const holders = 10000;                  // keep-alive load for 10,000 lease holders
for (const TTL of [2, 10, 30]) console.log(`TTL ${TTL}s: ${holders} holders send ${Math.round(holders / (TTL / 3))} keep-alives/s`);

আউটপুট:

TTL   renew   avg recovery   worst recovery   false expiries / 10k stalls
  2s   0.67s         2.0 s            2.3 s              408
 10s   3.33s         8.6 s           10.3 s               30
 30s  10.00s        25.3 s           30.3 s                7
TTL 2s: 10000 holders send 15000 keep-alives/s
TTL 10s: 10000 holders send 3000 keep-alives/s
TTL 30s: 10000 holders send 1000 keep-alives/s

TTL ২ সেকেন্ডে মৃত ধারকের পরে মাত্র প্রায় ২ সেকেন্ডে নতুন ধারক, কিন্তু ১০,০০০ বিরতির ৪০৮টিতে সুস্থ ধারকের lease ফুরিয়েছে — প্রায় ৪%, আর প্রতিটিই সম্ভাব্য দুই-ধারক মুহূর্ত। TTL ১০-এ recovery গড়ে ৮.৬ সেকেন্ড (সূত্রে ১০ − ৩.৩৩/২ + ০.৩ ≈ ৮.৬৩), false expiry ৩০. TTL ৩০-এ মৃত ধারকের জন্য গড়ে ২৫.৩ সেকেন্ড অপেক্ষা, তবু ৭টি false expiry — দীর্ঘতম বিরতিগুলো ৩০ সেকেন্ডকেও ছাড়ায়।

দুটি শিক্ষা। কোনো TTL-ই false expiry শূন্য করে না, কারণ বিরতির ঊর্ধ্বসীমা নেই — তাই fencing token TTL-এর পরিপূরক। আর ছোট TTL-এর দাম নেটওয়ার্কেও: ১০,০০০ ধারকে TTL ২ সেকেন্ডে সেকেন্ডে ১৫,০০০ keep-alive, TTL ১০-এ ৩,০০০।

ঘড়ির অনুমান

Lease দুটি আলাদা ঘড়িতে মাপা হয় — সেবার আর ধারকের। নিরাপত্তার জন্য ঘড়িগুলোকে একই সময় দেখাতে হয় না, কিন্তু কাছাকাছি গতিতে চলতে হয়। গতির পার্থক্য সর্বোচ্চ ρ (যেমন ০.১%) হলে ধারক নিজের হিসাবে lease শেষ ধরে TTL × (১ − ρ) − margin-এ। পর্ব ৩৯-এ দেখেছেন clock drift কেন হয়। ব্যবহারিক নিয়ম:

  • মেয়াদ গুনুন monotonic clock দিয়ে (Linux-এ CLOCK_MONOTONIC), wall-clock দিয়ে নয় — NTP wall-clock-কে লাফ দেওয়াতে পারে, monotonic ঘড়ি পেছায় না।
  • TTL রাখুন মাপা p99.9 বিরতি আর round trip-এর কয়েক গুণ; ZooKeeper-এ session timeout ডিফল্টে সাধারণত tickTime-এর ২ থেকে ২০ গুণের মধ্যে (documentation-এ যাচাই করুন)।
  • Chubby-র paper-এর lock-delay: ধারক ব্যর্থ হয়ে lock খালি হলে কিছুক্ষণ (paper অনুযায়ী সর্বোচ্চ এক মিনিট) অন্য কেউ সেটি পায় না, যাতে পুরোনো ধারকের দেরির বার্তা আগে শেষ হয় — fencing না বোঝে এমন সার্ভারের জন্য কাজ-চালানো সমাধান।

৯ · Lock ছাড়াই — CAS, idempotency, single-writer আর queue

নিরাপদ distributed lock-এ লাগে consensus-চালিত সেবা, lease, fencing token আর যাচাই করা storage. শেষ উপাদানটি — storage-এর শর্তসহ লেখা — নিজেই অনেক সমস্যা মেটায়। তাই আগের প্রশ্ন: lock কি আসলেই লাগে? চারটি বিকল্প প্রায়ই সরল আর নিরাপদ:

শর্তসহ লেখা (CAS)

“version এখনো ৩ হলে তবেই লেখা” — optimistic concurrency, পর্ব ২৪ — Transaction আর isolation level-এর version কলাম। etcd-এর Txn-এ mod_revision-এর তুলনা একই। এটি fencing-কে ডেটার ভেতরেই বসায়।

একটি সারি বা key-এ

Idempotency

কাজটি দুবার হলেও ফল একবারের হলে (পর্ব ৩৪, পর্ব ৮ — REST API নকশা) দুই ধারক আর বিপদ নয়; lock তখন শুধু efficiency-র, হালকা হলেও চলে। UNIQUE constraint-এ job + তারিখ লেখা একটি সরল রূপ।

বাইরের effect-এ

Single-writer partition

প্রতিটি key বা partition-এর একজনই মালিক-লেখক — যেমন log-এর প্রতিটি partition একটি consumer group-এর একজনই পড়ে (পর্ব ৩৩ — Log-ভিত্তিক streaming). লেখকেরা কখনো মুখোমুখি হয় না; মালিক বদলের সময় শুধু একটি epoch বা generation নম্বর লাগে।

single-writer

Queue দিয়ে ক্রম

একই অ্যাকাউন্টের সব বদল একটি queue বা partition-এ (key-এর hash দিয়ে) পাঠানো, আর একজন consumer ক্রমে প্রয়োগ করে (পর্ব ৩১ — Message queue). সংঘাত সারিতে দাঁড়িয়ে যায়, lock-এর দরকার থাকে না।

প্রতি key-এ ক্রম
সমস্যাসরলতম নিরাপদ পথDistributed lock কখন
শেষ সিট দুজনকে বিক্রিশর্তসহ update বা UNIQUE (পর্ব ২৪; পূর্ণ নকশা পর্ব ৬০)প্রায় কখনো নয়
রাতের job একবারjob@সময়-এর unique key + idempotent job (পর্ব ৩৭)scheduler-এর leader বাছতে, efficiency-র জন্য
Cache-এর মান একজনই হিসাব করবেএক Redis-এর SET NX PX (পর্ব ২৯)এটিই efficiency lock
Database-এর primary বাছাইConsensus বা coordination service + epochএখানে সত্যিই লাগে — fencing-সহ

আরও গভীরে: Chubby (Burrows, OSDI ২০০৬) আর ZooKeeper (Hunt ও সহকর্মীরা, USENIX ATC ২০১০)-এর paper দুটি নকশার পেছনের যুক্তি খোলাখুলি লেখে — একটি কেন lock service, অন্যটি কেন শুধু primitive. Martin Kleppmann-এর বই Designing Data-Intensive Applications (২০১৭)-এর অষ্টম অধ্যায়ে process pause আর fencing token-এর বিস্তারিত আলোচনা আছে। Kafka-র transaction-এ producer-এর epoch দিয়ে পুরোনো “zombie” producer-এর লেখা আটকানোর ব্যবস্থাকে documentation-এ zombie fencing বলা হয় — একই চিন্তা, অন্য জায়গায়। আর Kyle Kingsbury-র Jepsen প্রকল্প etcd, ZooKeeper আর Redis-এর মতো সিস্টেমের দাবি network বিচ্ছিন্নতা আর ঘড়ির গোলমালে পরীক্ষা করে প্রকাশ্য বিশ্লেষণ লিখেছে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Lease-ভিত্তিক lock-এ দুজন ধারক কীভাবে হয়? ধারক GC বা VM-বিরতিতে থামে, lease ফুরায়, আরেকজন পায়; প্রথমজন জেগে উঠে লেখে। প্রতিকার fencing token, storage-এর যাচাই।
  • Fencing token কেন এলোমেলো UUID হতে পারে না? Storage-কে বলতে হয় কোনটি নতুন; শুধু একঘেয়েভাবে বাড়তে থাকা নম্বরে “বড়-ছোট” আছে।
  • ZooKeeper-এর lock-এ প্রত্যেকে আগের node কেন দেখে? Herd effect এড়াতে: প্রতিটি ছাড়ায় একজন জাগে; সবাই দেখলে মোট N(N−১)/২ জাগা।
  • Redis-এর lock ছাড়ার সময় DEL কেন বিপজ্জনক? মেয়াদ পেরোলে key অন্যের হতে পারে; মান মিলিয়ে মোছা লাগে, একটি script-এ পারমাণবিকভাবে।
  • TTL ছোট রাখলে কী, বড় রাখলে কী? ছোট: দ্রুত recovery, কিন্তু বেশি false expiry আর keep-alive-এর চাপ; বড়: উল্টো। কোনোটিই fencing-এর বিকল্প নয়।

১০ · AI-এর যুগে — distributed lock-এর নকশা যাচাই

“দুটি সার্ভার একই job চালাচ্ছে, কী করব?” — AI প্রায়ই বলে: “Redis-এ SETNX দিয়ে distributed lock নিন।” দেখতে পরিষ্কার, কিন্তু এই পর্বের প্রায় সব ফাঁদ তাতে থাকতে পারে:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
মেয়াদ ছাড়া SETNX, বা মান না মিলিয়ে DELCrash-এ চিরস্থায়ী lock; মেয়াদ পেরোলে অন্যের lock মুছে যায়অংশ ৭-এর stepper-এর ধাপ ধরে কোডটি নিজে চালিয়ে দেখুন
“Redlock নিশ্চিত নিরাপত্তা দেয়” বা “Redlock অচল” — একপাক্ষিক রায়; lease বা timeout-এর মান বানিয়ে বলানির্ভর করে অনুমান আর efficiency বনাম correctness-এর ওপর; product-এর মান সংস্করণভেদে আলাদাজিজ্ঞাসা করুন: lock ব্যর্থ হলে ক্ষতি কী? কোন সময়-অনুমান লাগছে?
Process pause আর network দেরি উপেক্ষা, fencing নেইLease-ভিত্তিক সব lock-এ দুই ধারক সম্ভবপ্রশ্ন: “ধারক ২০ সেকেন্ড থেমে থাকলে কী লেখা হবে?” — অংশ ৬-এর simulator চালান
Database-এর শর্তসহ update যথেষ্ট, তবু ZooKeeper cluster, Redis আর microserviceপ্রতিটি নতুন সিস্টেম নতুন ব্যর্থতা; over-engineeringআগে অংশ ৯-এর বিকল্পগুলো যাচাই করুন
Coordination service-এর linearizability আর database-এর isolation গুলিয়ে ফেলাCAP-এর “consistency” আর ACID-এর আলাদা জিনিস (পর্ব ২৪ আর ৪০)জিজ্ঞাসা করুন: কোন অর্থে consistent?

AI-কে এমনভাবে প্রশ্ন দিন, যাতে প্রতিটি সিদ্ধান্তের পেছনে ব্যর্থতার ক্ষেত্র, সংখ্যা আর বিকল্প দেখাতে হয়:

তুমি একজন অভিজ্ঞ distributed systems ইঞ্জিনিয়ার। একটি কাল্পনিক mobile banking সেবায় প্রতি রাতে সুদ যোগ করার একটি job তিনটি সার্ভারে চলে; ঠিক একজনই চালাবে, আর একটি অ্যাকাউন্টে সুদ দুবার যোগ হওয়া চলবে না। ১. আগে বলো: এটি efficiency lock না correctness lock, আর কেন। Lock ছাড়া (শর্তসহ update, unique key, idempotency) সমাধান সম্ভব কি না, আগে সেটি দেখাও। ২. Lock লাগলে কোন সেবা (etcd, ZooKeeper, database, Redis) আর কেন; lease-এর TTL আর নবায়নের বিরতি সংখ্যাসহ হিসাব করো। ৩. ধারক ৩০ সেকেন্ড GC-তে থামলে ধাপে ধাপে কী ঘটবে, আর fencing token কোথা থেকে এসে storage-এ কীভাবে যাচাই হবে — SQL-সহ। ৪. Redlock নিয়ে মত দিলে দুই পক্ষের যুক্তি দাও। কোনো product-এর ডিফল্ট বা সীমা বললে সূত্র দাও; নিশ্চিত না হলে বলো "জানি না"।

AI-এর lock নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

Lock সেবা অনুমতি দেয়, নিরাপত্তা দেয় storage.
আগে জিজ্ঞাসা করুন lock লাগে কি না; লাগলে fencing ছাড়া নয়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। সূত্র মুখস্থ নয়, কেন কোন নকশা বেছে নেওয়া হয় সেটাই আসল।

প্র ০১Google কেন consensus-এর একটি library না দিয়ে Chubby নামে আলাদা একটি lock service বানিয়েছিল?

Burrows-এর ২০০৬ সালের paper এই প্রশ্নটি সরাসরি তোলে, কারণ বিকল্পটি স্বাভাবিক ছিল: প্রতিটি সার্ভিস নিজের ভেতরে Paxos-এর একটি library চালাক, আলাদা সেবার দরকার কী? Paper-এর যুক্তিগুলো আজও প্রাসঙ্গিক। প্রথমত, বেশিরভাগ সিস্টেম শুরুতে ছোট prototype হিসেবে জন্মায়, high availability-র কথা ভেবে নয়। পরে যখন একটি master বাছাই দরকার হয়, তখন বিদ্যমান কোডে একটি lock নেওয়ার দুই লাইন যোগ করা সহজ; পুরো প্রোগ্রামকে consensus-এর replicated state machine-এ রূপান্তর করা কঠিন।

দ্বিতীয়ত, নির্বাচনের পরে প্রায় সবসময় একটি ছোট তথ্য প্রকাশ করতে হয় — “নতুন master কে, তার ঠিকানা কী”। Lock service যদি ছোট ফাইলও রাখতে পারে, সেই প্রকাশের জন্য আরেকটি সেবা লাগে না। Chubby তাই lock আর ছোট ফাইল একসাথে দিত, আর paper অনুযায়ী বাস্তবে এর একটি বড় ব্যবহার দাঁড়ায় নাম আর config রাখার সেবা হিসেবে — আজ যাকে service discovery বলি।

তৃতীয়ত, consensus-এর জন্য বেশিরভাগ replica সচল থাকা লাগে। নিজের ভেতরে library চালালে প্রতিটি সার্ভিসকে অন্তত তিনটি, ভালো হলে পাঁচটি কপি চালাতে হতো। আলাদা একটি cell সেই দায় একবার মেটায়; client একটি হলেও সে নিরাপদে lock নিতে পারে। চতুর্থত, lock-এর interface প্রোগ্রামারদের পরিচিত — যদিও paper নিজেই সতর্ক করে যে distributed lock-কে একটি মেশিনের mutex-এর মতো ভাবলে ভুল হবে, আর তাই sequencer (fencing-এর মতো নম্বর) আর lock-delay-এর মতো ব্যবস্থা রাখা হয়েছিল।

দামও ছিল: একটি কেন্দ্রীয় সেবার বিভ্রাট বহু সিস্টেমে ছড়ায়, আর অনেক client-এর ঘন ঘন পড়া সামলাতে client-side cache লেগেছিল।

মূল উপলব্ধি: প্রযুক্তিগতভাবে শ্রেষ্ঠ নকশা সবসময় সবচেয়ে বেশি গৃহীত নকশা নয়; প্রোগ্রামাররা যা সহজে ব্যবহার করতে পারেন আর যা কম সার্ভার চায়, সেটিই প্রায়ই জেতে।

প্র ০২ZooKeeper কেন নিজে “lock” কমান্ড দেয় না, আর কেন তার পড়া একটু পুরোনো হতে পারে? এই দুটি সিদ্ধান্তের দাম আর লাভ কী?

২০১০ সালের ZooKeeper paper-এর শিরোনামেই উত্তরের ইঙ্গিত: “wait-free coordination”. Lock একটি blocking primitive — lock না পেলে অপেক্ষা। লেখকদের যুক্তি ছিল, সার্ভারের ভেতরে blocking primitive রাখলে একটি ধীর বা বিকল client অন্য সবাইকে আটকে রাখতে পারে, আর সার্ভারের কাজ জটিল হয়। তার বদলে ZooKeeper দেয় কয়েকটি wait-free অপারেশন — node বানানো, মোছা, পড়া, শর্তসহ লেখা — আর সাথে ephemeral, sequential node আর watch. এগুলো দিয়ে client নিজে lock, leader election, barrier, queue বানায়, যেমন অংশ ৪-এর রেসিপি। দাম: প্রত্যেককে রেসিপি ঠিকভাবে লিখতে হয়, আর ভুল সহজ — যেমন watch বাজলে আবার না দেখে lock ধরে নেওয়া, বা node বানানোর সময় connection হারালে node তৈরি হলো কি না না জানা। তাই Apache Curator-এর মতো library জনপ্রিয় হয়েছে, যা পরীক্ষিত রেসিপি দেয়।

দ্বিতীয় সিদ্ধান্তটি গতির জন্য। Coordination-এর কাজে পড়া লেখার চেয়ে অনেক বেশি। প্রতিটি পড়া leader হয়ে quorum-এ নিশ্চিত করলে পড়ার গতি লেখার মতোই সীমিত হতো। ZooKeeper তাই যেকোনো server-কে নিজের কপি থেকে পড়ার উত্তর দিতে দেয় — server বাড়ালে পড়ার ক্ষমতা বাড়ে। নিশ্চয়তা দুটি: লেখাগুলো linearizable, আর একজন client-এর অনুরোধগুলো তার পাঠানোর ক্রমে কার্যকর হয় (FIFO). পড়া কিন্তু কয়েক মুহূর্ত পুরোনো হতে পারে; সর্বশেষ মান দরকার হলে client আগে sync পাঠায়।

Lock-এর রেসিপিতে এটি সমস্যা নয়, কারণ সিদ্ধান্ত আসে লেখা (node বানানো, যার sequential নম্বর leader দেয়) আর watch-এর ক্রম থেকে। etcd ভিন্ন পথ নিয়েছে — ডিফল্ট পড়া linearizable, আর গতির জন্য client চাইলে “serializable” (স্থানীয়, হয়তো পুরোনো) পড়া বেছে নেয়।

মূল উপলব্ধি: ছোট, wait-free primitive আর স্পষ্ট নিশ্চয়তার তালিকা অনেক ধরনের coordination-এর ভিত্তি হতে পারে; কিন্তু তখন সঠিকতার দায় client-এর রেসিপিতে সরে যায়।

প্র ০৩যে storage fencing token যাচাই করতে পারে না — বাইরের পেমেন্ট API, ফাইল সিস্টেম বা ইমেইল সেবা — সেখানে থেমে থাকা ধারকের বিপদ কীভাবে সামলাবেন?

Fencing token-এর পুরো শক্তি একটি শর্তের ওপর: পথের শেষে কেউ নম্বরটি দেখে “না” বলতে পারে। আপনার নিজের database হলে সহজ — একটি fence কলাম আর শর্তসহ update. কিন্তু অনেক effect এমন জায়গায় যায়, যেখানে আপনার নম্বরের কোনো মানে নেই। চারটি কৌশল প্রচলিত, প্রায়ই একসাথে।

প্রথমত, idempotency key: পেমেন্ট গেটওয়েকে “এই কাজের key payout:2026-10-27:driver-881” পাঠানো। থেমে থাকা পুরোনো ধারক আর নতুন ধারক দুজনই একই key পাঠাবে, আর key মনে রাখা সেবা দ্বিতীয়টিতে আগের ফলই ফেরত দেবে। এখানে আমরা দুই ধারক ঠেকাচ্ছি না, দুই ধারকের ক্ষতি ঠেকাচ্ছি। অনেক পেমেন্ট API প্রকাশ্যে এই সুবিধা দেয়।

দ্বিতীয়ত, সেবার নিজের শর্তসহ লেখা: অনেক object storage আর cloud database-এ “এই version বা ETag এখনো থাকলে তবেই লেখা” ধরনের শর্তসহ লেখা আছে (সুবিধাটি সেবাভেদে আলাদা, documentation দেখুন)। নতুন ধারক কাজ শুরুর আগে একটি চিহ্ন লিখে version বদলালে পুরোনোর লেখা শর্তে আটকায় — fencing-এর স্থানীয় রূপ।

তৃতীয়ত, দুই ধাপে প্রকাশ: ফল সরাসরি চূড়ান্ত জায়গায় না লিখে token-সহ নামে লেখা (report.tmp.34), তারপর নিজের database-এ token যাচাই করে “চূড়ান্ত কোনটি” সেই নির্দেশক বদলানো। পুরোনো ধারকের ফাইল পড়ে থাকে, কিন্তু কেউ তাকে নির্দেশ করে না। চতুর্থত, পুরোনো STONITH-এর পথ: পুরোনো ধারককে নিশ্চিতভাবে থামানো — তার মেশিন বন্ধ, credential বাতিল, network-এ আলাদা করা — আর তারপরেই নতুনকে অনুমতি। HA cluster-এ shared disk রক্ষায় এটি দশকের পর দশক ব্যবহৃত, তবে এর নিজের ব্যর্থতার ক্ষেত্র আছে।

আর শেষ পাহারা মিলিয়ে দেখা — মুদি দোকানের খাতা রাতে ক্যাশবাক্সের সাথে মেলানোর মতো।

মূল উপলব্ধি: fencing-এর মূল চিন্তা “পুরোনো ধারককে চিনে আটকানো”, আর যেখানে নম্বর যাচাই অসম্ভব, সেখানে idempotency, শর্তসহ লেখা বা নির্দেশক বদলের মতো অন্য রূপে সেই চিন্তা বসাতে হয়।

প্র ০৪Lease ঘড়ির ওপর নির্ভর করে, অথচ distributed system-এ ঘড়ি অনির্ভরযোগ্য। তাহলে lease কীভাবে নিরাপদ হয়, আর কোথায় ভাঙে?

Gray আর Cheriton ১৯৮৯ সালে lease প্রস্তাবের সময়েই এই প্রশ্ন বিবেচনা করেছিলেন, আর উত্তরের মূল কথা: lease-এর জন্য দুটি ঘড়িকে একই সময় দেখাতে হয় না, শুধু একই গতিতে, মোটামুটি চলতে হয়। কারণ lease সময়বিন্দু নয়, সময়-দৈর্ঘ্য: “এখন থেকে ১০ সেকেন্ড”। সেবা অনুমতি দেওয়ার মুহূর্ত থেকে নিজের ঘড়িতে ১০ সেকেন্ড গোনে; ধারক অনুরোধ পাঠানোর আগের মুহূর্ত থেকে নিজের ঘড়িতে গোনে। ধারকের শুরুটা আগে, তাই তার হিসাবে lease আগে শেষ হয় — যতক্ষণ তার ঘড়ি সেবার ঘড়ির চেয়ে অনেক বেশি ধীরে না চলে। ঘড়ির গতির পার্থক্যের একটি ঊর্ধ্বসীমা ধরে ধারক বাড়তি ছাড় রাখে, আর নিরাপত্তা দাঁড়ায় সেই সীমার ওপর। Chubby-র paper-এও client নিজের lease-এর একটি রক্ষণশীল অনুমান রাখে ঠিক এভাবে।

কোথায় ভাঙে? প্রথমত, wall-clock দিয়ে গুনলে। NTP বড় পার্থক্য দেখলে ঘড়িকে লাফ দেওয়াতে পারে, কেউ হাতে সময় বদলাতে পারে, আর VM ঘুম থেকে উঠে ঘড়ি হঠাৎ এগিয়ে নিতে পারে। এই কারণেই মেয়াদ গোনার জন্য monotonic clock — যা কখনো পেছায় না। Redlock বিতর্কে Kleppmann-এর একটি প্রধান উদাহরণ ছিল এই লাফ; antirez-এর উত্তর ছিল operation-এর স্তরে লাফ নিষিদ্ধ করা। দুজনের কথাই দেখায় অনুমানটি কত গুরুত্বপূর্ণ।

দ্বিতীয়ত, ঘড়ি ঠিক থাকলেও ধারক নিজে থেমে থাকলে। ঘড়ি চলছে, কিন্তু যে কোড ঘড়ি দেখে সিদ্ধান্ত নেবে সেটিই চলছে না — অংশ ৬-এর পুরো সমস্যা। এখানে কোনো ঘড়ির নিখুঁততা সাহায্য করে না; সাহায্য করে fencing. তৃতীয়ত, দীর্ঘ network দেরি: লেখার packet lease থাকাকালীন পাঠানো, কিন্তু পৌঁছাল পরে। এটিও ঘড়ির নয়, “সময়মতো পৌঁছানো”-র অনুমানের ব্যর্থতা।

পর্ব ৩৯-এর Lamport clock সময়ের বদলে ক্রম দেয়; fencing token সেই চিন্তারই প্রয়োগ।

মূল উপলব্ধি: lease-এর liveness আসে ঘড়ি থেকে — ধারক মরলে একসময় মুক্তি; কিন্তু correctness-এর জন্য ঘড়ির ওপর ভরসা না করে ক্রমের ওপর, অর্থাৎ fencing token-এর ওপর ভরসা করা নিরাপদ।

প্র ০৫Log-ভিত্তিক stream সিস্টেম কীভাবে লাখো partition-এ “একজনই লেখক” নিশ্চিত করে distributed lock ছাড়াই? এখানে fencing কোথায় লুকিয়ে আছে?

একটি log-এর প্রতিটি partition-এ প্রতি মুহূর্তে একজন leader broker লেখা নেয়, আর একটি consumer group-এর ভেতরে প্রতিটি partition একজনই consumer পড়ে (পর্ব ৩৩)। প্রতিটি partition-এর জন্য আলাদা distributed lock নিলে coordination service-এ লাখো lock আর লাখো keep-alive — অসম্ভব চাপ। তার বদলে একটি কেন্দ্রীয় সিদ্ধান্ত-দাতা থাকে: cluster-এর একটি controller বা group-এর একটি coordinator, যে নিজে একবার নির্বাচিত (আগে Kafka-র ক্ষেত্রে ZooKeeper দিয়ে, নতুন সংস্করণে Raft-ভিত্তিক KRaft দিয়ে)। সে-ই ঠিক করে কোন partition কার, আর সেই সিদ্ধান্তের সাথে একটি নম্বর দেয়।

এই নম্বরগুলোই লুকানো fencing token. Partition-এর leader বদলালে একটি leader epoch বাড়ে; পুরোনো epoch-এর অনুরোধ follower-রা প্রত্যাখ্যান করে। Consumer group-এ প্রতিবার ভাগ বদলালে একটি generation নম্বর বাড়ে; পুরোনো generation-এর consumer offset commit করতে চাইলে coordinator ফিরিয়ে দেয়। আর Kafka-র transaction-এ প্রতিটি transactional.id-এর একটি producer epoch থাকে: একই id-র নতুন producer চালু হলে epoch বাড়ে, আর পুরোনো — documentation-এর ভাষায় “zombie” — producer-এর লেখা broker নাকচ করে। এর নাম zombie fencing.

কৌশলটির সাধারণ রূপ single-writer principle: সমস্যাকে এমনভাবে ভাগ করুন যাতে প্রতিটি টুকরার একজনই মালিক, আর মালিকানা বদলের বিরল মুহূর্তে একটি বাড়তে থাকা নম্বর দিন। প্রতিটি লেখায় lock লাগে না; শুধু মালিকানা বদলে coordination. এটি পর্ব ২৩-এর partitioning-এরই একটি ফল — সংঘাত এড়ানোর সবচেয়ে সস্তা উপায় হলো সংঘাতের সুযোগ না রাখা।

একই চিন্তা দেখা যায় database-এর primary-র epoch-এ (পর্ব ২২) আর Raft-এর term-এ (পর্ব ৪২)। নাম আলাদা — epoch, generation, term, zxid, revision — কিন্তু সবাই একটি প্রশ্নের উত্তর দেয়: “এই অনুরোধটি কি বর্তমান মালিকের, না অতীতের?”

মূল উপলব্ধি: বড় সিস্টেমে distributed lock সাধারণত প্রতিটি কাজের জন্য নয়, মালিকানা ঠিক করার বিরল মুহূর্তের জন্য; আর মালিকানার প্রতিটি সংস্করণে একটি বাড়তে থাকা নম্বরই আসল নিরাপত্তা।

অনুশীলন

আগে নিজে হিসাব করুন বা লিখুন, তারপর উত্তর মেলান। তৃতীয় প্রশ্নে আগে অনুমান, তারপর অংশ ৬-এর প্রোগ্রাম বদলে চালিয়ে যাচাই।

  1. হিসাব — lease, recovery আর herd: একটি scheduler-এর leader lease-এর TTL ১৫ সেকেন্ড, নবায়ন প্রতি ৫ সেকেন্ডে, আর lease ফুরানোর পরে নতুন leader দায়িত্ব নিতে ০.৫ সেকেন্ড লাগে (ধরা যাক)। (ক) Leader crash করলে নতুন leader আসতে সবচেয়ে বেশি কত সময়, আর crash-এর মুহূর্ত এলোমেলো হলে গড়ে কত? (খ) শেষ নবায়নের ৩ সেকেন্ড পরে leader ১৩ সেকেন্ডের একটি GC বিরতিতে পড়ল — lease কি ফুরাবে? ১০ সেকেন্ডের বিরতি হলে? (গ) একই নিয়মে ২০,০০০টি client session থাকলে সেকেন্ডে কতটি keep-alive? (ঘ) ২০০টি client একটি lock-এর সারিতে দাঁড়িয়ে; সবাই একে একে lock নিয়ে ছেড়ে দিলে “সবাই lock-কে দেখে” পদ্ধতিতে মোট কতবার জাগানো হবে, আর আগের-জনকে-দেখা রেসিপিতে কতবার?
    • (ক) সবচেয়ে খারাপ: ঠিক নবায়নের পরেই crash, তখন lease-এর পুরো ১৫ সেকেন্ড বাকি — ১৫ + ০.৫ = ১৫.৫ সেকেন্ড. গড়ে crash নবায়নের ৫ সেকেন্ডের জানালার মাঝখানে, অর্থাৎ শেষ নবায়নের ২.৫ সেকেন্ড পরে: ১৫ − ২.৫ + ০.৫ = ১৩.০ সেকেন্ড.
    • (খ) বিরতি শুরুর সময় lease-এর বাকি ১৫ − ৩ = ১২ সেকেন্ড। ১৩ > ১২, তাই হ্যাঁ, ফুরাবে — leader জাগার ১ সেকেন্ড আগে; সে নিজেকে leader ভেবে জাগবে, তাই fencing লাগবে। ১০ সেকেন্ডের বিরতিতে ১০ < ১২, ফুরাবে না, আর জাগার পরে ২ সেকেন্ড বাকি থাকতেই নবায়ন।
    • (গ) প্রতিটি session প্রতি ৫ সেকেন্ডে একটি: ২০,০০০ ÷ ৫ = ৪,০০০ keep-alive/s.
    • (ঘ) সবাই-দেখে: প্রথম ছাড়ায় ১৯৯ জন জাগে, তারপর ১৯৮, … ১ — মোট ২০০ × ১৯৯ ÷ ২ = ১৯,৯০০. রেসিপিতে প্রতিটি ছাড়ায় একজন: ১৯৯ — ঠিক একশো গুণ কম।
  2. নকশা সমালোচনা: একটি কাল্পনিক mobile banking সেবায় প্রতি রাতে সুদ যোগের job তিনটি সার্ভারে চলে। নকশা: (১) প্রত্যেক সার্ভার Redis-এ SETNX lock:interest 1 চেষ্টা করে, মেয়াদ নেই; (২) সফল হলে সব অ্যাকাউন্টে balance = balance + interest চালায়, ঘণ্টাখানেক লাগে; (৩) শেষে DEL lock:interest; (৪) Redis একটি primary আর একটি async replica, স্বয়ংক্রিয় failover. সমস্যাগুলো চিহ্নিত করে একটি নিরাপদ নকশা প্রস্তাব করুন।
    • (১) মেয়াদ নেই: lock-ধারী crash করলে lock চিরকাল থাকে, সুদ আর কখনো যোগ হয় না। মেয়াদ দিলে (SET … NX PX) আবার উল্টো বিপদ — এক ঘণ্টার কাজের মাঝে মেয়াদ ফুরালে দ্বিতীয় সার্ভার শুরু করবে।
    • (২) কাজটি idempotent নয়: balance + interest দুবার চললে সুদ দুবার — এটি correctness lock, অথচ নকশাটি efficiency lock-এর মানের।
    • (৩) মান না দেখে DEL: নিজের মেয়াদ ফুরানোর পরে অন্যের lock মুছে দিতে পারে; মান হিসেবে এলোমেলো token আর মান মিলিয়ে মোছা দরকার।
    • (৪) Async failover: replica-তে পৌঁছানোর আগে failover হলে lock হারায়।
    • নিরাপদ নকশা: Lock-কে নিরাপত্তার ভিত্তি না বানানো। প্রতিটি অ্যাকাউন্টের সুদ একটি ledger entry, key interest:{account}:{date}-এ UNIQUE constraint, আর balance-এর বদল একই transaction-এ — একই দিনের সুদ দ্বিতীয়বার ঢুকতেই পারে না। তখন scheduler-এর leader (etcd বা database lease) শুধু efficiency-র জন্য; কাজটি অ্যাকাউন্টের hash দিয়ে টুকরো করে job queue-তে দিলে দীর্ঘ lock-ও লাগে না (পর্ব ৩৭)।
  3. আউটপুট অনুমান করুন: অংশ ৬-এর lease_fencing.js-এ শেষ দুই লাইন মুছে শুধু run(true, 16); রাখুন — অর্থাৎ fencing চালু, কিন্তু B এবার লেখে t = ১৬-এ, A-এর জেগে ওঠার (t = ১৫) পরে। কোন লেখা প্রত্যাখ্যাত হবে, আর শেষ স্টক কত? আগে লিখুন, তারপর চালিয়ে মেলান।

    অনুমান: t = ১৫-এ A token ৩৩ নিয়ে লেখে, কিন্তু storage তখনো কোনো token দেখেনি (সর্বোচ্চ ০) — B পড়েছে, লেখেনি। তাই A-এর লেখা গৃহীত: স্টক ৯. তারপর t = ১৬-এ B তার পুরোনো পড়া (১০) থেকে ৮ লেখে token ৩৪-এ — গৃহীত। কোনো প্রত্যাখ্যান নেই, শেষ স্টক ৮ — এবার A-এর বিক্রি হারাল।

    আসল আউটপুট:

    WITH fencing (B writes at t=16)
    t=0  A gets lock, token 33, lease until t=10
    t=1  A reads stock=10
          A: GC pause from t=2 to t=15
    t=11  B gets lock, token 34, lease until t=21
    t=12  B reads stock=10
    t=15  A writes stock=9 with token 33 -> ok
    t=16  B writes stock=8 with token 34 -> ok
    t=16  B releases
    final stock=8 (correct: 10 - 1 - 2 = 7)

    Fencing থাকা সত্ত্বেও ফল ভুল, কারণ storage শুধু লেখায় token দেখে, আর নতুন ধারকের token দেখার আগেই পুরোনোজন লিখে ফেলেছে। প্রতিকার: B lock পেয়েই storage-এ token ৩৪ নিবন্ধন করবে (বা পড়াও token-সহ হবে) — তখন t = ১৫-এ A-এর ৩৩ প্রত্যাখ্যাত হতো। আরও শক্ত: B-এর লেখা শর্তসহ, “স্টক এখনো ১০ হলে তবেই ৮” (অংশ ৯-এর CAS).

  4. বেছে নিন — lock, না অন্য কিছু: প্রতিটির জন্য সবচেয়ে সরল নিরাপদ পথ বেছে কারণ লিখুন: (ক) জনপ্রিয় একটি পণ্যের পাতার cache মেয়াদ শেষে একজনই আবার বানাবে; (খ) একটি ট্রেনের শেষ সিট দুজনকে বিক্রি হওয়া ঠেকানো; (গ) একটি database cluster-এর নতুন primary বাছাই; (ঘ) একটি বাইরের SMS সেবা, যার শর্ত — একটি অ্যাকাউন্ট থেকে একসাথে একটির বেশি অনুরোধ নয়; (ঙ) কয়েকশো সার্ভিস instance-কে নতুন feature flag-এর খবর পৌঁছানো।
    • (ক) Efficiency lock: এক Redis-এ SET … NX PX, মান মিলিয়ে ছাড়া (পর্ব ২৯)। ব্যর্থ হলে বড়জোর দুবার হিসাব।
    • (খ) Lock নয় — শর্তসহ update বা UNIQUE (trip_id, seat_no) (পর্ব ২৪)। Database নিজেই সংঘাত ধরে।
    • (গ) Coordination service বা consensus + epoch: এখানে সত্যিকারের correctness lock; নতুন primary-র epoch বড়, আর পুরোনো primary-র লেখা fencing-এ আটকায় (পর্ব ২২, ৪২)।
    • (ঘ) একটি queue আর একজন worker: অনুরোধ ক্রমে যায়; idempotency key-সহ, যাতে retry-তে দ্বিগুণ SMS না যায়।
    • (ঙ) Lock নয় — config key-এ watch: flag etcd বা ZooKeeper-এর একটি key-এ, instance-রা watch রেখে বদলের খবর পায় (অংশ ৩)।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমZooKeeper-ধাঁচের lock recipe-তে একজন client lock নেওয়া থেকে ছাড়া পর্যন্ত যে ধাপগুলো পেরোয়, সেগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 /lock-এর নিচে একটি ephemeral-sequential node বানানো
  2. 📋 /lock-এর সব child পড়ে নিজের অবস্থান দেখা
  3. 👀 নিজে সবচেয়ে ছোট না হলে শুধু ঠিক আগের node-এ watch রাখা
  4. 🔔 Watch বাজলে আবার child-তালিকা পড়ে যাচাই করা
  5. 🔒 নিজে সবচেয়ে ছোট — lock পাওয়া, token-সহ কাজ করা
  6. 🗑️ কাজ শেষে নিজের node মুছে lock ছাড়া

একটি lock service প্রতিবার lock দেওয়ার সময় একটি বাড়তে থাকা fencing token দেয়। Client A token ৩৩ পেয়ে দীর্ঘ বিরতিতে থেমে ছিল; এর মধ্যে client B token ৩৪ পেয়ে storage-এ লিখেছে। এখন A জেগে উঠে token ৩৩-সহ লেখা পাঠাল। Token যাচাই করা storage কী করবে?

  • লেখাটি গ্রহণ করবে, কারণ A একসময় বৈধ ধারক ছিল
  • Lock service-কে জিজ্ঞাসা করে তারপর সিদ্ধান্ত নেবে
  • লেখাটি প্রত্যাখ্যান করবে, কারণ ৩৩ আগে দেখা ৩৪-এর চেয়ে ছোট
  • দুটি লেখা মিলিয়ে একটি মান বানাবে

৫০ জন client একটি ZooKeeper-ধাঁচের lock-এর অপেক্ষায়। সবাই যদি lock-এর parent node-এর child-তালিকায় watch রাখে, বর্তমান ধারক lock ছাড়লে কতজন জাগবে, আর “শুধু আগের node দেখা” রেসিপিতে কতজন?

  • ৫০ আর ০
  • ৫০ আর ১
  • ১,২২৫ আর ৪৯
  • ১ আর ১

নিচের কোন ক্ষেত্রে lock শুধু efficiency-র জন্য, অর্থাৎ মাঝে মাঝে দুজন একসাথে lock পেলেও ডেটা ভুল হয় না?

  • একটি অ্যাকাউন্ট থেকে টাকা কাটা, যেখানে কোনো idempotency নেই
  • ট্রেনের শেষ সিট বিক্রি
  • Database-এর primary বাছাই
  • Cache-এর মেয়াদ শেষে একটি পণ্যের পাতা আবার হিসাব করা

Redis-এ SET lock:x <token> NX PX 5000 দিয়ে নেওয়া lock ছাড়তে সরাসরি DEL lock:x না দিয়ে “মান নিজের token হলে তবেই মোছা” script কেন ব্যবহার করা হয়?

  • মেয়াদ পেরোনোর পরে key অন্য কারও হয়ে থাকলে ভুলে তার lock মুছে যাওয়া ঠেকাতে
  • কারণ DEL কমান্ডটি ধীর
  • Script একটি fencing token তৈরি করে দেয়
  • Script replica-তে lock পৌঁছানো নিশ্চিত করে

একটি leader lease-এর TTL ৯ সেকেন্ড, নবায়ন প্রতি ৩ সেকেন্ডে। Leader ঠিক একটি নবায়নের পরপরই crash করল। নতুন leader দায়িত্ব নিতে দখলের সময় বাদে মোটামুটি কতক্ষণ অপেক্ষা লাগবে?

  • প্রায় ৩ সেকেন্ড
  • প্রায় ৯ সেকেন্ড
  • প্রায় ১২ সেকেন্ড
  • সঙ্গে সঙ্গে, কারণ service crash টের পায়

ZooKeeper-এ একজন client-এর বানানো ephemeral node কখন সেবা নিজে থেকে মুছে দেয়?

  • Client-এর TCP সংযোগ এক মুহূর্তের জন্য কাটলেই
  • প্রতিদিন নির্দিষ্ট সময়ে
  • Client-এর session-এর মেয়াদ শেষ হলে (বা client নিজে মুছলে)
  • অন্য কোনো client সেটির ওপর watch রাখলে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Coordination = একজনই, একবারই, একই তথ্য — leader, একচেটিয়া অধিকার, config, service discovery. অনেক মেশিনে shared memory নেই, মৃত্যুর নিশ্চিত খবর নেই, একটি ঘড়ি নেই। আগে ঠিক করুন lock efficiency-র না correctness-এর।
  2. প্রতিটি distributed lock আসলে lease: TTL, TTL/৩-এ keep-alive, আর ধারক নিজের হিসাবে আগেভাগে থামে। ZooKeeper-এর ephemeral node, etcd-এর lease, Chubby-র session — consensus-চালিত ৩ বা ৫ node-এর সেবায়।
  3. Lock-এর রেসিপি: ephemeral-sequential node বা lease + create_revision; প্রত্যেকে শুধু আগের জনকে দেখে — herd নেই (N−১ বনাম N(N−১)/২)। Leader election একই রেসিপি।
  4. থেমে থাকা ধারক (GC, VM, network দেরি) lease ফুরানোর পরে লিখতে পারে — lock service তা ঠেকাতে পারে না। Fencing token: বাড়তে থাকা নম্বর, storage সর্বোচ্চটি মনে রেখে ছোটটি ফেরায়; নতুন ধারক token আগে নিবন্ধন করে।
  5. Redis SET NX PX + মান মিলিয়ে ছাড়া efficiency-তে যথেষ্ট; Redlock বিতর্কে মতভেদ সময়-অনুমান নিয়ে। TTL-এর trade-off: দ্রুত recovery বনাম false expiry. অনেক সময় সেরা lock — কোনো lock নয়: CAS, UNIQUE, idempotency, single-writer, queue.

পরের পর্বে: এই পর্বে বারবার প্রশ্ন ছিল “ধারক বেঁচে আছে কি না, কীভাবে জানব?” — heartbeat আর timeout দিয়ে। পর্ব ৪৪ — Membership আর failure detection — heartbeat, gossip আর SWIM-এ দেখবেন হাজারো node-এর cluster-এ কে জীবিত, কে বিকল, সেই খবর কেন্দ্রীয় সেবা ছাড়াই কীভাবে ছড়ায়, আর ভুল সন্দেহ কীভাবে কমানো যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান