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

Distributed system-এর কঠিন সত্য — partial failure, অনির্ভরযোগ্য network আর clock

The Hard Truths of Distributed Systems — Partial Failure, Unreliable Networks & Clocks

Taught in Bangla, this lesson opens the distributed systems module with why systems spread across machines and what it costs, the eight fallacies of distributed computing, partial failure, lost, delayed, duplicated and reordered messages, why slow and dead nodes look alike and how to pick timeouts, network partitions, clock drift, NTP, wall-clock versus monotonic time, process pauses, system models, safety versus liveness, Two Generals and FLP.

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

  • কেন সিস্টেম অনেক যন্ত্রে ছড়ায়, তার দাম কী, আর মডিউল ৭-এর নয়টি পর্ব কোন প্রশ্নের উত্তর দেয়
  • Distributed computing-এর আটটি ভুল ধারণা আর partial failure-এর আসল অর্থ
  • হারানো, দেরিতে আসা, দুবার আসা আর উল্টো ক্রমের বার্তা; ধীর আর মৃত node কেন আলাদা করা যায় না; timeout কীভাবে বাছবেন
  • Network partition, clock drift, NTP, wall-clock বনাম monotonic clock আর process pause — simulator আর stepper-এ
  • System model, safety বনাম liveness, Two Generals আর FLP impossibility — পরীক্ষা ও ইন্টারভিউর ভাষায়

১ · কেন অনেক যন্ত্র, আর তার দাম — মডিউল ৭-এর শুরু

আগের ছয়টি মডিউলে আমরা অনেক কৌশল ব্যবহার করেছি: replica, shard, queue, cache, outbox. প্রতিটিতে আমরা চুপচাপ কিছু জিনিস ধরে নিয়েছি — বার্তা শেষ পর্যন্ত পৌঁছাবে, ঘড়ি মোটামুটি ঠিক, আর কোনো node মরে গেলে আমরা তা বুঝতে পারব। এই মডিউলে সেই অনুমানগুলোই খুলে দেখব।

প্রথমে মূল প্রশ্ন: একটি যন্ত্রে সব চালালে এত ঝামেলা থাকে না, তবু আমরা অনেক যন্ত্রে যাই কেন? একটি distributed system মানে এমন কয়েকটি যন্ত্র (node), যারা শুধু network-এ বার্তা আদানপ্রদান করে একসাথে একটি কাজ করে, আর যাদের মধ্যে কোনো ভাগাভাগি করা memory বা একটি সাধারণ ঘড়ি নেই। এমন নকশার তিনটি বড় কারণ:

বড় মাপ (scale)

একটি যন্ত্রের CPU, memory আর ডিস্কের সীমা আছে। কোটি ব্যবহারকারীর ডেটা আর অনুরোধ একটিতে ধরে না, তাই কাজ ভাগ করতে হয় (পর্ব ১৩, পর্ব ২৩)।

horizontal scaling

ব্যর্থতা সহ্য করা

একটি যন্ত্র যেকোনো সময় নষ্ট হতে পারে। কয়েকটি কপি থাকলে একটি গেলেও সেবা চলে — fault tolerance (পর্ব ০৫, পর্ব ২২)।

redundancy

দূরত্ব আর latency

আলোর গতিরও সীমা আছে। ঢাকা থেকে আমেরিকার পূর্ব উপকূলে একবার যাওয়া-আসায় প্রায় ২০০ ms বা তার বেশি লাগে। ব্যবহারকারীর কাছাকাছি কপি রাখলে সেই দেরি কমে (পর্ব ০৪)।

geo-distribution

পাড়ার একটি মুদি দোকানের কথা ভাবুন। দোকানদার একাই একটি খাতায় সব বাকি লেখেন। খাতা একটিই, তাই কোনো ভুল বোঝাবুঝি নেই। দোকান বড় হয়ে পাঁচটি শাখা হলো, খাতাও পাঁচটি, হিসাব মেলে ফোনে। কোনো শাখা ফোন না ধরলে — দোকান বন্ধ, নেটওয়ার্ক নেই, নাকি কর্মচারী ব্যস্ত? দুই শাখার ঘড়ি দুই রকম সময় দেখালে কোন বাকি আগে লেখা হয়েছিল? বড় ব্যবসার সুবিধার সাথে এই অনিশ্চয়তাও এলো। Distributed system-এর পুরো গল্প এটাই।

দাম: partial failure; অনির্ভরযোগ্য network; “এখন কটা বাজে” নিয়েও অমিল; একমত হতে বাড়তি round trip; আর দশটি যন্ত্রের log জুড়ে ছড়ানো bug. তাই প্রথম নিয়ম: একটি যন্ত্রে কাজ চললে অকারণে distributed হবেন না।

মডিউল ৭-এর পথ

এই পর্ব বাকি আটটি পর্বের ভিত্তি। প্রতিটি পর্ব এখানকার একটি “কঠিন সত্য”-র জবাব:

পর্বযে প্রশ্নের উত্তর দেয়
৩৮ · এই পর্বকী কী ভাঙতে পারে, আর কোন নিয়মের মধ্যে আমরা নকশা করি?
৩৯ · সময় আর ক্রমঘড়ি বিশ্বাস করা না গেলে “কোনটি আগে ঘটল” কীভাবে বুঝব? (NTP, Lamport ও vector clock)
৪০ · CAP আর consistency modelPartition-এর সময় consistency আর availability-র মধ্যে কোনটি নেব?
৪১ · Quorum আর leaderless replicationকয়েকটি কপির মধ্যে কতজনের সম্মতি পেলে পড়া-লেখা নিরাপদ?
৪২ · Consensus আর Raftঅনেক node কীভাবে একটি সিদ্ধান্তে একমত হয়, কিছু মরে গেলেও?
৪৩ · Lock, lease আর fencing token“একসময়ে একজনই” কীভাবে নিশ্চিত করব, process থেমে গেলেও?
৪৪ · Membership আর failure detectionকে বেঁচে আছে, কে নেই — হাজার node-এ কীভাবে জানব?
৪৫ · Probabilistic data structureবিশাল ডেটায় অল্প memory-তে প্রায়-সঠিক উত্তর কীভাবে?
৪৬ · Conflict আর CRDTদুই দিকে একসাথে লেখা হলে কীভাবে নিজে থেকে মিলে যাবে?

২ · আটটি ভুল ধারণা — fallacies of distributed computing

১৯৯০-এর দশকে Sun Microsystems-এর প্রকৌশলীরা নতুনদের বারবার করা ভুল অনুমানগুলোর একটি তালিকা করেন — fallacies of distributed computing. প্রচলিত বর্ণনা অনুযায়ী প্রথম সাতটি L Peter Deutsch ও সহকর্মীদের, অষ্টমটি পরে James Gosling-এর; উৎস নিয়ে বর্ণনায় কিছুটা ভিন্নতা আছে (ইতিহাস পর্ব ০১-এর ভাবনার প্রশ্নে)। প্রতিটি ভাঙলে কী ঘটে, আর জবাব কোথায়:

ভুল ধারণাবাস্তবে যা ঘটেজবাব কোথায়
১. Network নির্ভরযোগ্যPacket হারায়, switch নষ্ট হয়, fiber কাটে, partition হয়পর্ব ৩৪, ৪৮; এই পর্বের অংশ ৪–৫
২. Latency শূন্যপ্রতিটি call-এ ms থেকে সেকেন্ড; fan-out-এ tail বাড়েপর্ব ০৪
৩. Bandwidth অসীমবড় payload, replication আর backup লিংক ভরে ফেলেপর্ব ০৩, ১২
৪. Network নিরাপদপথে আড়ি পাতা বা বার্তা বদলানোর ঝুঁকিপর্ব ০৭ (TLS), ৫২
৫. Topology বদলায় নাNode যোগ-বাদ, autoscaling, IP বদল, failoverপর্ব ১৪, ৪৪
৬. Administrator একজনইভিন্ন দল, ভিন্ন config, ভিন্ন release-এর সময়পর্ব ৫০, ৫৩
৭. পাঠানোর খরচ শূন্যSerialization-এর CPU, cloud-এর data transfer বিলপর্ব ১২
৮. Network সমজাতীয়ভিন্ন hardware, OS, protocol version আর mobile networkপর্ব ০৭, ১২
মূল ধারণা

আটটির সারকথা একটি বাক্যে: network একটি যন্ত্রের ভেতরের তারের মতো নয়, একটি অনিশ্চিত ডাকব্যবস্থা। একটি function call আর একটি network call দেখতে একই রকম হতে পারে (যেমন RPC-তে), কিন্তু তাদের ব্যর্থ হওয়ার ধরন সম্পূর্ণ আলাদা। এই পার্থক্য লুকিয়ে রাখার চেষ্টার সমস্যা নিয়ে পর্ব ০৯-এর ভাবনার প্রশ্নে Sun-এরই আরেকটি লেখা “A Note on Distributed Computing” (১৯৯৪)-এর কথা আছে।

৩ · Partial failure — এক যন্ত্রে “সব বা কিছুই না”, অনেক যন্ত্রে “কিছু, আর অজানা”

আপনার ল্যাপটপে একটি প্রোগ্রাম চালালে সাধারণত দুটি ফলের একটি পাবেন: কাজ হয়, নয়তো পুরো প্রোগ্রাম বা পুরো কম্পিউটার থেমে যায়। গুরুতর hardware ত্রুটিতে operating system ভুল উত্তর না দিয়ে নিজেই থেমে যায় (kernel panic) — নকশাকারেরা ইচ্ছা করেই অর্ধেক-ঠিক অবস্থার চেয়ে পুরোপুরি থেমে যাওয়া বেছে নিয়েছেন।

অনেক যন্ত্রে এই আরাম নেই। একই মুহূর্তে একটি node চলছে, একটি মরে গেছে, একটি অস্বাভাবিক ধীর, আর একটির সাথে network-এর যোগাযোগ কাটা। একে বলে partial failure. সবচেয়ে কঠিন ব্যাপার হলো, এটি অনির্ণেয়: একটি অনুরোধ পাঠালে তিনটি ফল সম্ভব — সফল, ব্যর্থ, আর অজানা। তৃতীয়টিই distributed system-কে আলাদা করে।

ফোন থেকে কাউকে ৫০০ টাকা পাঠাচ্ছেন। “Send” চাপার পর ঘুরতে থাকা চিহ্ন, তারপর মোবাইল ডেটা চলে গেল। টাকা কি গেছে? তিনটি সম্ভাবনা: অনুরোধ সার্ভারে পৌঁছায়ইনি; পৌঁছে টাকা কেটে নিয়েছে, শুধু উত্তরটা আপনার ফোনে আসেনি; অথবা সার্ভার এখনো ব্যস্ত, একটু পরে কাজ করবে। আপনার ফোনের পর্দায় তিনটি অবস্থা হুবহু এক। আবার চাপলে হয়তো দুবার কাটবে, না চাপলে হয়তো টাকা যায়নি।

নিচের stepper-এ ঠিক এই ঘটনাটি দেখুন। “পরে” চাপুন:

  1. Clientঅ্যাপ
  2. Networkঅনির্ভরযোগ্য
  3. Transfer সার্ভিসসার্ভার
  4. DBপ্রেরকের হিসাব
