সরাসরি পর্বে যান
পর্ব ৬১ · ৬৬-এর মধ্যে · মডিউল ৯

ক্যাপস্টোন — একটি distributed key-value store নকশা আর simulator

Capstone — Designing a Dynamo-style Distributed Key-Value Store, Part by Part, with Simulators

Taught in Bangla, this capstone lesson assembles a distributed key-value store part by part: requirements, estimates and a get/put/delete API, leaderless quorum versus Raft per shard, partitioning with a ring and virtual nodes, replication with preference lists and tunable R and W, vector clocks, siblings, LWW and CRDTs, hinted handoff, repair and gossip membership, node storage and operations, with four simulators and a self-assessment rubric.

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

  • একটি distributed key-value store-এর requirement, হিসাব আর API — নিজের সংখ্যা নিজে বের করে
  • Leaderless quorum বনাম প্রতি shard-এ Raft — কোন কাজে কোনটি, আর কেন
  • সাতটি অংশ জোড়া লাগানো: partitioning, replication, versioning, ব্যর্থতা সামলানো, membership, node-এর storage আর operations
  • চারটি simulator চালিয়ে প্রতিটি সিদ্ধান্তের দাম মাপা — ভাগের সমতা, p99, stale read, availability
  • প্রজেক্টটি নিজে বাড়ানোর পথ, নিজের কাজ মূল্যায়নের rubric, আর AI-এর দেওয়া নকশা যাচাইয়ের চেকলিস্ট

১ · সমস্যা আর পরিধি — “ভান্ডার” কী করবে, কী করবে না

মডিউল ৯-এর আগের সাতটি case study-তে key-value store ছিল একটি তৈরি যন্ত্র। URL shortener-এ ছোট code থেকে লম্বা URL, chat-এ প্রতিটি কথোপকথনের বার্তা, news feed-এ তৈরি করে রাখা timeline, notification-এ একই বার্তা দুবার না পাঠানোর চিহ্ন, ride-sharing-এ চালকের সর্বশেষ অবস্থান, video streaming-এ ভিডিওর metadata. আর ticket booking-এ দেখেছেন, কোথায় এমন store যথেষ্ট নয়। এবার যন্ত্রটির ঢাকনা খুলে নিজে একটি বানাব।

নাম দেওয়া যাক “ভান্ডার” (কাল্পনিক)। এটি একটি key-value store: প্রতিটি মান একটি key-এর নিচে থাকে, আর key দিয়েই পড়া, লেখা বা মোছা হয়। নকশা এগোবে পর্ব ৬-এর কাঠামোয় — পরিধি, হিসাব, API, ছবি, তারপর গভীরে। “গভীরে” অংশটি এখানে সাত টুকরো; প্রতিটি আগের কোনো পর্বের ধারণা, এক বাক্যে প্রয়োগ আর link-সহ। নতুন হলো টুকরোগুলো একসাথে কীভাবে বসে, আর একটি আরেকটির ওপর কী চাপ দেয়।

কারওয়ান বাজারের মতো একটি বড় পাইকারি আড়তের কথা ভাবুন, যার পাঁচটি গুদাম। প্রতিটি মালের লটে একটি নম্বর; নম্বর দেখেই ঠিক হয় লটটি কোন গুদামে যাবে, আর পাশের দুটি গুদাম একটি করে কপি-খাতা রাখে। যেকোনো গুদামের ম্যানেজার অর্ডার নিতে পারেন — নিজের না হলে ঠিক গুদামে পাঠিয়ে দেন। একটি গুদাম লোডশেডিংয়ে বন্ধ? পাশের গুদাম মাল রেখে একটি চিরকুট লাগায়: “এটি ৩ নম্বরের, খুললে পৌঁছে দিতে হবে।” ম্যানেজাররা চায়ের দোকানে নিয়মিত খবর বিনিময় করেন — কে খোলা, কে বন্ধ। আর মাস শেষে খাতা মেলানো হয়। “ভান্ডার”-এর সাতটি অংশ ঠিক এই সাতটি কাজ।

Functional requirement

  • get(key) — মান ফেরত দেয়। একসাথে হওয়া লেখায় একাধিক মান থাকলে সবগুলো, সাথে context নামের একটি সংস্করণ-চিহ্ন।
  • put(key, value, context) — মান লেখে; আগের পড়ার context সাথে দিলে store বোঝে এই লেখা কোন সংস্করণের পরে এসেছে।
  • delete(key, context) — মোছে; ভেতরে এটিও একটি বিশেষ লেখা।
  • প্রতিটি অনুরোধে consistency বাছাই — ONE, QUORUM বা ALL; একে বলে tunable consistency.

Non-functional requirement (ধরা যাক)

বিষয়লক্ষ্যকোন অংশ এটি রাখে
Availability৯৯.৯% সফল অনুরোধ, ৩০ দিনে মোটামুটি ৪৩.২ মিনিটের সমান ব্যর্থতাreplication, sloppy quorum, hinted handoff
Latencyএকটি region-এর ভেতরে get-এর p99 ১০ ms-এর নিচে, put-এর ২০ ms-এর নিচেসবচেয়ে দ্রুত W টির অপেক্ষা, LSM storage
Durability“OK” পাওয়া লেখা একটি node হারালেও টিকে থাকেN = ৩, W ≥ ২, WAL
Horizontal scalenode যোগ বা বাদে downtime নেই; মোটামুটি ১/N অংশ ডেটা সরেconsistent hashing, virtual node
Consistencyপ্রতিটি অনুরোধে বাছাই; কোনো লেখা নিঃশব্দে হারাবে না (default)R + W > N, vector clock

পরিধির বাইরে (অংশ ৯-এর “নিজে বাড়ান”-এ): একাধিক key জুড়ে transaction, value-এর field ধরে খোঁজা, key-এর ক্রমে range scan, একাধিক region, SQL. এগুলো বাদ রাখাই পরিধি ঠিক করা।

ইন্টারভিউয়ার যে প্রশ্নগুলো শুনতে চান: পড়া আর লেখার অনুপাত? Value-এর গড় ও সর্বোচ্চ আকার? একসাথে দুটি লেখার একটি হারালে চলে কি — cart-এ না, cache-এ হ্যাঁ? কোনো key-এ “আগে না থাকলে তবেই লেখা”-র শর্ত লাগবে? এক region না একাধিক? উত্তর বদলালে নকশাও বদলায় — অনুশীলন ২ দেখুন।

২ · হিসাব আর API — সংখ্যা থেকে তিনটি ক্রিয়া

সূত্রগুলো পর্ব ৩-এর; এখানে প্রয়োগ। অনুমান স্পষ্ট লেখা থাকলে আপত্তি এলে কোন সংখ্যা বদলাতে হবে তা দেখা যায়। “পরে” চাপুন:

ধরা যাক: 1e9 key · প্রতি item 1.1 KB · গড়ে 50k req/s · peak ×3 · 80% get · N = 3
Disk (সব কপি)
Peak অনুরোধ
Replica-স্তরের কাজ
Node
Bloom filter memory
Error budget
  1. অনুমান লেখা। ১০০ কোটি key; প্রতিটি item-এ গড়ে ১ KB মান আর key ও metadata মিলে আরও ১০০ byte, মোট ১.১ KB. গড়ে সেকেন্ডে ৫০ হাজার অনুরোধ, ব্যস্ত সময়ে তিন গুণ, ৮০% পড়া। ৯৯.৯% লক্ষ্যে ৩০ দিনে ব্যর্থতার বাজেট ০.১%, মোটামুটি ৪৩.২ মিনিটের সমান — পর্ব ৫০-এর error budget.
  2. Storage. কাঁচা ডেটা ১.১ TB; তিন কপিতে ৩.৩ TB; LSM-এর পুরোনো কপি আর compaction-এর জায়গা ধরে প্রায় ১.৩ গুণ (ধরা যাক), অর্থাৎ ৪.২৯ TB; ডিস্ক ৭০%-এর বেশি না ভরানোর নিয়মে মোটামুটি ৬.১ TB — কয়েকটি সার্ভারেই আঁটে।
  3. Peak অনুরোধ। ৫০ হাজার × ৩ = দেড় লাখ; তার ১ লাখ ২০ হাজার get আর ৩০ হাজার put.
  4. Replica-স্তরে কাজ। প্রতিটি put তিনটি replica-তে লেখা; প্রতিটি get, R = ২ হলে, দুটি replica থেকে পড়া। মোট ৯০ হাজার + ২ লাখ ৪০ হাজার = সেকেন্ডে ৩ লাখ ৩০ হাজার — client যা দেখে, তার দ্বিগুণের বেশি।
  5. Node-এর সংখ্যা। ধরা যাক একটি node সেকেন্ডে ২০ হাজার replica-কাজ সামলায়, আর আমরা ৭০%-এর বেশি ব্যস্ত রাখব না (পর্ব ৪-এ দেখেছেন, ব্যস্ততা বাড়লে queue-র অপেক্ষা লাফিয়ে বাড়ে)। ৩,৩০,০০০ ÷ ১৪,০০০ ≈ ২৩.৬, অর্থাৎ ২৪টি node. একটি বন্ধ হলে বাকি ২৩টি প্রত্যেকে প্রায় ১৪,৩৪৮ — ক্ষমতার ৭২%, এখনো চলে।
  6. প্রতি node-এ। ডিস্ক ৬.১ TB ÷ ২৪ ≈ ২৫৫ GB. Bloom filter: ৩০০ কোটি key-কপি × ১০ bit = ৩.৭৫ GB, node-প্রতি প্রায় ১৫৬ MB — RAM-এ সহজেই আঁটে, আর false-positive হার প্রায় ০.৮২% (অংশ ৮)। সিদ্ধান্ত: এই store-এ node-এর সংখ্যা ঠিক করে throughput, storage নয়।

সংখ্যাগুলো একটি ছোট script-এ মিলিয়ে নেওয়া। “একটি node সেকেন্ডে ২০ হাজার কাজ” কিন্তু অনুমান — আসল সংখ্যা আসে নিজের hardware-এ load test থেকে।

API — client যা দেখে

বাইরে থেকে তিনটি ক্রিয়া। ভেতরে node-দের মধ্যে আর client library থেকে node-এ সাধারণত একটি binary RPC (পর্ব ৯); বোঝার সুবিধার জন্য নিচে HTTP-রূপে দেখানো হলো:

GET /v1/kv/carts/cart:42?consistency=QUORUM
200 OK
{ "values": [["rice","oil"], ["rice","salt"]],      // দুটি sibling
  "context": "eyJBIjoyLCJCIjoxfQ" }                 // client-এর কাছে অস্বচ্ছ

PUT /v1/kv/carts/cart:42?consistency=QUORUM
{ "value": ["oil","rice","salt"], "context": "eyJBIjoyLCJCIjoxfQ" }
204 No Content

DELETE /v1/kv/carts/cart:42?consistency=QUORUM   (context সাথে)
204 No Content                                    // ভেতরে tombstone লেখা হয়

তিনটি নকশার সিদ্ধান্ত এই ছোট API-তেই লুকানো। প্রথম, context: get যে সংস্করণ-চিহ্ন দেয়, put তা ফেরত দেয় — একে বলে causal context. এতেই store বোঝে নতুন লেখাটি কোন কোন সংস্করণ “দেখে” লেখা হয়েছে (অংশ ৬)। দ্বিতীয়, values একটি তালিকা — একসাথে হওয়া লেখা থাকলে store সিদ্ধান্ত নেয় না, client-কে দেখায়। তৃতীয়, retry: timeout-এর পর একই put আবার পাঠালে খারাপ হলে একই মানের দুটি sibling হয়, যা client সহজেই মেলায়; তবে counter-এর “এক বাড়ানো”-য় idempotency key লাগবে (অংশ ৬)।

Data model আর partition key

Store-এর কাছে value একটি byte-এর থলি (পর্ব ২০)। প্রতিটি item: key, value বা sibling-তালিকা, vector clock, আর মোছা হলে tombstone-চিহ্ন। Partition key পুরো key-টি; তার hash ঠিক করে item কোন node-এ যাবে (পর্ব ২৩)। দাম: পাশাপাশি key আলাদা node-এ ছড়ায়, তাই range scan চলে না।

