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

Replication — leader-follower, multi-leader, leaderless আর replication lag

Replication — Leader–Follower, Sync vs Async, Replication Lag, Failover, Multi-Leader & Leaderless

Taught in Bangla, this lesson explains why data is replicated and where replication does not help. It compares statement, WAL and logical logs, the latency and data-loss trade-offs of synchronous, asynchronous and semi-synchronous replication, three replication-lag anomalies and their fixes such as read-your-writes, adding followers, failover with lost writes and reused IDs, multi-leader conflicts and topologies, leaderless designs, and cross-region replication, with simulators.

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

  • কেন replicate করা হয়, আর কোথায় replication সাহায্য করে না
  • Statement, WAL আর logical log; sync, async ও semi-sync-এর latency আর ডেটা হারানোর হিসাব
  • Replication lag-এর তিন anomaly আর প্রতিকার, একটি সিমুলেটরে মেপে
  • নতুন follower যোগ, failover-এ হারানো লেখা আর পুনর্ব্যবহৃত ID, স্বয়ংক্রিয় বনাম হাতে failover
  • Multi-leader-এর conflict ও topology, leaderless নকশা, তুলনা আর region পেরোনো replication-এর হিসাব

১ · কেন একই ডেটার কয়েকটি কপি — চার কারণ, তিন নকশা

একটি কাল্পনিক অনলাইন মুদি-বাজার সেবার পুরো database একটি সার্ভারে। এক দিন power supply নষ্ট — কেউ অর্ডার দিতে পারেন না। ঈদের আগের সন্ধ্যায় পড়ার চাপেই সার্ভার হাঁপিয়ে ওঠে। প্রবাসী ক্রেতারা দেখেন প্রতিটি পাতা ধীর। আর এক রাতে ডিস্কটিই নষ্ট। চার সমস্যারই সাধারণ উত্তর: একই ডেটার কপি অন্য যন্ত্রে রাখা আর হালনাগাদ রাখা — replication; প্রতিটি কপি একটি replica.

পাড়ার মুদি দোকানের বাকির খাতা ভাবুন। নতুন বাকি শুধু মূল দোকানের মূল খাতায় লেখা হয়। দুই শাখা দোকানে খাতার কপি; মালিক নিয়মিত নতুন লেখার তালিকা পাঠান, সহকারীরা ঠিক সেই ক্রমে তোলেন। ক্রেতা যেকোনো শাখায় নিজের বাকি দেখতে পারেন — কিন্তু শেষ তালিকা না উঠলে দেখবেন পুরোনো হিসাব, এটাই lag. মূল দোকানে আগুন লাগলে একটি শাখার খাতাই মূল হবে, আর তখনো না-পাঠানো লেখাগুলো চিরতরে হারাবে। এই পর্বের প্রায় সব ধারণা এই এক দৃশ্যেই।

🛟 Availability

একটি সার্ভার, rack বা zone মরলে আরেকটি কপি দায়িত্ব নেয় — failover, যার হিসাব পর্ব ৫-এ।

যন্ত্র মরলেও সেবা চলে

📖 পড়ার scaling

পড়ার অনুরোধ কয়েকটি read replica-তে ভাগ হয় — পড়া-ভারী সেবায় সবচেয়ে সহজ scaling.

শুধু পড়ার, লেখার নয়

🌏 ভৌগোলিক latency

দূরের ব্যবহারকারীর কাছে একটি কপি। ৩,০০০ km দূরের কপিতে যাওয়া-আসায় কাচের তারে (optical fiber) আলোরই অন্তত ৩০ ms লাগে — কোনো কৌশলে কমে না।

কাছের কপি, কম অপেক্ষা

💾 Durability

ডিস্ক নষ্ট হলেও ডেটা অন্য জায়গায় থাকে — durability বাড়ে। কিন্তু ভুল DELETE-ও সব কপিতে পৌঁছায়, তাই backup আলাদা জিনিস।

replication ≠ backup

ডেটা না বদলালে replication হতো একবার কপির কাজ। কঠিনতা পরিবর্তনে: লেখা কোথায় নেওয়া হবে, কীভাবে বাকি কপিতে যাবে, কপিগুলো কখন কতটা মিলবে। তিনটি বড় পরিবার — নিচের ছবিতে পাশাপাশি:

Replication-এর তিন নকশা

Single-leader, multi-leader and leaderless topologies

১ · Single-leader

Single-leader: সব লেখা leader-এ, leader থেকে log দুটি follower-এ, পড়া follower থেকেও অ্যাপ (client) Leader সব লেখা এখানে Follower 1 পড়া Follower 2 পড়া log লেখা ১টিতে · পড়া যেকোনোটিতে লেখা PostgreSQL, MySQL, MongoDB replica set

২ · Multi-leader

Multi-leader: প্রতিটি অঞ্চলে একটি leader, দুই leader-এর মাঝে দুই দিকে log অঞ্চল ১ অঞ্চল ২ Leader A লেখা নেয় Leader B লেখা নেয় Follower Follower A ⇄ B: দুই দিকে async log একই key দুই জায়গায় → conflict বহু ডেটা সেন্টার, offline অ্যাপ

৩ · Leaderless

Leaderless: client তিনটি replica-তে লেখে আর পড়ে, পুরোনো কপি read repair-এ ঠিক হয় client / coordinator R1 v2 R2 v2 R3 v1 read repair N = 3 · W = 2 · R = 2 Dynamo-ধাঁচ: Cassandra, Riak
ছবি ২২.১ — কঠিন তীর লেখা বা log, ড্যাশ তীর পড়া বা মেরামত। (১) একটি leader সব লেখার ক্রম ঠিক করে। (২) প্রতিটি অঞ্চলের leader স্থানীয় লেখা নেয়, তাই দুই জায়গায় একসাথে বদল সম্ভব। (৩) কোনো leader নেই; R3 লেখার সময় বন্ধ ছিল বলে পুরোনো v1 রয়ে গেছে।

একটি হিসাব — replication পড়া বাড়ায়, লেখা নয়

নতুনদের সাধারণ ভুল: “database ধীর? আরও replica দিন।” ধরা যাক: প্রতিটি সার্ভার সেকেন্ডে ১০,০০০টি কাজ (একটি পড়া বা লেখা) সামলায়; follower-এ একটি replicate হওয়া লেখা প্রয়োগও একটি কাজ; সেকেন্ডে ৩৬,০০০ পড়া আর ৪,০০০ লেখা। প্রতিটি সার্ভারকে (leader আর প্রতিটি follower) সব ৪,০০০ লেখা সামলাতে হয়, তাই পড়ার জন্য থাকে ৬,০০০। Leader ৬,০০০ পড়া নিলে বাকি ৩০,০০০ ÷ ৬,০০০ = ৫টি follower — মোট ৬টি সার্ভার। লেখা দ্বিগুণ (৮,০০০) হলে পড়ার জায়গা নামে ২,০০০-এ: (৩৬,০০০ − ২,০০০) ÷ ২,০০০ = ১৭টি follower, মোট ১৮টি — তিন গুণ! লেখা ১০,০০০ ছুঁলে কোনো সংখ্যক follower-ই সাহায্য করে না। লেখার চাপ ভাগের উপায় ডেটাকেই ভাগ করা — পর্ব ২৩. বাস্তবে সার্ভার পুরো ভরানো হয় না (পর্ব ৩-এর headroom), তাই আরও বেশি লাগে।

২ · Single-leader replication — লেখা এক জায়গায়, log সবার কাছে

Single-leader replication (leader-follower, primary-replica; পুরোনো লেখায় master-slave) সবচেয়ে প্রচলিত নকশা। নিয়ম তিনটি: সব লেখা যায় leader-এ; leader লেখাটি আগে নিজের write-ahead log-এ রাখে (WAL, পর্ব ১৯), তারপর পরিবর্তনটি replication log হিসেবে প্রতিটি follower-কে পাঠায়; প্রতিটি follower সেই log একই ক্রমে প্রয়োগ করে। পড়া চলে leader বা যেকোনো follower-এ। PostgreSQL, MySQL, MongoDB-র replica set, Redis, এমনকি Kafka-র প্রতিটি partition — নিজ নিয়মে সবাই এই ধাঁচ মানে।

ক্রম কেন জরুরি? Leader-এ পরপর “balance = ৫০০”, তারপর “balance = ৭০০”। উল্টো ক্রমে প্রয়োগ করা follower-এর শেষ মান ৫০০ — কপিটি নিঃশব্দে ভুল। তাই log-এর প্রতিটি রেকর্ডের একটি ক্রমিক অবস্থান থাকে: log sequence number, সংক্ষেপে LSN (MySQL-এ binlog-অবস্থান বা GTID দিয়ে একই কাজ হয়)। Follower কতটা পিছিয়ে, থেমে গেলে কোথা থেকে আবার শুরু করবে — সবই এই নম্বরে।

Log-এ কী পাঠানো হবে — তিনটি উপায়

একই পরিবর্তন তিনভাবে লেখা যায়। নিচের উদাহরণে একটি অর্ডার “shipped” হলো — দেখুন তিন রূপে log-এ কী যায়:

-- (1) Statement-based: the exact command the leader ran
UPDATE orders SET status = 'shipped', shipped_at = NOW() WHERE id = 1006;

-- (2) WAL / physical: which bytes changed on which page of which file
lsn=0/3A00F2B8 rel=orders block=412 offset=96 len=24 data=73 68 69 70 ...

-- (3) Logical / row-based: which row, which columns, final values
{"lsn": 1042, "table": "orders", "op": "update", "key": {"id": 1006},
 "new": {"status": "shipped", "shipped_at": "2026-10-28T10:00:00.120Z"}}

(১) Statement-based: follower হুবহু সেই কমান্ডটিই আবার চালায়। Log সবচেয়ে ছোট — এক লাখ সারির UPDATE-ও এক লাইন। কিন্তু উপরের কমান্ডেই ফাঁদ: NOW() follower-এ চলবে পরে, তাই shipped_at দুই কপিতে আলাদা। RAND(), UUID(), ORDER BY ছাড়া LIMIT, transaction-এর ক্রমে নির্ভরশীল auto-increment বা trigger — সবই কপিগুলোকে আলাদা করে দিতে পারে।

(২) WAL shipping (physical): storage engine-এর WAL-এ এমনিতেই প্রতিটি বাইট-বদল আছে; সেটিই পাঠানো হয়। Follower হয় বাইট ধরে হুবহু কপি — সব টেবিল, index আর schema বদলসহ। সরল ও দ্রুত; PostgreSQL-এর streaming replication এই পথে। দাম: log বাঁধা storage-এর format-এর সাথে, তাই সাধারণত একই major সংস্করণ লাগে — শূন্য-বিরতিতে upgrade কঠিন; টেবিল বাছাই করেও পাঠানো যায় না।

(৩) Logical (row-based): সারির স্তরের পরিবর্তন — কোন সারি ঢুকল, মুছল বা কোন column কী মানে বদলাল। NOW()-এর আসল মানটিই log-এ, তাই অনির্ধারণ নেই। Storage format থেকে আলাদা বলে ভিন্ন সংস্করণে, বাছাই করা টেবিলে, এমনকি অন্য সিস্টেমেও পাঠানো যায় — এর নাম change data capture (CDC): প্রতিটি পরিবর্তন event হয়ে যায় search index-এ (পর্ব ২৭), cache মোছায় বা একটি log stream-এ (পর্ব ৩৩)। দাম: এক লাখ সারির UPDATE মানে এক লাখ রেকর্ড, আর schema বদল (DDL) প্রায়ই আলাদা সামলাতে হয় — PostgreSQL-এর built-in logical replication লেখার সময়ে DDL পাঠায় না। MySQL-এর binlog-এর row format এই ধাঁচের, ৫.৭.৭ সংস্করণ থেকে default.

