Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ১/১৯কঠিন৪৫ মিনিট

কেন Distributed System কঠিন — ৮টা Fallacies আর partial failure

Why Distributed Systems Are Hard

Network নির্ভরযোগ্য ভাবার আটটা ভ্রান্তি ভেঙে দেখা যায় distributed-এর আসল শত্রু — partial failure, যেখানে কিছু node ভাঙে, বাকিরা চলে, আর কেউই পুরো সত্যটা জানে না।

এই লেসন শেষে আপনি পারবেন

  • ৮ fallacy-র প্রতিটা এক বাক্যে ব্যাখ্যা করে আধুনিক system-এ সেটার রূপ — retry storm, chatty API, egress bill — চিনতে পারবেন
  • Partial failure-কে defining property হিসেবে সংজ্ঞায়িত করে node-failure বনাম cluster-state-এর পার্থক্য দেখাতে পারবেন
  • Latency-কে bug নয় physics হিসেবে ব্যাখ্যা করে speed-of-light সীমা আর jitter-distribution সংখ্যায় দেখাতে পারবেন
  • Single-machine assumptions — atomic visibility, এক clock, এক memory — distributed-এ কোথায় মরে তা scenario-তে বলতে পারবেন
  • Shared-nothing ও no-global-truth premise মেনে module-এর arc — failure model, clock, causality — মানচিত্র আঁকতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

একটা program যখন একটা machine-এ চলে, তখন machine-টাই হলো সত্যের উৎস। Memory-তে কী লেখা আছে সেটা আছে; function call return করলে কাজটা হয়ে গেছে; time() দিয়ে পাওয়া সময়টা সব code-এর জন্য একই। এই অভ্যাসগুলো আমাদের রক্তে মিশে আছে।

আজ সেই ভিত্তি ভাঙছি। এবার program বসবে একাধিক machine-এ, আর যোগাযোগ চলবে network দিয়ে — TCP handshake আর replication দেখে এসেছেন networking আর databases module-এ। এখন প্রশ্ন: সেই যোগাযোগের মাঝে message হারালে, দেরিতে এলে, জাল হলে — কী হবে?

এই পুরো module-এর চালিকা প্রশ্ন একটাই:

কেউ কারো উপর ভরসা করতে না পারলে, একদল computer কীভাবে একমত হয়?

আজকের লেসন ভয়েজের প্রথম ধাপ — সমস্যাটা কেন কঠিন। তিনটা জিনিস: ৩০ বছরের আটটা ভ্রান্তি, distributed-এর defining বৈশিষ্ট্য — partial failure — আর হাতে-কলমে প্রমাণ: lossy channel-এর ওপর কিছু জানা অসম্ভব, যত logic-ই করো।

মূল ধারণা

আটটা Fallacy — প্রতিটা এক লাইনে

১৯৯৪-৯৭-এর দিকে Sun Microsystems-এর engineers — Peter Deutsch আর James Gosling-দের নামে পরিচিত — এক email-thread-এ তালিকা করেছিলেন নতুন distributed-programmer-দের আটটা ভুল assumption; তিন দশকেও একটাও পুরনো হয়নি:

#Fallacyএক বাক্যে বাস্তবতাআধুনিক রূপ
১Network reliablePacket drop হয় switch reboot-এ, fiber cut-এ, NIC fault-এRetry storm — হাজার client একসাথে retry করে server-কেই মেরে ফেলে
২Latency zeroLight-ই fiber-এ ২০০ km/ms — physics-এর সাথে deal করতে হবেCross-region round-trip 100ms+; user দেখে spinner
۳Bandwidth infiniteDatacenter-এর ভেতরেও link saturation বাস্তবএকটা backup job-ই office-এর video-call ডুবিয়ে দেয়
৪Network secureযে wire-এ data যায়, সেখানে শত্রুও পড়তে পারেZero-trust, mTLS — “internal network safe” ধারণা মৃত
৫Topology constantNode আসে-যায়, IP বদলায়, AZ failover হয়Service discovery (Consul/DNS) ছাড়া config এক সপ্তাহেও টেকে না
৬One administratorMulti-cloud-এ মালিকানা কয়েক সংস্থার ভাগে ভাগে“ওদের firewall নাকি আমাদের?” — incident-call-এর ক্লাসিক
৭Transport cost zeroCross-AZ/cross-region traffic-এর invoice আসেমাস শেষে বিল-শক থেকে data-locality design-এর জন্ম
৮Network homogeneousx86, ARM, Linux, Windows, gRPC, REST সব মিশ্রণProtocol mismatch-এর serialization bug আজও হয়

