Foundationপ্রথম নীতি থেকে
LEVEL 9অ্যাডভান্সড~১৮ ঘণ্টাPython

Chaos Lab

Chaos Lab

FaultBus injector দিয়ে distributed system-এ ইচ্ছে করে বিপদ ডাকা — delay/jitter/drop/partition/crash scenario DSL, fault-matrix sweep, percentile metrics, ASCII timeline আর findings report template।

মাইলস্টোন

আগে যা পড়া দরকার

কেন এই প্রজেক্ট

Distributed system ভাঙে সবসময় সবচেয়ে খারাপ মুহূর্তে, আর ভাঙার ভঙ্গি production-এর বাইরে rehearse করা যায় না বলেই মানুষ প্রথমবার দেখে। Chaos engineering-এর মূল চিন্তা উল্টো: ভাঙুন আগে, নিজের শর্তে, দিনের বেলায় — তাহলে রাত ৩টার incident দিনের ৩টার experiment হয়ে যায়।

কিন্তু যত্নহীন ভাঙা শুধুই noise। শৃঙ্খলা তিনটা জায়গায়: fault ঢোকানো একটা নিয়ন্ত্রিত জায়গা দিয়ে (FaultBus), ফল মাপা এক জায়গায় (metrics), আর সিদ্ধান্ত লেখা একটা ছকে (findings report)। Hypothesis ছাড়া ঢোকানো fault আর slot machine-এ টাকা ঢালার পার্থক্য কিছু নেই — এই lab-এ তাই প্রতিটা run-এর আগে লিখতেই হবে “কী ঘটবে ভাবছি, আর ভুললে বুঝব কী”।

ধাপে ধাপে

১. FaultBus — central injector আর scenario DSL

System-এর সব traffic একটা ফানেলের ভেতর দিয়ে যাবে — ওই ফানেলই FaultBus. Fault-গুলো declarative, কোডে নয় DSL-এ:

name: failover-under-jitter
steps:
  - at: 1.0   inject: jitter     targets: [n2, n3]  base_ms: 40  spread_ms: 60
  - at: 3.0   inject: partition  groups: [[n1, n2], [n3]]
  - at: 5.0   inject: crash      targets: [n1]
  - at: 7.0   clear: all
duration_s: 10
@dataclass
class Fault:
    kind: str                     # delay | jitter | drop | partition | crash
    targets: frozenset[str]
    prob: float = 0.0
    base_ms: int = 0
    spread_ms: int = 0

class FaultBus:
    def on_message(self, src, dst, send_fn):
        if not self._allowed(src, dst):
            self.record("dropped-by-partition", src, dst); return
        if f := self._matching_fault(dst, "drop"):
            if self.rng.random() < f.prob:
                self.record("dropped", src, dst); return
        if f := self._matching_fault(dst, ("delay", "jitter")):
            d = (f.base_ms + self.rng.uniform(0, f.spread_ms)) / 1000
            time.sleep(d)
        send_fn()

সুবিধা দুটো: scenario file version-control-এ থাকে, আর bus-এর record log-ই timeline-এর কাঁচামাল। Crash fault বাস্তবে thread kill — in-process victim-এ সেটাই সবচেয়ে সৎ।

Checkpoint: একটা scenario চালিয়ে bus log-এ প্রতিটা step-এর timestamp আর action মিলছে — timeline-এর ভিত্তি এখানেই।

২. Toy replicated system — victim workload

Victim সচেতনে অসম্পূর্ণ রাখুন: primary-backup replication, heartbeat failover, কিন্তু lease বা fencing নেই। কেন? কারণ তখনই partition-এ split-brain-এর দরজা খোলা থাকে — আর আপনার experiment সেটা ধরতে পারে কি না, সেটাই প্রশ্ন।

class ToyNode:
    def __init__(self, name, bus):
        self.bus = bus                    # সব send/receive bus-এর হাত ধরে
        self.kv = {}
        self.role = "follower"

    def replicate(self, entry):
        self.bus.send(self.primary, "append", entry)   # fault এখানেই বসতে পারে

Workload driver-ও instrumented — প্রতিটা client op-এ invoke আর complete timestamp, ফল result। Divergence check-এর জন্য শেষে সব node-এর kv snapshot নিয়ে diff করুন।

Checkpoint: কোনো fault ছাড়া baseline run — p99 latency, error rate 0, divergence 0। Baseline ছাড়া fault-run-এর সংখ্যার কোনো মানে নেই।

৩. Experiment runner — fault matrix sweep

একটা run দর্শন, একটা sweep বিজ্ঞান:

MATRIX = [
    {"kind": "drop",      "prob": p} for p in (0.01, 0.05, 0.10, 0.25)
] + [
    {"kind": "partition", "groups": g} for g in ([["n1"], ["n2", "n3"]],
                                                 [["n1", "n2"], ["n3"]])
]

for i, spec in enumerate(MATRIX):
    for seed in (11, 23, 47):             # প্রতি cell-এ ৩ repetition
        res = runner.run(scenario_for(spec), seed=seed)
        save(f"results/{spec['kind']}-{i}/seed-{seed}.json", res)

Runner প্রতি run-এ আলাদা directory বানায়: scenario.yaml, metrics.json, timeline.txt, manifest.json। Directory structure-টাই আপনার lab notebook — এক মাস পরেও কোন finding-এর পেছনের data খুঁজে পাওয়ার একমাত্র রাস্তা।

Checkpoint: sweep শেষে একটা summary.csv — প্রতি cell-এ median p95 আর error rate; চোখে পড়বে drop_prob 0.05→0.10-এ কোন metric লাফায়।

৪. Metrics collector