ধরনযা পাঠায়সুবিধাঝুঁকি বা দাম
Statement-basedলেখার কমান্ডছোট log, পড়ে বোঝা যায়অনির্ধারিত function, ক্রম আর trigger-এ কপি আলাদা
WAL shippingবাইট-স্তরের page বদলহুবহু কপি, সরল, সবকিছু আসেসংস্করণ ও format-এ বাঁধা; টেবিল বাছাই চলে না
Logical / rowসারির আগে-পরে মানভিন্ন সংস্করণ, বাছাই, CDCবড় batch-এ বড় log; DDL আলাদা সামলাতে হয়

PostgreSQL আর MySQL-এ এসব চালু করা আর lag মাপা — DBMS কোর্সের Backup, replication আর high availability পর্বে live SQL সহ আছে; এখানে আমরা নকশার প্রশ্নে থাকব।

৩ · Synchronous, asynchronous আর semi-synchronous — “OK” কখন বলবেন

সবচেয়ে বড় সিদ্ধান্ত: leader client-কে “commit সফল” কখন বলবে? Asynchronous-এ নিজের ডিস্কে লিখেই, log পরে যায়। Synchronous-এ follower “পেয়েছি” (ack) বলার পর। Semi-synchronous-এ অন্তত একটি follower-এর ack পেলেই, বাকিরা async. ধরা যাক: leader-এর fsync ১ ms; F1 একই ডেটা সেন্টারে, এক দিকে ০.৫ ms; F2 অন্য region-এ, এক দিকে ৩০ ms; follower-এর fsync ১ ms; leader নিজের fsync শেষে log পাঠায়।

  1. ClientUPDATE balanceOK-এর অপেক্ষায়
  2. Leaderfsync 1 msঢাকার ডেটা সেন্টার
  3. F10.5 ms দূরেএকই ডেটা সেন্টার
  4. F230 ms দূরেঅন্য region
ঘড়ি আর তিন ধরনের client
ঘড়ি
async client
semi-sync client
sync (সবার ack)
লেখাটি কয়টি ডিস্কে
  1. t = ০: লেখা এল। Leader UPDATE-টি নিজের WAL-এ যোগ করল (LSN ১০৪২)। তিন ধরনের client-ই অপেক্ষায়।
  2. t = ১ ms: leader-এর fsync শেষ। Async এখনই “OK” বলে — মোট ১ ms. Log এবার দুই follower-এর দিকে; লেখাটি এখনো মাত্র একটি ডিস্কে।
  3. t = ৩ ms: কাছের F1-এর ack. F1 log পেল ১.৫ ms-এ, fsync ২.৫ ms-এ, ack ফিরল ৩ ms-এ। Semi-sync client “OK” পেল; লেখাটি এখন দুটি যন্ত্রের ডিস্কে।
  4. t = ৩২ ms: দূরের F2 লিখল। Log পৌঁছাল ৩১ ms-এ, fsync ৩২ ms-এ — তিন কপি। কিন্তু ack-কে আবার ৩০ ms ফিরতে হবে।
  5. t = ৬২ ms: সবার ack. সবার ack-এর ব্যবস্থায় “OK” এল ৬২ ms-এ — async-এর ৬২ গুণ। একটি connection-এ পরপর লেখা হলে সেকেন্ডে প্রায় ১,০০০-এর বদলে প্রায় ১৬টি। এই দাম আলোর গতির; কোনো optimization-এ যায় না।
  6. পরীক্ষা ১: OK-এর পরপরই leader মরল। Log বেরোনোর আগেই বিদ্যুৎ গেল। Async client জানে টাকা পাঠানো সফল, অথচ কোনো follower-এ লেখাটি নেই — যাকেই promote করুন, হারাল। Semi-sync আর sync client উত্তর পায়নি; timeout-এর পর একই idempotency key দিয়ে আবার চেষ্টা করবে (পর্ব ৮)।
  7. পরীক্ষা ২: দূরের F2 বন্ধ। সবার ack চাইলে কোনো commit-ই শেষ হয় না — durability কিনতে গিয়ে availability গেল। Semi-sync চলছে, কারণ sync ভূমিকা F1-এর; F1 মরলে একটি async follower-কে sync বানানো হয়।

তাই “সব follower synchronous” প্রায় কেউ চালায় না। প্রচলিত বিন্যাস: একই region-এর একটি follower semi-sync (বাড়তি কয়েক ms), দূরের DR follower async. Async-এর ঝুঁকি মাপা যায়: leader হঠাৎ মরলে হারানো সফল-বলা লেখা ≈ লেখার হার × lag. সেকেন্ডে ২,০০০ লেখা আর ২০০ ms lag-এ প্রায় ৪০০টি; ভারী batch কাজে lag ৩০ সেকেন্ড হলে প্রায় ৬০,০০০। কতটা হারানো চলবে, সেই লক্ষ্য RPO (পর্ব ৫১)।

ধরনcommit-এর অপেক্ষা (উদাহরণে)leader হঠাৎ মরলেএকটি follower বন্ধ হলে
Async১ ms — নিজের ডিস্কশেষ lag-এর লেখা হারাতে পারেকিছুই হয় না
Semi-sync (১টি sync)৩ ms — কাছের followerসফল-বলা লেখা অন্তত একটি follower-এ; তাকেই promoteআরেকটি sync হয়; কিছু পণ্যে timeout-এ async-এ নামে
Sync (সবার ack)৬২ ms — দূরেরটির round tripকিছুই হারায় নাসব লেখা আটকে যায়

নাম এক, খুঁটিনাটি আলাদা। MySQL-এর semi-synchronous replication অন্তত একটি replica লেখাটি পেয়ে relay log-এ রাখা পর্যন্ত অপেক্ষা করে, আর নির্ধারিত সময়ে (default প্রায় ১০ সেকেন্ড; setting-এর নাম সংস্করণভেদে আলাদা) উত্তর না এলে async-এ নেমে যায় — চাপের মুহূর্তেই চুপিচুপি RPO শূন্য থাকে না। PostgreSQL-এর setting (synchronous_commit ইত্যাদি) DBMS কোর্সের replication পর্বে। আর “follower-এর ডিস্কে আছে” মানে “সেখান থেকে পড়লে দেখা যাবে” নয় — প্রয়োগে আরও দেরি হতে পারে। সংখ্যাগরিষ্ঠের ack-এ commit (Raft-ধাঁচ) আরেকটি বিকল্প, পর্ব ৪২-এ।

৪ · Replication lag — ব্যবহারকারী যা দেখেন, আর তিনটি নিশ্চয়তা

Async follower সবসময় একটু পিছিয়ে — replication lag. স্বাভাবিক দিনে কয়েক ms; follower ব্যস্ত হলে, নেটওয়ার্ক ধীর হলে বা বড় batch লেখায় সেকেন্ড, এমনকি মিনিট। লেখা থামলে শেষ পর্যন্ত সব কপি মিলবে — এটুকুই eventual consistency; “শেষ পর্যন্ত”-র কোনো সীমা নেই। উল্টো প্রান্তে strong consistency, যেন ডেটার একটিই কপি (কঠোর রূপ linearizability, পর্ব ৪০)। মাঝখানে কয়েকটি ছোট নিশ্চয়তা ব্যবহারকারীর চোখের তিনটি bug সারায়।

Nadia (user 77) ঠিকানা বদলালেন: মিরপুর ১০ → উত্তরা ৭
পড়া বা লেখা কোথায়
F1-এর অবস্থান (LSN)
F2-এর অবস্থান (LSN)
Nadia যা দেখলেন
ভাঙা নিশ্চয়তা
leader-এর বাড়তি চাপ
  1. লেখা leader-এ। একটি কাল্পনিক অনলাইন দোকানে Nadia ডেলিভারির ঠিকানা বদলালেন; leader-এ commit হলো LSN ১০৪২-এ। এই মুহূর্তে F1 ১০৪১-এ, F2 ১০৩৫-এ।
  2. ১০০ ms পরে পড়া গেল F2-তে। F2 তখনো ১০৩৫-এ — তার কাছে বদলটি ঘটেইনি। Nadia পুরোনো ঠিকানা দেখে ভাবলেন সেভ হয়নি। ভেঙেছে read-your-writes: নিজে যা লিখেছেন, পরে পড়লে অন্তত সেটি দেখার নিশ্চয়তা।
  3. দুবার refresh, সময় পেছনে। প্রথমটি F1-এ (১০৪২) — নতুন ঠিকানা; দ্বিতীয়টি F2-তে (১০৪০) — আবার পুরোনো! নতুন অবস্থা দেখার পর পুরোনো দেখা — monotonic reads ভাঙা। প্রতিটি কপি নিজের মতো ঠিক, তবু অ্যাপটি “ভাঙা” লাগে।
  4. প্রতিকার ১ — সময়-জানালা। Session-এ শেষ লেখার সময়; নিজের লেখার ১ সেকেন্ডের মধ্যে সেই ব্যবহারকারীর পড়া leader-এ। সরল, কিন্তু leader-এ চাপ বাড়ায়, আর lag জানালার চেয়ে বড় হলে আবার ভাঙে। আরও সরল রূপ: নিজের প্রোফাইলের মতো ডেটা সবসময় leader থেকে।
  5. প্রতিকার ২ — অবস্থান মেলানো। Session-এ লেখার LSN ১০৪২। Router জানে F2 ১০৪১-এ — চলবে না; F1 ১০৪৩-এ — চলবে। কেউ না পারলে leader থেকে পড়া। এতে leader-এ যায় শুধু সত্যিই পুরোনো পেত এমন পড়া। ফোন আর ল্যাপটপ দুটিতেই নিশ্চয়তা চাইলে অবস্থানটি সার্ভারের দিকে রাখতে হয়।
  6. প্রতিকার ৩ — এক replica-তে বাঁধা। User ID-র hash দেখে Nadia-র সব পড়া সবসময় F1-এ (পর্ব ১৬, পর্ব ১৫-এর sticky ধারণা) — সময় আর পেছনে যায় না। F1 মরলে অন্যটিতে সরানোর আগে দেখতে হয় সে Nadia-র শেষ দেখা অবস্থান পর্যন্ত এসেছে কি না।

কতবার এমন ঘটে? নিচের সিমুলেটরে F1-এর lag ২ ms + গড়ে ৩০ ms-এর এলোমেলো অংশ, F2-এর (অন্য region, বেশি ব্যস্ত) ৪০ ms + গড়ে ১৬০ ms. ব্যবহারকারী লিখে gap ms পরে পড়েন; তিন নিয়মে কত ভাগ পড়া পুরোনো আর কত ভাগ leader-এ যায়, গুনে দেখুন। Seed নির্দিষ্ট, তাই প্রতিবার একই ফল।

// A user writes, then reads `gap` ms later. Leader + 2 async followers.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = seed + 0x6D2B79F5 | 0;
    let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const rand = rng(22);
const exp = mean => -mean * Math.log(1 - rand());      // random delay, average = mean
const F = [{ d: 2, m: 30 }, { d: 40, m: 160 }];        // follower lag = d + exp(m) ms
const N = 20000, pct = x => (100 * x / N).toFixed(1) + "%";
console.log("gap   random   1s-window      LSN check");
console.log("ms    stale    stale leader   stale leader");
for (const gap of [20, 100, 500, 2000]) {
  let rnd = 0, winStale = 0, winLeader = 0, lsnStale = 0, lsnLeader = 0;
  for (let i = 0; i < N; i++) {
    const lag = F.map(f => f.d + exp(f.m));            // when F1, F2 apply this write
    const pick = rand() < 0.5 ? 0 : 1;                 // load balancer picks a follower
    const fresh = lag.map(l => l <= gap);              // has follower applied it by read time?
    if (!fresh[pick]) rnd++;                           // (a) plain random follower
    if (gap < 1000) winLeader++;                       // (b) own reads -> leader for 1 s
    else if (!fresh[pick]) winStale++;
    const to = fresh[pick] ? pick : fresh[1 - pick] ? 1 - pick : -1; // (c) position check
    if (to < 0) lsnLeader++;                           // no follower caught up -> leader
    else if (!fresh[to]) lsnStale++;                   // never happens: checked first
  }
  console.log(String(gap).padEnd(6) + pct(rnd).padEnd(9) + pct(winStale).padEnd(6) +
    pct(winLeader).padEnd(9) + pct(lsnStale).padEnd(6) + pct(lsnLeader));
}