Eight Fallacies-এর মূল শিক্ষা তালিকার জিনিসগুলো না — শিক্ষা হলো এগুলো assumption হিসেবে লেখা code compile হয়, test pass করে, production-এ গিয়ে ভাঙে। প্রতিটা fallacy আসলে single-machine অভ্যাসের ছায়া: local function call-এর জগতে network-এর এই reality-র কোনোটাই নেই।

Partial failure — defining property

Single machine-এ failure হলে all-or-nothing: process মরলে পুরোটাই মরে, আধা-জীবিত অবস্থা নেই। Distributed system-এ failure-এর এই সৌভাগ্য নেই:

Node A: ✓ running     Node B: ✗ crashed      Node C: ✓ running
                      Link B-C: ?            (A-র চোখে B মরা না C জীবিত?
                                              বলার উপায় নেই!)

Partial failure মানে: cluster-এর কিছু অংশ ভেঙে পড়ে, বাকিরা চলতে থাকে — আর চলন্ত অংশগুলোর কারো পক্ষে নিশ্চিতভাবে জানা সম্ভব না ঠিক কোনটা ভেঙেছে। B crash করেছে, cable খুলে গেছে, নাকি শুধু ধীর — বাইরে থেকে তিনটা হুবহু এক রকম দেখায় (উত্তর নেই)।

এটাই distributed-কে distributed করে দেয়। দুইটা consequence পুরো module-এর ভিত্তি:

  1. No global truth — এক মুহূর্তে গোটা সিস্টেমের state জানা কোনো node-এর পক্ষে অসম্ভব; প্রতিটা node নিজের local view-ই জানে।
  2. Detection ≠ fact — “B down” fact নয়, এক node-এর সন্দেহ; পরের লেসনে সন্দেহের পুরো taxonomy দাঁড়াবে।

Latency — bug নয়, physics

Mumbai থেকে Dublin: ~৭,৫০০ km, light fiber-এ ~২০০ km/ms — round-trip theoretical ন্যূনতম ~৭৫ ms, routing সহ বাস্তবে 120-150 ms। সংখ্যাটা optimization দিয়ে শূন্য হয় না — CDN, caching, region placement দূরত্ব কমায়, দূরত্বের সীমা ভাঙে না।

আরও গুরুত্বপূর্ণ: latency সংখ্যা নয়, distribution। একই route-এ p50 = 40 ms, p99 = 300 ms — queueing, GC pause, retransmission মিলে। Timeout-এর মতো threshold আসলে distribution-এর উপর বাজি — আর বাজি মাঝে মাঝে হারে।

ভেতরে কী ঘটছে

Shared memory-র মৃত্যু — কোন assumption কখন মরে

Single-machine programming-এর তিনটা স্তম্ভ, আর distributed- এ তিনটাই আলাদা মুহূর্তে মরে:

AssumptionSingle machineমৃত্যুর মুহূর্ত
Atomic visibilityWrite শেষ হলে সব reader নতুন মান দেখেReplication lag-এ follower পুরনো মান দেখে
One clocktime() সব thread-এর জন্য একদুই machine-এর ঘড়ি সেকেন্ডেও একমত নয় (physical-time লেসন)
One memoryState এক RAM/disk-এপ্রতি node-এ copy, sync করতে হয়, sync-ও ভাঙে

Atomic visibility-র মৃত্যু সবচেয়ে আগে কামড়ায়। Client A-র write server-1-এ commit; client B server-2-তে read — পুরনো মান। কেউ ভুল করেনি: দুই server সত্যি-ই দুই state নিয়ে বসে আছে, কারণ তথ্য পৌঁছায়নি (বা কখনোই পৌঁছাল না — partition!)। CAP-এর দ্বন্দ্ব databases module-এ দেখে এসেছেন; এখন থেকে প্রতিদিনের বাস্তবতা।