৩ · Architecture-এর সিদ্ধান্ত — leaderless quorum, না প্রতি shard-এ Raft

সবচেয়ে বড় সিদ্ধান্ত replication-এর ধরন। দুটি পরিচিত পথ: Amazon-এর Dynamo paper (২০০৭)-এর leaderless replication, যেখানে যেকোনো replica লেখা নেয় আর R ও W টির উত্তরে কাজ সফল (পর্ব ৪১); আর key-পরিসর ভাগ করে প্রতিটি ভাগে একটি করে Raft group — Multi-Raft — যেখানে প্রতিটি ভাগের একজন leader সব লেখার ক্রম ঠিক করে (পর্ব ৪২)।

প্রশ্নLeaderless quorum (Dynamo-ধাঁচ)প্রতি shard-এ Raft
Network ভাগ হলে লেখাsloppy quorum-এ দুই ভাগেই চলে; পরে conflict মেলাতে হয়শুধু majority-র ভাগে চলে; অন্য ভাগ থামে
Consistencyপ্রতি অনুরোধে বাছাই; R + W > N হলেও linearizable নয়প্রতি shard-এ linearizable; compare-and-set সহজ
Latencyসবচেয়ে দ্রুত W টির অপেক্ষা; যেকোনো node সামলায়leader পর্যন্ত যাওয়া + majority-র ack; leader ধীর হলে সবাই ধীর
একসাথে দুটি লেখাsibling, LWW বা CRDT — অ্যাপকে ভাবতে হয়ঘটে না; leader একটি ক্রম দেয়
বিকল node-এর পরfailover নেই; অন্যরা কাজ চালায়, পরে মেরামতনতুন leader election, কয়েকশো ms থেকে কয়েক সেকেন্ড থামা
খোলা উৎসের উদাহরণCassandra, Riak-এর ধারণা (প্রকাশিত বিবরণ অনুযায়ী)etcd (একটি group), TiKV, CockroachDB (প্রকাশিত নকশা অনুযায়ী)
কখন কোনটি

Leaderless বাছুন, যখন লেখা নেওয়াই সবচেয়ে জরুরি আর একসাথে হওয়া লেখা মেলানো যায়: shopping cart, session, সর্বশেষ অবস্থান, sensor-এর মাপ। Raft-per-shard বাছুন, যখন “একবারই”, “অনন্য” বা “আগের মান দেখে বদল” লাগে: wallet-এর balance, সিট বুকিং, username, config. “ভান্ডার”-এর default তাই leaderless; একটি keyspace-এ দ্বিতীয়টি যোগ করা অনুশীলন ২-এ।

পুরো ছবি

Client থেকে যেকোনো node, তারপর ring-এ key-এর preference list

Client → any node → coordinator → replicas on the ring (N = 3, W = 2, R = 2)

Client → B (যেকোনো node) → C (coordinator) → D, E; preference list [C, D, E] A B C D E Client put(cart:42) যেকোনো node cart:42 coordinator replica ২ replica ৩ লেখা ack ✓ preference list(cart:42) = [C, D, E] · N = 3 W = 2 ack বা R = 2 উত্তর পেলেই client ফল পায়
ছবি ৬১.১ — Client অনুরোধটি যেকোনো node-এ পাঠাতে পারে, এখানে B-তে। B নিজে cart:42-এর replica নয়, তাই ভাঙা তীরে অনুরোধটি preference list-এর প্রথম node C-তে পাঠায় — C হয় coordinator, নিজে লেখে আর D ও E-কে পাঠায়। C-র নিজের লেখা আর E-র ack (সবুজ) মিলে W = ২; D-র উত্তরের অপেক্ষা লাগে না। ring চেনে এমন client library সরাসরি C-তে গেলে একটি hop কমে।

প্রতিটি node একই software চালায়, কোনো “master” নেই। ভেতরে চারটি অংশ:

একটি node-এর ভেতরে

Request handler, membership & gossip, hinted-handoff queue, LSM storage engine

একটি node-এর চারটি অংশ: request handler, membership ও gossip, hinted handoff queue, LSM storage client বা অন্য node-এর অনুরোধ Node C (সব node একই) Request handler hash → preference list W/R গোনা, version মেলানো replica RPC Membership + gossip কে সচল: A B C E D: সন্দেহজনক (φ বেশি) Hinted handoff queue D-র জন্য রাখা লেখা D ফিরলে পাঠানো হবে Storage engine (LSM) WAL · disk, append Memtable · RAM, sorted SSTable 3 + Bloom SSTable 2 + Bloom SSTable 1 + Bloom পেছনে compaction: পুরোনো ফাইল মিশিয়ে নতুন ফাইল
ছবি ৬১.২ — Request handler অনুরোধের key hash করে preference list বের করে, membership-কে জিজ্ঞেস করে কে সচল, আর কোনো replica বন্ধ থাকলে লেখাটি hint হিসেবে queue-তে রাখে। নিজের কপি যায় storage engine-এ: আগে WAL, তারপর memtable; memtable ভরলে নতুন SSTable, প্রতিটির সাথে একটি Bloom filter. অংশ ৪ থেকে ৮ বাক্সগুলো একে একে খোলে।

সিদ্ধান্তের তালিকা — বিকল্পসহ

অংশবাছাইবিকল্প, আর কেন নয়
১ · Partitioningconsistent hashing, অনেক virtual noderange partitioning: range scan দেয়, কিন্তু hot spot আর split-এর জটিলতা
২ · ReplicationN = ৩, preference list, প্রতি অনুরোধে R আর Wsingle-leader: সহজ, কিন্তু leader বিকল হলে লেখা থামে
৩ · Versioningvector clock + sibling; keyspace-ভেদে LWW বা CRDTশুধু LWW: সহজ, কিন্তু একসাথে হওয়া লেখা নিঃশব্দে হারায়
৪ · ব্যর্থতাsloppy quorum, hinted handoff, read repair, Merkle anti-entropyশুধু strict quorum: দুটি replica বন্ধ হলে লেখা থামে
৫ · Membershipgossip + phi accrual failure detectorZooKeeper বা etcd-এ তালিকা: পরিষ্কার, কিন্তু বাড়তি একটি নির্ভরতা
৬ · StorageLSM-tree, প্রতি SSTable-এ Bloom filterB-tree: পড়া স্থির, কিন্তু এলোমেলো লেখা বেশি
৭ · OperationsSLI-SLO, একটি করে upgrade, snapshotসব একসাথে upgrade: ভুল হলে পুরো cluster

৪ · টুকরো ১: Partitioning — ring, virtual node আর node যোগ-বিয়োগ

প্রয়োগ এক বাক্যে: প্রতিটি node ring-এ V টি virtual node হিসেবে বসে, আর key যায় ঘড়ির কাঁটার দিকে প্রথম token-এর node-এ (পর্ব ১৬-এ ring, token আর প্রমাণ দেখেছেন)। প্রশ্ন এখানে সংখ্যার: V কত, আর পঞ্চম node এলে কত ডেটা সরবে? নিচের simulator ২০ হাজার key চারটি node-এ ভাগ করে, তারপর E যোগ করে। অবস্থান ঠিক করে FNV-1a hash, key বানায় একটি seeded PRNG (mulberry32) — প্রতিবার একই ফল:

// Part 1: ring with V virtual nodes per server. Spread of keys, and keys moved when E joins.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
function hash(s) {                                   // FNV-1a 32-bit + a final mix step
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193);
  h = Math.imul(h ^ (h >>> 16), 0x85ebca6b); h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
  return (h ^ (h >>> 16)) >>> 0;
}
function ring(servers, V) {                          // sorted tokens: [position, server]
  const t = [];
  for (const s of servers) for (let v = 0; v < V; v++) t.push([hash(s + ":" + v), s]);
  return t.sort((a, b) => a[0] - b[0]);
}
function owner(tokens, h) {                          // first token clockwise (binary search)
  let lo = 0, hi = tokens.length;
  while (lo < hi) { const m = (lo + hi) >> 1; if (tokens[m][0] < h) lo = m + 1; else hi = m; }
  return tokens[lo % tokens.length][1];
}
const rnd = mulberry32(61), K = 20000;
const keys = Array.from({ length: K }, () => hash("user:" + Math.floor(rnd() * 1e9)));
const four = ["A", "B", "C", "D"], five = [...four, "E"];
console.log("V".padEnd(6) + four.map(s => s.padStart(6)).join("") + "stddev".padStart(12) + "max/mean".padStart(10) + "moved".padStart(10));
for (const V of [1, 10, 100]) {
  const r4 = ring(four, V), r5 = ring(five, V), cnt = { A: 0, B: 0, C: 0, D: 0 };
  let moved = 0;
  for (const h of keys) { const o = owner(r4, h); cnt[o]++; if (owner(r5, h) !== o) moved++; }
  const c = four.map(s => cnt[s]), mean = K / 4;
  const sd = Math.sqrt(c.reduce((s, x) => s + (x - mean) ** 2, 0) / 4);
  console.log(String(V).padEnd(6) + c.map(x => String(x).padStart(6)).join("") + "   " +
    String(Math.round(sd)).padStart(9) + "    " + (Math.max(...c) / mean).toFixed(2).padStart(6) +
    "   " + (100 * moved / K).toFixed(1).padStart(6) + "%");
}
const naive = keys.filter(h => h % 4 !== h % 5).length;      // hash mod N: 4 -> 5 servers
console.log("naive hash mod N (4 -> 5 servers) moves " + (100 * naive / K).toFixed(1) + "% of keys; ideal is 20.0%");

আউটপুট:

V          A     B     C     D      stddev  max/mean     moved
1        219 11397  1848  6536        4361      2.28     32.5%
10      5957  3624  4654  5765         937      1.19     10.2%
100     5065  5387  5022  4526         308      1.08     17.0%
naive hash mod N (4 -> 5 servers) moves 80.1% of keys; ideal is 20.0%

গড় ভাগ ৫,০০০. V=1-এ B পায় 11397, A মাত্র 219; max/mean 2.28 মানে সবচেয়ে ব্যস্ত node গড়ের দ্বিগুণের বেশি টানে, আর capacity ঠিক হয় তাকে দেখেই। Standard deviation V = ১০-এ 937, V = ১০০-এ 308 — গড়ের প্রায় ৬%; প্রতি দশ গুণ V-তে মোটামুটি √১০ ≈ ৩ গুণ কম (৯৩৭ ÷ ৩০৮ ≈ ৩.০), এলোমেলো নমুনায় হুবহু নয়। শেষ কলাম: E এলে কত key সরে। Key শুধু নতুন node-এ যায়, পুরোনোদের মধ্যে অদলবদল নেই; তাই সরানো অংশ = E-এর নিজের ভাগ — V = ১-এ 32.5%, V = ১০-এ 10.2%, V = ১০০-এ 17.0%, আদর্শ ২০%-এর কাছে। আর hash mod N-এ 80.1% — mod-N hashing-এর ফাঁদ।

সিদ্ধান্ত: প্রতি node-এ ২৫৬টি virtual node (ধরা যাক), ২৪টি node-এ ৬,১৪৪টি token. বেশি token-এর দাম: membership-এ বড় তালিকা, anti-entropy-তে বেশি key-পরিসর। Cassandra-ধাঁচে সংখ্যাটি config-এ ঠিক হয়; default সংস্করণভেদে আলাদা।

Node যোগ হলে — কোন পরিসর কার হাতে যায়

যোগ দেওয়া মানে token বসানো আর পুরোনোদের থেকে ডেটা আনা — node bootstrap. ছোট একটি ring-এ দেখা যাক: অবস্থান ০ থেকে ৯৯, প্রতি node-এ একটি token, N = ৩, E বসবে ৫০-এ:

Ring 0–99 · N = 3 · প্রতি node-এ একটি token · E-এর token 50
E-এর অবস্থা
Token
(85, 10]-এর replica
(10, 35]-এর replica
(35, 50]-এর replica
E-তে ডেটা
মুছে ফেলা
  1. আগে। চারটি node-এর token ১০, ৩৫, ৬০, ৮৫. একটি key-এর কপি থাকে ঘড়ির কাঁটার দিকে প্রথম তিনটি node-এ: যেমন ৩৫ থেকে ৫০-এর মাঝের key যায় C, D, A-তে, কারণ ৩৫-এর পরে প্রথম token C-এর ৬০।
  2. E যোগ দিল। E একটি seed node-এর (এখানে A) সাথে যোগাযোগ করে, token ৫০ নেয় আর gossip-এ ছড়ায় “E যোগ দিচ্ছে”. এখনো E কোনো পড়ার উত্তর দেয় না — তার কাছে ডেটাই নেই।
  3. Pending range. তিনটি পরিসরের preference list বদলাবে। যতক্ষণ ডেটা আসছে, ততক্ষণ ওই পরিসরের নতুন লেখা পুরোনো তিন replica-তে আর E-তেও যায় — যাতে streaming চলাকালীন কোনো লেখা ফাঁকে না পড়ে। পড়া চলে পুরোনোদের থেকেই।
  4. Streaming. প্রতিটি পরিসরের ডেটা একটি বর্তমান replica থেকে E-তে আসে: (৮৫, ১০] A থেকে, (১০, ৩৫] B থেকে, (৩৫, ৫০] C থেকে — মোট ২৫ + ২৫ + ১৫ = ৬৫ একক। এটি পুরো SSTable ফাইল ধরে পাঠানো যায়, তাই দ্রুত; তবে network আর ডিস্কে চাপ পড়ে, তাই গতির একটি সীমা রাখা হয়।
  5. E স্বাভাবিক। Gossip-এ “E NORMAL” ছড়াল। এখন নতুন preference list: (৮৫, ১০] → A, B, E; (১০, ৩৫] → B, E, C; (৩৫, ৫০] → E, C, D. প্রতিটিতে E একজন পুরোনো replica-র জায়গা নিয়েছে।
  6. Cleanup. যাদের জায়গা গেছে — A, C আর D — নিজেদের পুরোনো কপি মোছে। মোট ৩০০ একক কপির ৬৫ একক সরেছে, ২১.৭%; অনেক virtual node-এ এই ভাগ ১/৫-এর কাছে আসে, যা উপরের simulator দেখিয়েছে। E স্বাভাবিক হওয়ার আগে মুছলে ঝুঁকি, পরে না মুছলে ডিস্কের অপচয়।

বাদ দেওয়া দুই রকম। পরিকল্পিত decommission-এ উল্টো streaming: যাওয়ার আগে node নিজের পরিসর নতুন replica-দের দিয়ে যায়। হঠাৎ বিকলতায় সাথে সাথে ডেটা সরানো ভুল — বেশিরভাগ বিকলতা ক্ষণিকের, আর প্রতিবার ring বদলালে বিশাল streaming শুরু হতো। Dynamo paper-ও বলে, স্থায়ী যোগ-বিয়োগ স্পষ্টভাবে ঘোষণা করা হয়; ক্ষণিকের বিকলতা সামলায় অংশ ৭-এর hint আর repair. Rebalancing-এর অন্য কৌশল পর্ব ২৩-এ।

৫ · টুকরো ২: Replication — preference list আর R, W

প্রয়োগ: প্রতিটি key-এর N = ৩ কপি, ring-এ ঘড়ির কাঁটার দিকে প্রথম তিনটি আলাদা physical node-এ — একই node-এর দুটি virtual node বাদ দিয়ে (preference list, পর্ব ২২)। তিনটি কপি তিনটি আলাদা rack বা availability zone-এও, নইলে একটি বিদ্যুৎ-বিভ্রাটেই তিনটি যায় — পর্ব ৫-এর correlated failure. লেখা সফল W টি ack-এ, পড়া R টি উত্তরে, আর R + W > N হলে পড়ার দল আর লেখার দল অন্তত একটি replica-তে মেলে — প্রমাণ পর্ব ৪১-এ।

প্রশ্নটি এবার সংখ্যার: একটি replica বন্ধ থাকলে কোন (R, W)-এ কী হয়? নিচের simulator একটি key-এ ১০ হাজার রাউন্ড চালায় — প্রতি রাউন্ডে একটি put, ঠিক পরেই একটি get. প্রতিটি বার্তার latency এলোমেলো: সাধারণত ১ থেকে কয়েক ms, ৩% বার্তায় ৪০ থেকে ৮০ ms (ধরা যাক GC pause বা ব্যস্ত ডিস্ক)। Replica ২ রাউন্ড ৩০০০ থেকে ৫৯৯৯ পর্যন্ত বন্ধ। যথেষ্ট replica না থাকলে অনুরোধ ১০০ ms-এ timeout হয়ে ব্যর্থ:

// Part 2: quorum put then get on N = 3 replicas. Replica 2 is down for rounds 3000-5999.
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 N = 3, ROUNDS = 10000, TIMEOUT = 100;
const p99 = a => { const s = [...a].sort((x, y) => x - y); return s[Math.ceil(0.99 * s.length) - 1]; };
function run(R, W) {
  const rnd = mulberry32(2026);                      // same latencies for every (R, W)
  const lat = () => 1 + -Math.log(1 - rnd()) + (rnd() < 0.03 ? 40 + 40 * rnd() : 0); // ms
  const has = [0, 0, 0];                             // version held by each replica
  let acked = 0, putOk = 0, getOk = 0, stale = 0; const pl = [], gl = [];
  for (let v = 1; v <= ROUNDS; v++) {
    const up = [true, !(v >= 3000 && v < 6000), true];
    const w = [0, 1, 2].map(i => ({ i, t: lat() })).filter(x => up[x.i]).sort((a, b) => a.t - b.t);
    if (w.length >= W) {                             // success after the W-th fastest ack
      const T = w[W - 1].t; acked = v; putOk++; pl.push(T);
      for (const x of w) if (x.t <= T) has[x.i] = v; // the slower replicas apply it later
    } else pl.push(TIMEOUT);
    const r = [0, 1, 2].map(i => ({ i, t: lat() })).filter(x => up[x.i]).sort((a, b) => a.t - b.t);
    if (r.length >= R) {
      getOk++; gl.push(r[R - 1].t);
      if (Math.max(...r.slice(0, R).map(x => has[x.i])) < acked) stale++;
    } else gl.push(TIMEOUT);
    for (const x of w) has[x.i] = v;                 // in-flight writes land before the next round
  }
  console.log(`(R,W)=(${R},${W})  put ok ${(100 * putOk / ROUNDS).toFixed(1)}%  get ok ${(100 * getOk / ROUNDS).toFixed(1)}%` +
    `  put p99 ${p99(pl).toFixed(1)} ms  get p99 ${p99(gl).toFixed(1)} ms  stale reads ${stale}`);
}
for (const [R, W] of [[1, 1], [2, 2], [1, 3]]) run(R, W);

আউটপুট:

(R,W)=(1,1)  put ok 100.0%  get ok 100.0%  put p99 3.1 ms  get p99 3.2 ms  stale reads 6116
(R,W)=(2,2)  put ok 100.0%  get ok 100.0%  put p99 61.3 ms  get p99 61.1 ms  stale reads 0
(R,W)=(1,3)  put ok 70.0%  get ok 100.0%  put p99 100.0 ms  get p99 3.2 ms  stale reads 0

তিনটি সারি, তিনটি শিক্ষা। (1,1) সবচেয়ে দ্রুত (p99 প্রায় ৩ ms), কিন্তু 6116টি পড়া — ৬১% — লেখার ঠিক পরে পুরোনো মান পেল (stale read): যে replica প্রথম ack দিল আর যে প্রথম উত্তর দিল, তারা প্রায়ই আলাদা। পড়া লেখার ঠিক পরে বলে এটি সবচেয়ে খারাপ অবস্থা। (2,2)-এ stale read 0 — overlap কাজ করছে — কিন্তু p99 61.3 ms. কেন? একই simulator-এ কোনো replica বন্ধ না রাখলে (2,2)-এর put p99 মাত্র ৪.৬ ms. Replica ২ বন্ধ থাকলে বাকি দুটির দুটিরই উত্তর লাগে, তাই ধীরতর জনই সময় ঠিক করে; দুটির অন্তত একটি ধীর হওয়ার সম্ভাবনা প্রায় ৬%, আর সময়ের ৩০% এমন — মোট প্রায় ১.৮% অনুরোধ ধীর, যা p99-এ ধরা পড়ে (tail latency, পর্ব ৪)। (1,3)-এ পড়া দ্রুত আর নির্ভুল, কিন্তু put সফল মাত্র 70.0% — একটি replica বন্ধ মানেই লেখা বন্ধ, আর ব্যর্থ put-গুলো timeout-এ 100.0 ms.

নকশার নিয়ম

“ভান্ডার”-এর default: N = ৩, W = ২, R = ২ — একটি replica বিকলেও পড়া-লেখা চলে, পুরোনো মানের নিশ্চয়তা থাকে। কিন্তু একটি বিকল থাকলেই p99 লাফায়; তাই SLO-র হিসাবে “সবাই সুস্থ” নয়, “একটি বন্ধ” অবস্থাও মাপতে হবে। প্রতিকার আছে: hedged request (দেরি হলে বাড়তি একটি replica-কে জিজ্ঞেস), আর বিকল replica দ্রুত চিহ্নিত করা (অংশ ৭)। প্রতিটি অনুরোধে consistency level বদলানো যায় — যেমন চালকের অবস্থানে ONE, cart-এ QUORUM.

৬ · টুকরো ৩: Versioning আর conflict — vector clock, sibling, LWW আর CRDT

Leaderless store-এ দুটি client একই key-এ প্রায় একসাথে লিখতে পারেন — বিশেষ করে network partition-এর সময়। কোনটি “পরে”, ঘড়ি দেখে বলা যায় না (পর্ব ৩৯)। “ভান্ডার” প্রতিটি সংস্করণের সাথে একটি vector clock রাখে: কোন coordinator এই key কতবার বদলেছে তার তালিকা। দুটি clock তুলনায় একটি আরেকটির আগে, পরে, অথবা দুটি concurrent — একসাথে হওয়া। Concurrent হলে store দুটিই রাখে — এরা siblings — আর পরের get-এ client-কে দুটিই দেখায়। Store-এর কাজে এটি আসলে version vector; Dynamo paper নাম দিয়েছিল vector clock, আর এই পর্বে দুটি নাম একই অর্থে।

নিচের simulator একটি কাল্পনিক অনলাইন দোকানের cart:42 নিয়ে: প্রথমে চাল, তারপর network ভাগ — A একদিকে, B আর C অন্যদিকে। দুই দিকে দুজন দুটি আলাদা জিনিস যোগ করেন। তারপর ভাগ জোড়া লাগে:

// Part 3: versioning cart:42 with vector clocks. Partition {A} | {B, C}, siblings, client merge.
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 show = c => "{" + Object.keys(c).sort().map(k => k + ":" + c[k]).join(", ") + "}";
const leq = (a, b) => Object.keys(a).every(k => a[k] <= (b[k] || 0));       // a happened before or equal b
const join = cs => cs.reduce((m, c) => { for (const k in c) m[k] = Math.max(m[k] || 0, c[k]); return m; }, {});
const store = { A: [], B: [], C: [] };               // each replica keeps a list of versions (siblings)
function apply(node, ver) {                          // keep only versions not dominated by another
  const all = [...store[node], ver].filter((x, i, arr) => arr.findIndex(y => show(y.c) === show(x.c)) === i);
  store[node] = all.filter(x => !all.some(y => y !== x && leq(x.c, y.c) && show(x.c) !== show(y.c)));
}
function put(coord, context, items, to) {            // the coordinator bumps its own entry
  const c = { ...context }; c[coord] = (c[coord] || 0) + 1;
  const ver = { c, items };
  for (const n of to) apply(n, ver);
  console.log(`put via ${coord} ${JSON.stringify(items)} clock ${show(c)}`); return ver;
}
const state = () => ["A", "B", "C"].map(n => n + "=" + store[n].map(v => show(v.c)).join(" & ")).join("  ");
const v1 = put("A", {}, ["rice"], ["A", "B", "C"]);           console.log("  " + state());
console.log("-- partition: {A} | {B, C}; both sides keep taking writes");
const v2 = put("A", v1.c, ["rice", "oil"], ["A"]);
const v3 = put("B", v1.c, ["rice", "salt"], ["B", "C"]);     console.log("  " + state());
console.log("-- partition heals; anti-entropy copies versions both ways");
for (const n of ["A", "B", "C"]) { apply(n, v2); apply(n, v3); }
console.log("  " + state());
console.log(`  v2 vs v3: ${leq(v2.c, v3.c) ? "v2 <= v3" : leq(v3.c, v2.c) ? "v3 <= v2" : "concurrent -> siblings"}`);
const got = store.C, ctx = join(got.map(v => v.c));
const merged = [...new Set(got.flatMap(v => v.items))].sort();
console.log(`get via C -> ${got.length} siblings, context ${show(ctx)}, client merges to ${JSON.stringify(merged)}`);
const rnd = mulberry32(2026), order = ["A", "B", "C"].map(n => [rnd(), n]).sort((a, b) => a[0] - b[0]).map(x => x[1]);
const v4 = put("C", ctx, merged, order);  console.log(`  delivered in order ${order.join(",")}  ` + state());
v2.ts = "10:00:05"; v3.ts = "10:00:07";             // wall-clock stamps from two different nodes
const lww = v2.ts > v3.ts ? v2 : v3, lost = lww === v2 ? v3 : v2;
console.log(`LWW instead (${v2.ts} vs ${v3.ts}): keeps ${JSON.stringify(lww.items)}, silently drops ${JSON.stringify(lost.items)}`);

আউটপুট:

put via A ["rice"] clock {A:1}
  A={A:1}  B={A:1}  C={A:1}
-- partition: {A} | {B, C}; both sides keep taking writes
put via A ["rice","oil"] clock {A:2}
put via B ["rice","salt"] clock {A:1, B:1}
  A={A:2}  B={A:1, B:1}  C={A:1, B:1}
-- partition heals; anti-entropy copies versions both ways
  A={A:2} & {A:1, B:1}  B={A:1, B:1} & {A:2}  C={A:1, B:1} & {A:2}
  v2 vs v3: concurrent -> siblings
get via C -> 2 siblings, context {A:2, B:1}, client merges to ["oil","rice","salt"]
put via C ["oil","rice","salt"] clock {A:2, B:1, C:1}
  delivered in order B,A,C  A={A:2, B:1, C:1}  B={A:2, B:1, C:1}  C={A:2, B:1, C:1}
LWW instead (10:00:05 vs 10:00:07): keeps ["rice","salt"], silently drops ["rice","oil"]

প্রথম লেখা {A:1}, তিন replica-তেই। ভাগের সময় A-এর দিকে তেল যোগ: context {A:1}, A নিজের ঘর বাড়াল — {A:2}. অন্যদিকে B-এর মাধ্যমে লবণ: একই context থেকে B নিজের ঘর যোগ করল — {A:1, B:1}. কোনোটিই অন্যটির সব ঘরে বড় বা সমান নয়, তাই concurrent -> siblings: ভাগ জোড়া লাগার পর প্রতিটি replica দুটি সংস্করণই রাখে। Client get করে দুটি মান আর মেলানো context {A:2, B:1} পান, নিজে মেলান (এখানে union — চাল, তেল, লবণ) আর সেই context দিয়ে put করেন। নতুন clock {A:2, B:1, C:1} দুটি sibling-কেই ছাড়িয়ে যায়, তাই replica-রা যে ক্রমেই পাক (B,A,C), সবাই একটিই সংস্করণে মেলে। শেষ লাইন বিকল্পটি দেখায়: last-write-wins-এ বড় timestamp-এর লবণ টেকে, তেল নিঃশব্দে হারায় — কেউ জানবেও না।

প্রতিটি keyspace-এ আলাদা conflict-নীতি

নীতিকোথায় মানায়দাম
Vector clock + sibling (default)cart, পছন্দের তালিকা — মেলানোর নিয়ম অ্যাপ জানেclient-এ merge function লিখতে হয়; union-এ মোছা জিনিস ফিরে আসতে পারে
LWWcache, চালকের সর্বশেষ অবস্থান — পুরোনো মান হারালেও ক্ষতি নেইঘড়ির পার্থক্যে নতুন লেখাও হারাতে পারে
CRDT counterlike-এর সংখ্যা, দেখার সংখ্যাশুধু নির্দিষ্ট data type; প্রতি replica-র ঘর রাখতে হয়

Counter-এ sibling চলে না: দুই দিকে ১০ থেকে দুজনে ১ করে বাড়ালে দুটি sibling-ই ১১, অথচ উত্তর ১২. সমাধান G-Counter-এর মতো CRDT — প্রতিটি replica শুধু নিজের ঘর বাড়ায়, merge প্রতিটি ঘরের বড়টি নেয়, মান সব ঘরের যোগফল; কমানোও লাগলে PN-Counter, যেখানে বাড়ানো আর কমানোর এমন দুটি আলাদা তালিকা (পর্ব ৪৬)। “ভান্ডার”-এ তাই counters নামের keyspace-এ put-এর বদলে increment(key, delta, request_id) — request_id রাখতে হয়, কারণ retry-তে একই বাড়ানো দুবার গোনা চলবে না।

দুটি বাস্তব সমস্যা। এক, clock বড় হতে থাকে; Dynamo paper একটি সীমা পেরোলে সবচেয়ে পুরোনো ঘর ফেলার কথা বলে (প্রশ্ন ৩)। দুই, delete: মোছা মানে tombstone লেখা, কারণ শুধু মুছে দিলে পুরোনো কোনো replica-র কপি repair-এর সময় মানটি “ফিরিয়ে” আনত। Tombstone কখন চূড়ান্তভাবে মোছা নিরাপদ, তা অংশ ৮-এর সাথে জড়ানো।

৭ · টুকরো ৪ ও ৫: ব্যর্থতা সামলানো আর membership — hint, repair আর gossip

প্রয়োগ, তিনটি স্তরে (যন্ত্রপাতি আর প্রমাণ পর্ব ৪১-এ): ক্ষণিকের বিকলতায় sloppy quorum আর hinted handoff — বন্ধ replica-র লেখা পরের সুস্থ node একটি নোটসহ রাখে; প্রায়ই পড়া key-এ read repair — পড়ার সময় পুরোনো কপি পেলে ঠিক করে দেওয়া; আর বাকি সবকিছুর জন্য পেছনে চলা anti-entropy, যেখানে প্রতিটি key-পরিসরের Merkle tree তুলনা করে শুধু অমিল অংশ পাঠানো হয়। কে বন্ধ, সে খবর আসে membership থেকে: প্রতিটি node gossip protocol-এ প্রতি সেকেন্ডে কয়েকজনের সাথে তালিকা বিনিময় করে, আর phi accrual failure detector heartbeat-এর দেরি দেখে সন্দেহের মাত্রা দেয় (পর্ব ৪৪)। নিচে একটি লেখা, যখন D বন্ধ:

  1. Clientput(cart:42)B-তে পাঠায়
  2. Ccoordinatorlist-এর প্রথম
  3. D, Ereplica ২, ৩D বন্ধ
  4. Ahint queueD-র জন্য রাখে
preference list(cart:42) = [C, D, E] · N = 3 · W = 2
সময়
C
D
E
A-এর hint queue
Client
  1. অনুরোধ এল। Client B-তে পাঠাল। B hash করে দেখল cart:42-এর preference list [C, D, E]; নিজে replica নয়, তাই অনুরোধটি C-তে পাঠাল — C হলো coordinator. Context {A:2,B:1,C:1}.
  2. Membership দেখা। C-এর gossip-তালিকায় D সন্দেহজনক — তার heartbeat অনেকক্ষণ বাড়েনি, φ সীমা ছাড়িয়েছে। তাই C লেখাটি D-কে না পাঠিয়ে ring-এ পরের সুস্থ node A-কে দেবে, সাথে নোট “আসল মালিক D”. লক্ষ্য তিনটি: C, E আর A.
  3. C নিজে লিখল। আগে WAL-এ যোগ, তারপর memtable-এ; clock-এ C-এর ঘর বাড়ল — {A:2,B:1,C:2}. এটি W-এর প্রথম ack. A লেখাটি নিজের সাধারণ ডেটায় না মিশিয়ে hint queue-তে রাখল।
  4. E-র ack — client “OK”. C আর E মিলে দুটি, W পূর্ণ; ২.৯ ms-এ B-এর মাধ্যমে client 204 পেলেন। E-ও বন্ধ থাকলে A-র ack গুনে লেখা চলত — সেটিই sloppy quorum.
  5. ২০ মিনিট পরে D ফিরল। D-র কাছে এখনো v2. Gossip-এ D-এর heartbeat আবার বাড়ছে দেখে সবাই তাকে সচল ধরল; A দেখল D-র জন্য একটি hint জমা আছে।
  6. Hint পৌঁছাল। A লেখাটি D-কে পাঠাল, D নিজের WAL আর memtable-এ লিখে ack দিল, A hint মুছল — তিনটি নির্ধারিত কপিতেই v3. D ফেরার আগে A নিজে মরলে hint-টি হারাত; তখন C বা E-এর সাথে D-এর Merkle tree তুলনা অমিলটি খুঁজে ঠিক করত।

এবার সব অংশ একসাথে। নিচের mini store-এ পাঁচটি node, প্রতিটির ৮টি virtual node, N = ৩, W = ২, R = ২. Workload: ৩০০টি key-এ ৮,০০০টি অনুরোধ, অর্ধেক put. D বন্ধ থাকে অনুরোধ ২০০০ থেকে ৫৯৯৯ পর্যন্ত, আর তার মাঝে E-ও বন্ধ ৪০০০ থেকে ৫৯৯৯. ফেরার পর gossip-এ খবর ছড়িয়ে hint পৌঁছাতে লাগে ৩০০টি অনুরোধের সময়, যার নাম DETECT. তিনটি নীতি পাশাপাশি — strict quorum, hint ছাড়া sloppy quorum, আর sloppy quorum + hinted handoff:

// Part 4: mini store. 5 nodes, ring (8 vnodes each), N=3, W=2, R=2. D down 2000-5999, E down 4000-5999.
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 hash = s => { let h = 0x811c9dc5; for (const ch of s) h = Math.imul(h ^ ch.charCodeAt(0), 0x01000193);
  h = Math.imul(h ^ (h >>> 16), 0x85ebca6b); h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); return (h ^ (h >>> 16)) >>> 0; };
const NODES = ["A", "B", "C", "D", "E"], N = 3, W = 2, R = 2, OPS = 8000, KEYS = 300, DETECT = 300;
const tokens = NODES.flatMap(n => [...Array(8).keys()].map(v => [hash(n + ":" + v), n])).sort((a, b) => a[0] - b[0]);
const walk = k => { const h = hash("k" + k), out = [];            // distinct nodes clockwise from the key
  for (let i = tokens.findIndex(t => t[0] >= h), j = 0; out.length < 5; j++) {
    const n = tokens[((i < 0 ? 0 : i) + j) % tokens.length][1]; if (!out.includes(n)) out.push(n); } return out; };