আউটপুট:

gap   random   1s-window      LSN check
ms    stale    stale leader   stale leader
20    77.4%    0.0%  100.0%   0.0%  54.9%
100   36.2%    0.0%  100.0%   0.0%  2.7%
500   2.9%     0.0%  100.0%   0.0%  0.0%
2000  0.0%     0.0%  0.0%     0.0%  0.0%

লিখেই ১০০ ms পরে পড়লে এলোমেলো follower-এ 36.2% পড়া পুরোনো — প্রায় প্রতি তিনজনে একজন নিজের বদল দেখেন না। সূত্রেও মেলে: এই ধাঁচের lag-এ P(lag > g) = e−(g − d)/m; F1-এর জন্য e−৯৮/৩০ ≈ ০.০৩৮, F2-এর e−৬০/১৬০ ≈ ০.৬৮৭, গড় ≈ ০.৩৬৩। সময়-জানালা পুরোনো পড়া শূন্য করে, কিন্তু প্রথম ১ সেকেন্ডের সব পড়া (100.0%) leader-এ ঠেলে দেয়। অবস্থান মেলানোর নিয়মে পুরোনো পড়াও শূন্য, আর leader-এ যায় মাত্র 2.7% — যখন দুই follower-ই পিছিয়ে (০.০৩৮ × ০.৬৮৭ ≈ ০.০২৬)। gap ২০ ms-এ দেখুন: F2-এর স্থির ৪০ ms দেরি গ্যাপের চেয়ে বড়, তাই সে কখনোই ধরতে পারে না।

তৃতীয় anomaly — উত্তর আগে, প্রশ্ন পরে

একটি কাল্পনিক group chat-এ Rumana জিজ্ঞেস করলেন, “কাল সভা কয়টায়?” Tanvir উত্তর দিলেন, “সকাল ১০টায়।” বার্তা দুটি আলাদা partition-এ গেছে (পর্ব ২৩), যারা আলাদা গতিতে replicate হয়। Rafi আগে দেখলেন উত্তর, কয়েক সেকেন্ড পরে প্রশ্ন। Consistent prefix reads বলে: লেখাগুলো যে ক্রমে ঘটেছে, পাঠক সেই ক্রমের একটি শুরুর অংশই দেখবেন — মাঝখান বাদ পড়ে পরেরটা আসবে না। একটি log-এ ক্রম আপনিই রক্ষা হয়; সমস্যা আসে ডেটা কয়েকটি স্বাধীন log-এ ভাগ হলে। প্রতিকার: কারণ-ফলে বাঁধা লেখা একই partition-এ (একটি chat-এর সব বার্তা এক জায়গায়), অথবা প্রতিটি লেখার সাথে “কোনটি দেখে লিখেছি” তথ্য — vector clock, পর্ব ৩৯-এ।

ব্যবহারকারী যা দেখেনযে নিশ্চয়তা ভাঙলপ্রচলিত প্রতিকার
নিজের পোস্ট বা বদল উধাওRead-your-writesনিজের ডেটা leader থেকে; লেখার পর সময়-জানালা; লেখার LSN বা timestamp মিলিয়ে route
refresh-এ নতুন, তারপর আবার পুরোনোMonotonic readsব্যবহারকারীকে এক replica-তে বাঁধা; দেখা সর্বোচ্চ অবস্থান মনে রাখা
উত্তর আগে, প্রশ্ন পরেConsistent prefixসম্পর্কিত লেখা এক partition-এ; কারণ-ফলের তথ্য সাথে রাখা
নকশার নিয়ম

Async replica-তে পড়ার সময় ধরে নিন lag কয়েক মিনিটও হতে পারে। প্রতিটি পাতার জন্য জিজ্ঞেস করুন: কয়েক সেকেন্ড পুরোনো ডেটায় ক্ষতি কী? খবরের তালিকা — প্রায় কিছুই না; সদ্য-পাঠানো টাকার রসিদ — অনেক। দ্বিতীয় ধরনের পড়া leader-এ বা অবস্থান মিলিয়ে পাঠান, আর lag একটি সীমা পেরোলে replica-কে পড়ার pool থেকে সরান — lag মাপার কাজটি পর্ব ৫০-এ।

৫ · নতুন follower, বিকল follower আর leader-এর মৃত্যু — failover-এর ভেতরে

সেবা না থামিয়ে নতুন follower যোগ

চলন্ত database-এর ফাইল সরাসরি কপি করলে কপির মাঝেই ডেটা বদলায়; পুরো database lock করলে সেবা থামে। প্রচলিত পথ: leader-এর একটি consistent snapshot নেওয়া (lock ছাড়া, backup tool দিয়ে) আর সেই মুহূর্তের log-অবস্থান লিখে রাখা; snapshot নতুন যন্ত্রে বসানো; তারপর নতুন follower leader-এর কাছে ওই অবস্থানের পরের সব পরিবর্তন চেয়ে নিয়ে প্রয়োগ করে, অর্থাৎ catch up করে; lag স্বাভাবিক হলে পড়ার pool-এ যোগ। লুকানো শর্ত: snapshot-এর অবস্থান থেকে সব log leader-কে ধরে রাখতে হবে, নইলে আবার snapshot. উল্টো বিপদও আছে — মরা follower-এর জন্য log ধরে রাখতে গিয়ে leader-এর ডিস্ক ভরে যেতে পারে (PostgreSQL-এর replication slot-এর পরিচিত ফাঁদ)। Follower বিকল হলে কাজ সহজ: সে জানে কোন LSN পর্যন্ত প্রয়োগ করেছিল, ফিরে এসে সেখান থেকে বাকিটা চায়।

Leader বিকল — failover-এর তিন কাজ

পর্ব ৫-এ failover-এর সময়রেখা (detect, fence, promote, redirect) আর split-brain দেখেছেন; এখানে দেখব replication-এর চোখে কী হারায়। কাজ তিনটি: (১) মৃত্যু টের পাওয়া — নির্দিষ্ট সময় heartbeat না এলে timeout-এ মৃত ঘোষণা (পর্ব ৪৪); (২) নতুন leader বাছা — সাধারণত যার log সবচেয়ে এগিয়ে; নিরাপদ সিদ্ধান্তে লাগে leader election বা একটি controller (পর্ব ৪২); (৩) নতুন করে সাজানো — client আর follower-রা নতুন leader-এর দিকে, আর পুরোনো leader ফিরে যেন নিজেকে leader না ভাবে।

কাল্পনিক বাস-টিকিট সেবা · async replication · booking_id = auto-increment
সময়
পুরোনো leader L
F1 (একই region)
F2 (অন্য region)
হারানো “OK” লেখা
ব্যবহারকারী দেখছেন
  1. স্বাভাবিক অবস্থা। Leader L-এর log ৫০০৩ পর্যন্ত: ৫০০২-এ Rahim-এর সিট A12 (পেমেন্ট P-91), ৫০০৩-এ Karim-এর সিট B4 — দুজনই “বুকিং সফল” SMS পেয়েছেন। Replication async, তাই F1 ৫০০১-এ, দূরের F2 ৪৯৯৮-এ। Leader-এর মেয়াদ-নম্বর (epoch) ৭।
  2. t = ০: L-এর বিদ্যুৎ গেল। ৫০০২ আর ৫০০৩ শুধু L-এর ডিস্কে, কোনো follower-এ নেই। Monitor এখনো কিছু জানে না।
  3. t = ১০ সেকেন্ড: মৃত ঘোষণা। Heartbeat না পেয়ে monitor L-কে মৃত ঘোষণা করল। Timeout ছোট হলে ক্ষণিকের ধীরতাতেই অকারণ failover; বড় হলে প্রতিবার বিভ্রাট লম্বা। চাপে ধীর leader-কে মৃত ভাবলে নতুন leader-ও সেই চাপে পড়ে।
  4. t = ১২ সেকেন্ড: সবচেয়ে এগিয়ে থাকা follower. F1 (৫০০১) বনাম F2 (৪৯৯৮) — F1 promote হলো, epoch ৮। F2 এখন F1-কে follow করে। পুরোনো L-এর বিদ্যুৎ কাটা হলো, যাতে ফিরে লেখা নিতে না পারে — এটাই fencing. কিন্তু Rahim আর Karim-এর “সফল” বুকিং কোথাও নেই।
  5. t = ২০ সেকেন্ড: হারানোর চেয়েও খারাপ। F1-এর auto-increment counter ৫০০১-এ ছিল, তাই Mitu-র নতুন বুকিং পেল নম্বর ৫০০২। অথচ পেমেন্ট সিস্টেম, SMS আর cache-এ ৫০০২ মানে Rahim-এর বুকিং ও P-91 — টাকা বা ফেরত ভুল মানুষের সাথে জুড়ে যেতে পারে। হারানো লেখার নম্বর আবার ব্যবহৃত হয়।
  6. পুরোনো leader ফিরল। তার ৫০০২–৫০০৩ নতুন ইতিহাসের সাথে মেলে না। সে লিখতে চাইলে epoch ৭ বলে বাতিল (fencing token, পর্ব ৪৩)। বাড়তি অংশ কেটে (rewind) তাকে follower বানানো হয়, কাটা রেকর্ড আলাদা রেখে হাতে মেলানো হয়। শিক্ষা: টাকার লেখায় semi-sync, আর এমন ID যা ফিরে আসে না (UUID বা Snowflake-ধাঁচ, পর্ব ২৩)।

Async আর semi-sync-এ ঠিক কোন লেখা ঝুঁকিতে, বের করা যায়। নিচের সিমুলেটরে leader প্রতি ১০ ms-এ একটি লেখা নেয় (w1…w12) আর t = ১১৫ ms-এ মরে। F1 কাছে: এক দিকে ০.৫ ms আর গড়ে ৮ ms এলোমেলো দেরি; F2 দূরে: ৩০ ms আর গড়ে ২০ ms. প্রতিটি follower log প্রয়োগ করে ক্রম মেনে, আর semi-sync-এ “OK” আসে প্রথম ack-এর পরে:

// One write every 10 ms; the leader crashes at t = 115 ms. Which "OK"ed writes are lost?
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = seed + 0x6D2B79F5 | 0;
    let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const rand = rng(5), exp = m => -m * Math.log(1 - rand());
const CRASH = 115;
const F = [{ name: "F1", net: 0.5, m: 8 }, { name: "F2", net: 30, m: 20 }]; // one-way ms
const last = [0, 0], W = [];                           // each follower applies IN ORDER
for (let k = 1; k <= 12; k++) {
  const t = (k - 1) * 10, w = { k, t, local: t + 1, at: [] };   // 1 ms local fsync
  F.forEach((f, i) => { last[i] = Math.max(last[i], t + f.net + exp(f.m)); w.at[i] = last[i]; });
  w.semi = Math.max(w.local, Math.min(...F.map((f, i) => w.at[i] + f.net))); // 1st follower ack
  W.push(w);
}
console.log("write  sent  F1 has it  F2 has it  semi-sync OK");
for (const w of W) console.log(("w" + w.k).padEnd(7) + String(w.t).padEnd(6) +
  w.at.map(a => (a.toFixed(1) + (a > CRASH ? " x" : "")).padEnd(11)).join("") + w.semi.toFixed(1));