Shared-nothing দর্শন

Design-এর প্রথম প্রশ্ন: কী ভাগ করা যাবে? Shared memory/disk সস্তা দেখায়, কিন্তু storage-ই হয় single point of contention আর failure। Shared-nothing — প্রতিটা node-এর নিজের CPU, memory, disk, যোগাযোগ শুধু message-এ — Google-যুগ থেকে প্রায় সব NoSQL ও Kubernetes cluster-এর দর্শন।

দাম: প্রতিটা তথ্য-exchange network-এর fallacy-জগতে ঘটে। লাভ: কোনো একক component-এর ভাঙা গোটা সিস্টেম ভাঙে না — ভাঙা সবসময় partial, আর partial ভাঙার সাথে বাঁচতে জানলেই horizontal scale।

Detection-এর একমাত্র অস্ত্র — timeout, এবং তার দোষ

B-র কাছে উত্তর না এলে A কী করবে? একমাত্র পথ: সময়সীমা ঠিক করে “এর পরে নীরবতা = সম্ভবত ভাঙা” ঘোষণা। কিন্তু অনুমানটার তিনটা ফুটো:

  1. ধীর ≠ মৃত — timeout ছোট হলে জীবিত node মৃত ঘোষণা পায় (false suspicion); বড় হলে আসল মৃত্যুর detection দেরি। দুই দোষ একসাথে শূন্য করা অসম্ভব — পরের লেসনে প্রমাণসহ।
  2. উত্তর না-আসা ≠ কাজ-না-হওয়া — request পৌঁছেছে, server কাজ করেছে, শুধু response হারাল। A timeout দেখল; B সফলতা দেখল; দুই ইতিহাস চিরকালের জন্য ভিন্ন হতে পারে।
  3. Retry duplicate বানায় — ১-এর ফলোআপ: retry করলে B কাজটা আবার করবে (double charge!); না করলে হয়তো আসল request-টাই হারায়েছিল। Idempotency-র প্রয়োজন এখান থেকেই (idempotency- exactly-once লেসনের ভিত্তি)।
এই module-এর খোলা মানচিত্র — আজ যে দেয়ালে দাঁড়ালাম
  1. আজ: কেন কঠিন৮ fallacy, partial failure, no global truth
  2. পরের: Failure Modelsভাঙার শক্তি-সোপান: crash, omission, timing, Byzantine
  3. তারপর: Physical Timeঘড়ি সিঙ্ক করার চেষ্টা আর NTP-র সীমা
  4. Logical Clocksphysics ছেড়ে causality-কেই সময় বানানো

উদাহরণ

একটা হারানো response-এর ফরেনসিক

দৃশ্য: payment-service (node A) inventory-service (node B)-কে RESERVE item-42 পাঠায়। Wire-এ ঘটনাক্রম:

t=0ms     A -> B : REQ  RESERVE item-42        [পৌঁছায়]
t=1ms     B          : ledger-এ reserve লিখল   [কাজ সম্পন্ন]
t=1ms     B -> A : RESP OK                    [এই packet হারাল!]
t=2000ms  A          : timeout — "B কি পেল?"

এখন A-র সামনে চারটা পথ, চারটাই দোষাক্রষ্ট:

  1. Retry করো → B আবার reserve করবে — double-reserve, stock ভুল।
  2. Fail ঘোষণা করো → অথচ item reserved; user “ব্যর্থ” দেখে আবার চেষ্টা করলেও একই ফাঁদ।
  3. Query করে জেনে নাও → query-র response-ও হারাতে পারে — regress অসীম; আর উত্তর আসার আগেই state বদলাতে পারে (race)।
  4. ঝুলিয়ে রাখো → কতক্ষণ? Reconciliation loop লাগবে — problem solve হলো না, স্থগিত হলো।