const down = { D: [2000, 6000], E: [4000, 6000] }, up = (n, t) => !(down[n] && t >= down[n][0] && t < down[n][1]);
function run(mode) {                                 // "strict", "sloppy, no handoff", "sloppy + hints"
  const rnd = mulberry32(54), has = {}, acked = {}, hints = [];
  NODES.forEach(n => has[n] = {});
  let ok = 0, tries = 0, stale = 0, delivered = 0;
  for (let t = 0; t < OPS; t++) {
    for (const n in down) if (mode === "sloppy + hints" && t === down[n][1] + DETECT)   // gossip says n is back
      for (const h of hints.filter(h => h.to === n && !h.done)) { has[n][h.k] = Math.max(has[n][h.k] || 0, h.v); h.done = true; delivered++; }
    const k = Math.floor(rnd() * KEYS), isPut = rnd() < 0.5, w = walk(k), home = w.slice(0, N);
    const set = mode === "strict" ? home.filter(n => up(n, t)) : w.filter(n => up(n, t)).slice(0, N);
    const pick = set.map(n => [rnd(), n]).sort((a, b) => a[0] - b[0]).map(x => x[1]);   // fastest replies first
    tries++;
    if (isPut) {
      for (const n of set) has[n][k] = t + 1;        // partial writes stay even if the put fails
      if (mode === "sloppy + hints") set.filter(n => !home.includes(n)).forEach((n, i) =>
        hints.push({ to: home.filter(x => !up(x, t))[i], k, v: t + 1 }));
      if (set.length >= W) { ok++; acked[k] = t + 1; }
    } else if (set.length >= R) {
      ok++; if (Math.max(...pick.slice(0, R).map(n => has[n][k] || 0)) < (acked[k] || 0)) stale++;
    }
  }
  console.log(mode.padEnd(19) + `availability ${(100 * ok / tries).toFixed(2)}%  stale reads ${stale}` +
    `  hints stored ${hints.length}  delivered ${delivered}`);
}
["strict", "sloppy, no handoff", "sloppy + hints"].forEach(run);

আউটপুট:

strict             availability 93.29%  stale reads 0  hints stored 0  delivered 0
sloppy, no handoff availability 100.00%  stale reads 28  hints stored 0  delivered 0
sloppy + hints     availability 100.00%  stale reads 13  hints stored 1847  delivered 1847

strict-এ availability 93.29%, stale read 0: যে key-এর তিন replica-র দুটিই D আর E (৩০০টির ৮৯টি key), তাদের অনুরোধ ৪০০০ থেকে ৫৯৯৯-এর মধ্যে ব্যর্থ — একটি replica দিয়ে W বা R পূর্ণ হয় না। ৯৯.৯% লক্ষ্য ভাঙল। Sloppy quorum-এ availability 100.00%, কারণ পরের সুস্থ node-রা জায়গা নেয়। কিন্তু hint না থাকলে (sloppy, no handoff) D আর E ফিরে এসে outage-এর লেখাগুলো কখনো পায় না: পড়ার দল যখন D আর E-কেই বাছে, পুরোনো মান — মোট 28টি stale read, ৭৬৩৪ নম্বর অনুরোধ পর্যন্ত চলতে থাকে (anti-entropy থাকলে শেষ পর্যন্ত মিলত; এখানে রাখা হয়নি)। Hint চালু থাকলে 1847টি hint জমে, সবগুলো পৌঁছায়, stale read নামে 13-এ — এর ১২টি ফেরার পরের ৩০০টি অনুরোধের ফাঁকে, hint পৌঁছানোর আগে; আর একটি (অনুরোধ ৪৯৫৪) outage চলাকালীন, যখন পড়ার দলে এমন দুটি বিকল্প node ছিল যাদের কাছে আগের লেখাটিই নেই। Sloppy quorum তাই availability কেনে consistency-র দামে — পর্ব ৪০-এর trade-off, সংখ্যায়।

সরলীকরণ: বার্তা তাৎক্ষণিক, বিকল্প node hint-এর ডেটাও পড়ায় দেয়, read repair আর anti-entropy নেই, “সবচেয়ে দ্রুত দুটি” মানে এলোমেলো দুটি। আর gossip-এর তালিকা consensus নয় — দুটি node কিছুক্ষণ ভিন্ন তালিকা দেখতে পারে; তাই “কে সচল” দিয়ে শুধু কাকে পাঠাবেন তা ঠিক করুন, ডেটার মালিকানা নয় (পর্ব ৪৪)।

৮ · টুকরো ৬ ও ৭: প্রতিটি node-এর storage আর operations

টুকরো ৬ — LSM storage আর Bloom filter

প্রয়োগ: প্রতিটি node-এ একটি LSM-tree (পর্ব ১৯)। লেখা আগে WAL-এ, তারপর RAM-এর memtable-এ; memtable ভরলে একটি অপরিবর্তনীয়, সাজানো SSTable হয়ে ডিস্কে নামে। পড়া memtable থেকে, তারপর নতুন থেকে পুরোনো SSTable. প্রতিটি SSTable-এর সাথে একটি Bloom filter বলে দেয় “এই key এখানে অবশ্যই নেই”, ফলে বেশিরভাগ ফাইলে ডিস্ক ছুঁতেই হয় না (পর্ব ৪৫)। প্রতি key-এ ১০ bit আর ৭টি hash-এ false-positive হার (১ − e−৭/১০)৭ ≈ ০.৮২% — অংশ ২-এর হিসাবে node-প্রতি ১৫৬ MB.

কেন LSM? প্রতিটি put তিনটি node-এ লেখা, hint আর streaming-ও লেখা; LSM এলোমেলো লেখাকে একটানা লেখায় বদলায়। অপরিবর্তনীয় ফাইল আরও তিনটি কাজ সহজ করে: streaming-এ পুরো ফাইল পাঠানো, Merkle tree-র জন্য স্থির ডেটা, আর সস্তা snapshot. দাম হলো পেছনের compaction — পুরোনো ফাইল মিশিয়ে নতুন ফাইল লেখা, যা ডিস্ক আর CPU খায় আর মাঝে মাঝে p99 বাড়ায়।

অংশগুলোর মধ্যে একটি সূক্ষ্ম সংযোগ: compaction tombstone কখন চূড়ান্তভাবে মুছবে? খুব আগে মুছলে, কোনো replica যে delete-টি পায়নি, সে পরের anti-entropy-তে পুরোনো মান “ফিরিয়ে” দেবে — মোছা cart আবার দেখা দেবে। তাই tombstone রাখতে হয় অন্তত পুরো cluster-এ একবার repair শেষ হওয়ার সময় পর্যন্ত। Cassandra-ধাঁচে এর নাম grace period, default কয়েক দিন (সংস্করণভেদে মিলিয়ে নিন)। অর্থাৎ repair-এর সময়সূচি আর compaction-এর নিয়ম একে অপরের ওপর নির্ভর করে।

টুকরো ৭ — Operations: মাপা, বদলানো আর ফিরিয়ে আনা

কী মাপবেন (পর্ব ৫০)। SLI: সফল get আর put-এর অনুপাত; get আর put-এর আলাদা p99, প্রতিটি consistency level-এ আলাদা করে; নমুনা নিয়ে মাপা stale read-এর হার (যেমন read repair কতবার পুরোনো কপি পেল)। SLO: ৩০ দিনে ৯৯.৯% সফল, আর সেই হিসাবে error budget. পেছনের স্বাস্থ্য: জমা hint-এর সংখ্যা ও বয়স, শেষ সফল repair-এর বয়স, compaction-এর জমা কাজ, ডিস্ক কতটা ভরা, আর node-প্রতি অনুরোধের ভাগ।

Alertকেন
error budget দ্রুত পুড়ছে (দুই জানালার burn rate)ব্যবহারকারী এখনই ব্যর্থতা দেখছেন
কোনো node-এ hint কয়েক ঘণ্টা ধরে জমছেবিকলতা ক্ষণিকের নয়; replace বা repair লাগবে
শেষ সফল repair grace period-এর কাছাকাছিমোছা ডেটা ফিরে আসার ঝুঁকি
ডিস্ক ৭০%-এর বেশি ভরাcompaction-এর জায়গা ফুরোলে লেখা থামতে পারে

Upgrade (পর্ব ৫৩)। Rolling-ভাবে একটি করে node: memtable ডিস্কে নামানো, বন্ধ, নতুন সংস্করণ, চালু, hint পৌঁছানো আর p99 স্বাভাবিক হওয়া পর্যন্ত অপেক্ষা, তারপর পরেরটি — একই preference list-এর দুটি বন্ধ হলে strict quorum ভাঙে। পুরোনো আর নতুন সংস্করণকে একে অপরের বার্তা বুঝতে হবে, তাই format বদলায় পেছনে-সামঞ্জস্য রেখে (পর্ব ১২)।

Backup (পর্ব ৫১)। Replication backup নয় — ভুল মোছা তিন কপিতেই ছড়ায়। SSTable অপরিবর্তনীয় বলে snapshot সস্তা: ফাইলগুলোর hard link নিয়ে অন্য account বা region-এর object storage-এ কপি। কঠিন অংশ: সব node-এর snapshot একই মুহূর্তের নয়, তাই restore-এর পর repair লাগে; আর snapshot-এর পরের লেখার জন্য commit log জমাতে হয়। Restore-এর মহড়া ছাড়া RTO অজানা।

৯ · নিজে বাড়ান — প্রজেক্ট, বিস্তার, ভাঙন আর নিজের মূল্যায়ন

চারটি simulator একটি প্রজেক্টের কঙ্কাল। পছন্দের ভাষায় একটি প্রক্রিয়ার ভেতরে কয়েকটি “node” দিয়ে শুরু করুন, পরে আসল network আর ডিস্কে যান; প্রতিটি ধাপে ব্যর্থতা ঘটিয়ে নিশ্চয়তা যাচাইয়ের একটি পরীক্ষা লিখুন।

প্রজেক্ট চেকলিস্ট

তিনটি বিস্তার

Transaction

এক key-এ compare-and-set quorum দিয়ে হয় না — সেই keyspace-এ প্রতি পরিসরে Raft (অনুশীলন ২)। একাধিক key জুড়ে two-phase commit বা saga (পর্ব ২৫)।

শুরু: একটি keyspace-এ CAS

Secondary index

Value-এর field ধরে খোঁজা। Local index-এ প্রতিটি node নিজের ডেটার index রাখে, খোঁজ যায় সব node-এ; global index নিজেই ভাগ করা, লেখায় দুই জায়গা বদলায় (পর্ব ২১)।

শুরু: local index

Multi-region

প্রতিটি region-এ পুরো ring; ভেতরে quorum (LOCAL_QUORUM-এর মতো), বাইরে asynchronous — দুই region-এ একই key-এ লেখায় vector clock আরও জরুরি (পর্ব ৫১)।

শুরু: দুই region, async

কী ভাঙতে পারে — আর ১০ গুণ বড় হলে

  • Hot key: একটি ভাইরাল key-এর সব অনুরোধ একই তিন node-এ; virtual node এটি ছড়ায় না (প্রশ্ন ২)।
  • বড় value বা লম্বা sibling-তালিকা: আকার আর sibling-সংখ্যার সীমা না থাকলে একটি key-ই node-কে ধীর করে।
  • ১০ গুণ, ২৪০টি node: সবার সাথে সবার heartbeat O(n²), তাই gossip; repair-এর খরচ ডেটার সাথে বাড়ে, তাই পরিসর ভাগ করে সপ্তাহজুড়ে ছড়ানো।
  • একসাথে বিকলতা: একই ভুল config সব node-এ গেলে replication বাঁচায় না; বাঁচায় ধাপে ধাপে config বদল।
  • নিরাপত্তা (প্রতিরক্ষা): node-দের মধ্যে mTLS, ডিস্কে encryption, keyspace-প্রতি অনুমতি আর tenant-প্রতি rate limit (পর্ব ৫২, পর্ব ৪৯)।
  • খরচ: তিন কপি মানে তিন গুণ ডিস্ক; আবার বানানো যায় এমন cache-ধরনের keyspace-এ N = ২ বা TTL যথেষ্ট।