const pos = F.map((f, i) => W.filter(w => w.at[i] <= CRASH).length);  // log position at crash
console.log("crash at " + CRASH + " ms: F1 has w1..w" + pos[0] + ", F2 has w1..w" + pos[1]);
for (const mode of ["async", "semi-sync"]) {
  const ok = W.filter(w => (mode === "async" ? w.local : w.semi) < CRASH);
  const avg = ok.reduce((s, w) => s + (mode === "async" ? w.local : w.semi) - w.t, 0) / ok.length;
  const lost = p => ok.filter(w => w.k > p).map(w => "w" + w.k).join(" ") || "none";
  console.log(mode.padEnd(10) + ": OK for " + ok.length + "/12 writes, avg wait " + avg.toFixed(1) + " ms");
  console.log("            promote F1 -> lost " + lost(pos[0]) + " | promote F2 -> lost " + lost(pos[1]));
}

আউটপুট:

write  sent  F1 has it  F2 has it  semi-sync OK
w1     0     9.9        59.6       10.4
w2     10    12.5       59.6       13.0
w3     20    21.2       67.9       21.7
w4     30    40.7       72.3       41.2
w5     40    59.4       75.2       59.9
w6     50    62.6       107.0      63.1
w7     60    69.1       107.0      69.6
w8     70    75.5       107.1      76.0
w9     80    98.6       116.8 x    99.1
w10    90    101.8      145.7 x    102.3
w11    100   102.8      145.7 x    103.3
w12    110   123.1 x    145.7 x    123.6
crash at 115 ms: F1 has w1..w11, F2 has w1..w8
async     : OK for 12/12 writes, avg wait 1.0 ms
            promote F1 -> lost w12 | promote F2 -> lost w9 w10 w11 w12
semi-sync : OK for 11/12 writes, avg wait 10.0 ms
            promote F1 -> lost none | promote F2 -> lost w9 w10 w11

x মানে crash-এর মুহূর্তে follower লেখাটি পায়নি। Async ১২টিকেই “OK” বলেছে, গড়ে ১ ms-এ; F1-কে promote করলেও w12 হারায়, F2-কে করলে চারটি। Semi-sync-এ গড় অপেক্ষা ১০ ms, আর w12 “OK” পায়নি (ack আসত ১২৩.৬ ms-এ) — client timeout পেয়ে আবার চেষ্টা করবে। সবচেয়ে এগিয়ে থাকা F1-কে promote করলে একটি “OK”-ও হারায় না, কারণ প্রতিটি follower-এর log leader-এর log-এর শুরুর অংশ: সবচেয়ে লম্বাটিতে বাকি সবার সব লেখা আছে, আর প্রতিটি “OK” লেখা অন্তত একজনের কাছে ছিল। কিন্তু শেষ লাইন: F1-এর নাগাল না পেয়ে F2-কে promote করলে semi-sync-এও তিনটি “OK” হারায় — তাই failover-এর আগে সবার অবস্থান জানা, বা পিছিয়ে থাকাকে আগে ধরে নিতে দেওয়া জরুরি।

স্বয়ংক্রিয় না হাতে?

স্বয়ংক্রিয় failover রাত ৩টায় সেকেন্ডে কাজ সারে; হাতে failover-এ মানুষ বুঝে সিদ্ধান্ত নেন, কিন্তু মিনিট যায়। স্বয়ংক্রিয় ব্যবস্থার বিপদ চারটি: ক্ষণিকের network partition-কে মৃত্যু ভাবা; পিছিয়ে থাকা follower promote করে লেখা হারানো; পুরোনো leader-কে থামাতে না পেরে split-brain; আর বারবার failover-এর দোলাচল। প্রচলিত নীতি: region-এর ভেতরে স্বয়ংক্রিয়, কড়া fencing-সহ; region পেরোনো failover-এ মানুষের অনুমোদন (পর্ব ৫-এর GitHub ২০১৮ ঘটনা)। পরিকল্পিত leader বদল — switchover — নিরাপদ: নতুন leader আগে পুরো log পর্যন্ত আসে, তাই কিছু হারায় না।

৬ · Multi-leader replication — একাধিক জায়গায় লেখা

তিনটি পরিস্থিতিতে single-leader কষ্টের। বহু ডেটা সেন্টার: leader এক অঞ্চলে হলে দূরের প্রতিটি লেখায় round trip, আর সেই ডেটা সেন্টার বিচ্ছিন্ন হলে সবখানে লেখা বন্ধ। প্রতিটি ডেটা সেন্টারে একটি leader রাখলে লেখা স্থানীয় আর দ্রুত; leader-রা async-এ বদল বিনিময় করে — multi-leader replication. Offline client: নেটওয়ার্ক ছাড়া চলা নোট বা জরিপের অ্যাপে প্রতিটি ফোনের database একটি leader, নেটওয়ার্ক পেলে অন্যদের সাথে মেলে — lag তখন ঘণ্টা বা দিন; Apache CouchDB-র মতো কিছু database এমন sync মাথায় রেখেই বানানো। একসাথে সম্পাদনা: প্রতিটি browser-এর কপি যেন একটি leader; অনেক collaborative editor operational transformation বা CRDT ব্যবহার করে (পর্ব ৪৬)।

দাম একটাই, কিন্তু বড়: write conflict. দুই অঞ্চলে একই শিরোনাম একই মুহূর্তে বদলানো হলো — দুই leader-ই স্থানীয়ভাবে “OK” বলেছে, পরে বিনিময়ে দুই মান। লেখার আগে দুই leader কথা বললে স্থানীয় লেখার সুবিধাই থাকে না, তাই conflict পরে মেটাতে হয়। সবচেয়ে ভালো কৌশল প্রায়ই conflict এড়ানো: একজন ব্যবহারকারীর সব লেখা তাঁর “বাড়ির” ডেটা সেন্টারে — প্রতিটি রেকর্ডের কার্যত একটিই leader. এটিও ভাঙে, যখন বাড়ির ডেটা সেন্টার বিকল বা ব্যবহারকারী অন্য দেশে।

Conflict মেটানোর উপায় — কোনটিতে কী হারায়

শর্ত একটাই: সব replica শেষে একই মানে পৌঁছাবে। পথ কয়েকটি (conflict resolution):

  • Last-write-wins (LWW): সবচেয়ে বড় timestamp জেতে, বাকিগুলো নিঃশব্দে বাদ। সহজ, সব কপি একই ফলে পৌঁছায় — কিন্তু “OK” লেখাও হারায়, আর ঘড়ির পার্থক্যে (clock skew) আসলে পরের লেখাটিই হারতে পারে। “অঞ্চল ১ সবসময় জেতে” ধাঁচের অগ্রাধিকারও একইভাবে হারায়।
  • মিলিয়ে দেওয়া (merge): দুটি তালিকার union, বা দুটি শিরোনাম জুড়ে “ক / খ”।
  • দুটিই রেখে দেওয়া: database দুটি সংস্করণই (siblings) রাখে; পড়ার সময় অ্যাপ বা ব্যবহারকারী মেলান। কোনটি কোনটিকে “দেখে” লেখা, তা ধরে version vector (পর্ব ৩৯)।
  • Conflict-মুক্ত data type: CRDT — যেকোনো ক্রমে মিলিয়ে একই ফল দেয় এমন counter, set বা টেক্সট (পর্ব ৪৬)।

দুই leader — A (ঢাকা) আর B (সিঙ্গাপুর) — একে অপরের খবর পাওয়ার আগেই একই key-তে লেখা নিল; B-র ঘড়ি ৩০০ ms পিছিয়ে। ঈদের বাজারের তালিকার শিরোনাম আর জিনিস — দুটি key-তে LWW আর version vector পাশাপাশি:

// Two leaders, A (Dhaka DC) and B (Singapore DC), accept writes to the same key before
// hearing about each other (replication delay 150 ms). B's clock runs 300 ms behind.
const skew = { A: 0, B: -300 };
const writes = [ // [real time ms, leader, key, value, version vector the writer had seen]
  [1000, "A", "title", "Eid list", { A: 1 }],
  [1100, "B", "title", "Eid list (final)", { B: 1 }],      // B has not seen A's write yet
  [2000, "A", "cart", ["sugar", "milk"], { A: 1 }],
  [2050, "B", "cart", ["sugar", "rice"], { B: 1 }],
  [3000, "A", "title", "Eid list (final)", { A: 2, B: 1 }], // user resolved, saw both
].map(([real, at, key, value, vv]) => ({ real, at, key, value, vv, ts: real + skew[at] }));

const lww = {}, sib = {};                     // final state on every replica
const covers = (a, b) => Object.keys(b).every(n => (a[n] || 0) >= b[n]); // a has seen b?
for (const w of writes) {
  // 1) last-write-wins: keep only the biggest timestamp, silently drop the rest
  if (!lww[w.key] || w.ts > lww[w.key].ts) lww[w.key] = w;
  // 2) version vectors: drop only versions the new write has seen; keep concurrent ones
  sib[w.key] = (sib[w.key] || []).filter(s => !covers(w.vv, s.vv)).concat([w]);
  const show = s => JSON.stringify(s.value) + "@" + s.at;
  console.log(`t=${w.real} ${w.at} ts=${w.ts} ${w.key}: LWW -> ${show(lww[w.key])}` +
    ` | kept: ${sib[w.key].map(show).join(" + ")}`);
}
const union = [...new Set(sib.cart.flatMap(s => s.value))];
console.log("cart after merge (union of versions): " + JSON.stringify(union));

আউটপুট:

t=1000 A ts=1000 title: LWW -> "Eid list"@A | kept: "Eid list"@A
t=1100 B ts=800 title: LWW -> "Eid list"@A | kept: "Eid list"@A + "Eid list (final)"@B
t=2000 A ts=2000 cart: LWW -> ["sugar","milk"]@A | kept: ["sugar","milk"]@A
t=2050 B ts=1750 cart: LWW -> ["sugar","milk"]@A | kept: ["sugar","milk"]@A + ["sugar","rice"]@B
t=3000 A ts=3000 title: LWW -> "Eid list (final)"@A | kept: "Eid list (final)"@A
cart after merge (union of versions): ["sugar","milk","rice"]

দ্বিতীয় লাইন: B আসল সময়ে পরে (১১০০ ms) লিখেছে, কিন্তু পিছিয়ে থাকা ঘড়িতে তার timestamp 800 — LWW আগের লেখাটি রাখল, পরের সম্পাদনা নিঃশব্দে হারাল। চতুর্থ লাইনে ঘড়ি নির্ভুল হলেও একটি হারাত: দুটি তালিকা একসাথে লেখা, LWW একটিই রাখে — “rice” উধাও। Version vector দুটিই রাখে, কারণ {A: 1} আর {B: 1}-এর কোনোটি অন্যটিকে “দেখেনি”; union সব জিনিস ফেরায়। পঞ্চম লাইনে ব্যবহারকারী দুটি শিরোনাম দেখে একটি বাছলেন; সেই লেখার vector {A: 2, B: 1} আগের দুটিকেই ঢেকে দেয়, তাই একটিই সংস্করণ টিকল। দুই পদ্ধতিই লেখা পৌঁছানোর ক্রমের ওপর নির্ভর করে না, তাই সব replica একই ফলে মেলে।

কে কার কাছে পাঠায় — replication topology

দুইয়ের বেশি leader হলে বদল কোন পথে ছড়াবে, তার নকশা — replication topology:

⭕ বৃত্তাকার (circular)

প্রতিটি leader পাশেরজনকে পাঠায়, সে তার পাশেরজনকে। বার্তা কম, কিন্তু একটি node বিকল হলে বৃত্ত ভেঙে যায় — হাতে নতুন করে না জোড়া পর্যন্ত বদল আটকে থাকে।

একটি ভাঙলেই প্রবাহ বন্ধ

⭐ তারকা (star)

একটি কেন্দ্রীয় node সবার কাছ থেকে নিয়ে সবাইকে পাঠায়। সাজানো সহজ, কিন্তু কেন্দ্রটি নিজেই single point of failure আর বাধা।

কেন্দ্র মরলে সব থামে