ভেতরের সত্য বনাম client-এর জ্ঞান
সার্ভারে কাজ হয়েছে?
Client যা জানে
ঝুঁকি
  1. পাঠানো। Client একটি transfer পাঠাল, যার পরিচয় t-71, আর ২ সেকেন্ডের timer চালু করল। প্রেরকের balance ২০০০ টাকা।
  2. পৌঁছাল, কাজ হলো। সার্ভিস ৫০০ টাকা কাটল (balance ১৫০০) আর reply পাঠাল। ভেতরের সত্য: কাজ সম্পন্ন।
  3. Reply হারাল। ফেরার পথে reply হারিয়ে গেল। সার্ভিস ভাবছে সব ঠিক; client এখনো কিছুই জানে না।
  4. Timeout. ২ সেকেন্ড পেরোল। Client-এর হাতে শুধু একটি তথ্য: “উত্তর আসেনি”. এখান থেকে ঝুঁকি শুরু — key ছাড়া আবার পাঠালে টাকা দুবার কাটবে।
  5. তিন জগৎ, এক চেহারা। Request পথে হারালে, বা সার্ভিস শুধু ধীর হলে, client ঠিক এই একই জিনিস দেখত। বাইরে থেকে তিনটি ঘটনা আলাদা করার কোনো উপায় নেই। DB-তে ১৫০০ না ২০০০, client জানে না।
  6. নিরাপদ retry. সমাধান অনুমান নয়, নকশা: একই idempotency key t-71 দিয়ে আবার পাঠানো। সার্ভিস key চিনে আগের ফল ফেরত দিল, টাকা আর কাটল না (বিস্তারিত পর্ব ৩৪ আর পর্ব ০৮)।
মূল ধারণা

একটি remote call-এর ফল তিন রকম: সফল, ব্যর্থ, অজানা। অজানা অবস্থা সামলানোর তিনটি পথ: (১) কাজটিকে idempotent বানিয়ে নিশ্চিন্তে retry; (২) পরে অবস্থা জিজ্ঞেস করা (“t-71-এর কী হলো?”); (৩) দিনশেষে দুই পক্ষের খাতা মিলিয়ে দেখা (reconciliation). কোনো network বা library এই দায়িত্ব নিয়ে নেবে না।

এর পেছনের নকশা-নীতি end-to-end argument (Saltzer, Reed ও Clark, ১৯৮৪): পুরো যাচাই হয় শুধু দুই প্রান্তের অ্যাপ্লিকেশনে; TCP-র মতো নিচের স্তর কাজটি সস্তা করে, প্রতিস্থাপন করে না (ভাবনার প্রশ্ন ০৫)।

৪ · অনির্ভরযোগ্য network — হারানো, দেরি, duplicate আর উল্টো ক্রম

ডেটা সেন্টার আর ইন্টারনেটের network হলো packet-switched: প্রতিটি বার্তা ছোট packet-এ ভাগ হয়ে switch আর router-এর queue পেরিয়ে যায়। এমন network কোনো প্রতিশ্রুতি দেয় না যে বার্তা কখন পৌঁছাবে, বা আদৌ পৌঁছাবে কি না। একে বলে asynchronous network. নিচের ছবিতে চার রকম গোলমাল আর দুটি node-এর ঘড়ি একসাথে দেখুন — পুরো পর্বের মানচিত্র এটিই:

Client, অনির্ভরযোগ্য network আর দুই ঘড়ির দুই node

Messages can be lost, delayed, duplicated or reordered; clocks drift apart

Client, network-এর চার গোলমাল আর দুই node-এর সরে যাওয়া ঘড়ি Client Network · অনির্ভরযোগ্য Node A Node B NTP সময়-সার্ভার অ্যাপ · request পাঠায় হারায় (lost): m1 পৌঁছায়ই না দেরি (delay): m2 আসে ২ s পরে দুবার (duplicate): m3, m3 উল্টো ক্রম (reorder): m5, তারপর m4 10:00:00.000 drift +40 ppm দিনে প্রায় +৩.৫ s মাঝে মাঝে GC-তে থামে 10:00:00.180 A-র চেয়ে +১৮০ ms drift −25 ppm দিনে প্রায় −২.২ s sync করে, তবু ± কয়েক ms কারণ: queue, retransmit, পথ বদল
ছবি ৩৮.১ — একই মুহূর্তে দুই node-এর ঘড়ি ১৮০ ms আলাদা, আর প্রতিদিন আরও সরে যায় (৪০ ppm মানে দিনে ৮৬,৪০০ × ০.০০০০৪ ≈ ৩.৫ সেকেন্ড)। NTP তাদের কাছাকাছি টানে, কিন্তু পুরো মেলায় না।

কারণগুলো সাধারণ: ভিড়ে switch-এর buffer ভরে packet ফেলে দেয় (packet loss); queue, retransmission বা প্রাপকের GC দেরি বাড়ায়, আর দেরির কোনো ঊর্ধ্বসীমা নেই; timeout-এর পরে আবার পাঠালে প্রথম কপিটিও শেষে পৌঁছায় — duplicate; দুই পথ বা retry-তে ক্রম উল্টায় (out-of-order delivery). TCP একটি connection-এর ভেতরে এর অনেকটা সামলায় (Computer Networks-এর TCP reliability পর্বে), কিন্তু connection কেটে গেলে বা অ্যাপ নিজে retry করলে সেই নিশ্চয়তা থাকে না, আর দেরি সে দূর করে না।

ধীর নাকি মৃত — বাইরে থেকে বোঝার উপায় নেই

গ্রামে থাকা চাচাকে ফোন করলেন, কেউ ধরছে না। ফোন বন্ধ? নেটওয়ার্কের বাইরে? চার্জ নেই? নাকি তিনি মাঠে কাজে ব্যস্ত, একটু পরেই ধরবেন? আপনি কতক্ষণ অপেক্ষা করবেন, তারপর পাশের বাড়িতে খোঁজ নেবেন? দুই মিনিটে হাল ছাড়লে হয়তো অকারণে প্রতিবেশীকে বিরক্ত করবেন; দুই দিন অপেক্ষা করলে সত্যিকারের বিপদে দেরি হয়ে যাবে। এই দ্বিধার নামই timeout বাছাই।

কখনো বাড়তি সংকেত মেলে: যন্ত্র চালু কিন্তু process মরে গেলে OS সাথে সাথে “connection refused” ফেরত দিতে পারে। কিন্তু পুরো যন্ত্র বন্ধ বা পথ কাটা হলে কোনো সংকেতই আসে না। তখন একমাত্র উপায় অপেক্ষা করা, আর একসময় timeout ঘোষণা করা। Timeout পেরোনো মানে “মৃত” নয়, মানে “এতক্ষণ অপেক্ষার পরেও খবর নেই, তাই মৃত বলে সন্দেহ করছি”। এই সন্দেহের কাজটি যে অংশ করে, তাকে বলে failure detector; নিয়মিত heartbeat দিয়ে বড় মাপে কীভাবে কাজ করে, তা পর্ব ৪৪-এ।

Timeout কত হবে? একটি simulator

ধরা যাক ২০,০০০টি RPC. দুই দিকেই ০.৫% বার্তা হারায়। স্বাভাবিক round trip গড়ে প্রায় ২০ ms, কিন্তু ৩% call-এ কোনো GC pause, queue বা retransmit বাড়তি ০.১ থেকে ১.৫ সেকেন্ড যোগ করে। Client শুধু দেখে reply এলো কি না। কয়েকটি timeout-এ দেখি, timeout-গুলোর কতটা আসলে “ধীর, কিন্তু সফল”:

// 20,000 RPCs over an unreliable network. The client sees only "reply" or "timeout".
function mulberry32(a) { return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
  t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const rnd = mulberry32(38), N = 20000;
const P_REQ_LOST = 0.005, P_REPLY_LOST = 0.005;   // assumed loss on each direction
const calls = [];
for (let i = 0; i < N; i++) {
  const reqLost = rnd() < P_REQ_LOST, replyLost = rnd() < P_REPLY_LOST;
  let rtt = 8 + -12 * Math.log(1 - rnd());          // normal round trip: ~8 ms + exponential, mean 20 ms
  if (rnd() < 0.03) rtt += 100 + rnd() * 1400;      // 3%: a GC pause, queue or retransmit adds 0.1-1.5 s
  calls.push({ reqLost, replyLost, rtt });
}
const dead = calls.filter(c => c.reqLost || c.replyLost).length;
console.log("calls " + N + ", replies that never come: " + dead + " (work done anyway in " +
  calls.filter(c => !c.reqLost && c.replyLost).length + " of them)");
console.log("timeout_ms  timeouts  really_lost  slow_but_ok  false_share  wait_to_detect_loss");
for (const T of [50, 100, 250, 500, 1000, 2000]) {
  let timeouts = 0, slowOk = 0;
  for (const c of calls) {
    const noReply = c.reqLost || c.replyLost;
    if (noReply || c.rtt > T) { timeouts++; if (!noReply) slowOk++; }
  }
  console.log(String(T).padStart(10) + String(timeouts).padStart(10) + String(dead).padStart(13) +
    String(slowOk).padStart(13) + ((100 * slowOk / timeouts).toFixed(1) + "%").padStart(13) +
    (T + " ms").padStart(21));
}

আউটপুট:

calls 20000, replies that never come: 217 (work done anyway in 106 of them)
timeout_ms  timeouts  really_lost  slow_but_ok  false_share  wait_to_detect_loss
        50      1385          217         1168        84.3%                50 ms
       100       814          217          597        73.3%               100 ms
       250       753          217          536        71.2%               250 ms
       500       658          217          441        67.0%               500 ms
      1000       450          217          233        51.8%              1000 ms
      2000       217          217            0         0.0%              2000 ms

প্রথম লাইনটি আগে পড়ুন: ২১৭টি reply কখনো আসবে না, কিন্তু তার মধ্যে ১০৬টিতে সার্ভারে কাজ হয়ে গেছে — শুধু reply হারিয়েছে। ৫০ ms timeout-এ ১৩৮৫টি timeout, যার ১১৬৮টি (৮৪.৩%) আসলে ধীর কিন্তু সফল call: এগুলোকে “ব্যর্থ” ধরে retry করলে কাজ দুবার হবে, আর সুস্থ node-কে মৃত ভাবা হবে। ২০০০ ms-এ ভুয়া সন্দেহ শূন্য, কিন্তু প্রতিটি সত্যিকারের হারানো ধরতে পুরো ২ সেকেন্ড অপেক্ষা। ১০০ থেকে ১০০০ ms-এ ভুয়া সন্দেহ কমেছে ধীরে, কারণ ৩% “থেমে থাকা” call-এর দেরি ০.১–১.৫ s জুড়ে ছড়ানো। কোনো timeout-ই একসাথে দ্রুত আর নির্ভুল নয়।

ছোট timeoutবড় timeout
সত্যিকারের ব্যর্থতা ধরাদ্রুতধীর; ব্যবহারকারী অপেক্ষায়
ভুয়া সন্দেহ (false suspicion)বেশি; সুস্থ node-কে বাদ, অকারণ failoverকম
Retry-এর চাপবেশি; ভিড়ের সময় ভিড় আরও বাড়েকম
আটকে থাকা thread আর connectionকমবেশি; resource ফুরাতে পারে

তাহলে কীভাবে বাছবেন? কয়েকটি নিয়ম:

  1. মেপে বাছুন, অনুমানে নয়: সুস্থ অবস্থার p99 বা p99.9-এর একটু ওপরে (পর্ব ০৪-এর tail latency). TCP-ও মাপা RTT থেকে নিজের timeout হিসাব করে।
  2. পুরো অনুরোধের একটি deadline: ওপরে ২ s বাকি থাকলে নিচের call-এ ৫ s অপেক্ষার মানে নেই।
  3. Retry সীমিত, ছড়ানো আর নিরাপদ: exponential backoff, jitter, শুধু idempotent কাজে — না হলে retry storm (পর্ব ১৮; resilience pattern পর্ব ৪৮-এ)।
  4. অনুরোধ ছাড়া আর node বাদ দেওয়া আলাদা সিদ্ধান্ত: প্রথমটি সস্তা; failover ব্যয়বহুল, তাই তাতে কয়েকবারের ব্যর্থতা আর কয়েকজনের মত লাগে (পর্ব ৪৪)।

৫ · Network partition — যখন network দুই ভাগ

একটি network partition হলো এমন অবস্থা, যেখানে node-গুলো দুই বা বেশি দলে ভাগ হয়ে যায়: প্রতিটি দলের ভেতরে কথা চলে, দলের বাইরে নয়। সব node-ই চালু, তবু পুরো সিস্টেম আর একটি নয়। কারণ: switch নষ্ট, ভুল firewall নিয়ম, fiber কাটা, রাউটিংয়ের ভুল বা এত ভিড় যে heartbeat পৌঁছায় না।

কুয়াশায় ফেরি বন্ধ। নদীর দুই পাড়ের মানুষ ভালোই আছেন, দোকানপাট খোলা, কিন্তু এক পাড়ের খবর অন্য পাড়ে যাচ্ছে না। দুই পাড়ের দুই দোকান যদি একই শেষ টিকিটটি দুজনকে বিক্রি করে দেয়, ফেরি খোলার পর ঝগড়া অনিবার্য। Partition-এর মূল প্রশ্ন এটাই: বিচ্ছিন্ন অবস্থায় কোন পক্ষ সিদ্ধান্ত নেওয়ার অধিকার রাখে?

“এগুলো তো কালেভদ্রে ঘটে” — এই ধারণা বাস্তব নয়। Peter Bailis আর Kyle Kingsbury-র “The Network is Reliable” (ACM Queue, ২০১৪) প্রবন্ধে বিভিন্ন প্রতিষ্ঠানের প্রকাশিত বিভ্রাটের বিবরণ জড়ো করে দেখানো হয়েছে, বড় সিস্টেমে partition নিয়মিত ঘটনা। নিচের stepper-এ পাঁচটি node-এর একটি cluster (replication-সহ, একজন leader) দুই ভাগ হলো: ২টি এক পাশে, ৩টি অন্য পাশে। নিয়ম: কোনো লেখা quorum অর্থাৎ ৫-এর মধ্যে অন্তত ৩ জনের কাছে পৌঁছালে তবেই commit.

  1. পক্ষ ক · N1, N2সংখ্যালঘু
  2. মাঝের লিংকswitch বা fiber
  3. পক্ষ খ · N3, N4, N5সংখ্যাগুরু
Cluster-এর অবস্থা (majority = ৩)
লেখা commit করতে পারে
Commit হওয়া x
দুই leader?
  1. স্বাভাবিক। N1 leader (term 4), বাকিরা follower. Commit হওয়া মান x = ৫. প্রতিটি লেখা ৫-এর মধ্যে ৩ জনের কাছে পৌঁছালে commit.
  2. লিংক কাটা। N1, N2 এক পাশে; N3, N4, N5 অন্য পাশে। কেউ crash করেনি, তবু N1 এখনো নিজেকে leader ভাবে।
  3. সংখ্যালঘু leader আটকে গেল। Client N1-কে x = ৭ লিখতে বলল। N1 নিজেসহ মাত্র ২/৫ ack পায় — majority নয়, তাই commit হয় না। নিয়মটি না থাকলে এখানেই split-brain শুরু হতো।
  4. সংখ্যাগুরু নতুন leader বেছে নিল। Heartbeat না পেয়ে N3, N4, N5 election করল; N3 term ৫-এ ৩টি ভোটে leader. দুজন নিজেকে leader ভাবে, কিন্তু commit পারে শুধু N3.
  5. সংখ্যাগুরুর লেখা চলছে। x = ৯ পৌঁছাল N3, N4, N5-এ — ৩/৫, commit. ক পাশের client-রা timeout পাচ্ছেন: এটাই consistency রাখার দাম, সেই পাশে availability নেই (পর্ব ৪০)।
  6. জোড়া লাগল। N1 বড় term ৫ দেখে বুঝল সে পুরোনো leader; follower হয়ে গেল, commit-না-হওয়া x = ৭ বাদ দিল আর x = ৯ নিল। কোনো commit হওয়া লেখা হারায়নি। এটিই পর্ব ৪২-এর Raft-এর মূল চিন্তা।
কেন majority কাজ করে — ছোট প্রমাণ

n টি node-কে দুটি আলাদা (পরস্পর-বিচ্ছিন্ন) দলে ভাগ করলে দুই দলের আকারের যোগফল n. যদি দুই দলই n/2-এর বেশি হতো, যোগফল n-এর বেশি হতো — অসম্ভব। তাই যেকোনো partition-এ সর্বোচ্চ একটি দল majority পায়। একইভাবে যেকোনো দুটি majority অন্তত একজন সদস্যে মেলে, তাই নতুন leader পুরোনো commit-এর খবর পায়। জোড় সংখ্যার ফাঁদটি খেয়াল করুন: ৬টি node ৩ | ৩ ভাগ হলে majority লাগে ৪, কোনো দলই পায় না, পুরো সিস্টেমের লেখা বন্ধ। সেজন্য cluster সাধারণত ৩, ৫ বা ৭ node-এর।

বাস্তবের partition সবসময় পরিচ্ছন্ন নয় — কখনো একমুখী (A-র বার্তা B পায়, উল্টোটা নয়), কখনো আংশিক (A আর C দুজনেই B-কে পায়, নিজেদের নয়)। Majority নিয়ম আর term নম্বর এসবেও safety রাখে, যদিও অগ্রগতি কখন ফিরবে বলা কঠিন। Majority ছাড়া লেখা নিলে দুই পক্ষই লেখা নেয় — split-brain, যার পরিষ্কারের খরচ বিশাল (ভাবনার প্রশ্ন ০১)। কোন সিস্টেম partition-এ লেখা থামায় আর কোনটি দুই পাশে নিয়ে পরে মেলায়, তা পর্ব ৪০ আর পর্ব ৪১-এর বিষয়।

৬ · অনির্ভরযোগ্য ঘড়ি — drift, NTP আর wall-clock বনাম monotonic

প্রতিটি কম্পিউটারের ভেতরে একটি ছোট quartz crystal কাঁপে, আর সেই কম্পন গুনে ঘড়ি চলে। কিন্তু কোনো দুটি crystal হুবহু এক হারে কাঁপে না, আর তাপমাত্রা বদলালে হার বদলায়। ফলে ঘড়ি ধীরে ধীরে সঠিক সময় থেকে সরে যায় — একে বলে clock drift, মাপা হয় ppm-এ (parts per million, প্রতি ১০ লাখে কত)। ৫০ ppm মানে প্রতি সেকেন্ডে ৫০ মাইক্রোসেকেন্ড ভুল, দিনে প্রায় ৪.৩ সেকেন্ড। Google-এর Spanner গবেষণাপত্র (২০১২) প্রকাশিত বর্ণনায় সবচেয়ে খারাপ ক্ষেত্রে প্রায় ২০০ ppm drift ধরে নিয়েছিল — দিনে প্রায় ১৭ সেকেন্ড।

বাড়ির দেয়ালঘড়ি আর হাতঘড়ি প্রায়ই কয়েক মিনিট এদিক-ওদিক; মাঝে মাঝে মোবাইল দেখে ঠিক করেন। কিন্তু রান্নার মাঝখানে কাঁটা পাঁচ মিনিট পিছিয়ে দিলে, তরকারি কতক্ষণ চুলায় ছিল দেয়ালঘড়ি দেখে আর বলা যাবে না। কম্পিউটারের ঘড়িতেও ঠিক এই সমস্যা।

ঘড়ি ঠিক রাখতে যন্ত্রগুলো NTP (Network Time Protocol) দিয়ে সময়-সার্ভার থেকে সময় নেয়। কিন্তু NTP নিজেও network-এর ওপর চলে: যাওয়া আর ফেরার দেরি সমান না হলে হিসাবে ভুল ঢোকে। সাধারণত একই ডেটা সেন্টারে মিলিসেকেন্ডের নিচে, ইন্টারনেট পেরিয়ে কয়েক থেকে কয়েক দশ মিলিসেকেন্ডের পার্থক্য থেকে যায় (সেটআপভেদে অনেক আলাদা); ভুল config বা আটকে যাওয়া NTP-তে আরও বেশি। দুই যন্ত্রের এই পার্থক্যকে বলে skew. পার্থক্য বেশি হলে NTP এক লাফে ঘড়ি বদলে দিতে পারে — সামনে বা পেছনে। এর সাথে আছে leap second: পৃথিবীর ঘূর্ণনের সাথে মেলাতে মাঝে মাঝে এক সেকেন্ড যোগ করা হয়েছে, যা বহু সফটওয়্যারকে বিপদে ফেলেছে (ভাবনার প্রশ্ন ০২)।

দুই ধরনের ঘড়ি

Wall-clock (time-of-day)Monotonic clock
কী বলেক্যালেন্ডারের তারিখ আর সময়কোনো একটি অনির্দিষ্ট বিন্দু থেকে কত সময় গেল
পেছনে যেতে পারে?হ্যাঁ — NTP-র লাফ, হাতে বদল, leap secondনা, শুধু সামনে যায়
দুই যন্ত্রে তুলনা?মোটামুটি, skew-এর ভুলসহঅর্থহীন — প্রতিটির শুরুর বিন্দু আলাদা
কাজে লাগেlog-এ সময় লেখা, “রাত ২টায় backup”timeout, latency মাপা, lease-এর মেয়াদ
উদাহরণLinux CLOCK_REALTIME, Java currentTimeMillis(), JS Date.now()Linux CLOCK_MONOTONIC, Java nanoTime(), JS performance.now()

নিচের stepper-এ দেখুন, wall-clock দিয়ে timeout মাপলে কী হয়। Timeout ১ সেকেন্ড:

  1. Wall-clocktime-of-day
  2. Monotonicশুধু সামনে
  3. Timeout সিদ্ধান্ত১ s সীমা
  1. শুরু। একটি request পাঠানো হলো। Wall-clock বলছে ১০:০০:০০.০০০, monotonic clock ৫০০০.০০০ সেকেন্ড (এই সংখ্যার নিজের কোনো অর্থ নেই, শুধু পার্থক্যের অর্থ আছে)।
  2. NTP ঘড়ি পিছিয়ে দিল। ১০০ ms পরে NTP দেখল ঘড়ি ১.৫ সেকেন্ড এগিয়ে ছিল, তাই এক লাফে পিছিয়ে দিল: wall-clock এখন ০৯:৫৯:৫৮.৬০০. Monotonic clock এই লাফের খবরই রাখে না — ৫০০০.১০০.
  3. Reply এলো। শুরু থেকে আসলে ২০০ ms গেছে। Wall-clock-এ হিসাব: ৫৮.৭০০ − ৬০.০০০ = −১.৩০০ সেকেন্ড — ঋণাত্মক সময়! এমন সংখ্যা কোনো কোডে latency বা ভাগের হিসাবে ঢুকলে crash বা অদ্ভুত আচরণ হতে পারে। Monotonic-এ ঠিক ০.২০০ s.
  4. উল্টোটাও হয়। আরেকটি request, শুরু ১০:০০:১০.০০০-এ। ৫০ ms পরে ঘড়ি ৩ সেকেন্ড সামনে লাফাল (যেমন একটি VM থেমে থাকার পর সময় মেলাল)। Wall-clock বলে ৩.০৫ s গেছে, ১ s-এর সীমা পার — ভুয়া timeout, অথচ পেরিয়েছে মাত্র ০.০৫ s. নিয়ম: সময়ের দৈর্ঘ্য মাপুন monotonic clock-এ।

Timestamp দিয়ে ক্রম ঠিক করবেন না। Last-write-wins-এ বড় timestamp জেতে; কিন্তু যে যন্ত্রের ঘড়ি ১৮০ ms এগিয়ে, তার পুরোনো লেখাও “নতুন” দেখায়, আর পরের সঠিক লেখাটি কোনো error ছাড়াই হারায় (পর্ব ২২-এর multi-leader অংশ)। ঘড়ি ছাড়া “আগে-পরে” বোঝার উপায় — Lamport clock আর vector clock — পর্ব ৩৯-এ; conflict মেলানোর কাঠামো পর্ব ৪৬-এ।

৭ · Process pause — না জেনেই থেমে থাকা, আর একটি lease-এর বিপদ

এবার আরও অস্বস্তিকর একটি সত্য। আপনার কোডের দুটি পরপর লাইনের মাঝে process কয়েক সেকেন্ড — কখনো আরও বেশি — থেমে থাকতে পারে, আর জেগে উঠে সে টেরও পায় না। একে বলে process pause. কারণগুলো খুবই সাধারণ:

  • Garbage collection: Java, Go-র মতো runtime-এ কিছু GC ধাপ সব thread থামায় (stop-the-world) — সাধারণত ms, খারাপ ক্ষেত্রে সেকেন্ড।
  • Virtual machine থামানো: hypervisor VM-কে থামিয়ে অন্য যন্ত্রে সরাতে পারে (live migration; Operating Systems-এর virtual machine পর্বে)।
  • OS আর যন্ত্র: swapping, ধীর disk-এর অপেক্ষা, signal দিয়ে থামানো, ল্যাপটপের ঢাকনা বন্ধ।

Distributed system-এ এটা বিপজ্জনক, কারণ থেমে থাকার সময়ে বাকি পৃথিবী এগিয়ে যায়। সবচেয়ে চেনা উদাহরণ lease: একটি সময়সীমাসহ অধিকার, যেমন “পরের ১০ সেকেন্ড শুধু আপনি এই ফাইলে লিখবেন”. নিচের stepper-এ দেখুন:

  1. Lock serviceমেয়াদের হিসাব রাখে
  2. Node Aপুরোনো মালিক
  3. Node Bনতুন মালিক
  4. Storageফাইল বা ডেটা
সময়রেখা
প্রকৃত সময়
Lock service-এর চোখে owner
A নিজেকে ভাবে
  1. t = ০: lease. Lock service A-কে ১০ সেকেন্ডের lease দিল, সাথে একটি ক্রমিক নম্বর — token ৩৩. Storage এ পর্যন্ত সবচেয়ে বড় token দেখেছে ৩২.
  2. t = ৮: যাচাই। A নিজের monotonic clock দেখে নিশ্চিত হলো, মেয়াদের এখনো ২ সেকেন্ড বাকি। ২ সেকেন্ডের এই নিরাপত্তা-ফাঁক যথেষ্ট মনে হয়।
  3. যাচাইয়ের ঠিক পরে GC pause. “যাচাই করলাম” আর “লিখছি” লাইনের মাঝে A-র সব thread থেমে গেল। A-র কাছে এই সময়টুকু অস্তিত্বহীন।
  4. t = ১০: মেয়াদ শেষ। Lock service-এর চোখে A আর মালিক নয়। নিয়ম মেনে lease B-কে দিল, token ৩৪. B লিখল; storage ৩৪ গ্রহণ করল।
  5. t = ১১.৫: A জেগে উঠল। ৩.৫ সেকেন্ডের pause শেষে A লিখতে শুরু করল, এখনো ভাবছে সে মালিক। t = ১০ থেকে ১১.৫ পর্যন্ত ১.৫ সেকেন্ড দুজন মালিক। কোনো সুরক্ষা না থাকলে A-র লেখা B-র লেখার ওপর বসে ডেটা নষ্ট করবে।
  6. Fencing token বাঁচাল। Storage দেখল A-র token ৩৩, অথচ সে আগেই ৩৪ দেখেছে — পুরোনো মালিক। লেখা প্রত্যাখ্যান। এই কৌশলকে বলে fencing token; lock আর lease-এর পুরো নকশা পর্ব ৪৩-এ।

কত ঘন ঘন এমন হয়? নিচের simulator প্রথমে কয়েকটি নির্দিষ্ট pause-এ “দুই মালিকের” সময় হিসাব করে। তারপর ১ লাখ lease চক্র চালায়, একটি ধরে নেওয়া মিশ্রণে: বেশিরভাগ pause ৫০ ms-এর নিচে, ১% চক্রে ০.২–৩ s-এর GC pause, আর ০.০৫% চক্রে ৫–৪০ s-এর VM থামা। ধরা যাক B lease পেয়েই লেখে:

// A 10 s lease. Holder A checks "still mine?" at 8 s, then (maybe) pauses, then writes.
// The lock service gives the lease to B at 10 s. Overlap = time A writes while B also owns it.
function mulberry32(a) { return () => { a |= 0; 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 TTL = 10, CHECK = 8;                     // seconds; 2 s safety margin

function overlap(pause) { const writeAt = CHECK + pause; return Math.max(0, writeAt - TTL); }
console.log("pause_s  A_writes_at  B_owner_from  two_owners_for");
for (const p of [0.05, 1, 2, 3.5, 15, 40])
  console.log(String(p).padStart(7) + (CHECK + p).toFixed(2).padStart(13) + TTL.toFixed(2).padStart(14) +
    (overlap(p).toFixed(2) + " s").padStart(16));

// 100,000 lease cycles with an assumed pause mix: mostly tiny, sometimes GC, rarely a VM freeze.
const rnd = mulberry32(43); let bad = 0, worst = 0, rejected = 0, maxToken = 0;
for (let i = 0; i < 100000; i++) {
  const r = rnd(); let pause = rnd() * 0.05;                  // 0-50 ms scheduling noise
  if (r < 0.01) pause = 0.2 + rnd() * 2.8;                    // 1%: GC pause 0.2-3 s
  if (r < 0.0005) pause = 5 + rnd() * 35;                     // 0.05%: VM suspended 5-40 s
  const ov = overlap(pause), tokenA = 2 * i + 1, tokenB = 2 * i + 2;
  if (ov > 0) { bad++; worst = Math.max(worst, ov);
    maxToken = tokenB;                                        // B wrote first with the newer token
    if (tokenA < maxToken) rejected++; }                      // storage refuses A's stale token
  else maxToken = tokenA;
}
console.log("cycles 100000, two-owner cycles: " + bad + ", worst overlap: " + worst.toFixed(2) +
  " s, stale writes refused by fencing: " + rejected);

আউটপুট:

pause_s  A_writes_at  B_owner_from  two_owners_for
   0.05         8.05         10.00          0.00 s
      1         9.00         10.00          0.00 s
      2        10.00         10.00          0.00 s
    3.5        11.50         10.00          1.50 s
     15        23.00         10.00         13.00 s
     40        48.00         10.00         38.00 s
cycles 100000, two-owner cycles: 391, worst overlap: 37.07 s, stale writes refused by fencing: 391

২ সেকেন্ডের নিরাপত্তা-ফাঁক ২ s পর্যন্ত pause সামলায়; ৩.৫ s-এর pause-এ ১.৫০ s দুই মালিক — stepper-এর হুবহু সংখ্যা। ১ লাখ চক্রে ৩৯১টিতে দুই মালিক, সবচেয়ে খারাপটি ৩৭.০৭ s. হিসাবটি মেলে: ২ s-এর বেশি GC pause-এর সম্ভাবনা ০.০০৯৫ × (১/২.৮) ≈ ০.০০৩৪, আর VM-থামা ০.০০০৫ — মোট প্রায় ০.০০৩৯, অর্থাৎ প্রায় ৩৮৯টি। কোনো সীমিত ফাঁক সীমাহীন pause ঠেকাতে পারে না; কিন্তু storage-এ token যাচাই থাকলে ৩৯১টি পুরোনো লেখার প্রতিটি আটকে যায় — সময় নয়, ক্রমিক নম্বরই নিরাপত্তা দেয়।

মূল ধারণা

যে node সময়ের ওপর ভিত্তি করে কিছু “জানে” (আমি leader, lease আমার, cache-এর মান তাজা), সে যেকোনো মুহূর্তে ভুল হতে পারে, কারণ যাচাই আর কাজের মাঝে সে থেমে থাকতে পারে। তাই সত্য নির্ধারণ করে majority-র সিদ্ধান্ত বা রিসোর্স নিজে (fencing token দিয়ে), একটি node-এর নিজের ধারণা নয়।

৮ · System model — নকশার আগে নিয়মের খাতা

এত অনিশ্চয়তার মধ্যে কোনো algorithm “ঠিক” কি না, বলব কীভাবে? উত্তর: আগে লিখে নিতে হয় কোন কোন ব্যর্থতা ঘটতে পারে বলে ধরছি। এই লিখিত অনুমানের নাম system model. একটি algorithm তার model-এর ভেতরে প্রমাণসহ ঠিক; model-এর বাইরের ঘটনা ঘটলে কোনো প্রতিশ্রুতি নেই। Model-এর দুটি অক্ষ: সময় আর ব্যর্থতা।

ট্রেনের সময়সূচি ভাবুন। এক দেশে ট্রেন ঠিক সময়ে আসবেই, দেরি মানে দুর্ঘটনা — synchronous. আরেক দেশে সাধারণত সময়মতো, কিন্তু কুয়াশার মৌসুমে অজানা দেরি — partially synchronous. তৃতীয় দেশে কখন আসবে কোনো ধারণাই নেই, শুধু একদিন আসবে — asynchronous; সেখানে সময় দেখে কোনো সিদ্ধান্তই নেওয়া যায় না।

সময়ের modelঅনুমানবাস্তবতা
Synchronousবার্তার দেরি, ঘড়ির drift আর process-এর গতি — তিনটিরই জানা ঊর্ধ্বসীমা আছেসাধারণ ডেটা সেন্টারে সত্য নয়; বিশেষ real-time hardware-এ কাছাকাছি
Partially synchronousবেশিরভাগ সময় সীমার ভেতরে, কিন্তু মাঝে মাঝে অজানা সময়ের জন্য সীমা ভাঙেবাস্তবের সবচেয়ে কাছের; Dwork, Lynch ও Stockmeyer (১৯৮৮)-এর কাঠামো
Asynchronousসময় নিয়ে কোনো অনুমানই নেই; ঘড়ি বা timeout ব্যবহার চলে নাখুব কড়া; এখানে যা কাজ করে, সবখানে করে — কিন্তু অনেক কিছু অসম্ভব
ব্যর্থতার modelNode কী করতে পারেকোথায় ধরা হয়
Crash-stopহঠাৎ থেমে যায়, আর কখনো ফেরে নাতাত্ত্বিক বিশ্লেষণ; FLP-র model
Crash-recoveryথেমে যায়, পরে ফেরে; memory-র সব হারায়, disk-এ লেখা থাকেবেশিরভাগ ডেটাবেস আর consensus সিস্টেম (Raft, ZooKeeper, etcd)
Byzantineযা খুশি: ভুল বা পরস্পরবিরোধী বার্তা, ইচ্ছাকৃত মিথ্যাঅবিশ্বাসী পক্ষের মধ্যে (blockchain), কিছু উড়োজাহাজ ও মহাকাশ ব্যবস্থা

Byzantine নামটি Lamport, Shostak ও Pease-এর “The Byzantine Generals Problem” (১৯৮২) থেকে। Crash হওয়া কর্মচারী অফিসে আসেন না — বোঝা যায়; Byzantine কর্মচারী এসে খাতায় ইচ্ছা করে ভুল লেখেন — অনেক কঠিন। f টি ব্যর্থতা সামলাতে crash model-এ ২f + ১টি node লাগে, Byzantine-এ ৩f + ১টি, বার্তাও অনেক বেশি। নিজের ডেটা সেন্টারে সব node একই মালিকের, তাই বেশিরভাগ সিস্টেম ধরে নেয় partial synchrony + crash-recovery, Byzantine নয় — সাথে checksum, TLS আর ইনপুট যাচাইয়ের মতো সীমিত সুরক্ষা (ভাবনার প্রশ্ন ০৪)।

৯ · Safety, liveness আর দুটি অসম্ভবতা — Two Generals আর FLP

একটি distributed algorithm-এর কাছে আমরা দুই ধরনের প্রতিশ্রুতি চাই। এই ভাগটি Leslie Lamport-এর ১৯৭৭ সালের একটি লেখা থেকে প্রচলিত, আর Alpern ও Schneider (১৯৮৫) গাণিতিকভাবে দেখান যে যেকোনো প্রতিশ্রুতিকে এই দুই ধরনের মিশ্রণ হিসেবে লেখা যায়:

Safety — খারাপ কিছু কখনো ঘটবে না

একই term-এ দুজন leader নয়; একটি lock-এর দুজন মালিক নয়; টাকা হাওয়া থেকে তৈরি হবে না। ভাঙলে ঠিক কোন মুহূর্তে ভাঙল দেখানো যায়, আর তা আর ফেরানো যায় না।

safety property

Liveness — ভালো কিছু শেষ পর্যন্ত ঘটবে

প্রতিটি অনুরোধ একসময় উত্তর পাবে; একজন leader একসময় নির্বাচিত হবে। কোনো নির্দিষ্ট মুহূর্তে ভাঙা প্রমাণ করা যায় না — “হয়তো একটু পরেই হবে।”

liveness property

নকশার সোনালি নিয়ম: safety সব পরিস্থিতিতে, partition বা pause চলাকালেও; liveness শর্তসাপেক্ষ হতে পারে — “network ভালো হলে এগোবে।” অংশ ৫-এর stepper-এ সংখ্যালঘু পক্ষ লেখা থামিয়ে liveness ছাড়ল, কিন্তু safety অটুট রইল।

Two Generals — হারাতে পারে এমন পথে নিশ্চিত ঐকমত্য নেই

Two Generals’ Problem-এর গল্প আর ইতিহাস পর্ব ০৮-এর ভাবনার প্রশ্নে আছে: দুই সেনাপতি A আর B, একসাথে আক্রমণ করলে জয়, একা করলে বিপর্যয়, আর বার্তাবাহক ধরা পড়তে পারে। বার্তা বাড়িয়ে লাভ হয় কি? প্রোটোকল k: বার্তাবাহক পালা করে যায় (A থেকে B, B থেকে A …) মোট k জন; কেউ ধরা পড়লে শিকল থামে। সেনাপতি আক্রমণ করেন কেবল যদি তাঁর প্রাপ্য প্রতিটি বার্তা পৌঁছায়। ধরা যাক প্রতিটি বার্তাবাহক ২০% সম্ভাবনায় ধরা পড়ে:

// Two generals: messengers alternate A->B, B->A, A->B ... ; each is captured with probability p.
// Rule: a general attacks only if every messenger addressed to him in the protocol arrived.
function mulberry32(a) { return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
  t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const rnd = mulberry32(2), P = 0.2, TRIALS = 100000;

function play(k, lostAt) {               // lostAt = index of the first captured messenger (k+1 = none)
  let gotA = 0, gotB = 0;                // messengers each general received
  for (let m = 1; m <= k && m < lostAt; m++) { if (m % 2) gotB++; else gotA++; }
  const needB = Math.ceil(k / 2), needA = Math.floor(k / 2);
  return [gotA === needA, gotB === needB];          // [A attacks?, B attacks?]
}
console.log("k  both_attack  neither  only_one(DISASTER)   run 'all arrive but the last'");
for (let k = 1; k <= 6; k++) {
  let both = 0, none = 0, one = 0;
  for (let t = 0; t < TRIALS; t++) {
    let lostAt = k + 1;
    for (let m = 1; m <= k; m++) if (rnd() < P) { lostAt = m; break; }
    const [a, b] = play(k, lostAt);
    if (a && b) both++; else if (!a && !b) none++; else one++;
  }
  const [la, lb] = play(k, k);
  console.log(k + String(both).padStart(13) + String(none).padStart(9) + String(one).padStart(20) +
    "   A " + (la ? "attacks" : "waits  ") + " / B " + (lb ? "attacks" : "waits"));
}

আউটপুট:

k  both_attack  neither  only_one(DISASTER)   run 'all arrive but the last'
1        79808        0               20192   A attacks / B waits
2        64046    20127               15827   A waits   / B attacks
3        51481    35845               12674   A attacks / B waits
4        40829    48903               10268   A waits   / B attacks
5        32889    58938                8173   A attacks / B waits
6        26135    67287                6578   A waits   / B attacks

বার্তা বাড়ালে বিপর্যয় (একজন একা আক্রমণ) কমে — ২০,১৯২ থেকে ৬,৫৭৮ — কিন্তু কখনো শূন্য হয় না; তাত্ত্বিক মান ০.২ × ০.৮k−১, যেমন k = ৬-এ ০.০৬৫৫. “দুজনেই আক্রমণ”-ও কমে (০.৮k). শেষ কলামটিই প্রমাণের হৃদয়: প্রতিটি k-তে “শেষ বার্তাটি ছাড়া সব পৌঁছাল” — এমন একটি ঘটনায় একজন আক্রমণ করেন, অন্যজন অপেক্ষা করেন। শেষ বার্তার প্রেরক জানতেই পারেন না সেটি পৌঁছাল কি না।

প্রমাণের খসড়া

ধরুন কোনো প্রোটোকল সব বার্তা পৌঁছালে দুজনকে আক্রমণ করায় এবং কখনো বিপর্যয় ঘটায় না। এর সবচেয়ে ছোট সফল ঘটনাটি নিন, যেখানে n টি বার্তা পৌঁছেছে। এবার শেষ বার্তাটি হারানো ঘটনা ভাবুন। শেষ বার্তার প্রেরকের কাছে দুটি ঘটনা হুবহু এক — সে একই জিনিস পাঠিয়েছে, একই জিনিস পেয়েছে — তাই সে একই সিদ্ধান্ত নেবে: আক্রমণ। বিপর্যয় এড়াতে প্রাপককেও তখন আক্রমণ করতে হবে, অর্থাৎ শেষ বার্তা ছাড়াই, n − ১টি বার্তায় দুজন আক্রমণ করছেন। এটি “সবচেয়ে ছোট” অনুমানের বিরোধী। বারবার একই যুক্তি চালালে শূন্য বার্তাতেই আক্রমণের সিদ্ধান্ত লাগে, যা অর্থহীন। অতএব হারাতে পারে এমন পথে কোনো সসীম প্রোটোকল নিশ্চিত ঐকমত্য দেয় না।

বাস্তবের শিক্ষা: “দুই পক্ষ একই মুহূর্তে নিশ্চিত হবে” — এমন নকশা চাইবেন না। একজন সিদ্ধান্ত নিয়ে টেকসইভাবে লিখবে, অন্যজন একসময় জানবে, retry নিরাপদ হবে। পর্ব ২৫-এর two-phase commit ঠিক এভাবেই coordinator-এর লেখা সিদ্ধান্তকে সত্যের উৎস বানায়, আর পর্ব ৩৪-এর idempotency retry-কে নিরাপদ করে।

FLP — asynchronous model-এ একটি crash-ই যথেষ্ট

১৯৮৫ সালে Michael Fischer, Nancy Lynch আর Michael Paterson প্রমাণ করেন (FLP impossibility): পুরোপুরি asynchronous model-এ, যেখানে বার্তা হারায় না কিন্তু দেরির কোনো সীমা নেই, মাত্র একটি process-ও crash করতে পারলে কোনো deterministic algorithm নিশ্চয়তা দিতে পারে না যে সব ক্ষেত্রে consensus শেষ হবে। খেয়াল করুন, এখানে network Two Generals-এর চেয়েও ভালো — কোনো বার্তা হারায় না — তবু অসম্ভব।

অন্তর্দৃষ্টিটি অংশ ৪-এর পুরোনো সমস্যা: ধীর আর মৃত আলাদা করা যায় না। একটি process-এর মত না পেলে algorithm হয় অপেক্ষা করবে (সে মৃত হলে চিরকাল), নয়তো তাকে ছাড়াই এগোবে (সে শুধু ধীর হলে, তার দেরিতে আসা বার্তা সিদ্ধান্ত বদলে দিতে পারত)। প্রমাণ দেখায়, বার্তার সময় এমনভাবে সাজানো যায় যে algorithm চিরকাল “এখনো দুই দিকেই যেতে পারে” অবস্থায় থাকে। সূক্ষ্মতাটি গুরুত্বপূর্ণ: FLP বলে না consensus সাধারণত হয় না; বলে কিছু দুর্ভাগা সময়-বিন্যাসে liveness-এর নিশ্চয়তা নেই, safety রাখা সম্ভবই। তাই etcd বা ZooKeeper partial synchrony ধরে timeout আর এলোমেলো election timeout ব্যবহার করে; network খারাপ হলে থেমে থাকে, ভুল সিদ্ধান্ত নেয় না (পর্ব ৪২; ভাবনার প্রশ্ন ০৩)।

আরও গভীরে: Martin Kleppmann-এর বই “Designing Data-Intensive Applications” (২০১৭)-এর অষ্টম অধ্যায় এই পর্বের প্রায় সব বিষয়ের একটি চমৎকার বিস্তৃত রূপ। FLP-র মূল গবেষণাপত্র “Impossibility of Distributed Consensus with One Faulty Process” (Journal of the ACM, ১৯৮৫); তা এড়ানোর দুই বিখ্যাত পথ হলো randomization (Ben-Or, ১৯৮৩) আর অবিশ্বস্ত failure detector (Chandra ও Toueg, ১৯৯৬)। আর Kyle Kingsbury-র Jepsen প্রকল্প বহু বছর ধরে আসল ডেটাবেসে কৃত্রিম partition আর clock-এর লাফ ঘটিয়ে তাদের প্রতিশ্রুতি পরীক্ষা করে প্রকাশ করছে — distributed system-এর দাবি যাচাইয়ের সবচেয়ে শিক্ষণীয় উৎসগুলোর একটি।

নকশায় এর মানে — সত্য থেকে কৌশল

কঠিন সত্যকৌশলকোথায়
Call-এর ফল অজানা হতে পারেIdempotency key, অবস্থা জিজ্ঞেস, reconciliationপর্ব ৩৪
ধীর আর মৃত আলাদা নয়মাপা timeout, failure detector, কয়েকজনের মতপর্ব ৪৪, ৪৮
Partition হয়Majority quorum, CP বনাম AP-র সচেতন পছন্দপর্ব ৪০, ৪১
ঘড়ি সরে আর লাফায়দৈর্ঘ্যে monotonic clock, ক্রমে logical clockপর্ব ৩৯
Process না জেনে থামেLease + fencing tokenপর্ব ৪৩
একমত হওয়া কঠিনConsensus (Raft) — safety সবসময়, liveness শর্তেপর্ব ৪২
দুই পাশে লেখা হয়CRDT বা নিয়মসহ conflict মেলানোপর্ব ৪৬

পরীক্ষা ও ইন্টারভিউতে:

  • Partial failure কী, আর এক যন্ত্রের ব্যর্থতা থেকে কীভাবে আলাদা? — কিছু অংশ চলে, কিছু চলে না, আর একটি call-এর ফল অজানা থাকতে পারে; এক যন্ত্রে সাধারণত সব বা কিছুই না।
  • Timeout পেরোলে কী সিদ্ধান্ত নেওয়া যায়? — শুধু “উত্তর আসেনি”; কাজ হয়েছে কি না অজানা, তাই retry কেবল idempotent কাজে।
  • ৫ node-এর cluster ৩ | ২ ভাগ হলে কে লেখা নেবে? — majority (৩) পক্ষ; ২-এর পক্ষ থামবে, নইলে split-brain.
  • Timeout মাপতে কোন ঘড়ি? — monotonic; wall-clock NTP-তে পেছনে বা সামনে লাফাতে পারে।
  • FLP কী বলে? — asynchronous model-এ একটি crash থাকলেও কোনো deterministic consensus সব ক্ষেত্রে শেষ হওয়ার নিশ্চয়তা দেয় না; বাস্তবে partial synchrony আর timeout দিয়ে এড়ানো হয়।

১০ · AI-এর যুগে distributed systems-এর অনুমান যাচাই

AI-কে “একটি নির্ভরযোগ্য leader election বানিয়ে দাও” বললে প্রায়ই সুন্দর কোড আসে, যা চুপচাপ ধরে নেয় network নির্ভরযোগ্য আর ঘড়ি ঠিক — অর্থাৎ অংশ ২-এর ভুল ধারণাগুলো। চেনা ভুলগুলো:

AI-এর ভুলকেমন দেখায়কীভাবে যাচাই করবেন
সাধারণ, ভারী উত্তর“Kafka, Redis আর microservices যোগ করুন” — অথচ একটি যন্ত্রেই কাজ চলতজিজ্ঞেস করুন: distributed হওয়ার আসল কারণ কী, scale, ব্যর্থতা নাকি দূরত্ব?
Timeout = মৃতএকটি heartbeat না এলেই failoverLatency বণ্টন মেপে ভুয়া সন্দেহের হার হিসাব করুন (অংশ ৪-এর simulator)
Majority ছাড়া leader“যে আগে lock পায় সেই leader”, কোনো term বা quorum নেই৩ | ২ partition কল্পনা করুন: দুই পাশে কি লেখা চলে?
Wall-clock-এ মাপ বা ক্রমDate.now() দিয়ে timeout, timestamp দিয়ে last-write-winsঘড়ি ১.৫ s পিছিয়ে দিয়ে চালান; dedicated monotonic API খুঁজুন
হিসাবের ভুলppm থেকে দিনে কত সেকেন্ড, বা majority-র আকার ভুলনিজে হিসাব: drift × সময়; majority = ⌊n/২⌋ + ১
ধারণা গুলিয়ে ফেলাACID-এর consistency আর CAP-এর consistency এক বলা; FLP মানে “consensus অসম্ভব”সংজ্ঞা আর model লিখে মিলিয়ে নিন (পর্ব ৪০)
বানানো সংখ্যা“NTP সবসময় ১ ms-এর মধ্যে রাখে”, কোনো পণ্যের ডিফল্ট timeoutঅফিশিয়াল ডকুমেন্টেশন আর নিজের মাপা skew দেখুন
অতি-নকশানিজের ডেটা সেন্টারের ভেতরে Byzantine fault toleranceজিজ্ঞেস করুন: কে মিথ্যা বলতে পারে? না পারলে crash model-ই যথেষ্ট

AI-কে নকশাকার নয়, ব্যর্থতা খোঁজা reviewer বানান। আগে নিজের system model লিখুন, তারপর এভাবে প্রশ্ন করুন:

তুমি একজন অভিজ্ঞ distributed systems reviewer. নিচে আমার নকশা: ৫টি node, একজন leader, client-এর লেখা leader নেয় আর follower-দের পাঠায়; leader একটি ১০ সেকেন্ডের lease রাখে। [এখানে নকশা, pseudocode আর timeout-এর মানগুলো পেস্ট করুন] ১. আমার system model কী ধরে নিচ্ছে, স্পষ্ট করে লেখো: সময়ের model (synchronous / partially synchronous / asynchronous) আর ব্যর্থতার model (crash-stop / crash-recovery / Byzantine). ২. প্রতিটি remote call-এ reply হারালে, request হারালে আর call শুধু ধীর হলে কী ঘটে — একটি টেবিলে দেখাও; কোথায় কাজ দুবার বা একবারও হয় না। ৩. ৩ | ২ partition, একমুখী partition আর leader-এর ১৫ সেকেন্ডের GC pause — প্রতিটিতে দুজন leader বা দুজন lease-মালিক হতে পারে কি না, আর storage কীভাবে পুরোনো লেখা আটকাবে। ৪. কোথায় wall-clock আর কোথায় monotonic clock ব্যবহার হচ্ছে; ঘড়ি ২ সেকেন্ড পেছনে বা সামনে লাফালে কী ভাঙবে। ৫. কোন প্রতিশ্রুতি safety আর কোনটি liveness; partition-এর সময় কোনটি ছাড়ছি। ৬. সংখ্যা বা পণ্যের সীমা বললে উৎস দাও, নিশ্চিত না হলে স্পষ্ট লেখো; নতুন প্রযুক্তি যোগের আগে সরলতর বিকল্পটি বলো।

AI-এর উত্তর মানার আগে — চেকলিস্ট

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

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

প্র ০১মাত্র কয়েক দশ সেকেন্ডের একটি network বিচ্ছিন্নতা কীভাবে এক দিনের বেশি সময়ের বিভ্রাট ঘটাতে পারে?

সবচেয়ে বেশি উদ্ধৃত উদাহরণগুলোর একটি GitHub-এর ২০১৮ সালের ২১ অক্টোবরের ঘটনা, যার বিস্তারিত বিশ্লেষণ GitHub নিজে প্রকাশ করেছিল। প্রকাশিত বিবরণ অনুযায়ী, রুটিন রক্ষণাবেক্ষণের সময় আমেরিকার পূর্ব উপকূলের একটি network hub আর মূল ডেটা সেন্টারের মধ্যে প্রায় ৪৩ সেকেন্ডের জন্য যোগাযোগ কেটে যায়। এই অল্প সময়েই MySQL cluster পরিচালনার স্বয়ংক্রিয় ব্যবস্থাটি (Orchestrator, যা নিজের সিদ্ধান্তে consensus ব্যবহার করে) সিদ্ধান্ত নেয় যে পূর্বের primary-রা নাগালের বাইরে, আর পশ্চিম উপকূলের replica-দের primary বানিয়ে দেয়।

যোগাযোগ ফিরে এলে দেখা গেল দুটি সমস্যা। প্রথমত, পূর্ব দিকের primary-তে কিছু লেখা হয়েছিল যা পশ্চিমে পৌঁছায়নি — অর্থাৎ দুই পাশে দুই রকম ইতিহাস। দ্বিতীয়ত, অ্যাপ্লিকেশনগুলো এখন মহাদেশ পেরিয়ে দূরের primary-তে লিখছিল, ফলে প্রতিটি লেখায় অনেক বেশি latency. দলটি ইচ্ছা করে ডেটার সঠিকতাকে অগ্রাধিকার দেয়: দ্রুত সেবা ফেরানোর বদলে backup থেকে পুনরুদ্ধার আর দুই পাশের লেখা মেলানোর কাজ করে। প্রকাশিত হিসাবে আংশিক বিভ্রাট চলে চব্বিশ ঘণ্টারও বেশি।

এখানে এই পর্বের প্রায় সব সত্য একসাথে। ৪৩ সেকেন্ডের partition-কে failure detector “মৃত” ভেবেছিল — ধীর আর মৃত আলাদা করা যায়নি। Failover নিজে নিরাপদ ছিল না, কারণ পুরোনো primary-র অপ্রতিলিপিত লেখা ছিল (replication lag, পর্ব ২২)। আর region পেরোনো failover topology বদলে দিল, যা অনেক নির্ভরশীল অংশ আগে থেকে ধরে নেয়নি। পরবর্তী শিক্ষা হিসেবে GitHub জানায়, তারা এমন cross-region promotion-এর নিয়ম কড়া করছে।

তাই অনেক দল region-এর ভেতরে স্বয়ংক্রিয় failover রাখে, কিন্তু region পেরোনো failover-এ মানুষের সিদ্ধান্ত বা অনেক বেশি প্রমাণ চায় (পর্ব ০৫; disaster recovery পর্ব ৫১-এ)।

মূল উপলব্ধি: Partition-এর দৈর্ঘ্য নয়, তার প্রতিক্রিয়ায় সিস্টেম কী করে — সেটাই ক্ষতির মাপ ঠিক করে। স্বয়ংক্রিয় failover নিজেই একটি distributed সিদ্ধান্ত, আর তারও partial failure হয়।

প্র ০২কম্পিউটারের ঘড়ি কি সত্যিই পেছনে যেতে পারে? Leap second-এর ঘটনাগুলো কী শিখিয়েছে?

হ্যাঁ, পারে, আর এর বাস্তব দাম আছে। পৃথিবীর ঘূর্ণন পুরোপুরি স্থির নয়, তাই atomic clock-ভিত্তিক সময়কে সূর্যের সময়ের কাছাকাছি রাখতে ১৯৭২ সাল থেকে মাঝে মাঝে একটি leap second যোগ করা হয়েছে। সেই রাতে ঘড়ি দেখায় ২৩:৫৯:৫৯, তারপর ২৩:৫৯:৬০, তারপর ০০:০০:০০. অনেক সিস্টেম “৬০ সেকেন্ড” বোঝে না, তাই ঘড়িকে এক সেকেন্ড পিছিয়ে দেয় বা একই সেকেন্ড দুবার দেখায়।

২০১২ সালের ৩০ জুনের leap second-এ Linux kernel-এর একটি সমস্যায় অনেক সার্ভারে হঠাৎ CPU ব্যবহার অস্বাভাবিক বেড়ে যাওয়ার খবর ব্যাপকভাবে প্রকাশিত হয়েছিল, বিশেষ করে Java আর কিছু ডেটাবেস চালানো যন্ত্রে। ২০১৭ সালের ১ জানুয়ারির leap second নিয়ে Cloudflare একটি বিস্তারিত engineering blog প্রকাশ করে: তাদের DNS সফটওয়্যারের একটি অংশ (Go-তে লেখা) দুটি wall-clock মান বিয়োগ করে সময়ের দৈর্ঘ্য বের করছিল; ঘড়ি পেছনে যাওয়ায় ফল ঋণাত্মক হলো, আর সেই ঋণাত্মক সংখ্যা একটি হিসাবে ঢুকে কিছু DNS অনুরোধ ব্যর্থ করল। ঠিক অংশ ৬-এর stepper-এর ঘটনা। সেই বছরই Go ভাষার সংস্করণ ১.৯-এ time.Now()-এর সাথে স্বয়ংক্রিয়ভাবে একটি monotonic মান রাখা শুরু হয়, যাতে দুটি সময়ের বিয়োগ নিরাপদ থাকে।

আরেকটি পথ নেয় কিছু বড় প্রতিষ্ঠান: “leap smear”. প্রকাশিত বর্ণনা অনুযায়ী Google বাড়তি সেকেন্ডটিকে প্রায় ২৪ ঘণ্টা জুড়ে ছড়িয়ে দেয়, যাতে ঘড়ি কখনো লাফায় না, শুধু সেই সময়টুকু সামান্য ধীরে চলে। তবে smear করা আর না-করা দুটি সার্ভারের ঘড়ি তখন আধা সেকেন্ড পর্যন্ত আলাদা থাকে। শেষে আন্তর্জাতিক ওজন ও পরিমাপ সম্মেলন (CGPM) ২০২২ সালে সিদ্ধান্ত নেয়, ২০৩৫ সালের মধ্যে leap second বন্ধ বা ব্যাপকভাবে কমানো হবে — বিস্তারিত বাস্তবায়ন এখনো আলোচনাধীন।

তবে leap second না থাকলেও ভুল NTP, হাতে সময় বদল বা VM জেগে ওঠায় ঘড়ি লাফাবে। তাই দৈর্ঘ্য মাপুন monotonic clock-এ, আর পরীক্ষায় ঘড়ি ইচ্ছা করে সরিয়ে দেখুন।

মূল উপলব্ধি: “সময় সবসময় সামনে যায়” প্রকৃতির নিয়ম, কিন্তু কম্পিউটারের wall-clock-এর নয়। যে কোড দুটি wall-clock মান বিয়োগ করে, সে একদিন ঋণাত্মক সংখ্যা পাবেই।

প্র ০৩FLP যদি বলে consensus-এর নিশ্চয়তা অসম্ভব, তাহলে etcd আর ZooKeeper-এর মতো সিস্টেম প্রতিদিন নির্ভরযোগ্যভাবে চলে কীভাবে?

কারণ FLP একটি নির্দিষ্ট, খুব কড়া model-এর ফল, আর বাস্তবের সিস্টেম সেই model-এর ঠিক ভেতরে থাকে না। FLP-র তিনটি শর্ত: পুরোপুরি asynchronous (কোনো timeout নেই), deterministic algorithm, আর সব সময়-বিন্যাসে শেষ হওয়ার নিশ্চয়তা। যেকোনো একটি শর্ত শিথিল করলে পথ খোলে।

সময়ের অনুমান যোগ করা: Dwork, Lynch ও Stockmeyer (১৯৮৮) দেখান, partial synchrony-তে — যেখানে একসময় না একসময় দেরি সীমার ভেতরে আসে — consensus সম্ভব, যতক্ষণ সংখ্যাগরিষ্ঠ সচল। Paxos (Lamport, ১৯৯৮ সালে প্রকাশিত “The Part-Time Parliament”) আর Raft (Ongaro ও Ousterhout, ২০১৪) এই পথের: safety কোনো সময়ের অনুমানের ওপর নির্ভর করে না, কিন্তু অগ্রগতির জন্য timeout লাগে।

Randomization: Ben-Or (১৯৮৩) দেখান, এলোমেলো সিদ্ধান্ত নিলে “চিরকাল দুই দিকেই যেতে পারে” অবস্থা থেকে বেরোনোর সম্ভাবনা ১-এর দিকে যায়। Raft-এর এলোমেলো election timeout একই চিন্তার ব্যবহারিক রূপ: দুজন একসাথে প্রার্থী হয়ে ভোট ভাগ হলে, পরের দফায় সম্ভবত একজন আগে জাগবে।

Failure detector: Chandra ও Toueg (১৯৯৬) দেখান, এমনকি ভুল করা failure detector-ও যথেষ্ট, যদি সে একসময় অন্তত একটি সচল process-কে স্থায়ীভাবে বিশ্বাস করে আর সংখ্যাগরিষ্ঠ process সচল থাকে।

বাস্তবে এর অর্থ: etcd বা ZooKeeper খুব খারাপ network-এ লম্বা সময় নতুন লেখা নিতে না-ও পারে — leader বারবার বদলায়, বা কোনো majority গঠিত হয় না। কিন্তু তারা দুটি পরস্পরবিরোধী মান commit করে না। Operator-রা এটাই দেখেন: বিভ্রাট হয় “আটকে যাওয়া” রূপে, “ভুল উত্তর” রূপে নয়। Jepsen-এর মতো পরীক্ষায় যে bug ধরা পড়েছে, সেগুলো সাধারণত বাস্তবায়নের ভুল, তত্ত্বের নয়।

মূল উপলব্ধি: অসম্ভবতার ফল দরজা বন্ধ করে না, দেখিয়ে দেয় কোন দরজা দিয়ে যেতে হবে। প্রতিটি প্রমাণের শর্ত পড়ুন — কোনটি আপনি ছাড়তে রাজি, সেটাই নকশা।

প্র ০৪Byzantine ব্যর্থতা সবচেয়ে খারাপ — তাহলে সব সিস্টেম Byzantine fault tolerant কেন বানানো হয় না?

দাম আর লাভের হিসাব মেলে না। Lamport, Shostak ও Pease-এর কাজ (১৯৮২) থেকে জানা, f টি বিশ্বাসঘাতক node সহ্য করতে অন্তত ৩f + ১টি node লাগে, যেখানে crash ব্যর্থতায় লাগে ২f + ১. একটি ব্যর্থতা সহ্য করতে তাই ৩-এর বদলে ৪টি node, আর বার্তা অনেক বেশি — প্রতিটি node-কে অন্যদের দাবি যাচাই করতে হয়। Castro ও Liskov-এর PBFT (১৯৯৯) একে ব্যবহারযোগ্য করেছিল, তবু খরচ crash-ভিত্তিক প্রোটোকলের চেয়ে বেশি।

বড় কথা হলো, নিজের ডেটা সেন্টারে Byzantine সুরক্ষা প্রায়ই আসল ঝুঁকি ঠেকায় না। সব node একই প্রতিষ্ঠানের, একই কোড চালায়। একটি software bug সব replica-তে একসাথে থাকে — ৪টির ৪টিই একই ভুল করলে কোনো ভোটে তা ধরা পড়ে না (correlated failure). আর কোনো আক্রমণকারী যদি সব যন্ত্রের নিয়ন্ত্রণ নিয়ে নেয়, তখনও প্রোটোকল সাহায্য করে না।

Byzantine সুরক্ষা অর্থবহ যেখানে পক্ষগুলো একে অপরকে বিশ্বাস করে না। Bitcoin (২০০৮ সালের white paper) খোলা সদস্যপদের ব্যবস্থায় proof-of-work দিয়ে একরকম সম্ভাবনাভিত্তিক ঐকমত্য গড়ে; অনেক অনুমতিভিত্তিক blockchain PBFT-ধাঁচের প্রোটোকল ব্যবহার করে। উড়োজাহাজ আর মহাকাশযানের নিয়ন্ত্রণে বহু বছর ধরে ভিন্ন hardware-এর একাধিক কম্পিউটার ভোট দিয়ে সিদ্ধান্ত নেয়, কারণ সেখানে একটি sensor-এর ভুল মাপও মানুষের জীবনের প্রশ্ন।

তবে ডেটা সেন্টারেও “চুপচাপ ভুল” বাস্তব। Google-এর গবেষকদের “Cores that don’t count” (HotOS, ২০২১) আর Meta-র প্রায় একই সময়ের প্রকাশনা জানায়, বিশাল বহরে কিছু CPU কখনো কখনো কোনো সংকেত ছাড়াই ভুল হিসাব করে। এর জবাব পূর্ণ BFT নয়, বরং লক্ষ্যভিত্তিক সুরক্ষা: end-to-end checksum, গুরুত্বপূর্ণ হিসাব দুবার করে মেলানো, সন্দেহজনক যন্ত্র বহর থেকে সরানো।

মূল উপলব্ধি: ব্যর্থতার model বাছাই একটি ঝুঁকির সিদ্ধান্ত — কে মিথ্যা বলতে পারে, আর ভুল হলে ক্ষতি কত। বেশিরভাগ সিস্টেমের জন্য crash-recovery + checksum-ই সঠিক ভারসাম্য।

প্র ০৫End-to-end argument কী বলে? TCP নির্ভরযোগ্য হলেও অ্যাপ্লিকেশনকে কেন নিজে যাচাই করতে হয়?

Jerome Saltzer, David Reed আর David Clark-এর “End-to-End Arguments in System Design” (ACM Transactions on Computer Systems, ১৯৮৪; আগের সংস্করণ ১৯৮১) কম্পিউটার নকশার সবচেয়ে প্রভাবশালী লেখাগুলোর একটি। তাঁদের উদাহরণ ছিল “সাবধানী ফাইল পাঠানো”: একটি ফাইল এক যন্ত্রের ডিস্ক থেকে আরেক যন্ত্রের ডিস্কে যাবে। Network প্রতিটি packet নির্ভুলভাবে পৌঁছে দিলেও ভুল ঢুকতে পারে পড়ার সময় ডিস্কে, memory-তে, প্রোগ্রামের bug-এ, লেখার সময় গন্তব্যের ডিস্কে। একমাত্র নিশ্চিত যাচাই: গন্তব্যে পুরো ফাইলের checksum হিসাব করে উৎসের সাথে মেলানো, না মিললে আবার পাঠানো। এই যাচাই থাকলে network-এর নিজস্ব নির্ভরযোগ্যতা আর সঠিকতার জন্য আবশ্যক থাকে না — থাকে শুধু গতি বাড়ানোর উপায় হিসেবে।

এই পর্বের ভাষায়: TCP একটি connection-এর ভেতরে byte-গুলোর ক্রম আর সম্পূর্ণতা রাখে। কিন্তু reply হারানোর পর client নতুন connection খুলে আবার পাঠালে TCP সেটিকে নতুন বার্তাই ভাবে। তাই “টাকা দুবার কাটবে না” — এই নিশ্চয়তা আসে অ্যাপ্লিকেশনের দুই প্রান্ত থেকে: অভিপ্রায়ের জন্মস্থানে একটি idempotency key (পর্ব ০৮), আর effect-এর পাশে duplicate চেনার চিহ্ন (পর্ব ৩৪)। Object storage-এ প্রতিটি অংশের checksum (পর্ব ২৬) একই নীতির রূপ।

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

মূল উপলব্ধি: কোন স্তর কোন নিশ্চয়তা দেয়, তা লিখে রাখুন। যে নিশ্চয়তা ব্যবহারকারীর কাছে গুরুত্বপূর্ণ, তার শেষ যাচাই সেই প্রান্তে, যেখানে অভিপ্রায় জন্মায় আর যেখানে effect ঘটে।

অনুশীলন

খাতা-কলম নিয়ে বসুন — হিসাবগুলো নিজে করে তারপর উত্তর মেলান।

  1. হিসাব — timeout আর ঘড়ি: একটি সার্ভিসে সেকেন্ডে ২,০০০টি call আসে। মাপা তথ্য: ০.০৮% reply ১ সেকেন্ডের বেশি সময় নেয় কিন্তু শেষে আসে, আর ০.০০৩% reply কখনো আসে না। Timeout ১ s. (ক) সেকেন্ডে কয়টি timeout, তার কয়টি ভুয়া, আর ভুয়ার ভাগ কত? (খ) দিনে কয়টি ভুয়া timeout, আর key ছাড়া retry করলে কয়টি কাজ দুবার হতে পারে? (গ) দুটি যন্ত্রের ঘড়ি একসাথে মেলানো হলো, তারপর ১ ঘণ্টা NTP বন্ধ; একটি +৫০ ppm, অন্যটি −৫০ ppm হারে সরে। শেষে পার্থক্য কত? (ঘ) ১০ s-এর একটি lease-এ দুই যন্ত্রের মোট drift ৪০০ ppm হলে মেয়াদের হিসাবে ভুল কত — এটি কি GC pause-এর চেয়ে বড় ঝুঁকি?

    (ক) Timeout = ২০০০ × (০.০০০৮ + ০.০০০০৩) = ১.৬৬টি/সেকেন্ড। ভুয়া (ধীর কিন্তু সফল) = ২০০০ × ০.০০০৮ = ১.৬টি, সত্যিকারের = ০.০৬টি। ভুয়ার ভাগ = ০.০০০৮ / ০.০০০৮৩ ≈ ৯৬.৪% — প্রায় প্রতিটি timeout-ই আসলে সফল call.

    (খ) দিনে ১.৬ × ৮৬,৪০০ = ১,৩৮,২৪০টি ভুয়া timeout. প্রতিটিতে key ছাড়া retry করলে ততগুলো কাজ দুবার হতে পারে (যেমন দুবার টাকা কাটা) — যেখানে সত্যিকারের হারানো মাত্র ০.০৬ × ৮৬,৪০০ = ৫,১৮৪টি।

    (গ) প্রতিটি ঘড়ি ১ ঘণ্টায় ৫০ × ১০−৬ × ৩৬০০ = ০.১৮ s সরে, উল্টো দিকে। পার্থক্য = ২ × ০.১৮ = ০.৩৬ s.

    (ঘ) ১০ s × ৪০০ × ১০−৬ = ০.০০৪ s = ৪ ms. একটি কয়েক সেকেন্ডের GC pause-এর তুলনায় নগণ্য। Lease-এর নিরাপত্তা-ফাঁকে drift-এর জন্য সামান্য জায়গা রাখলেই চলে; আসল বিপদ pause, আর তার জবাব fencing token.

  2. Critique — এই leader election-এ কী ভুল? একজন সহকর্মী লিখেছেন: “৩টি node. প্রতিটি node প্রতি সেকেন্ডে leader-কে ping করে। ৫০০ ms-এর মধ্যে উত্তর না এলে নিজেকে leader ঘোষণা করে আর লেখা নিতে শুরু করে। Leader নিজের lease-এর মেয়াদ দেখে System.currentTimeMillis() দিয়ে, আর lease থাকলে সরাসরি ডেটাবেসে লেখে।” অন্তত চারটি সমস্যা আর তাদের সমাধান লিখুন।
    • একটি timeout-এই মৃত ঘোষণা: ৫০০ ms মাপা latency থেকে বাছা হয়নি; একটি GC pause বা ভিড়েই ভুয়া সন্দেহ। সমাধান: p99.9-ভিত্তিক timeout, কয়েকবারের ব্যর্থতা, বা phi-accrual-ধাঁচের failure detector (পর্ব ৪৪)।
    • Majority ছাড়া নিজেকে leader: দুটি follower একসাথে timeout পেলে দুজনেই leader, আর পুরোনো leader-ও বেঁচে থাকলে তিনজন — split-brain. সমাধান: term নম্বরসহ ভোট, ৩-এর মধ্যে ২টি ভোট পেলে তবেই leader (পর্ব ৪২)।
    • Wall-clock-এ মেয়াদ: currentTimeMillis() NTP-তে লাফাতে পারে; মেয়াদ বাড়তি বা কম মনে হবে। সমাধান: monotonic clock (nanoTime()) আর drift-এর জন্য নিরাপত্তা-ফাঁক।
    • Fencing নেই: lease যাচাই আর লেখার মাঝে pause হলে পুরোনো leader লিখে ফেলবে। সমাধান: প্রতিটি লেখায় term বা fencing token, আর ডেটাবেসে শর্ত — পুরোনো token প্রত্যাখ্যান (পর্ব ৪৩)।
  3. Output অনুমান করুন: নিচের কোডে সময় সেকেন্ডে, আর timeout-এর সীমা ০.৪ s. চালানোর আগে লিখুন কী ছাপবে:
    let wall = 1000.0, mono = 50.0;
    const startW = wall, startM = mono;
    mono += 0.2; wall += 0.2;      // 200 ms pass
    wall -= 1.5;                   // NTP steps the wall clock back
    mono += 0.3; wall += 0.3;      // 300 ms more
    console.log((wall - startW).toFixed(1), (mono - startM).toFixed(1));
    console.log(wall - startW > 0.4 ? "timeout" : "ok", mono - startM > 0.4 ? "timeout" : "ok");
    -1.0 0.5
    ok timeout

    আসলে গেছে ০.২ + ০.৩ = ০.৫ s, তাই monotonic হিসাবে ০.৫ — সীমা ০.৪ পেরিয়েছে, ঠিকভাবেই “timeout”. Wall-clock-এ ০.৫ − ১.৫ = −১.০ s, ঋণাত্মক — তাই তুলনায় “ok”, অর্থাৎ সত্যিকারের timeout ধরা পড়ল না। অংশ ৬-এর stepper-এ উল্টো ঘটনাও দেখেছেন: সামনে লাফালে ভুয়া timeout. দুই দিকেই ভুল, তাই দৈর্ঘ্য মাপায় wall-clock নয়।

  4. নকশা — কোন system model? প্রতিটির জন্য সময়ের model আর ব্যর্থতার model বেছে নিন, এক লাইনে কারণসহ: (ক) একটি ডেটা সেন্টারের ভেতরে ৫-node-এর configuration store (খ) অপরিচিত, পরস্পরকে অবিশ্বাসী প্রতিষ্ঠানগুলোর মধ্যে একটি ভাগাভাগি করা খাতা (গ) একটি mobile অ্যাপ, যা offline-এ বাজারের তালিকা রাখে আর পরে sync করে (ঘ) একটি কারখানার রোবটের নিয়ন্ত্রণ, যেখানে ১০ ms-এর মধ্যে সাড়া না দিলে দুর্ঘটনা।
    • (ক) Partial synchrony + crash-recovery: একই মালিক, একই কোড; Raft আর disk-এ log; Byzantine সুরক্ষা অপ্রয়োজনীয় খরচ।
    • (খ) Partial synchrony (বা asynchronous) + Byzantine: কেউ মিথ্যা বলতে পারে; PBFT বা blockchain-ধাঁচের প্রোটোকল, ৩f + ১ node.
    • (গ) Asynchronous + crash-recovery: ফোন দিনের পর দিন বিচ্ছিন্ন থাকতে পারে; দুই পাশে লেখা চলবে, তাই CRDT (পর্ব ৪৬)।
    • (ঘ) Synchronous + crash-stop: বিশেষ real-time hardware-এ দেরির সীমা নিশ্চিত; দেরি মানেই ব্যর্থতা, সাথে সাথে নিরাপদ অবস্থায় থামা।

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

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

ক্রমএকটি টাকা পাঠানোর call-এ reply না এলে নিরাপদ পথের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔑 অনুরোধে একটি idempotency key বসানো
  2. 📤 পাঠানো আর monotonic clock-এ timer চালু করা
  3. ⏱️ মাপা p99.9-এর ওপরের timeout পেরোলে “ফল অজানা” ধরা
  4. 🔁 Backoff আর jitter দিয়ে একই key-তে আবার পাঠানো
  5. ✅ সার্ভার key চিনে আগের ফল ফেরত দেয়, কাজ দ্বিতীয়বার হয় না
  6. 📒 Retry ফুরালে অবস্থা জিজ্ঞেস বা দিনশেষে খাতা মেলানো

একটি client “৫০০ টাকা পাঠান” অনুরোধ পাঠাল, আর ২ সেকেন্ডের timeout পেরোল। Client নিশ্চিতভাবে কী জানে?

  • টাকা পাঠানো হয়নি, তাই নিশ্চিন্তে আবার পাঠানো যায়
  • টাকা পাঠানো হয়ে গেছে, শুধু reply হারিয়েছে
  • শুধু এটুকু যে ২ সেকেন্ডে উত্তর আসেনি; কাজ হয়েছে কি না অজানা
  • সার্ভারটি crash করেছে

দুটি যন্ত্রের ঘড়ি একসাথে মেলানো হলো। একটি +৫০ ppm, অন্যটি −৫০ ppm হারে সরে, আর ১ ঘণ্টা কোনো sync হয়নি। শেষে দুই ঘড়ির পার্থক্য সর্বোচ্চ কত?

  • ০.১৮ সেকেন্ড
  • ০.৩৬ সেকেন্ড
  • ৩.৬ সেকেন্ড
  • ১৮ মিলিসেকেন্ড

একটি সার্ভিসে প্রতিটি call-এর timeout মাপতে কোন ঘড়ি ব্যবহার করা উচিত?

  • Wall-clock (time-of-day), কারণ এটি সবচেয়ে সঠিক
  • প্রতিবার NTP সার্ভার থেকে সময় এনে
  • সার্ভারের পাঠানো timestamp আর নিজের ঘড়ির পার্থক্য দিয়ে
  • Monotonic clock, যা কখনো পেছনে যায় না

নিচের কোনটি একটি safety property?

  • একই term-এ কখনো দুজন leader থাকবে না
  • প্রতিটি অনুরোধ একসময় উত্তর পাবে
  • Leader মরে গেলে শেষ পর্যন্ত একজন নতুন leader নির্বাচিত হবে
  • Partition সেরে গেলে সব replica শেষ পর্যন্ত একই মানে আসবে

FLP impossibility (Fischer, Lynch, Paterson, ১৯৮৫) আসলে কী বলে?

  • বাস্তবের কোনো সিস্টেমে consensus করা সম্ভব নয়
  • পুরো asynchronous model-এ একটি process crash করতে পারলেও কোনো deterministic algorithm সব ক্ষেত্রে consensus শেষ হওয়ার নিশ্চয়তা দেয় না
  • বার্তা হারাতে পারলে দুই পক্ষ কখনো নিশ্চিত ঐকমত্যে পৌঁছায় না
  • Byzantine node থাকলে ৩f + ১টি node লাগে

৫টি node-এর একটি cluster, যেখানে লেখা commit হয় majority (৩টি)-র ack পেলে। Network ভাগ হলো: N1, N2 এক পাশে; N3, N4, N5 অন্য পাশে। কী হওয়া উচিত?

  • দুই পাশই লেখা নিতে থাকবে, পরে মিলিয়ে নেওয়া হবে
  • যে পাশে পুরোনো leader আছে, শুধু সেই পাশ লেখা নেবে
  • শুধু N3, N4, N5-এর পাশ লেখা commit করতে পারবে; অন্য পাশ থামবে
  • কোনো পাশই লেখা নিতে পারবে না

শব্দকোষ

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

মনে রাখুন

  1. অনেক যন্ত্রের দাম: scale, fault tolerance আর কম latency-র বদলে partial failure; একটি remote call-এর ফল সফল, ব্যর্থ বা অজানা — অজানার জবাব idempotency আর reconciliation.
  2. Network: বার্তা হারায়, দেরি করে, দুবার আসে, ক্রম উল্টায়; ধীর আর মৃত আলাদা নয়, তাই timeout মানে শুধু সন্দেহ — মেপে বাছুন, ছোট হলে ভুয়া সন্দেহ, বড় হলে দেরিতে ধরা।
  3. Partition: সর্বোচ্চ একটি পক্ষ majority পায় (⌊n/২⌋ + ১), তাই শুধু সেই পক্ষ এগোয়; অন্য পক্ষ থামে, নইলে split-brain. Cluster বিজোড় আকারের।
  4. ঘড়ি আর pause: drift (ppm), NTP-র skew আর লাফ; দৈর্ঘ্য মাপুন monotonic clock-এ, ক্রম বুঝুন logical clock-এ। GC বা VM pause-এ lease-এর মেয়াদ পেরোতে পারে — fencing token দিয়ে রিসোর্স নিজেই পুরোনো মালিককে আটকায়।
  5. Model আর অসম্ভবতা: সময় (synchronous, partially synchronous, asynchronous) আর ব্যর্থতা (crash-stop, crash-recovery, Byzantine) আগে লিখুন। Two Generals আর FLP-র শিক্ষা: safety সবসময়, liveness শর্তসাপেক্ষ।

পরের পর্বে: পর্ব ৩৯ — সময় আর ক্রম: NTP, Lamport clock আর vector clock. ঘড়িকে বিশ্বাস করা যায় না জেনে এবার দেখব, কোন ঘটনা কোনটির আগে আর কোনটি কোনটির কারণ — তা ঘড়ি ছাড়াই কীভাবে বোঝা যায়।

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

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