মূল ক্ষতি কোথায়: B-র ইতিহাসে কাজটা ঘটেছে, A-র ইতিহাসে ঘটেনি — এবং কোনো পরীক্ষাই A-কে নিশ্চিতভাবে বলতে পারে না কোনটা সত্য। এটা implementation bug নয়; lossy channel + partial failure-এর গাণিতিক অনিবার্যতা। Production-এর idempotency key, saga, reconciliation — সবই এই এক দৃশ্যের trade-off-সাজানো।

ছবিতে

Space-time diagram — distributed reasoning-এর মূল ভাষা

  সফল                response হারাল           request-ই হারাল
──────             ──────────────           ──────────────
A ──REQ──►         A ──REQ──►               A ──REQ──✗ (drop)
              ◄──RESP──                    (B কিছুই দেখেনি)
B  act()          B  act()
              ✗──RESP── (drop)

A-র চোখে:      timeout                 timeout
B-র চোখে:      "কাজ হয়েছে!"            "কিছুই ঘটেনি"
observer:      ???                     ???

A-র করণীয় তিন case-এ ভিন্ন হওয়া উচিত — কিন্তু আলাদা করার information A-র কাছেই নেই।

তিনটা outcome, বাইরে থেকে অভিন্ন — পার্থক্য শুধু wire-এর ওপারে, যেটা A কখনো দেখতেই পায় না।
RTT density
    ▲
    │      ╭─╮  fast node
    │     ╱   ╲        ╭──╮   loaded node (জীবিত!)
    │    ╱     ╲      ╱    ╲
    │   ╱       ╲    ╱      ╲
    └──────────────┼───────────► ms
                  100
                   ↑ threshold: ডানদিকের জীবিতদেরও "মৃত" ঘোষণা
Timeout = distribution-এর উপর বাজি — জীবিত-but-loaded node-এর RTT threshold ছাড়িয়ে গেলে সে false-positive।

নিজে চালিয়ে দেখুন

EXPERIMENT

দুই process, এক lossy pipe — অজান্তে-থাকার অনুভূতি

macOS / Linux (Python 3)· ১৫ মিনিট

amb.py — parent client, child server; দুই মুখেই 35% message-drop:

import random, time
from multiprocessing import Process, Pipe

P_DROP = 0.35

def server(conn):
    rng = random.Random(9)
    while conn.poll(3):
        msg = conn.recv()
        if rng.random() < P_DROP:      # server-এর চোখে request আসেইনি
            continue
        print(f"   [server] {msg} দেখেছি, কাজটা করলাম")   # ground truth!
        conn.send("ACK:" + str(msg))

if __name__ == "__main__":
    parent, child = Pipe()
    p = Process(target=server, args=(child,))
    p.start()
    rng = random.Random(29)
    for req in ("r1", "r2", "r3"):
        parent.send(req)
        t0, got = time.perf_counter(), False
        while time.perf_counter() - t0 < 0.9:
            if parent.poll(0.05):
                ack = parent.recv()
                if rng.random() < P_DROP:     # ACK পথেই হারাল
                    continue                  # অথচ server কাজ করেছে
                print(f"{req}: উত্তর {ack} "
                      f"({time.perf_counter()-t0:.2f}s)")
                got = True
                break
        if not got:
            print(f"{req}: TIMEOUT — server-এর ভেতরে কী হলো? "
                  f"জানার উপায় নেই")
    p.join()

এক run-এর প্রত্যাশিত ধরন (timing machine-ভেদে বদলাবে):

   [server] r1 দেখেছি, কাজটা করলাম
   [server] r2 দেখেছি, কাজটা করলাম
r1: উত্তর ACK:r1 (0.25s)
r2: TIMEOUT — server-এর ভেতরে কী হলো? জানার উপায় নেই
r3: TIMEOUT — server-এর ভেতরে কী হলো? জানার উপায় নেই

Ground-truth লাইনগুলো মিলিয়ে পড়ুন — r2-এর timeout-এ server কাজ করে ফেলেছে; r3-এ server কিছুই দেখেইনি। Client-এর log-এ দুটোর অক্ষরের পার্থক্য নেই।

দ্বিতীয় অংশ — jitter মাপুন:

ping -c 20 127.0.0.1          # localhost baseline
ping -c 20 example.com        # বাস্তব WAN
# macOS: stddev column; Linux: mdev column দেখুন