🕸️ সবাই সবার কাছে (all-to-all)

প্রতিটি leader সরাসরি সবাইকে পাঠায় — একটি পথ ভাঙলেও অন্য পথ থাকে। কিন্তু দ্রুত পথের বার্তা ধীর পথেরটিকে ছাড়িয়ে যায়: কোনো replica-তে একটি সারির “update” তার “insert”-এর আগেই পৌঁছায়।

ক্রম উল্টাতে পারে

বৃত্ত আর তারকায় প্রতিটি বদলের সাথে “কোন node এটি দেখেছে” তালিকা থাকে, যাতে বদলটি অনন্তকাল না ঘোরে। All-to-all-এর ক্রম-সমস্যা timestamp-এ মেটে না (ঘড়ি অনির্ভরযোগ্য, পর্ব ৩৮); লাগে version vector-এর মতো কারণ-ফলের হিসাব। আর একটি গভীর সীমা: “একই সিট দুবার বিক্রি নয়” বা “username অনন্য” — দুই leader আলাদাভাবে যাচাই করলে দুই জায়গাতেই পাশ করে যায়। এমন ডেটার জন্য একটি leader (বা consensus) লাগে, বহু-leader নকশাতেও।

৭ · Leaderless replication — Dynamo-ধাঁচে সবাই লেখা নেয়

২০০৭ সালে Amazon-এর প্রকৌশলীরা (DeCandia ও সহকর্মীরা) SOSP সম্মেলনে “Dynamo: Amazon’s Highly Available Key-value Store” প্রকাশ করেন — লক্ষ্য shopping cart-এর মতো সেবা, যা যেকোনো ব্যর্থতায়ও লেখা নেবে। এর ধারণা নিয়ে এসেছে Riak, Apache Cassandra (Dynamo আর Bigtable মিলিয়ে) আর ScyllaDB. সতর্কতা: নাম মিললেও Amazon-এর DynamoDB সেবার নকশা paper-এর Dynamo থেকে আলাদা বলে জানা যায়।

Leaderless replication-এ কোনো leader নেই, তাই failover-ও নেই। প্রতিটি key থাকে N টি replica-তে (consistent hashing-এর preference list, পর্ব ১৬)। Client, বা coordinator হিসেবে যেকোনো node, লেখাটি N টির সবাইকে পাঠায় আর W টির ack পেলেই সফল ধরে; পড়ায় R টিকে জিজ্ঞেস করে সংস্করণ-নম্বর দেখে সবচেয়ে নতুনটি নেয়। R + W > N হলে পড়ার দল আর লেখার দল অন্তত একটি replica-তে মেলে, তাই সাধারণত নতুন মান চোখে পড়ে — এর নাম quorum; প্রমাণ আর “সাধারণত” কেন “সবসময়” নয়, তা পর্ব ৪১-এর বিষয়।

ছবি ২২.১-এর তৃতীয় অংশে N = ৩, W = ২, R = ২। লেখার সময় R3 বন্ধ ছিল; R1 আর R2 ack দিল, লেখা সফল — R3-তে রয়ে গেল পুরোনো v1. পরে পড়া গেল R2 আর R3-তে: উত্তর v2 আর v1; client নতুনটি (v2) নিল। পুরোনো কপি দুইভাবে ঠিক হয়:

  • Read repair: পড়ার সময়ই পুরোনো উত্তর দেখে coordinator R3-কে v2 লিখে দেয় — প্রায়ই পড়া ডেটা দ্রুত ঠিক হয়, কম পড়া ডেটা নয়।
  • Anti-entropy: পেছনের একটি প্রক্রিয়া replica-দের নিয়মিত তুলনা করে যার যা নেই তা পাঠায়; Merkle tree-এর hash মিলিয়ে অল্প ডেটাতেই অমিল খোঁজা হয় (Dynamo paper-এর কৌশল)। ক্রম মানে না, দেরিও হতে পারে।

নির্ধারিত replica বন্ধ থাকলে অন্য node লেখাটি একটি নোটসহ সাময়িক রাখে, পরে আসল replica-কে পৌঁছে দেয় — hinted handoff (পর্ব ৪১)। সুবিধা: একটি node ধীর বা মৃত হলেও বাকিরা কাজ চালায়, failover-এর সিদ্ধান্ত লাগে না, ধীর replica এড়িয়ে latency-র লেজ ছোট থাকে। দাম: লেখার একক ক্রম নেই, তাই একই key-তে একসাথে দুটি লেখায় multi-leader-এর মতোই conflict — version vector আর sibling, অথবা LWW; Cassandra-র মতো কিছু ব্যবস্থায় column-ভিত্তিক LWW-ই default. আর quorum থাকলেও linearizability আপনাআপনি আসে না (পর্ব ৪০, ৪১)।

৮ · তিন নকশার তুলনা আর region পেরোনো replication

বিষয়Single-leaderMulti-leaderLeaderless
লেখা কোথায় যায়শুধু leader-এযেকোনো leader-এ (সাধারণত প্রতি অঞ্চলে একটি)কয়েকটি replica-তে একসাথে, W টির ack
লেখার conflictনেই — leader ক্রম ঠিক করেআছে, পরে মেটাতে হয়আছে — সংস্করণ আর merge
দূরের অঞ্চলে লেখার latencyleader-এর অঞ্চল পর্যন্ত round tripস্থানীয়, কমনকশাভেদে — কাছের replica-রা ack দিলে কম
Node বা অঞ্চল বিকল হলেfailover লাগে — সেকেন্ড থেকে মিনিট, ঝুঁকিসহঅন্য অঞ্চল লেখা নিতেই থাকেfailover নেই, বাকিরা চালায়
অনন্যতা, “বেশি বিক্রি নয়”সহজে রক্ষা হয়দুই জায়গায় একসাথে ভাঙতে পারেএকই সমস্যা; বাড়তি ব্যবস্থা লাগে
উদাহরণPostgreSQL, MySQL, MongoDB replica setবহু-DC নকশা, offline অ্যাপ, collaborative editorCassandra, Riak, ScyllaDB
কখন বেছে নেবেনবেশিরভাগ ক্ষেত্রে, প্রথম পছন্দঅঞ্চলে স্থানীয় লেখা বা offline কাজ জরুরিবিপুল লেখা, সবসময় লেখা নেওয়া জরুরি, কিছু পুরোনো পড়া চলে

Region পেরোনো replication — দূরত্ব আর খরচের হিসাব

Latency: ধরা যাক দুই region ৩,০০০ km দূরে। কাচের তারে আলো সেকেন্ডে প্রায় ২,০০,০০০ km যায়, তাই যাওয়া-আসায় অন্তত ২ × ৩,০০০ ÷ ২,০০,০০০ সেকেন্ড = ৩০ ms; আসল পথ সরলরেখা নয়, তাই বাস্তবে সাধারণত বেশি (পর্ব ৭)। দূরের region sync হলে প্রতিটি commit-এ অন্তত এটুকু যোগ হয়। খরচ: ধরা যাক সেকেন্ডে ২,০০০ লেখা, প্রতিটির log গড়ে ১ KB (১,০০০ বাইট) — স্রোত সেকেন্ডে ২ MB, দিনে ১৭২.৮ GB, ৩০ দিনে প্রায় ৫.১৮ TB, প্রতিটি দূরের region-এর জন্য আলাদা। অনেক cloud প্রদানকারী region-এর মাঝে ডেটা পাঠাতে প্রতি GB হিসাবে দাম নেয়; হার প্রদানকারী ও সময়ভেদে আলাদা, তাই দামের পাতায় মিলিয়ে নিন। Compression আর শুধু দরকারি টেবিল পাঠানো বিল কমায়।

প্রচলিত নকশা চারটি: (১) এক region-এ leader, অন্যগুলোতে async follower — পড়া স্থানীয়, দূরের follower বিপর্যয়ের বিকল্প (পর্ব ৫১); (২) ডেটা ব্যবহারকারীর “বাড়ির” region অনুযায়ী ভাগ, প্রতিটি ভাগের leader নিজের region-এ; (৩) multi-leader, conflict মেনে নিয়ে; (৪) region পেরোনো consensus, যেখানে commit-এ লাগে সংখ্যাগরিষ্ঠের round trip (পর্ব ৪২)। কিছু দেশের আইনে নির্দিষ্ট ডেটা দেশের ভেতরে রাখার শর্ত থাকতে পারে — কোন কপি কোথায়, সেটিও তখন নকশার অংশ।

আরও গভীরে: Read-your-writes আর monotonic reads নাম দুটি এসেছে Xerox PARC-এর Bayou প্রকল্প থেকে — Terry ও সহকর্মীদের “Session Guarantees for Weakly Consistent Replicated Data” (১৯৯৪), যেখানে আরও আছে writes-follow-reads আর monotonic writes. van Renesse ও Schneider-এর chain replication (OSDI ২০০৪) ভিন্ন পথ দেখায়: replica-রা একটি শিকলে, লেখা ঢোকে মাথায়, “OK” আর পড়া আসে লেজ থেকে — তাই প্রতিটি পড়া সব কপিতে পৌঁছানো লেখাই দেখে। পুরো বিষয়ের পরিচিত পাঠ্য Martin Kleppmann-এর Designing Data-Intensive Applications (২০১৭)-এর replication অধ্যায়।

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

  • Sync আর async-এর trade-off? — Async দ্রুত, কিন্তু leader মরলে শেষ lag-এর লেখা হারাতে পারে; sync-এ RPO শূন্য, কিন্তু প্রতি commit-এ round trip আর follower বন্ধ হলে লেখা আটকে।
  • Read replica-তে নিজের লেখা না দেখা কীভাবে ঠেকাবেন? — নিজের ডেটা leader থেকে, সময়-জানালা, বা লেখার LSN মিলিয়ে route.
  • Failover-এ কী ভুল হতে পারে? — হারানো লেখা, ID-র পুনর্ব্যবহার, split-brain, ভুল timeout.
  • Multi-leader-এর conflict কীভাবে মেটাবেন? — সম্ভব হলে এড়ানো, নইলে LWW (হারায়), merge, sibling বা CRDT.
  • Leaderless-এ পুরোনো replica কীভাবে ঠিক হয়? — Read repair আর anti-entropy.

৯ · AI-এর যুগে replication-এর নকশা যাচাই

“Database-কে highly available আর scalable বানাও” লিখলে AI মুহূর্তে বলে: “read replica, multi-master, সাথে Kafka আর Redis.” শুনতে পেশাদার, কিন্তু replication-এ কিছু ভুল AI প্রায় নিয়ম করে করে:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“Read replica দিলেই database scale হবে”Replication পড়া বাড়ায়, লেখা নয়; প্রতিটি follower সব লেখা প্রয়োগ করেলেখার হার দিয়ে অংশ ১-এর হিসাব নিজে করুন
“Region পেরিয়ে synchronous দিন, কিছু হারাবে না”প্রতিটি commit-এ দূরের round trip; follower বা পথ বন্ধ হলে লেখা আটকেদূরত্ব থেকে ন্যূনতম RTT বের করে commit latency যোগ; follower মরলে কী হবে জিজ্ঞেস করুন
Async replica-কে “strongly consistent” বলা; ACID আর CAP-এর consistency গুলিয়ে ফেলাACID-এর C ডেটার নিয়ম রক্ষা; replica-দের মিল আলাদা প্রশ্ন (পর্ব ৪০)Lag সিমুলেটর চালান; কোন নিশ্চয়তা (read-your-writes, monotonic) দেওয়া হচ্ছে লিখিয়ে নিন
“HA-র জন্য multi-master চালু করুন”Conflict, অনন্যতা আর auto-increment-এর সমস্যা চুপচাপ উপেক্ষাজিজ্ঞেস করুন: কোন ডেটা দুই জায়গায় একসাথে বদলাতে পারে, conflict-এর নিয়ম কী?
“LWW দিয়েই conflict মিটে যাবে”ঘড়ির পার্থক্যে আর একসাথে লেখায় নিঃশব্দে লেখা হারায়অংশ ৬-এর demo-তে নিজের ডেটা বসিয়ে দেখুন
বানানো setting, default বা দাম; “replication থাকলে backup লাগে না”Timeout, parameter-এর নাম আর দাম সংস্করণ ও প্রদানকারীভেদে আলাদা; ভুল DELETE সব কপিতে পৌঁছায়অফিসিয়াল documentation; point-in-time backup আর restore-মহড়া