def percentiles(sorted_samples, qs=(50, 95, 99)):
    out = {}
    for q in qs:
        idx = min(len(sorted_samples) - 1, round(q / 100 * len(sorted_samples)))
        out[f"p{q}"] = sorted_samples[idx]
    return out

def divergence(nodes) -> int:
    snaps = [n.snapshot_kv() for n in nodes]
    return max(sum(a != b for a, b in zip(s, snaps[0])) for s in snaps)

তিনটা metric-ই দরকার, একটাও কম নয়: latency percentiles বলে user কী অনুভব করছে, error rate বলে কতটা অনুরোধ ফেরত খেয়েছে, divergence বলে ভেতরের state কতটা ছেড়াছাড়ি। Mean latency-তে দেখার নাম মাপা নয় — fault-এর ক্ষতি tail-এ লুকিয়ে থাকে, তাই p95/p99-ই আসল চরিত্র।

Checkpoint: jitter scenario-তে mean প্রায় অপরিবর্তিত থেকে p99 দুই গুণ — এই একটা ফলই percentile কেন দেখতে হয়, তার প্রমাণ।

৫. ASCII timeline renderer

সংখ্যার টেবিলে fault আর ফলের সম্পর্ক চোখে পড়ে না; এক সেকেন্ড-বাকেট timeline-এ পড়ে:

scenario: partition-n1 | seed 11
t(s)      0    2    4    6    8    10
fault     ....████████████....│....
errors    ....................██....
p95(ms)   12   15  210  480  520  18
diverg.    0    0    1    1    2    2
BUCKET = {"none": ".", "low": "+", "high": "#"}

def render_timeline(records, bucket_s=0.5):
    for b_start in range(0, records.duration_s, bucket_s):
        faults = records.fault_active(b_start, bucket_s)
        errors = records.error_rate(b_start, bucket_s)
        print(BUCKET["high" if errors > 0.2 else "low" if errors else "none"],
              end="")

Renderer-টাই hypothesis test-এর চোখ — fault window-এর ঠিক পরে error burst এলে causality সন্দেহের জায়গা হয়, fault-এর ভেতরেই এলে resilience-এর প্রমাণ।

Checkpoint: partition heal-এর পরে error rate কত দ্রুত baseline- এ ফেরে — recovery time-টা timeline থেকেই মেপে নোটে লিখুন।

৬. Findings report template

প্রতিটা meaningful run-এর পরে একটা file, এই ছকে:

## Finding: split-brain under n1-partition
- Hypothesis: single-node partition-এ backup promote হবে, heal-এ
  পুরনো primary-এর লেখা হারাবে
- Injection: at 3s partition [[n1],[n2,n3]], workload 20 writes/s
- Observation: heal-এর পরে n1-এ 7টা write, বাকিদের সেখানে 12টা;
  divergence 5 entries, কোনো alert নেই
- Root cause: promote-এ fencing নেই — দুই primary সমান্তরালে লিখেছে
- Fix + re-run: fencing token যোগ, একই scenario — divergence 0
- Production echo: কোন symptom-এ আজ এই bug সন্দেহ করব

শেষ লাইনটাই সবচেয়ে দামি — lab-এর ফল production intuition-এ বদলানোর সেতু। Template খালি জায়গা সহ repo-তে থাকবে, লেখা ঐচ্ছিক নয়।

Checkpoint: অন্তত তিনটা finding file, প্রতিটাতে fix+re-run অংশ ভরা — re-run ছাড়া finding report অর্ধেক।

৭. Reproducibility — seeded runs

master = random.SeedSequence(seed)
children = master.spawn(3)             # bus, workload, scheduler আলাদা stream
bus.rng = random.Random(children[0])

নিয়ম: প্রতিটা component-এর নিজস্ব RNG stream (একটা shared rng হলে component যোগ-বিয়োগ করলেই সব interleaving বদলে যায়), manifest.json-এ seed, scenario hash, code version। Assertion একটাই: একই seed-এ দুই run — timeline byte-identical।

$ python lab.py --scenario s.yaml --seed 11 --verify
run#1 sha256=9f2c...  run#2 sha256=9f2c...  IDENTICAL ✓

Non-determinism যেখানে ঢুকতে চায় (wall-clock sleep, thread scheduling) — সেখানে virtual clock বা deterministic scheduler নিয়ে আসতে হবে। ওই কষ্টটাই deterministic simulation testing-এর প্রবেশদ্বার।

Checkpoint: CI-তে --verify flag — কেউ nondeterministic কোড ঢুকিয়ে দিলে build লাল হবে।

নিজেকে চ্যালেঞ্জ করুন

  1. Clock-skew fault — এক node-এর ঘড়ি ২ সেকেন্ড এগিয়ে দিন, LWW-ধাঁচা logic কোথায় ভাঙে দেখান
  2. Message reorder fault — FIFO ভেঙে causal delivery gate-এর মতো hold-back লাগে কি না পরীক্ষা করুন
  3. Slow-disk injection — fsync-এ artificial delay, commit latency-র উপরে ফল মাপুন
  4. Statistical rigor — প্রতি cell-এ n=30 run, confidence interval-সহ comparison
  5. Raft-কে victim বানান — নিজের raft implementation project-এর cluster FaultBus-এর পেছনে বসিয়ে election storm মাপুন
  6. SLO burn-rate — metrics থেকে error budget burn হিসাব করে alert rule-এর ন্যায্যতা প্রমাণ করুন

যা এখানে শিখলেন, পরে যেখানে লাগবে

এখানেপরে
Scenario DSL + FaultBusProduction chaos platform (Litmus, FIS) পড়া সহজ
Percentile-first mindsetSLO/error-budget culture-র প্রথম ভাষা
Findings report ছকPostmortem লেখার অভ্যাস — hypothesis/evidence/fix
Seeded determinismDeterministic simulation testing — FoundationDB-র পথ