৬০ সেকেন্ডের wrap-up: “ভান্ডার” একটি leaderless key-value store. Key ring-এ বসে consistent hashing আর অনেক virtual node দিয়ে; প্রতিটি key-এর তিন কপি আলাদা zone-এ, W = ২ আর R = ২-এ একটি বিকল সহ্য করে পুরোনো মানের নিশ্চয়তা থাকে। একসাথে হওয়া লেখা vector clock-এ sibling হয়; counter-এ CRDT, cache-এ LWW. ক্ষণিকের বিকলতায় sloppy quorum আর hint, বাকিটা read repair আর Merkle anti-entropy; কে সচল জানায় gossip. প্রতি node-এ WAL-সহ LSM আর Bloom filter. হিসাবে ২৪টি node — বাধা throughput. দুর্বলতা: compare-and-set নেই, তাই balance-এর মতো keyspace-এ Raft-ভিত্তিক পথ; আর একটি replica বিকলে p99 বাড়ে, তাই hedged request.

নিজের মূল্যায়ন — rubric

মাপকাঠিশুরুচলনসইদৃঢ়
Requirement আর হিসাবশুধু “scalable হবে”QPS আর storage আছেঅনুমান লেখা, replica-স্তরের কাজ থেকে node-এর সংখ্যা
Replication-এর বাছাই“Cassandra নেব”R + W > N বলতে পারেনleaderless বনাম Raft-এর শর্ত, আর কোন keyspace-এ কোনটি
Conflictউল্লেখ নেইLWW বলেছেনvector clock, sibling, CRDT — কোথায় কী হারায়
ব্যর্থতা“replica আছে”hinted handoff বলেছেনক্ষণিক বনাম স্থায়ী বিকলতা, তিন স্তরের মেরামত, tombstone-এর ঝুঁকি
প্রমাণদাবি শুধুহাতে হিসাবsimulator বা পরীক্ষায় মাপা সংখ্যা, সরলীকরণসহ
Operationsনেইmonitoring আছেSLI-SLO, alert, rolling upgrade, restore-এর মহড়া

আরও গভীরে: মূল উৎস DeCandia ও সহকর্মীদের “Dynamo: Amazon’s Highly Available Key-value Store” (SOSP ২০০৭); LSM-এর ধারণা O’Neil ও সহকর্মীদের paper (১৯৯৬) আর Google-এর Bigtable (OSDI ২০০৬) থেকে, আর Cassandra এই দুই ধারা মেলায় (Lakshman ও Malik, প্রকাশ ২০০৯–২০১০)। পরের প্রজন্মের উত্তর দেখতে পড়ুন Amazon DynamoDB-র USENIX ATC ২০২২-এর paper — সেখানে প্রতিটি partition-এর replica-রা Multi-Paxos দিয়ে চলে বলে বর্ণিত (প্রশ্ন ১)। আর নিজের store পরীক্ষার পদ্ধতি নিয়ে Kyle Kingsbury-র Jepsen বিশ্লেষণগুলো (প্রশ্ন ৫)।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় “একটি distributed key-value store নকশা করুন” এলে দুর্বল আর শক্ত উত্তরের পার্থক্য:

  • দুর্বল: “Redis cluster আর Cassandra নিন, Kafka দিয়ে replicate করুন।” — শক্ত: নিজের অংশগুলো নাম ধরে, প্রতিটির বিকল্প আর দামসহ।
  • N = ৩, W = ২, R = ২-এ কয়টি বিকল সহ্য হয়? — পড়া আর লেখা দুটিতেই N − W = N − R = ১টি; একটি বিকলে p99 বাড়ে, কারণ বাকি দুটির দুটিরই অপেক্ষা।
  • ৪ থেকে ৫ node-এ কত key সরে? — consistent hashing-এ আদর্শ ১/৫ = ২০%, hash mod N-এ প্রায় ৪/৫ = ৮০%.
  • দুটি clock {A:2} আর {A:1, B:1}? — কোনোটি অন্যটির সব ঘরে বড় বা সমান নয়, তাই concurrent — sibling.
  • Sloppy quorum কী কেনে, কী হারায়? — লেখার availability কেনে; R + W > N-এর overlap hint পৌঁছানো পর্যন্ত হারায়।

১০ · AI-এর যুগে — AI-এর লেখা KV store নকশা আর কোড যাচাই

“একটি distributed key-value store নকশা করো আর কোড লেখো” — AI কয়েক সেকেন্ডে ring, replica আর একটি ছোট প্রোগ্রাম দেবে, যা প্রায়ই চলে আর প্রায়ই ভুল। এখানকার ভুল স্বাভাবিক অবস্থায় দেখা দেয় না; দেখা দেয় বিকলতায়, একসাথে হওয়া লেখায় আর node যোগের মুহূর্তে। খুঁজুন:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“Kafka, Redis আর microservices যোগ করুন”বাড়তি অংশ নতুন বিকলতা আনেজিজ্ঞেস করুন: এটি না থাকলে কোন requirement ভাঙে?
Preference list-এ একই physical node-এর দুটি virtual node“তিন কপি” আসলে দুটিকোডে distinct-node যাচাই খুঁজুন; ছোট ring-এ চালান
Hash-এর জন্য hashCode() বা সাধারণ যোগফল, ১টি tokenভাগ অসম — simulator-এ V = ১-এ max/mean ২.২৮অংশ ৪-এর মতো standard deviation মাপুন
“R + W > N, তাই strongly consistent”; ACID আর CAP-এর consistency মেশানোsloppy quorum, চলমান লেখা আর LWW-এ ফাঁক থাকেলেখা চলাকালীন পড়া আর দুটি node বিকলের ক্ষেত্র নিজে চালান
Conflict-এ client-এর timestamp দিয়ে LWW, কিছু না বলেএকসাথে হওয়া লেখা নিঃশব্দে হারায়অংশ ৬-এর মতো ভাগের মধ্যে দুটি লেখা চালান
Hint memory-তে, সীমাহীন; delete-এ সরাসরি মুছে ফেলাদীর্ঘ বিকলতায় memory ফুরোয়; মোছা ডেটা ফিরে আসেhint-এর আকার আর বয়সের সীমা, tombstone আর grace period খুঁজুন
হিসাবে replica-গুণ বাদ; বানানো সীমা বা defaultnode-এর সংখ্যা তিন ভাগের এক ভাগ আসে; পণ্যের default সংস্করণভেদে আলাদাঅংশ ২-এর হিসাব নিজে করুন; documentation মিলিয়ে নিন

AI-কে requirement, অনুমান আর যাচাইয়ের শর্ত দিন — আর পরীক্ষাযোগ্য উত্তর চান:

তুমি একজন অভিজ্ঞ distributed systems ইঞ্জিনিয়ার। একটি কাল্পনিক Dynamo-ধাঁচের key-value store নকশা করো। Requirement: get, put(context সহ), delete; ৩০ দিনে ৯৯.৯% সফল অনুরোধ; একটি region-এ get-এর p99 ১০ ms-এর নিচে; প্রতি অনুরোধে ONE, QUORUM বা ALL; node যোগ বা বাদে downtime নেই। অনুমান: ১০০ কোটি key, প্রতি item ১.১ KB, গড়ে সেকেন্ডে ৫০ হাজার অনুরোধ, ব্যস্ত সময়ে তিন গুণ, ৮০% পড়া, N = ৩। ১. Storage, peak QPS, replica-স্তরের কাজ আর node-এর সংখ্যা ধাপে ধাপে হিসাব করো; প্রতিটি অনুমান আলাদা করে লেখো। ২. Leaderless quorum আর প্রতি shard-এ Raft — দুটির তুলনা করে কোনটি কেন বাছলে বলো, আর কোন keyspace-এ অন্যটি লাগবে। ৩. একটি replica বন্ধ, দুটি বন্ধ, network ভাগ, node যোগের মাঝে — প্রতিটি ক্ষেত্রে পড়া-লেখার কী হয়, পুরোনো মান কখন আসে। ৪. Conflict কীভাবে চেনো আর মেলাও; LWW ব্যবহার করলে কী হারায়। ৫. ring আর quorum-এর ৪০ লাইনের একটি JavaScript simulator দাও, seed দেওয়া PRNG দিয়ে, যাতে আমি চালিয়ে ফল মেলাতে পারি। ৬. কোনো পণ্যের default, সীমা বা দাম নিজে থেকে লিখবে না; দরকার হলে লিখবে "documentation-এ যাচাই করতে হবে"।

AI-এর দেওয়া KV store নকশা বা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

AI দেয় দ্রুত খসড়া, যাচাইয়ের দায় আপনার।
Node বন্ধ করুন, network ভাগ করুন, তারপর গুনুন — কোন “OK” পাওয়া লেখা হারাল?

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

ইন্টারভিউয়ার নকশা শোনার পর এভাবে চাপ দেন। প্রতিটি নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Dynamo আর Amazon-এর পরের DynamoDB কি একই নকশা? পরের প্রজন্ম leaderless পথ থেকে সরে গেল কেন?

নাম মিললেও নকশা আলাদা, আর এই পার্থক্যটি নিজেই একটি শিক্ষা। ২০০৭ সালের Dynamo paper একটি অভ্যন্তরীণ ব্যবস্থার কথা বলে, যা প্রতিটি সেবা-দল নিজে চালাত: leaderless replication, sloppy quorum, vector clock আর client-এর কাছে sibling. লক্ষ্য ছিল shopping cart-এর মতো সেবায় “সবসময় লেখা নেওয়া”, আর তার দাম — conflict মেলানো — অ্যাপ-লেখকের ঘাড়ে।

২০১২ সালে আসা DynamoDB একটি পরিচালিত (managed) সেবা। এর নকশা নিয়ে ২০২২ সালের USENIX ATC paper অনুযায়ী প্রতিটি partition-এর replica-রা একটি replication group, যারা Multi-Paxos দিয়ে একজন leader বাছে; লেখা যায় leader-এর মাধ্যমে, আর strongly consistent পড়ার বিকল্প আছে। অর্থাৎ পরের প্রজন্ম অনেকটা এই পর্বের “Raft-per-shard” পথের কাছাকাছি। একটি যুক্তিসঙ্গত ব্যাখ্যা: হাজারো অপরিচিত গ্রাহকের সেবায় “নিজের অ্যাপে sibling মেলান” বলা কঠিন, সরল আচরণের দাম বেশি।

তাহলে কি leaderless ভুল ছিল? না। দুটির শর্ত আলাদা। Leaderless নকশায় network ভাগেও লেখা চলে, কোনো failover-এর থামা নেই, আর tail latency ছোট রাখা সহজ, কারণ সবচেয়ে দ্রুত W টির অপেক্ষা। দাম: অ্যাপকে conflict বুঝতে হয়, আর “আগে না থাকলে লেখা”-র মতো শর্ত দেওয়া যায় না। Leader-ভিত্তিক নকশায় ক্রম আর শর্তযুক্ত লেখা সহজ, কিন্তু leader বিকল হলে কিছুক্ষণ থামা, আর লেখা সবসময় leader পর্যন্ত যায়। Cassandra-ও পরে শর্তযুক্ত লেখার জন্য Paxos-ভিত্তিক “lightweight transaction” যোগ করেছে বলে প্রকাশিত।

ইন্টারভিউতে তাই প্রশ্নটির শক্ত উত্তর হলো ব্যবহারকারী চেনা: আপনার store-এর ব্যবহারকারী কি নিজের conflict নিজে বুঝবেন, নাকি তাঁদের সরল চুক্তি দরকার? অভিজ্ঞ অভ্যন্তরীণ দলের জন্য leaderless চলে; সবার জন্য খোলা সেবায় সাধারণত leader-ভিত্তিক।

মূল উপলব্ধি: নকশা একটি সময়ের, একটি ব্যবহারকারী-দলের উত্তর। একই কোম্পানি একই সমস্যায় পরে উল্টো পথে গেছে — কারণ requirement বদলেছে, প্রযুক্তি নয়।

প্র ০২একটি key-এ হঠাৎ সেকেন্ডে লাখো পড়া এল — ধরুন একটি ভাইরাল পোস্টের like-এর সংখ্যা। Consistent hashing তো আছে, তাহলে সমস্যা কোথায়?