সাধারণ প্যাটার্ন:

127.0.0.1    min/avg/max/stddev ≈ 0.04/0.07/0.15/0.03 ms
example.com  min/avg/max/stddev ≈ 12/14.2/31/4.5 ms
                                  ^^^ spread = jitter!

Localhost spread প্রায় শূন্য — single-machine অভ্যাস। WAN-এ max/min ২-৩× স্বাভাবিক: 10ms timeout বসালে বৈধ packet-ও শিকার হবে। এই stddev-ই diagram-এর histogram-এর বাস্তব ছায়া।

এটা কী প্রমাণ করে

Request-response-এর মৌলিক ambiguity টা theory নয় — দুই real process-এর মাঝে ACK হারালে client-এর চোখে 'server acted' আর 'server never heard' হুবহু এক দেখায়, যদি না ground-truth print ঢুকিয়ে দেওয়া হয়।

নিজে বানান

BUILD IT

UnreliableChannel — হারানো, দেরি, reorder সহ request-response lab

Python · ●●○○○
  1. দুই দিকের জন্য আলাদা queue — প্রতিটা message-এ arrival tick
  2. send()-এ তিন knob: P_DROP, DELAY range, P_LAG (reorder-এর উৎস)
  3. Server drain করে action নেয়, RESP পাঠায়; client timeout-এ retry
  4. শেষে per-request রিপোর্ট: কতবার sent, কতবার acted, উত্তর এলো কি
  5. Verdict column পড়ে AMBIGUOUS আর DUPLICATE row শনাক্ত করুন

লক্ষ্য: আজকের সব theory একটা 70-line simulation-এ — যেখানে seed fixed, তাই প্রতিটা line-এর ব্যাখ্যা সম্ভব। Channel-এর knobs বদলে নিজের incident বানান।

import random
from collections import deque

P_DROP, DELAY, P_LAG = 0.20, (0, 3), 0.25
TIMEOUT, HORIZON, N_REQ = 6, 42, 4

class UnreliableChannel:
    """দুই দিকের জন্য আলাদা queue — drop, delay, reorder তিনটাই বাস্তব"""

    def __init__(self):
        self.rng  = random.Random(42)
        self.link = {"A->B": deque(), "B->A": deque()}
        self.now  = 0

    def send(self, src, kind, mid):
        dst, ln = ("B", "A->B") if src == "A" else ("A", "B->A")
        if self.rng.random() < P_DROP:
            print(f"t={self.now:>2}  {src}->{dst}  DROP  {kind}#{mid}")
            return
        d = self.rng.randint(*DELAY)
        if self.rng.random() < P_LAG:
            d += self.rng.randint(2, 5)      # পথে আটকে গেল — reorder!
        self.link[ln].append((self.now + d, kind, mid))
        print(f"t={self.now:>2}  {src}->{dst}  SEND  {kind}#{mid}  eta+{d}")

    def drain(self, dst):
        ln   = "A->B" if dst == "B" else "B->A"
        q    = self.link[ln]
        due  = [w for w in q if w[0] <= self.now]
        for w in due:
            q.remove(w)
        return [(k, m) for _, k, m in due]

ch       = UnreliableChannel()
acted    = {}                     # server আসলে কতবার কাজ করল
attempts = {}                     # client কতবার পাঠাল
answered = set()                  # কোন id-র উত্তর পৌঁছাল
pending  = {}                     # id -> শেষ পাঠানোর tick
next_id  = 0

for t in range(HORIZON):
    ch.now = t
    if next_id < N_REQ and not pending:               # একসাথে একটাই request
        pending[next_id], attempts[next_id] = t, 1
        ch.send("A", "REQ", next_id)
        next_id += 1
    for mid, t0 in list(pending.items()):             # client-এর ধৈর্য
        if t - t0 >= TIMEOUT:
            del pending[mid]
            if attempts[mid] < 2:
                attempts[mid] += 1
                pending[mid] = t
                ch.send("A", "REQ", mid)              # retry — নিরাপদ? (দেখুন!)
            else:
                print(f"t={t:>2}  A         FAIL   REQ#{mid} — উত্তর নেই")
    for kind, mid in ch.drain("A"):
        answered.add(mid)
        pending.pop(mid, None)
        print(f"t={t:>2}  A         RESP   #{mid} এলো")
    for kind, mid in ch.drain("B"):
        acted[mid] = acted.get(mid, 0) + 1            # server কাজটা করল
        ch.send("B", "RESP", mid)