AI-কে তথ্য আর শর্ত দিন, আর প্রতিটি সংখ্যা হিসাবসহ চান — যাতে আপনি মিলিয়ে দেখতে পারেন:

তুমি একজন অভিজ্ঞ database reliability ইঞ্জিনিয়ার। একটি কাল্পনিক অনলাইন বাস-টিকিট সেবার জন্য replication নকশা করো। তথ্য: পিকে সেকেন্ডে ১,৫০০টি লেখা আর ২০,০০০টি পড়া। ব্যবহারকারী মূলত একটি দেশে; একটি DR region আছে ২,৫০০ km দূরে। বুকিং আর পেমেন্টে "OK" পাওয়া কোনো লেখা হারানো চলবে না; সিটের তালিকা কয়েক সেকেন্ড পুরোনো হলে চলে। ১. কোথায় sync, semi-sync আর async — প্রতিটির commit latency হিসাবসহ। দূরত্ব থেকে ন্যূনতম RTT বের করো; কাচে আলোর গতি সেকেন্ডে ২ লাখ কিলোমিটার ধরো। ২. কয়টি follower লাগবে — একটি সার্ভার সেকেন্ডে ১০,০০০ কাজ সামলায় আর replicate হওয়া লেখার খরচও একটি কাজ ধরে ধাপে ধাপে হিসাব দেখাও। ৩. Read-your-writes আর monotonic reads কীভাবে নিশ্চিত হবে। ৪. Leader মরলে failover-এর ধাপ, কী হারাতে পারে, split-brain আর ID-র পুনর্ব্যবহার কীভাবে ঠেকাবে। ৫. কোনো পণ্যের setting, default বা দাম নিজে থেকে লিখবে না; দরকার হলে লিখবে "documentation-এ যাচাই করতে হবে"। সব অনুমান আলাদা তালিকায় দাও।

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

AI দেয় দ্রুত খসড়া, যাচাইয়ের দায় আপনার।
হিসাব করুন কী হারাতে পারে, কতটা দেরি হয়, আর leader মরলে ঠিক কী ঘটে।

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

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

প্র ০১একটি পিছিয়ে থাকা follower-কে leader বানানোয় অন্য ব্যবহারকারীর ব্যক্তিগত তথ্য ভুল মানুষের কাছে গেল — GitHub-এর ২০১২ সালের ঘটনা থেকে কী শিখি?

Martin Kleppmann তাঁর Designing Data-Intensive Applications বইয়ে GitHub-এর ২০১২ সালের সেপ্টেম্বরের প্রকাশিত বিবরণ উদ্ধৃত করে ঘটনাটি বর্ণনা করেছেন। একটি failover-এ MySQL-এর একটি পিছিয়ে থাকা follower leader হয়। নতুন সারির primary key আসত একটি auto-increment counter থেকে, আর নতুন leader-এর counter পুরোনো leader-এর চেয়ে পিছিয়ে ছিল। ফলে সে এমন কিছু key আবার দিতে থাকে, যা পুরোনো leader আগেই অন্য সারিকে দিয়েছিল। সেই key-গুলো একটি Redis store-এও ব্যবহৃত হচ্ছিল; MySQL আর Redis-এর মধ্যে অমিলের কারণে কিছু ব্যক্তিগত তথ্য ভুল ব্যবহারকারীদের কাছে প্রকাশ পায়।

এই পর্বের stepper-এ Rahim আর Mitu-র বুকিংয়ের গল্প ঠিক এটাই। শিক্ষা কয়েকটি স্তরে:

  • হারানো লেখা কেবল হারায় না: database ভুলে গেলেও cache, search index, পেমেন্ট সিস্টেম, পাঠানো SMS সেটি মনে রাখে — তখন একই ID দুই অর্থ পায়।
  • এমন ID, যা ফিরে আসে না: UUID বা Snowflake-ধাঁচের ID (পর্ব ২৩) leader-এর counter-এর ওপর নির্ভর করে না; counter রাখতেই হলে নতুন leader নিরাপদ ফাঁক রেখে শুরু করতে পারে।
  • Promote-এর আগে সবার অবস্থান দেখুন, আর গুরুত্বপূর্ণ ডেটায় semi-sync বা majority commit — “OK” লেখা আরেকটি যন্ত্রে না থাকলে কোনো নিয়মই তা বাঁচায় না।

সমস্যাটি ধরা পড়েছিল ডেটার অমিল থেকে, কোনো error থেকে নয় — failover-এর পর দুই সিস্টেমের ID মেলানোর নিয়মিত পরীক্ষাই এমন নিঃশব্দ ভুল ধরে।

মূল উপলব্ধি: Async failover-এ হারানো লেখা একটি ডেটা-শুদ্ধতার ঘটনা, শুধু availability-র নয়; ID-র নকশা, promote-এর নিয়ম আর বাইরের সিস্টেমের সাথে মিল — তিনটিই replication-নকশার অংশ।

প্র ০২Amazon-এর Dynamo কেন “সবসময় লেখা নেবে” নীতি বেছে নিয়েছিল, আর তার দামে shopping cart-এ মুছে ফেলা জিনিস কেন ফিরে আসতে পারত?

২০০৭ সালের Dynamo paper-এর শুরুতেই ব্যবসার যুক্তি: ক্রেতা ব্যর্থতার মুহূর্তেও — একটি ডিস্ক, সার্ভার, এমনকি ডেটা সেন্টারের সমস্যাতেও — cart-এ জিনিস যোগ করতে পারবেন; “যোগ করা যাচ্ছে না” মানে সরাসরি বিক্রি হারানো। তাই নকশার কেন্দ্রে availability: leader নেই, যেকোনো সচল replica লেখা নেয়, নির্ধারিত replica বন্ধ থাকলে hinted handoff.

দাম: একই cart-এ দুই জায়গায় একসাথে বদলে দুটি সংস্করণ। Dynamo প্রতিটির সাথে vector clock রাখত, যাতে বোঝা যায় কোনটি পরে আর কোনগুলো একসাথে; একসাথে তৈরি সংস্করণ মেলানোর দায় ছিল অ্যাপের, পড়ার সময়। Cart-এর নিয়ম: সংস্করণগুলোর union.

এবার একটি দৃশ্য ভাবুন। Cart-এ ছিল {চাল, ডাল}। একটি replica-তে ক্রেতা ডাল সরালেন → {চাল}। একই সময়ে আরেকটি replica-তে (যে তখনো আগের অবস্থা দেখছে) তিনি তেল যোগ করলেন → {চাল, ডাল, তেল}। Union করলে {চাল, ডাল, তেল} — সরানো ডাল ফিরে এল! Paper নিজেই স্বীকার করে: এভাবে “add to cart” কখনো হারায় না, কিন্তু মুছে ফেলা জিনিস কখনো কখনো আবার দেখা দিতে পারে। Amazon এই দাম মেনে নিয়েছিল, কারণ চেকআউটের আগে ক্রেতা cart দেখে ঠিক করে নেন, আর একটি যোগ হারানোর চেয়ে একটি অবাঞ্ছিত জিনিস ফিরে আসা কম ক্ষতিকর।

পরে এই সমস্যার পরিষ্কার সমাধান এসেছে CRDT-র রূপে: যেমন OR-Set, যেখানে প্রতিটি যোগের একটি অনন্য চিহ্ন থাকে আর সরানো মানে নির্দিষ্ট চিহ্নগুলো সরানো, তাই একসাথে হওয়া যোগ আর সরানোর অর্থ ঠিক থাকে (পর্ব ৪৬)।

মূল উপলব্ধি: Consistency-র মাত্রা প্রযুক্তির নয়, ব্যবসার সিদ্ধান্ত — কোন ভুল সহ্য করা যায় আর কোনটি নয়, সেটি আগে ঠিক করে তারপর replication-এর নকশা বাছতে হয়।

প্র ০৩Statement পাঠানো তো সবচেয়ে ছোট আর সহজ — তাহলে MySQL কেন default হিসেবে row-ভিত্তিক log-এ গেল, আর logical log আজ এত জনপ্রিয় কেন?

MySQL-এর replication বহু বছর statement-ভিত্তিক ছিল। ২০০৮ সালের MySQL ৫.১ row-based আর mixed format যোগ করে, আর ৫.৭.৭ সংস্করণে (২০১৫) default হয় row. কারণটি এই পর্বের অংশ ২-এর তালিকা: NOW(), RAND(), UUID(); ORDER BY ছাড়া LIMIT-ওয়ালা UPDATE বা DELETE, যেখানে কোন সারি বাছা হবে তা নির্ভর করে storage-এর ভেতরের ক্রমের ওপর; একসাথে চলা transaction-এ auto-increment-এর ক্রম; আর trigger বা stored procedure-এর পার্শ্বপ্রতিক্রিয়া। প্রতিটি ক্ষেত্রে leader আর follower একই কমান্ড চালিয়ে আলাদা ফল পেতে পারে — আর সেই অমিল কোনো error দেয় না, শুধু ধীরে ধীরে কপিগুলো আলাদা হতে থাকে।

Row-ভিত্তিক log-এ follower কিছু “হিসাব” করে না; leader যে ফল পেয়েছে, তা-ই বসায়। অনির্ধারণের প্রশ্নই ওঠে না। আর storage engine-এর বাইট-format থেকে আলাদা বলে আরও তিনটি দরজা খোলে:

  • শূন্য-বিরতিতে upgrade আর migration: পুরোনো সংস্করণ থেকে নতুন সংস্করণে, এমনকি অন্য hardware-এ, logical replication চালিয়ে কপি তৈরি করে শেষে স্থান বদল।
  • বাছাই করা replication: শুধু কয়েকটি টেবিল একটি রিপোর্টের database-এ।
  • Change data capture: Debezium-ধাঁচের open-source tool MySQL-এর binlog বা PostgreSQL-এর logical decoding পড়ে প্রতিটি পরিবর্তন একটি log stream-এ পাঠায় (পর্ব ৩৩); সেখান থেকে search index, cache মোছা, analytics — সবাই একই উৎস থেকে হালনাগাদ থাকে। “Database-এ লিখে আবার আলাদা করে queue-তে পাঠাই” ধাঁচের দ্বৈত লেখার অমিল এড়াতে এটি outbox pattern-এর (পর্ব ২৫) আত্মীয়।

দামও আছে: দশ লাখ সারির এক কমান্ডে দশ লাখ রেকর্ড; সারি চিনতে primary key প্রায় বাধ্যতামূলক; schema বদল আলাদা সামলাতে হয়। MySQL-এর mixed format তাই নিরাপদ কমান্ড statement হিসেবে, অনিরাপদগুলো row হিসেবে পাঠায়।

মূল উপলব্ধি: Replication log শুধু কপি বানানোর যন্ত্র নয় — এটি database-এর পরিবর্তনের ইতিহাস; সেই ইতিহাস যত স্পষ্ট আর storage-নিরপেক্ষ, তত বেশি সিস্টেম নির্ভয়ে তার ওপর দাঁড়াতে পারে।

প্র ০৪“সবচেয়ে নতুন timestamp জেতে” — শুনতে ন্যায্য, তবু last-write-wins কেন বিপজ্জনক? Google Spanner-এর TrueTime এ সমস্যার কী উত্তর দেয়?