Consistent hashing ভার ছড়ায় key-দের মধ্যে, একটি key-এর ভেতরে নয়। একটি key সবসময় একই তিনটি replica-তে থাকে; virtual node যত বাড়ান, ওই key-এর সব অনুরোধ ওই তিনটিতেই যায়। অংশ ২-এর হিসাবে একটি node সেকেন্ডে ২০ হাজার replica-কাজ সামলায় (ধরা যাক); R = ২-এ সেকেন্ডে এক লাখ get মানে ওই তিন node-এ দুই লাখ কাজ — তিনটিই ডুববে, আর তাদের ভাগের অন্য সব key-ও ধীর হবে। ঈদের আগে টার্মিনালে কাউন্টার অনেক, কিন্তু সবাই একটি বাসের কাউন্টারের সামনে — ঠিক তেমন।

প্রতিকার কয়েক স্তরে। প্রথম, পড়ার ভার ছড়ানো: QUORUM-এর বদলে এই key-এ ONE, আর তিন replica-র যেকোনোটি থেকে পড়া — ভার তিন ভাগ হয়, কিন্তু কয়েক মুহূর্ত পুরোনো মানের ঝুঁকি মেনে। দ্বিতীয়, store-এর সামনে cache: client library বা একটি cache-স্তরে কয়েক সেকেন্ডের TTL দিয়ে মানটি রাখা (পর্ব ৩০-এ hot key-এর কৌশল)। like-এর সংখ্যা এক সেকেন্ড পুরোনো হলে কারও ক্ষতি নেই। তৃতীয়, একসাথে আসা একই key-এর পড়াগুলো মিলিয়ে store-এ একবার যাওয়া।

লেখাও hot হলে আলাদা কৌশল। Like-এর সংখ্যা একটি counter, তাই একে কয়েকটি টুকরো key-এ ভাগ করা যায় — likes:post9#0 থেকে #15, প্রতিটি লেখা এলোমেলো একটি টুকরোয়, পড়ার সময় যোগফল। টুকরোগুলো আলাদা node-এ পড়ে, তাই লেখার ভার ১৬ ভাগ।

সবশেষে চিহ্নিত করা: node-প্রতি ভাগের metric আর সবচেয়ে বেশি অনুরোধের key-এর তালিকা (count-min sketch দিয়ে কম memory-তে, পর্ব ৪৫)। না মাপলে আপনি দেখবেন শুধু “তিনটি node ধীর”, কারণ খুঁজে পাবেন না।

মূল উপলব্ধি: partitioning সমান ভাগের প্রতিশ্রুতি দেয় key-এর সংখ্যায়, অনুরোধের সংখ্যায় নয়। বাস্তবের অনুরোধ প্রায়ই অসম (Zipf-এর মতো), তাই hot key-এর পরিকল্পনা আলাদা করে লাগে।

প্র ০৩Vector clock-এ কি সমস্যা নেই? একই coordinator-এ একসাথে দুটি লেখা এলে, বা clock বড় হয়ে গেলে কী হয়?

আছে, আর এখানেই অনেক প্রথম সংস্করণ ভুল করে। প্রথম সমস্যা: ধরুন দুজন client একই context {A:1} নিয়ে প্রায় একসাথে A-কে দুটি আলাদা মান পাঠালেন। A যদি শুধু context-এ নিজের ঘর বাড়ায়, দুটি লেখাই পায় {A:2} — একই clock, আলাদা মান। Store ভাবে দুটি একই সংস্করণ, তাই একটি নিঃশব্দে হারায় — ঠিক সেই ক্ষতি, যা ঠেকাতে vector clock এনেছিলাম। সমাধান হলো coordinator-কে নিজের কাছে ওই key-এর সর্বোচ্চ counter মনে রেখে তার পরের সংখ্যা দেওয়া, যাতে দ্বিতীয়টি পায় {A:3}, যা context-এর ভিত্তিতে প্রথমটির সাথে concurrent বলে চেনা যায়। এই সূক্ষ্মতা ঠিকভাবে ধরতে Preguiça ও সহকর্মীরা dotted version vector প্রস্তাব করেন, আর প্রকাশিত বিবরণ অনুযায়ী Riak ২.০ থেকে এটি ব্যবহার করে। এতে অপ্রয়োজনীয় sibling-এর বিস্ফোরণও কমে।

দ্বিতীয় সমস্যা: আকার। প্রতিটি coordinator-এর একটি ঘর; ২৪টি node-এর যেকোনোটি coordinator হলে clock ২৪ ঘরের হতে পারে, প্রতিটি item-এর সাথে। Dynamo paper-এর উত্তর: coordinator সাধারণত preference list-এর প্রথম কয়েকটি node, তাই ঘর কম থাকে; আর একটি সীমা (উদাহরণ হিসেবে ১০) পেরোলে সবচেয়ে পুরোনো ঘর ফেলা হয়, প্রতিটি ঘরের সাথে শেষ বদলের সময় রেখে। ফেলার দাম: খুব কম ক্ষেত্রে দুটি আসলে-ক্রমিক সংস্করণ concurrent মনে হয়ে অপ্রয়োজনীয় sibling তৈরি হয় — ডেটা হারায় না, client-কে একটু বেশি মেলাতে হয়। Paper অনুযায়ী বাস্তবে এটি সমস্যা হয়নি, তবে তা Dynamo-র workload-এর অভিজ্ঞতা; আপনার workload-এ মেপে দেখতে হবে।

তৃতীয় সূক্ষ্মতা: client যদি context ছাড়া put করেন, store সেটিকে সবকিছুর সাথে concurrent ধরবে — প্রতিটি অসতর্ক put একটি নতুন sibling. তাই client library-তে নিয়ম: put-এর আগে get, আর context ফেরত।

মূল উপলব্ধি: vector clock একটি ধারণা; নিরাপদ বাস্তবায়নে counter কে দেয়, কীভাবে ছাঁটা হয় আর context ছাড়া লেখা কীভাবে সামলানো হয় — এই তিনটি সিদ্ধান্ত আলাদা করে লিখতে হয়।

প্র ০৪একটি node-এর ডিস্ক পুরো নষ্ট হয়ে গেছে — আর ফিরবে না। Hinted handoff কি এখানে কাজে আসে? নতুন node কীভাবে আনবেন?

না, আর এখানেই ক্ষণিক আর স্থায়ী বিকলতার পার্থক্য জরুরি। Hint রাখা হয় “node কিছুক্ষণ পরে ফিরবে” ধরে নিয়ে; তার সময়-সীমা থাকে (Cassandra-ধাঁচে কয়েক ঘণ্টা, সংস্করণভেদে মিলিয়ে নিন), কারণ দিনের পর দিন hint জমালে অন্য node-দের ডিস্ক ভরে। অনুশীলন ১-এর হিসাবে ব্যস্ত সময়ে এক ঘণ্টাতেই প্রায় ১৪.৮৫ GB hint জমে। তাছাড়া hint আছে শুধু বিকলতার পরের লেখার; আগের সব ডেটা — node-এর ২৫৫ GB — hint-এ নেই।

পথ তাই আলাদা। প্রথমে ঘোষণা: অপারেটর বা স্বয়ংক্রিয় প্রক্রিয়া স্পষ্টভাবে বলবে “এই node স্থায়ীভাবে গেছে” — নিজে থেকে নয়, কারণ ভুল ঘোষণায় অকারণ বিশাল streaming শুরু হবে। তারপর দুটি পথ: নতুন যন্ত্রকে একই token দিয়ে “replace” হিসেবে আনা, যাতে শুধু ওই node-এর পরিসরগুলো অন্য replica-দের থেকে stream হয়; অথবা ring থেকে সরিয়ে পরিসর বাকিদের মধ্যে ভাগ করা। Replace সাধারণত কম ডেটা সরায়।

Streaming চলাকালীন ঝুঁকিটি দেখুন: যে পরিসরগুলোর তিন কপির একটি গেছে, সেখানে এখন দুটি কপি। এ সময় আরেকটি বিকলতা হলে একটি কপি বাকি — W = ২-এ লেখা থামবে, আর ওই শেষ কপিও গেলে ডেটা হারাবে। তাই পুনর্গঠন যত দ্রুত তত ভালো, আর virtual node এখানে বড় সুবিধা: নষ্ট node-এর পরিসর অনেক node-এ ছড়ানো, তাই ২৫৫ GB বাকি ২৩টি ভাগ করে পাঠায় — প্রতিজনের ভাগ মোটামুটি ১১ GB.

শেষে যাচাই: নতুন node-এর সাথে প্রতিটি replica-র Merkle tree মিলিয়ে নিশ্চিত হওয়া যে streaming-এ কিছু বাদ পড়েনি। আর পুরো প্রক্রিয়ার runbook ও মহড়া (পর্ব ৫১)।

মূল উপলব্ধি: hinted handoff আর repair ক্ষণিকের বিকলতার জন্য; স্থায়ী বিকলতা একটি স্পষ্ট, মাপা, পরীক্ষা করা প্রক্রিয়া, আর তার গতিই ঠিক করে দ্বিতীয় বিকলতায় ডেটা হারানোর ঝুঁকি।

প্র ০৫আপনার store যে কথা রাখছে — কোনো “OK” পাওয়া লেখা হারাচ্ছে না — তা কীভাবে প্রমাণ করবেন?

সাধারণ unit test এখানে প্রায় কিছুই প্রমাণ করে না, কারণ ভুলগুলো থাকে বিরল সময়-বিন্যাসে: ঠিক যখন একটি node বন্ধ হলো, একটি hint পৌঁছাল, আর একটি পড়া এল। এই সমস্যা নিয়ে সবচেয়ে পরিচিত কাজ Kyle Kingsbury-র Jepsen (২০১৩ থেকে)। পদ্ধতিটি সরল: অনেক client একসাথে পড়া-লেখা চালায় আর প্রতিটি অনুরোধের শুরু, শেষ আর ফল লিখে রাখে; এর মাঝে একটি “nemesis” network ভাগ করে, node মারে, ঘড়ি সরায়। শেষে একটি checker দেখে, লেখা ইতিহাসটি দাবি করা consistency model-এর সাথে মেলে কি না — যেমন linearizable হলে কোনো ক্রম আছে কি না (পর্ব ৪০)। প্রকাশিত বিশ্লেষণে বহু জনপ্রিয় database-এ documentation-এর দাবির সাথে না মেলা আচরণ ধরা পড়েছে।

দ্বিতীয় পদ্ধতি deterministic simulation. FoundationDB-র দল প্রকাশ্যে বর্ণনা করেছে, তারা পুরো cluster একটি প্রক্রিয়ার ভেতরে simulate করে — network, ডিস্ক, ঘড়ি সব নকল, আর সব এলোমেলোতা একটি seed থেকে। ফলে একটি বিরল ভুল একবার ধরা পড়লে একই seed দিয়ে হুবহু আবার ঘটানো যায়। এই পর্বের simulator-গুলো সেই ভাবনার খুব ছোট রূপ — mulberry32-এর seed বদলালে নতুন ইতিহাস, একই seed-এ একই ফল।

তৃতীয় স্তর production-এ: read repair কতবার পুরোনো কপি পেল তা গোনা, Merkle tree-তে অমিলের হার দেখা, আর game day-তে ইচ্ছা করে node বন্ধ করে SLI দেখা — পর্ব ৪৮-এর chaos engineering.

সবশেষে আনুষ্ঠানিক যাচাই: protocol-টি TLA+-এর মতো ভাষায় লিখে model checker দিয়ে ছোট সব অবস্থা পরীক্ষা — কোডের আগেই নকশার ভুল ধরা।

মূল উপলব্ধি: distributed store-এর ঠিক থাকা “চলছে” দেখে বোঝা যায় না; ইতিহাস লিখে, বিকলতা ঘটিয়ে, আর একটি checker দিয়ে মিলিয়ে প্রমাণ করতে হয় — আর প্রতিটি নতুন ভুলকে পুনরায় ঘটানো যায় এমন পরীক্ষায় বদলাতে হয়।

অনুশীলন