print("\n=== অস্পষ্টতা রিপোর্ট ===")
print(f"{'req':>4} {'sent':>5} {'acted':>6} {'resp':>5}  client-এর চোখে")
for i in range(N_REQ):
    s, a, r = attempts[i], acted.get(i, 0), i in answered
    if r:
        v = "OK" if a == 1 else "DUPLICATE ACTION (আসল ক্ষতি!)"
    elif a == 0:
        v = "clean fail — retry নিরাপদ"
    else:
        v = "AMBIGUOUS: কাজ হয়ে গেছে, client জানে না"
    print(f"{i:>4} {s:>5} {a:>6} {'yes' if r else 'no':>5}  {v}")

প্রত্যাশিত output (seed fixed — হুবহু এটাই আসবে):

t= 0  A->B  SEND  REQ#0  eta+0
t= 0  B->A  SEND  RESP#0  eta+1
t= 1  A         RESP   #0 এলো
t= 2  A->B  SEND  REQ#1  eta+0
t= 2  B->A  DROP  RESP#1
t= 8  A->B  DROP  REQ#1
t=14  A         FAIL   REQ#1 — উত্তর নেই
t=15  A->B  SEND  REQ#2  eta+0
t=15  B->A  SEND  RESP#2  eta+7
t=21  A->B  SEND  REQ#2  eta+0
t=21  B->A  DROP  RESP#2
t=22  A         RESP   #2 এলো
t=23  A->B  SEND  REQ#3  eta+6
t=29  A->B  DROP  REQ#3
t=29  B->A  SEND  RESP#3  eta+2
t=31  A         RESP   #3 এলো

=== অস্পষ্টতা রিপোর্ট ===
 req  sent  acted  resp  client-এর চোখে
   0     1      1   yes  OK
   1     2      1    no  AMBIGUOUS: কাজ হয়ে গেছে, client জানে না
   2     2      2   yes  DUPLICATE ACTION (আসল ক্ষতি!)
   3     2      1   yes  OK

রিপোর্ট লাইন-বাই-লাইন:

  • req#1 — AMBIGUOUS: প্রথম RESP dropped, retry-র REQ-ও dropped — server চিরকাল “reserved” ভেবে বসে আছে, client জানেই না।
  • req#2 — DUPLICATE ACTION: প্রথম attempt সফলই ছিল, উত্তর শুধু দেরিতে (eta+7 > timeout 6); retry-তে server দ্বিতীয়বার কাজ করল — payment-এর জগতে double-charge।
  • req#3: প্রথম REQ delayed (eta+6), timeout-এর ঠিক আগে পৌঁছাল; retry dropped — ভাগ্য ভালো হলে এমনও হয়।

নিজে বাড়ান:

  1. TIMEOUT = 2 করুন — DUPLICATE সংখ্যার বিস্ফোরণই aggressive timeout-এর বিল।
  2. Idempotency-key যোগ করুন — server dedupe করলে কোন verdict বদলায়, কোনটা না? AMBIGUOUS কি মরে?
  3. Third node C observer বসান — সে কোন প্রশ্নেরও উত্তর দিতে পারে না, প্রমাণ করুন।

বাস্তব সিস্টেমে

বাস্তব জগতে এই পাঠের চালান

Retry storms। Outage-এ হাজার client-এর retry timer একসাথে fire করে — recovery-moment-এ server দ্বিগুণ load মেরে পড়ে। সমাধান: exponential backoff + jitter।

Idempotency keys। Stripe-এর payment API প্রতিটা POST-এ Idempotency-Key header নেয় — build-এর duplicate-action বন্ধ করতেই: “একই intent, execute একবারই।”