LWW-এর ভেতরে দুটি আলাদা সমস্যা। প্রথমটি ঘড়ির। সার্ভারের quartz ঘড়ি তাপমাত্রা আর বয়সে একটু দ্রুত বা ধীর হয়; NTP মেলায়, কিন্তু নির্ভুলতা সাধারণত একই নেটওয়ার্কে কয়েক ms থেকে ইন্টারনেট পেরোলে কয়েক দশ ms, আর ভুল config বা virtual machine থেমে থাকায় আরও খারাপ (পর্ব ৩৮, ৩৯)। অংশ ৬-এর demo-তে মাত্র ৩০০ ms-এর পার্থক্যেই B-র পরের সম্পাদনা নিঃশব্দে হারিয়েছে — কোনো log-এ error ছাড়াই।

দ্বিতীয়টি গভীরতর: ঘড়ি নিখুঁত হলেও একসাথে হওয়া দুটি লেখার একটিকে LWW ফেলে দেয়; কেউ কারও লেখা না দেখলে “পরে” কথাটির অর্থই নেই। তাই LWW নিরাপদ কেবল যেখানে ফেলে দেওয়া চলে — cache, “সর্বশেষ জানা অবস্থান”, বা একবারই লেখা হয় এমন UUID-key-এর রেকর্ড।

Google-এর Spanner (Corbett ও সহকর্মীরা, OSDI ২০১২) ঘড়ির অনিশ্চয়তাকে লুকায় না, মেপে ফেলে। তাদের TrueTime API একটি নির্দিষ্ট মুহূর্ত নয়, একটি পরিসর দেয় — “এখন সময় এই দুইয়ের মাঝে কোথাও”। এজন্য ডেটা সেন্টারে GPS receiver আর atomic clock-এর বহর রাখা হয়; paper অনুযায়ী অনিশ্চয়তা সাধারণত কয়েক ms. Commit-এর সময় Spanner ইচ্ছা করে সেই অনিশ্চয়তার সমান সময় অপেক্ষা করে (commit wait), যাতে পরের যেকোনো transaction-এর timestamp নিশ্চিতভাবে বড় হয়। ফলে timestamp-এর ক্রম আসল সময়ের ক্রম মানে। দাম: বিশেষ hardware আর প্রতিটি commit-এ কয়েক ms অপেক্ষা।

যাঁদের এমন hardware নেই, তাঁদের হাতে অন্য যন্ত্র: Lamport-এর logical clock (১৯৭৮) কারণ-ফলের ক্রম রক্ষা করে, আর version vector একসাথে হওয়া লেখা চিনিয়ে দেয় — হারানোর বদলে দুটিই রাখা যায়।

মূল উপলব্ধি: Physical time-এ “কে পরে” ঠিক করা একটি অনুমান, প্রমাণ নয়; হয় অনিশ্চয়তা মেপে অপেক্ষা করুন, নয়তো কারণ-ফলের হিসাব রাখুন — নইলে LWW নিঃশব্দে ডেটা ফেলে দেবে।

প্র ০৫একটি নির্দিষ্ট sync follower, না সংখ্যাগরিষ্ঠের ack — Raft-ধাঁচের majority commit semi-sync-এর চেয়ে কোথায় ভালো, আর region-এ কপি সাজানোয় latency-র হিসাব কী দাঁড়ায়?

Semi-sync-এ “OK” মানে লেখাটি অন্তত একটি follower-এর কাছে আছে। কিন্তু failover-এর মুহূর্তে সেই follower-এর নাগাল না পেলে ভুল কাউকে promote করে “OK” লেখা হারানো সম্ভব (অংশ ৫-এর সিমুলেটরের শেষ লাইন); আর কে leader হবে, তা ঠিক করে বাইরের একটি monitor — যার নিজের ভুলেই split-brain হতে পারে।

Raft (Ongaro ও Ousterhout, ২০১৪) আর তার পূর্বসূরি Paxos-পরিবার দুটি কাজকে একটি নিয়মে বাঁধে। Leader একটি লেখাকে committed ধরে তখনই, যখন সংখ্যাগরিষ্ঠ সদস্য (নিজেকে ধরে) সেটি নিজের log-এ রেখেছে। আর নতুন leader হতে লাগে সংখ্যাগরিষ্ঠের ভোট, যারা কেবল এমন প্রার্থীকে ভোট দেয় যার log অন্তত তাদের মতো হালনাগাদ। দুটি সংখ্যাগরিষ্ঠ দল সবসময় অন্তত একজন সদস্যে মেলে — তাই committed প্রতিটি লেখা যেকোনো নতুন leader-এর কাছে থাকবেই। বাইরের বিচারক লাগে না; প্রোটোকল নিজেই নিশ্চয়তা দেয় (বিস্তারিত পর্ব ৪২)। etcd আর Consul-এর মতো coordination সেবা, আর CockroachDB-র মতো distributed SQL ডেটাবেস, তাদের প্রকাশিত documentation অনুযায়ী Raft ব্যবহার করে।

Latency-র হিসাব সরল কিন্তু চমকপ্রদ। ৩টি কপি — leader region A-তে, বাকি দুটি B (RTT ৩০ ms) আর C (RTT ৮০ ms)-তে। সংখ্যাগরিষ্ঠ = ২, অর্থাৎ leader আর সবচেয়ে কাছের একজন: commit-এ মোটামুটি ৩০ ms, দূরের C-র অপেক্ষা লাগে না। সব কপি এক region-এ রাখলে commit ১–২ ms, কিন্তু সেই region হারালে সংখ্যাগরিষ্ঠও হারায়। প্রচলিত আপস: ৫টি কপি ২ + ২ + ১ করে তিন region-এ — এক region হারালেও ৩টি বাকি, আর commit লাগে কাছের region-এর round trip.

মূল উপলব্ধি: “কয়জনের ack চাই” প্রশ্নটি আসলে durability, availability আর latency-র তিনমুখী হিসাব; সংখ্যাগরিষ্ঠের নিয়ম সেই হিসাবকে প্রমাণযোগ্য করে, আর কপিগুলো কোথায় বসাবেন তা ঠিক করে দেয় আপনি কতটা দাম দেবেন।

অনুশীলন

খাতা-কলম আর উপরের সিমুলেটর পাশে রাখুন। আগে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. Replication-এর ধরন বাছাই: তিনটি কাল্পনিক সেবার জন্য replication-এর নকশা বলুন, কারণসহ। (ক) একটি mobile banking সেবার লেনদেনের খাতা — ব্যবহারকারী মূলত একটি দেশে, “OK” পাওয়া কোনো লেনদেন হারানো চলবে না, প্রতিটি লেনদেনে সার্ভারের দিকে প্রায় ২০০ ms সময় বরাদ্দ। (খ) একটি খবরের সাইটের মন্তব্য — পড়া লেখার চেয়ে কয়েকশো গুণ, অন্যের মন্তব্য কয়েক সেকেন্ড দেরিতে দেখালে চলে। (গ) একটি NGO-র মাঠ-জরিপের অ্যাপ — গ্রামে কয়েক দিন নেটওয়ার্ক থাকে না, কয়েকজন কর্মী একই পরিবারের তথ্য বদলাতে পারেন।

    (ক) Single-leader, semi-sync + async DR. একই region-এর আরেকটি zone-এ একটি semi-sync follower — বাড়তি কয়েক ms, কিন্তু প্রতিটি “OK” অন্তত দুটি যন্ত্রে। দূরের DR region-এ async follower; region-বিপর্যয়েও কিছু হারানো না চাইলে DR-কেও sync রাখতে হবে — ২,৫০০ km-এ প্রতি commit-এ অন্তত ২৫ ms, বাজেটে চলে, কিন্তু DR পথ বন্ধ হলে লেখা আটকানোর ঝুঁকি। Timeout-এর পর একই idempotency key দিয়ে retry; লেনদেনের পর ব্যালান্স leader থেকে পড়া। Zone-এর ভেতরে স্বয়ংক্রিয় failover কড়া fencing-সহ, region পেরোনোটি মানুষের অনুমোদনে। Multi-leader নয় — “ব্যালান্স ঋণাত্মক নয়” নিয়ম এক জায়গায় যাচাই হতে হবে।

    (খ) Single-leader, async, কয়েকটি read replica. পড়ার চাপ replica আর cache-এ। মন্তব্যকারী নিজের মন্তব্য দেখবেন লেখার LSN মিলিয়ে route করে, বা অ্যাপে সাথে সাথে দেখিয়ে। Lag একটি সীমা (ধরুন ১০ সেকেন্ড) পেরোলে replica-কে pool থেকে সরানো। Semi-sync-এর বাড়তি latency এখানে অপ্রয়োজনীয়।

    (গ) Multi-leader — প্রতিটি ফোন একটি leader. নতুন রেকর্ডে UUID, তাই নতুন রেকর্ডে conflict নেই। একই পরিবারের ভিন্ন field দুজন বদলালে দুটিই রাখা; একই field বদলালে দুটি সংস্করণ রেখে supervisor-কে বেছে নিতে দেওয়া; গণনা বা তালিকায় CRDT (পর্ব ৪৬)। LWW চলবে না — মাঠের ফোনের ঘড়ি প্রায়ই ভুল, আর হারানো তথ্য আবার সংগ্রহ ব্যয়বহুল।

  2. পুরোনো পড়ার প্রত্যাশিত সংখ্যা: একটি কাল্পনিক সামাজিক অ্যাপে ঘণ্টায় ৩০,০০০ বার কেউ পোস্ট করে ২০০ ms পরে পাতা খোলেন। পড়া সমান সম্ভাবনায় তিনটি async follower-এর যেকোনোটিতে যায়। F1 আর F2-এর lag গড়ে ১০০ ms, F3-এর গড়ে ৪০০ ms; ধরুন lag এমনভাবে ছড়ানো যে P(lag > g) = e−g/গড়, আর তিনটি follower-এর lag স্বাধীন। (ক) ঘণ্টায় কতবার নিজের পোস্ট দেখা যাবে না? (খ) অবস্থান মেলানোর নিয়মে (যে follower ধরেছে সেখানে, কেউ না ধরলে leader) ঘণ্টায় কয়টি পড়া leader-এ যাবে? (গ) F3-কে পড়ার pool থেকে সরালে (ক)-এর উত্তর কত? (ঘ) F3 থাকলে পুরোনো পড়া ১%-এ নামাতে পাতা খোলার আগে মোটামুটি কত অপেক্ষা লাগবে?

    (ক) F1, F2: e−২০০/১০০ = e−২ ≈ ০.১৩৫৩; F3: e−২০০/৪০০ = e−০.৫ ≈ ০.৬০৬৫। গড় = (০.১৩৫৩ + ০.১৩৫৩ + ০.৬০৬৫) ÷ ৩ ≈ ০.২৯২৪। ঘণ্টায় ৩০,০০০ × ০.২৯২৪ ≈ ৮,৭৭২ বার — প্রতি দশজনে প্রায় তিনজন।

    (খ) leader-এ যায় শুধু যখন তিনটিই পিছিয়ে: ০.১৩৫৩ × ০.১৩৫৩ × ০.৬০৬৫ ≈ ০.০১১১ → ঘণ্টায় প্রায় ৩৩৩টি। সময়-জানালার নিয়মে এর বদলে ৩০,০০০টিই যেত।

    (গ) দুটিরই গড় ১০০ ms: e−২ ≈ ০.১৩৫৩ → ঘণ্টায় প্রায় ৪,০৬০ বার, অর্ধেকের বেশি কম। তবে বাকি দুটির পড়ার চাপ দেড় গুণ, যা তাদের lag বাড়াতে পারে।

    (ঘ) শর্ত (২e−g/১০০ + e−g/৪০০) ÷ ৩ = ০.০১। বড় g-তে প্রথম পদ নগণ্য, তাই e−g/৪০০ ≈ ০.০৩ → g ≈ ৪০০ × ln(১০০ ÷ ৩) ≈ ১,৪০৩ ms. ধীর follower একাই অভিজ্ঞতা ঠিক করে — তাকে সারানো বা সরানোই আসল প্রতিকার।

  3. Failover বিশ্লেষণ: একটি কাল্পনিক অনলাইন দোকানে async replication. Leader-এর log: ৭০০১ অর্ডার A1 (Rahim) · ৭০০২ A1-এর পেমেন্ট P-501 · ৭০০৩ অর্ডার A2 (Mitu) · ৭০০৪ পণ্য ১২-এর মজুদ ৫ → ৪ · ৭০০৫ অর্ডার A3 (Karim) · ৭০০৬ A3-এর পেমেন্ট P-502. ছয়টিই client-কে “OK” দেওয়া হয়েছিল, তারপর leader মরল। তখন F1 (একই region, একটি লম্বা query-তে আটকে ছিল) ৭০০৪ পর্যন্ত, আর F2 (DR region) ৭০০৫ পর্যন্ত প্রয়োগ করেছিল। অর্ডার নম্বর আসে auto-increment থেকে। (ক) F1 বা F2-কে promote করলে কী কী হারায়? (খ) সবচেয়ে ভালো পরিকল্পনা কী? (গ) পুরোনো leader ফিরে এলে কী করবেন? (ঘ) Semi-sync (যেকোনো একটি follower-এর ack) চালু থাকলে ছবিটা কেমন হতো?

    (ক) F1: ৭০০৫ আর ৭০০৬ হারায় — Karim-এর অর্ডার ও পেমেন্ট; তার ওপর পরের অর্ডার পাবে নম্বর A3, অথচ gateway-তে P-502 জুড়ে আছে “A3”-এর সাথে। F2: শুধু ৭০০৬ — Karim-এর অর্ডার “unpaid”, অথচ টাকা কাটা।

    (খ) F1-কে আগে F2-এর কাছ থেকে ৭০০৫ নিয়ে ধরে নিতে দিন, তারপর promote — leader মূল region-এ থাকে, হারায় শুধু ৭০০৬। (F2-কে leader করলে প্রতিটি লেখায় region পেরোনোর দেরি।) ৭০০৬ gateway-র রেকর্ড আর idempotency key দিয়ে মিলিয়ে হাতে ফেরান; নতুন leader-এর counter নিরাপদ ফাঁক রেখে এগিয়ে দিন, যাতে A3 আবার না আসে।

    (গ) তাকে leader হিসেবে লেখা নিতে দেবেন না — fencing আর নতুন epoch তার লেখা প্রত্যাখ্যান করবে। না-মেলা অংশ (৭০০৬) কেটে আলাদা রেখে হাতে মেলান, তারপর follower হিসেবে যোগ।

    (ঘ) প্রতিটি “OK” লেখা অন্তত একটি follower-এর কাছে থাকত; follower-দের log শুরুর অংশ বলে সবচেয়ে এগিয়ে থাকাটির কাছে সবই থাকত। ঝুঁকি শুধু দুই ক্ষেত্রে: পিছিয়ে থাকাকে ধরে না নিয়ে promote করা, বা semi-sync timeout-এ চুপিচুপি async-এ নামা।

  4. Output অনুমান করুন: অংশ ৪-এর lag সিমুলেটরে F2-এর lag কমিয়ে { d: 40, m: 80 } করা হলো (বাকি সব একই)। চালানোর আগে সূত্র দিয়ে অনুমান করুন: gap ২০, ১০০ আর ৫০০ ms-এ “random stale” কত হবে, আর ১০০ ms-এ “LSN check”-এর leader-ভাগ কত? তারপর কোডে বদলটি করে চালিয়ে মিলিয়ে নিন।

    সূত্র: P(lag > g) = e−(g − d)/m (g < d হলে ১)। F1 অপরিবর্তিত: g = ১০০-তে ০.০৩৮১। নতুন F2: g = ২০ — ২০ < ৪০, তাই এখনো ১; g = ১০০ — e−৬০/৮০ ≈ ০.৪৭২; g = ৫০০ — e−৪৬০/৮০ ≈ ০.০০৩২।

    • g = ২০: (০.৫৪৯ + ১) ÷ ২ ≈ ৭৭.৪% — একটুও বদলায় না! F2-এর স্থির ৪০ ms দেরি (দূরত্ব) গ্যাপের চেয়ে বড়; এলোমেলো অংশ কমিয়ে লাভ নেই।
    • g = ১০০: (০.০৩৮১ + ০.৪৭২) ÷ ২ ≈ ২৫.৫%; LSN leader-ভাগ ০.০৩৮১ × ০.৪৭২ ≈ ১.৮%.
    • g = ৫০০: প্রায় (০ + ০.০০৩২) ÷ ২ ≈ ০.১৬%.

    চালানোর পর আসল আউটপুট:

    gap   random   1s-window      LSN check
    ms    stale    stale leader   stale leader
    20    77.4%    0.0%  100.0%   0.0%  54.9%
    100   25.6%    0.0%  100.0%   0.0%  1.8%
    500   0.2%     0.0%  100.0%   0.0%  0.0%
    2000  0.0%     0.0%  0.0%     0.0%  0.0%

    সূত্রের সাথে মেলে (নমুনার এলোমেলোতায় ২৫.৫-এর জায়গায় ২৫.৬)। শিক্ষা: দূরত্বের স্থির অংশ আর ব্যস্ততার এলোমেলো অংশ আলাদা প্রতিকার চায়; প্রথমটি কমে কেবল কাছের replica-তে পড়ায়।

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

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