হিসাব, requirement বদল, একটি ভুল নকশার সমালোচনা আর simulator-এর ফল আগে থেকে বলা — চারটিই উত্তরসহ।

  1. Hint-এর হিসাব: “ভান্ডার”-এ ২৪টি node, ব্যস্ত সময়ে সেকেন্ডে ৩০ হাজার put, N = ৩, প্রতি item ১.১ KB. একটি node ব্যস্ত সময়ে এক ঘণ্টা বন্ধ থাকল। মোটামুটি কয়টি hint জমবে, কত GB? তিন ঘণ্টা বন্ধ থাকলে? Virtual node থাকলে এই বোঝা কার ঘাড়ে পড়ে?

    Replica-স্তরে লেখা: ৩০,০০০ × ৩ = সেকেন্ডে ৯০ হাজার, ২৪টি node-এ সমান ভাগে node-প্রতি ৩,৭৫০. বন্ধ node-এর ভাগের এই লেখাগুলো hint হবে: ৩,৭৫০ × ৩,৬০০ = ১ কোটি ৩৫ লাখ hint, × ১.১ KB ≈ ১৪.৮৫ GB এক ঘণ্টায়। তিন ঘণ্টায় ৪ কোটি ৫ লাখ hint, প্রায় ৪৪.৫৫ GB. Virtual node থাকলে বন্ধ node-এর পরিসর ring-জুড়ে ছড়ানো, তাই hint বাকি ২৩টিতে মোটামুটি ভাগ হয় — এক ঘণ্টায় প্রতিটিতে প্রায় ০.৬৫ GB; একটি token-এ সব বোঝা পড়ত পরের এক-দুটি node-এ। শিক্ষা: hint-এর ডিস্ক-বাজেট আর সময়-সীমা লাগে; লম্বা বিকলতায় repair-ই সস্তা।

  2. Requirement বদল: পণ্য দল বলল, “ভান্ডার”-এ একটি নতুন keyspace wallets লাগবে — কাল্পনিক একটি mobile financial service-এর balance. শর্ত: কোনো লেখা হারানো চলবে না, আর “balance ৫০০-এর বেশি হলে তবেই ২০০ কাটা” ধরনের শর্তযুক্ত বদল লাগবে। বর্তমান নকশায় কী ভাঙে, আর কীভাবে বদলাবেন?

    যা ভাঙে: R + W > N শর্তযুক্ত বদল দেয় না — দুটি coordinator একসাথে ৬০০ পড়ে দুজনেই ২০০ কাটলে sibling হবে, আর কোনো merge function জানে না কোনটি বৈধ ছিল। Sloppy quorum-এ network ভাগের দুই দিকেই কাটা চলবে। LWW-এ একটি কাটা নিঃশব্দে হারাবে।

    বদল: শুধু wallets keyspace-এর জন্য আলাদা পথ — key-পরিসর ভাগ করে প্রতিটি পরিসরে তিন replica-র একটি Raft group (Multi-Raft, পর্ব ৪২)। API-তে put_if(key, value, expected_version): leader log-এর ক্রমে যাচাই করে লেখে, না মিললে 409. পড়া leader থেকে (read index বা lease-সহ), যাতে linearizable থাকে। Sloppy quorum আর hint এই keyspace-এ বন্ধ — majority না থাকলে লেখা থামবে, আর সেটাই ঠিক। দাম: leader পর্যন্ত বাড়তি hop, leader বদলে কয়েক সেকেন্ড থামা, আর p99 বেশি — তাই অন্য keyspace-এ leaderless থাকে। আরও ভালো: balance-কে একটি মান না রেখে double-entry ledger-এর entry হিসেবে রাখা (পর্ব ৬০), আর প্রতিটি বদলে idempotency key.

  3. সমালোচনা: একজন সহকর্মীর নকশা: “N = ৩, W = ১, R = ১ — দ্রুততম। Conflict-এ client-এর পাঠানো timestamp দিয়ে LWW. Hint memory-তে রাখি, সীমা নেই। Node মৃত ধরি পরপর তিনটি heartbeat (প্রতি সেকেন্ডে) না এলে, আর মৃত ধরলেই তার ডেটা অন্যদের মধ্যে ভাগ করে দিই। তিন replica একই rack-এ, latency কম।” অন্তত পাঁচটি সমস্যা আর প্রতিকার লিখুন।
    • W = ১, R = ১: overlap নেই — অংশ ৫-এর simulator-এ ৬১% stale read; ack দেওয়া একমাত্র node মরলে লেখাই হারায়। প্রতিকার: default W = ২, R = ২.
    • Client-এর timestamp-এ LWW: ভুল ঘড়ির একটি ফোন সবার লেখা ঢেকে দিতে পারে, concurrent লেখা নিঃশব্দে হারায়। প্রতিকার: vector clock + sibling, বা CRDT.
    • সীমাহীন hint memory-তে: লম্বা বিকলতায় memory ফুরোয়, আর node restart-এ সব hint হারায়। প্রতিকার: ডিস্কে hint, আকার আর সময়ের সীমা, তারপর repair.
    • তিন সেকেন্ডে মৃত ঘোষণা আর সাথে সাথে ডেটা সরানো: একটি GC pause-এই ভুল ঘোষণা আর বিশাল streaming. প্রতিকার: phi accrual-এ “সন্দেহ”, স্থায়ী বাদ শুধু স্পষ্ট ঘোষণায়।
    • এক rack-এ তিন replica: একটি switch বা বিদ্যুৎ লাইনেই তিনটি কপি যায় — correlated failure. প্রতিকার: preference list-এ rack বা zone-ভেদ বাধ্যতামূলক।
    • বাদ পড়া বিষয়: read repair, anti-entropy আর tombstone নেই — বিরল পড়া key অমিল থাকে, মোছা ডেটা ফেরে।
  4. ফল আগে বলুন: অংশ ৭-এর mini_store.js-এ শুধু DETECT = 300 বদলে DETECT = 0 করলেন — অর্থাৎ node ফেরার মুহূর্তেই hint পৌঁছায়। তিনটি সারির কোন কোন সংখ্যা বদলাবে, আর নতুন মান কী হবে বলে মনে করেন? তারপর চালিয়ে মিলিয়ে নিন।

    শুধু তৃতীয় সারির stale read বদলায়: 13 থেকে 1. চালালে আসে:

    strict             availability 93.29%  stale reads 0  hints stored 0  delivered 0
    sloppy, no handoff availability 100.00%  stale reads 28  hints stored 0  delivered 0
    sloppy + hints     availability 100.00%  stale reads 1  hints stored 1847  delivered 1847

    কারণ: প্রথম দুই নীতি DETECT ব্যবহারই করে না। তৃতীয়টিতে ১৩টির ১২টি stale read ছিল ফেরার পরের ৩০০ অনুরোধের ফাঁকে, hint পৌঁছানোর আগে — সেই ফাঁক এখন শূন্য। বাকি একটি (অনুরোধ ৪৯৫৪) ঘটেছিল outage চলাকালীন, যখন পড়ার দলের দুটি বিকল্প node-এর কাছে আগের লেখাই ছিল না; hint যত দ্রুতই পৌঁছাক, এটি ঠেকায় না — ঠেকাত read repair বা strict quorum. Hint-এর সংখ্যা 1847 একই, কারণ কতগুলো লেখা বিকল্প node-এ গেছে তা DETECT-এর ওপর নির্ভর করে না। বাস্তবে DETECT শূন্য হয় না; শিক্ষা হলো, ফেরার পরের জানালা যত ছোট, stale read তত কম।

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

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

ক্রমDynamo-ধাঁচের store-এ একটি put-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Client যেকোনো একটি node-এ অনুরোধ পাঠায়
  2. Key hash করে ring থেকে preference list বের করা, আর অনুরোধ coordinator-এ পৌঁছানো
  3. Coordinator vector clock-এ নিজের ঘর বাড়িয়ে নিজের WAL আর memtable-এ লেখে
  4. বাকি replica-দের পাঠানো; বন্ধ replica-র লেখা পরের সুস্থ node-এ hint হিসেবে
  5. W টি ack পেলে client-কে সফল জানানো
  6. বন্ধ replica ফিরলে hint পৌঁছানো, বাকি অমিল anti-entropy-তে

একটি store-এ ৪টি সার্ভার, consistent hashing আর অনেক virtual node. পঞ্চম সার্ভার যোগ হলে আদর্শভাবে মোট key-এর কত অংশ সরবে?

  • প্রায় ৮০%
  • প্রায় ২৫%
  • প্রায় ২০%
  • প্রায় ৫০%

N = 3, W = 2, R = 2. তিনটি replica-র একটি বন্ধ। কোন কথাটি ঠিক?

  • পড়া আর লেখা দুটিই থেমে যায়
  • পড়া-লেখা চলে, কিন্তু p99 বাড়তে পারে, কারণ বাকি দুটির দুটিরই অপেক্ষা
  • পুরোনো মান পড়ার ঝুঁকি স্বাভাবিকভাবে অনেক বেড়ে যায়
  • শুধু লেখা চলে, পড়া থামে

একই key-এর দুটি সংস্করণের vector clock {A:2} আর {A:1, B:1}. Store কী করবে?

  • {A:2} নতুন, তাই অন্যটি ফেলে দেবে
  • {A:1, B:1}-এ ঘর বেশি, তাই সেটি রাখবে
  • দুটির timestamp তুলনা করে বড়টি রাখবে
  • দুটিই sibling হিসেবে রাখবে, কারণ এরা concurrent

Leaderless store-এ hinted handoff-এর মূল কাজ কী?

  • সাময়িক বন্ধ replica-র লেখা অন্য node রেখে পরে তাকে পৌঁছে দেওয়া
  • স্থায়ীভাবে নষ্ট node-এর সব পুরোনো ডেটা ফিরিয়ে আনা
  • প্রতিটি পড়াকে linearizable করা
  • বিকল leader-এর জায়গায় নতুন leader বাছা

একটি LSM-ভিত্তিক node-এ প্রতিটি SSTable-এর সাথে Bloom filter রাখার প্রধান লাভ কী?

  • লেখা দ্রুত হয়, কারণ WAL লাগে না
  • Compaction আর লাগে না
  • যে ফাইলে key নেই সেখানে ডিস্ক না ছুঁয়ে এগোনো যায়
  • Key-এর মান সরাসরি Bloom filter থেকে পড়া যায়

কোন keyspace-এর জন্য leaderless quorum-এর বদলে প্রতি shard-এ Raft বেছে নেওয়া সবচেয়ে যুক্তিসঙ্গত?

  • চালকের সর্বশেষ অবস্থান
  • Wallet-এর balance, যেখানে শর্তযুক্ত বদল লাগে
  • Shopping cart
  • একটি পোস্টের like-এর সংখ্যা

শব্দকোষ

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

মনে রাখুন

  1. নকশা শুরু হয় requirement আর হিসাব দিয়ে: replica-স্তরের কাজ থেকে node-এর সংখ্যা — “ভান্ডার”-এ বাধা throughput, storage নয়।
  2. Leaderless যখন লেখা নেওয়াই আগে আর conflict মেলানো যায়; Raft-per-shard যখন শর্তযুক্ত বদল বা অনন্যতা লাগে — একই store-এ keyspace-ভেদে দুটিই চলতে পারে।
  3. Ring-এ অনেক virtual node ভাগ সমান করে আর node যোগে প্রায় ১/N সরায়; N = ৩, W = ২, R = ২ একটি বিকল সহ্য করে, কিন্তু তখন p99 বাড়ে।
  4. Vector clock একসাথে হওয়া লেখা sibling হিসেবে রাখে; LWW একটি নিঃশব্দে হারায়; counter-এ CRDT.
  5. ক্ষণিকের বিকলতায় sloppy quorum + hint, বাকিটা read repair আর Merkle anti-entropy; tombstone, repair আর compaction-এর সময়সূচি একসাথে ভাবতে হয়।

পরের পর্বে (পর্ব ৬২): মডিউল ১০ — পরীক্ষা ও ইন্টারভিউ প্রস্তুতি — শুরু হবে estimation আর trade-off-এর অনুশীলন দিয়ে: QPS, storage, availability, quorum আর key সরানোর হিসাব, প্রতিটির পূর্ণ সমাধানসহ।

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

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