Chaos engineering। Netflix Chaos Monkey, AWS Fault Injection — UnreliableChannel-এর production-grade আত্মীয়; fallacy-গুলো আগে থেকেই ভাঙতে দেওয়ার দর্শন।

Egress bill ও region design। Fallacy ৭-এর চালান — cross-AZ chatter কমাতে read-locality, batched replication, cell-based architecture জন্মায়। Physics + economics দুটোই design-input।

যে ভুলগুলো সবাই করে

“ভালো library আর framework ব্যবহার করলে network-এর এই সমস্যাগুলো সমাধান হয়ে যায়।”

Framework retry, pooling, serialization সামলায় — কিন্তু ambiguity সমাধানযোগ্যই নয়, information-theoretic সীমা। “Response হারাল, server acted কি না” — সেই তথ্য client-এর কাছে পৃথিবীতেই নেই; framework trade-off লুকায়, সীমা ভাঙে না।

“Timeout হলে নিশ্চিতভাবে ধরে নেওয়া যায় অন্য পক্ষ মরে গেছে।”

সঠিক reading: “আমার জানালার মধ্যে উত্তর আসেনি” — এতটুকুই। Peer মরতে পারে, ধীর হতে পারে, উত্তর পথে হারাতে পারে। Algorithm timeout-কে fact নয়, suspicion হিসেবে ব্যবহার করে; suspicion ভুল হতে পারে — পরের লেসনে খরচ মাপবেন।

“Partial failure মানে অর্ধেক machine ভাঙে — সবাই জানে কে ভাঙল।”

ভাঙা অদৃশ্যভাবে partial: কে ভাঙল (node? link?), কি ভাঙল (request? response?), ভাঙলই কি না — উত্তর নেই। আর ভাঙা-অভাঙা অংশের মাঝে communication চলতেই থাকে; “majority ঠিক আছে”-র হিসাব নিজেই কঠিন (quorum-replication অপেক্ষমাণ)।

“Latency কমাতে "faster network" কিনলেই হবে।”

Bandwidth কিন্তু latency কেনা যায় না — Mumbai-Dublin 120ms 400Gbps link-এও। খেলা হয় দূরত্ব কমিয়ে (edge/region), round-trip কমিয়ে (batching/pipelining), wait লুকিয়ে (async)। Zero-latency promise লুকায়, মুছে না — বিল পরে সুদে আসে।

বুঝেছেন কি না দেখুন

1

আট fallacy-র মধ্যে প্রথম তিনটা (reliable, zero-latency, infinite bandwidth) এক বাক্যে বলুন, আর প্রতিটার একটা করে আধুনিক উদাহরণ।

স্মরণ

Network reliable — না, fiber cut/switch reboot-এ packet হারায়; উদাহরণ retry storm। Latency zero — না, light-speed bound আছে; উদাহরণ cross-region 100ms+ round-trip। Bandwidth infinite — না, link saturation হয়; উদাহরণ backup job-এ video-call ডুবে যাওয়া।

2

“Partial failure নিজে সমস্যা নয়, সমস্যা তা সনাক্ত করা” — সঠিক ও অসম্পূর্ণ অংশ আলাদা করুন।

যুক্তি

সঠিক: slow/dead/partitioned বাইরে থেকে এক — detection-অনিশ্চয়তাই কেন্দ্রীয় কষ্ট। অসম্পূর্ণ: partial failure নিজেও সমস্যা — ভাঙা- অভাঙা অংশ একসাথে চলতে থাকে বলে দুই ভিন্ন ইতিহাস জন্মায় (split-brain-এর বীজ); detection-অনিশ্চয়তা সেটাকে আরও কঠিন করে, বাদ দেয় না।

3

Build-এর রিপোর্টে req#1 “AMBIGUOUS” আর req#2 “DUPLICATE ACTION” — কোন কোন packet হারায়, আর retry কেন একটায় ক্ষতি করেনি অথচ আরেকটায় করেছে?

প্রয়োগ

req#1: প্রথম RESP + retry-র REQ — দুটোই dropped; acted=1 কিন্তু client কিছুই পায়নি। Retry নিজে dropped বলে দ্বিতীয় action-ও হয়নি। req#2: প্রথম RESP হারায়নি, শুধু দেরিতে (eta+7 > timeout 6); retry-র REQ পৌঁছে server দ্বিতীয়বার action নিল। পার্থক্য: হারানো বনাম দেরি — client-এর চোখে invisible।