ক্রমসেবা না থামিয়ে একটি নতুন follower যোগ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📸 Leader-এর একটি consistent snapshot নেওয়া, সাথে সেই মুহূর্তের log-অবস্থান লিখে রাখা
  2. 📦 Snapshot নতুন যন্ত্রে কপি করে বসানো
  3. 📨 নতুন follower leader-এর কাছে ওই অবস্থানের পরের সব পরিবর্তন চায়
  4. 🏃 জমে থাকা পরিবর্তন প্রয়োগ করে leader-কে ধরে ফেলা (catch up)
  5. ✅ Lag স্বাভাবিক হলে পড়ার অনুরোধ নেওয়া শুরু

একটি single-leader database-এ সেকেন্ডে ৫,০০০ লেখা আর ২০,০০০ পড়া আসে, আর leader প্রায় পুরো ব্যস্ত। লেখা দ্বিগুণ হলে আরও read replica যোগ করলে কী হবে?

  • লেখার ক্ষমতাও বাড়বে, কারণ replica-রাও লেখা নেয়
  • পড়ার ক্ষমতা বাড়বে, কিন্তু লেখার সীমা leader-এই আটকে থাকবে
  • Replication lag শূন্যে নেমে আসবে
  • Leader-এর লেখার চাপ replica-দের মধ্যে সমানভাবে ভাগ হবে

একটি leader-এর একটি semi-synchronous follower (F1) আর দুটি asynchronous follower আছে। Client “commit সফল” উত্তর পেলে নিশ্চিতভাবে কী বলা যায়?

  • তিনটি follower-ই লেখাটি পেয়েছে
  • যেকোনো follower থেকে এখনই পড়লে নতুন মানই আসবে
  • লেখাটি leader আর অন্তত একটি follower-এর কাছে আছে, তবে follower থেকে পড়ায় এখনই দেখা যাবে এমন নিশ্চয়তা নেই
  • কিছুই না — semi-sync আর async একই জিনিস

Rina একটি মন্তব্য পোস্ট করে সাথে সাথে পাতা refresh করলেন — মন্তব্যটি নেই; কয়েক সেকেন্ড পরে আবার refresh করলে দেখা গেল। পড়াগুলো async read replica থেকে আসে। কোন নিশ্চয়তা ভেঙেছে, আর সহজ প্রতিকার কী?

  • Read-your-writes; লেখার পর সেই ব্যবহারকারীর পড়া কিছু সময় leader-এ, বা লেখার log-অবস্থান পর্যন্ত পৌঁছানো replica-তে পাঠানো
  • Linearizability; সব read replica বন্ধ করে দেওয়া
  • Consistent prefix; টেবিলটি sharding করা
  • ACID-এর consistency; একটি foreign key যোগ করা

দুটি leader A আর B একই key-তে লেখা নেয়, আর conflict মেটায় last-write-wins দিয়ে (বড় timestamp জেতে)। B-এর ঘড়ি ৩০০ ms পিছিয়ে। আসল সময় ১,০০০ ms-এ A লেখে x = 1, আর ১,১০০ ms-এ B লেখে x = 2; তখনো কেউ কারও লেখা দেখেনি। সব replica মিলে যাওয়ার পর x কত?

  • x = 2, কারণ B আসল সময়ে পরে লিখেছে
  • x = 1, আর B-র পরের লেখাটি নিঃশব্দে হারায়
  • দুটি মানই রাখা থাকে, পড়ার সময় বেছে নিতে হয়
  • একেক replica-তে একেক মান থেকে যায়

Asynchronous replication-এ leader ৯,০০০ নম্বর লেখা পর্যন্ত client-দের “OK” দিয়েছে। মৃত্যুর মুহূর্তে F1 ৮,৯৯০ পর্যন্ত আর F2 ৮,৯৭৫ পর্যন্ত প্রয়োগ করেছিল, আর অন্য কোথাও কপি নেই। সবচেয়ে কম ক্ষতিতে কাকে promote করবেন, আর কয়টি “OK” পাওয়া লেখা হারাবে?

  • F2; ২৫টি লেখা হারাবে
  • F1; কিছুই হারাবে না, কারণ বাকিটা F2 থেকে আনা যাবে
  • যেকোনোটি; কিছুই হারাবে না, কারণ client-রা আবার পাঠাবে
  • F1; ১০টি লেখা হারাবে

“সবাই সবার কাছে” (all-to-all) multi-leader topology-তে একটি replica-য় একটি সারির update তার insert-এর আগেই পৌঁছাল। এর মূল কারণ আর প্রতিকার কী?

  • বৃত্তাকার পথে বার্তা ঘুরছে; প্রতিটি বার্তায় node-তালিকা রাখা
  • ঘড়ির পার্থক্য; NTP আরও ঘন ঘন চালানো
  • আলাদা পথে বার্তা আলাদা গতিতে যায়; version vector-এর মতো কারণ-ফলের হিসাব রাখা
  • একটি node বিকল; কেন্দ্রীয় একটি node বসানো

শব্দকোষ

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

মনে রাখুন

  1. Replication দেয় availability, পড়ার scaling, কাছের কপি আর durability — লেখা scale করে না, ভুল DELETE থেকেও বাঁচায় না।
  2. Single-leader-এ সব লেখা leader-এ, log একই ক্রমে follower-দের কাছে — statement, WAL বা row-ভিত্তিক; row-ভিত্তিক log CDC-র দরজা খোলে।
  3. Async-এ leader মরলে প্রায় “লেখার হার × lag”-টি লেখা হারাতে পারে; semi-sync অন্তত একটি কপি নিশ্চিত করে; সবার ack চাইলে একটি follower মরলেই লেখা থামে।
  4. Lag-এর তিন anomaly — read-your-writes, monotonic reads, consistent prefix — সারায় leader বা LSN-ভিত্তিক routing, এক replica-তে বাঁধা আর সম্পর্কিত লেখা এক partition-এ।
  5. Failover-এ সবচেয়ে এগিয়ে থাকা follower, fencing আর ফিরে না আসা ID; multi-leader আর leaderless-এ LWW হারায়, সংস্করণ রেখে মেলালে বা CRDT-তে হারায় না।

পরের পর্বে: Replication প্রতিটি কপিতে সব ডেটা রাখে, তাই লেখার চাপ আর ডেটার আকার এক যন্ত্রের সীমা ছাড়ালে আর চলে না। এবার ডেটাকেই ভাগ করব — range, hash, hot spot, rebalancing আর অনন্য ID তৈরি।

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

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