4

Experiment-এ r2 আর r3 দুটোই “TIMEOUT” — অথচ ground truth-এ একটায় server acted, আরেকটায় দেখেইনি। এই অভিন্নতা কী প্রমাণ করে, আর কোন machinery আলাদা করতে পারে?

যুক্তি

প্রমাণ: observable behavior থেকে underlying fact-এ inference অসম্ভব — দুই ভিন্ন বিশ্ব একই signature দেয়। আলাদা করতে persistent idempotency-record লাগবে (server action-এর receipt রাখবে, client query করবে) — কিন্তু query-র response-এর নিজস্ব ambiguity আসবে; সমস্যা এক স্তর উপরে ওঠে, মরে না।

5

Checkout-service payment-gateway-কে charge request পাঠায়। 30s timeout হলে retry করবেন কি না — আর কী mechanism-এ সিদ্ধান্তটা নিরাপদ?

ডিজাইন

Blind retry নিষেধ — double-charge ঝুঁকি। নিরাপদ পথ: unique idempotency-key + gateway-side dedupe window; একই key-তে backoff + jitter সহ retry (gateway এক charge enforce করবে), সঙ্গে reconciliation job পরে records match করবে। Key দ্বিতীয় action আটকায়, “first action হয়েছিল কি না”-র অজ্ঞতা থেকে যায় — অজ্ঞতাকে harmless বানাই, দূর করি না।

6

Fallacy ৫ (topology changes) আর ৬ (one administrator) মিলে কোন design-সিদ্ধান্ত জন্ম দেয়?

ডিজাইন

৫: autoscaling/failure/deploy topology ক্রমাগত বদলায়। ৬: multi-org, multi-cloud — নিয়ম ও policy ভিন্ন। একসাথে: design dynamic membership আর heterogeneity assume করে বানাতে হয় — service discovery, config-as-data, protocol versioning, explicit API contract। Static config file আর implicit coupling এই দুই fallacy-র সবচেয়ে সস্তা শিকার।

এরপর কী

এরপর কী

আজ দেখলাম ভাঙা অনিবার্য, আর তা চেনা অনিশ্চিত। কিন্তু “ভাঙা” বলতে কত রকম? Machine বন্ধ, message ফেলে দেওয়া, দেরিতে উত্তর, আর সরাসরি মিথ্যা বলা — পার্থক্যগুলো algorithm-design-এ বিশাল: যে consensus চার ধরনের ভাঙা সইতে পারে, সে পঞ্চমটার জন্য তিনগুণ replica চায়।

পরের লেসনে:

  • Adversary-strength ladder: crash-stop → crash-recovery → omission → timing → Byzantine — সংজ্ঞা ও বাস্তব বাসিন্দা
  • কোন system কোন model assume করে — consensus বনাম blockchain-এর 3f+1 overhead-এর প্রথম হিসাব
  • Fail-stop idealization কেন সুবিধাজনক মিথ্যা
  • Timeout-suspicion-এর false-positive হার নিজের হাতে মাপা

আজকের UnreliableChannel message ফেলে দিল; পরের লেসনে প্রতিটা ফেলা-রূপের নামকরণ — নাম ছাড়া reasoning চলে না।

আরও পড়ুন

  • The Eight Fallacies of Distributed Computing — Peter Deutsch-এর attributed email thread — Peter Deutsch ও James Gosling (Sun Microsystems engineers) · Fallacy-তালিকাটার উৎসপত্র — ১৯৯০-এর দশকের internal Sun discussion থেকে ছড়িয়ে পড়া কিন্তু আজও প্রতিটা production incident-এ জীবন্ত
  • Distributed Systems: Principles and Paradigms, Chapter 8 — Fault Tolerance — Andrew S. Tanenbaum, Maarten van Steen · Failure-এর classification আর reliability-র মৌলিক ধারণাগুলোর পাঠ্যপুস্তক treatment — এই module-এর পুরো backbone