Distributed Tracing — request-এর পুরো জীবন দেখা
Distributed Tracing: Spans, Propagation, Sampling
Microservice-jungle-এ এক request কোথায় সময় খেল — logs বা metrics বলে না; trace-span-tree, traceparent-header, sampling-economics আর critical-path-analysis মিলে সেই মানচিত্র আঁকে।
আগে এটা বুঝি
আগের লেসনে shard-cluster সাজালাম। এখন production-এ checkout ঢুকল, p99 ২০০ ms → ১.৪ s; dashboard বলছে gateway-latency উঠেছে, logs ভরা সবুজ লাইন। প্রশ্ন উত্তর হচ্ছে না: সময়টা গেল কোথায়? Gateway? Cart? Inventory? নাকি মাঝের network?
এই darkness-ই microservice debugging-এর daily-reality — ঔষধ distributed tracing: প্রতিটা request-এ একটা বিশেষ ID রওয়ানা, service-সীমানায় ID হাতবদল, প্রতিটা service নিজের কাণ্ডের একটা span (শুরু-মেয়াদ-পরিচয়) রেকর্ড। শেষে span-জোড়া লাগিয়ে request-এর পুরো জীবনী — কে কাকে ডাকল, কে কত সময় নিল, দোষ কার ঘাড়ে।
Module-এর শেষ লেসন — observability-র তৃতীয় স্তম্ভ। আজ: span-tree anatomy, traceparent byte-dissection, HTTP/queue-boundary-তে context-ভ্রমণ, sampling- economics, critical-path — তারপর নিজের হাতে একটা TraceCollector।
মূল ধারণা
Trace = causal span-tree
Span-এর anatomy:
trace_id : পুরো request-জীবনের এক ID — সব span ভাগ করে
span_id : এই কাণ্ডের নিজের ID
parent_id: কার ডাকে জন্ম — গাছের edge এখানেই
name : 'GET /checkout' / 'db.query'
start, duration : wall-clock — অতএব সন্দেহপ্রবণ!
tags : http.status=500, db.statement...গাছ কেন? Call-graph-ই causality: root-span প্রবেশদ্বার, child ওর ডাকা downstream-work। Parallel calls = এক parent-এর একাধিক child — wall-clock sum না, max-chain: ধীরতম root→leaf শাখাই end-to-end-এর ঘড়ি — critical path।
W3C traceparent — ২৯ byte-এর identity-card
| Region | Byte | Hex | উদাহরণ | মানে |
|---|---|---|---|---|
| version | ১ | ২ | 00 | Format-version; ff forbidden |
| trace-id | ১৬ | ৩২ | 4bf92f...4736 | Global request-ID; all-zero invalid |
| parent-id | ৮ | ১৬ | 00f067aa0ba902b7 | Caller-র span_id; all-zero invalid |
| trace-flags | ১ | ২ | 01 | Bit-0 sampled — ‘রাখবে?’ |
00 - 4bf92f3577b34da6a3ce929d0e0e4736 - 00f067aa0ba902b7 - 01
└┬┘ └──────────────┬───────────────────┘ └───────┬──────┘ └┬┘
version trace-id parent-id flagsদুই সূক্ষ্মতা: parent-id = immediate caller-এর span_id, তাই প্রতি hop-এ বদলায় — trace-id থিতিমান। আর flags-এর sampled-bit propagation-এর আগেই ঠিক হয় head-sampling-এ; tail-sampling-এ বিটটা প্রায় decorative।
Context propagation — boundary পেরোনো
- Client → Gatewaygateway নতুন trace_id + span_id জন্ম দেয়; call-path-এ traceparent header ঢোকে
- Gateway span closestart/duration/tags রেকর্ড, async exporter — request-path ব্লক হয় না
- Gateway → Cartheader থেকে trace_id পড়ে, নিজের span_id দিয়ে নতুন traceparent — parent-id এবার gateway-র span_id
- Cart → QueueHTTP নেই — context message-carrier-এ (headers/attributes); envelope-এর ভেতর আরেক খাম
- Consumer → Inventoryconsumer context extract করে child-span বানায় — chain queue-জুড়েও ভাঙে না
- Collector assemblyspan আসে এলোমেলো order-এ; parent_id দিয়েই গাছ ফিরে গড়ে — timestamp দিয়ে না
মূল নীতি: in-process-এ thread-local, cross-process-এ wire, async-এ carrier — mechanism বদলায়, identity ভাঙে না; queue-world-এ producer-context consumer-এ পৌঁছানোই design-requirement।
Sampling — retention economics
সব trace? 10,000 req/s × ৮ span × ~250 byte ≈ 20 MB/s raw — দিনে ~1.7 TB, বেশিরভাগ healthy-request-এর নকল গল্প। দুই ঔষধ:
| Strategy | সিদ্ধান্ত কখন | রাখে | হারায় |
|---|---|---|---|
| Head sampling | Request-শুরুতে coin-flip | Random 1% সমভাগে | দুর্লভ error-trace-ও! |
| Tail sampling | Trace-complete-এর পরে | 100% errors + slowest + baseline % | Buffer-cost, delayed visibility |
1%-এ error-burst (200 failed) থেকে captured ≈ ২টা — কোনো দিন শূন্যও। Tail errors always-keep; দাম decision-window পর্যন্ত buffering। Retention-ও policy: hot-index ৭ দিন, cold ৩০ — query-pattern-ই দাম ঠিক করে।
Critical path — ভিড়ের মধ্যে দোষী
Parallel children: auth-check 33, cart-call 537, cache-lookup 12; end-to-end 562। Sum 582 — অসম্ভব: overlap, আর দৈর্ঘ্য ঠিক করে longest cumulative chain — gateway→cart→inventory। auth-check optimize = বৃথা; inventory-র index = p99 অর্ধেক।
ভেতরে কী ঘটছে
Clock-skew — waterfall যখন মিথ্যা বলে
Spans wall-clock timestamp আনে — আর machine-ঘড়ি agree করে না। Assembled tree-তে child parent-এর start-এর আগে দেখাতে পারে — impossible, routine। তিন প্রতিকার: causality-clamp (child-interval parent-এ clip), relative-time rendering, skew-detection-as-signal — build-এ তৃতীয়টাই। Duration মাপা monotonic-clock-এ, cross-service alignment wall-clock-এ — division-of-labor না বুঝলে skew-bug debug-এ সপ্তাহ যায়।
Assembly — out-of-order যে নিয়ম, exception না
Exporter async — retry, batch-flush, queue-lag; span আসে inventory-আগে gateway-পরে। Assembly তাই arrival-order-assumption রাখেই না:
1. ingest(span): trace_id → {span_id → span} map — ব্যস
2. children_map: parent_span_id-key-এ group
3. roots: parent_span_id সেটে নেই — তারাই গোড়া
4. missing-parent → ORPHAN-flag — গাছ ভাঙার বদলে reportকোথাও timestamp-sort নেই — order-এর উৎস parent-edge, time না; physical-time অবিশ্বস্ত, logical-relation ভরসা — module-এর পুরনো শিক্ষা।
OpenTelemetry landscape — কে কী
- OTel API — code-এর span-vocabulary, vendor-neutral
- SDK + auto-instrumentation — http/grpc/kafka hook; span+propagation auto
- Collector + OTLP — receive-process-export pipeline; backend pluggable
OTel storage/UI দেয় না — instrumentation-standard দেয়; lock-in এখন config-file-এর ব্যাপার।
তিন স্তম্ভের সেতু
Metrics বলে কী খারাপ, logs কী ঘটেছে, traces কোথায়। সেতু trace_id: log-line-এ ছাপলে সেই log waterfall-এ click-away; exemplar-link dashboard থেকে দোষী span-এ। Maturity মাপা যায় এই সেতুর সংখ্যায়।
উদাহরণ
একটা trace-এর পুরো জীবন — হাতে পড়া
Checkout-request, তিন service + এক queue-hop; collector-এ পৌঁছানো spans (ms):
g1 gateway GET /checkout start=0 dur=562 parent=''
a9 auth-check verify_token start=2 dur=33 parent=g1 parallel!
c1 cart process_cart start=8 dur=537 parent=g1
i1 inventory reserve_stock start=15 dur=503 parent=c1
q1 cart.publish order.event start=549 dur=2 parent=c1
k1 notifier.consume start=601 dur=41 parent=q1(link)তিন নিয়মে পড়ুন: (১) tree — g1-এর children a9,c1; c1-এর i1,q1। (২) wall vs sum — children sum ৫৭০ ≠ ৫৬২: a9 parallel, c1-ই নিয়ন্ত্রক। (৩) chain — g1→c1→i1 longest = critical path; reserve_stock-ই ৫০৩ ms-এর মালিক। Queue-hop: k1-এর parent q1 — consumer producer-span-এর সন্তান, মাঝে সেকেন্ধণ্টা আর এক broker; causality preserved, timing decoupled।
ছবিতে
SPAN TREE WATERFALL (width = end-to-end)
g1 gateway g1 │█████████████████████████ 562ms
├─ a9 auth-check (parallel) ├─a9 │███ 33ms
└─ c1 cart └─c1 │·████████████████████████ 537ms ←critical
├─ i1 inventory ├─i1 │··███████████████████████ 503ms ←critical
│ │ │ (৫০৩ ms — এইখানেই যুদ্ধ)
└─ q1 publish └─q1 │·······················█ 2ms
└─ k1 consume └─k1│·······················████ 41msনিজে চালিয়ে দেখুন
তিন-service toy chain — manual propagation, waterfall, ×10 slowdown
"""trace_lab.py — তিন-service toy chain, manual propagation"""
import threading
import time
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SPANS, LOCK = [], threading.Lock()
DELAY_C = 0.05 # scenario-2 তে ০.৫ করে ×10 slow
def make_handler(name, downstream):
class H(BaseHTTPRequestHandler):
def do_GET(self):
t0 = time.perf_counter()
# extraction — থাকলে নাও, না থাকলে root-span জন্ম দাও
tid = self.headers.get('X-Trace-Id', uuid.uuid4().hex[:12])
parent = self.headers.get('X-Parent-Span', '')
sid = uuid.uuid4().hex[:6]
time.sleep({'gateway': 0.02, 'cart': 0.03}.get(name, 0))
if name == 'inventory':
time.sleep(DELAY_C) # C-র নিজস্ব কাজ
child_ms = 0.0
if downstream: # injection — context হাতবদল
port, _ = downstream
req = urllib.request.Request(
f'http://127.0.0.1:{port}/call',
headers={'X-Trace-Id': tid, 'X-Parent-Span': sid})
tc = time.perf_counter()
urllib.request.urlopen(req).read()
child_ms = (time.perf_counter() - tc) * 1000
dur = (time.perf_counter() - t0) * 1000
with LOCK:
SPANS.append(dict(tid=tid, sid=sid, pid=parent,
name=name, t0=t0, dur=dur,
child_ms=child_ms))
self.send_response(200)
self.end_headers()
self.wfile.write(b'ok')
def log_message(self, *_a): pass
return H
def start_chain(base_port, c_delay):
global DELAY_C
DELAY_C = c_delay
ports = {nm: base_port + i for i, nm in
enumerate(['gateway', 'cart', 'inventory'])}
servers = []
for nm, port in ports.items():
down = ((ports['cart'], 'cart') if nm == 'gateway'
else ((ports['inventory'], 'inventory')
if nm == 'cart' else None))
srv = ThreadingHTTPServer(('127.0.0.1', port),
make_handler(nm, down))
threading.Thread(target=srv.serve_forever, daemon=True).start()
servers.append(srv)
return servers
def one_request(port, label):
SPANS.clear()
time.sleep(0.15)
urllib.request.urlopen(f'http://127.0.0.1:{port}/').read()
spans = sorted(SPANS, key=lambda s: s['t0'])
origin = spans[0]['t0']
total = max(s['t0'] + s['dur'] / 1000 for s in spans) - origin
W = 60
print(f'\n--- {label} (end-to-end ~{total*1000:.0f}ms) ---')
for s in spans:
off = round((s['t0'] - origin) / total * W)
wdt = max(1, round((s['dur'] / 1000) / total * W))
wait = '' if s['child_ms'] == 0 else \
f' (downstream-wait {s["child_ms"]:.0f}ms)'
print(f'{s["name"]:>10} │{"·"*off}{"█"*wdt} '
f'{s["dur"]:6.1f}ms{wait}')
s1 = start_chain(8930, c_delay=0.05)
one_request(8930, 'scenario-1: healthy chain (C fast)')
for srv in s1:
srv.shutdown()
start_chain(8940, c_delay=0.50)
one_request(8940, 'scenario-2: C slowed x10')
print('\nদেখুন: downstream-wait ফুলেছে, self-time (dur − wait) একই —'
'\nদোষী C, waterfall-ই proof।')নমুনা output (ms-মান machine-load-এ ওঠানামা করবে):
--- scenario-1: healthy chain (C fast) (end-to-end ~113ms) ---
gateway │██████████████████████████████████████████████████████████ 111.7ms (downstream-wait 90ms)
cart │·············██████████████████████████████████████████████ 88.9ms (downstream-wait 55ms)
inventory │·······························████████████████████████████ 51.0ms
--- scenario-2: C slowed x10 (end-to-end ~562ms) ---
gateway │████████████████████████████████████████████████████████████ 562.3ms (downstream-wait 540ms)
cart │··█████████████████████████████████████████████████████████ 538.9ms (downstream-wait 506ms)
inventory │······██████████████████████████████████████████████████████ 504.7msতিনটা পাঠ:
- Gateway-র 112 ms-এর ভেতর নিজের কাজ ~22 ms — waterfall ছাড়া dashboard-এ gateway-ই দোষী দেখত। End-to-end ৫× হলেও কারো self-time বদলায়নি।
- Header ছিল মাত্র trace-id + parent-id — traceparent-এর হ্রাসকৃত রূপ।
Header-দুটো হাতে বহন করলেই তিন process-এর span এক tree-তে বাঁধে — magic নেই; service C ×10 ধীর করলে upstream-দের duration ফুলে যায় কিন্তু self-time একই থাকে — critical path C-তে সরেছে, সংখ্যায় প্রমাণ।
নিজে বানান
TraceCollector — out-of-order assembly, waterfall, critical-path, diagnostics
- Ingest — JSON span, যেকোনো order-এ trace-map-এ
- Assembly — children_map parent-edge দিয়ে, roots missing-parent logic-এ
- Waterfall renderer — origin-relative bar-chart, width-normalized
- Critical path — root→leaf max cumulative-duration chain highlight
- Diagnostics — orphan, clock-skew, parent-overrun report
"""trace_collector.py — ingestion, out-of-order assembly, waterfall,
critical-path, orphan/skew diagnostics (~110 line)"""
import json
class TraceCollector:
"""Backend-collector-স্বাদ: spans যেকোনো order-এ আসবে বলেই ধরে নাও"""
def __init__(self):
self.traces = {} # trace_id -> {span_id: span}
def ingest(self, raw): # str-json বা dict, order-free
sp = json.loads(raw) if isinstance(raw, str) else dict(raw)
self.traces.setdefault(sp['trace_id'], {})[sp['span_id']] = sp
def spans(self, tid):
return list(self.traces.get(tid, {}).values())
def by_id(self, tid):
return self.traces.get(tid, {})
def children_map(self, tid):
ch = {}
for s in self.spans(tid):
ch.setdefault(s['parent_span_id'], []).append(s)
for lst in ch.values():
lst.sort(key=lambda s: s['start_ms'])
return ch
def roots(self, tid):
ids = set(self.by_id(tid))
return [s for s in self.spans(tid)
if s['parent_span_id'] not in ids]
def critical_path(self, tid): # root→leaf max cumulative (simplified!)
ch = self.children_map(tid)
memo = {}
def best(s):
if s['span_id'] not in memo:
kids = ch.get(s['span_id'], [])
kids = ch.get(s['span_id'], [])
if kids:
bk, bv = max((best(k) for k in kids),
key=lambda t: t[1])
memo[s['span_id']] = ([s] + bk, s['duration_ms'] + bv)
else:
memo[s['span_id']] = ([s], s['duration_ms'])
return memo[s['span_id']]
rs = self.roots(tid)
return max((best(r) for r in rs), key=lambda t: t[1])[0] if rs else []
def waterfall(self, tid, width=58):
spans = sorted(self.spans(tid), key=lambda s: s['start_ms'])
if not spans:
print('(empty trace)')
return
cp_ids = {s['span_id'] for s in self.critical_path(tid)}
origin = min(s['start_ms'] for s in spans)
total = max(max(s['start_ms'] + s['duration_ms'] for s in spans)
- origin, 1e-9)
for s in spans:
off = round((s['start_ms'] - origin) / total * width)
w = max(1, round(s['duration_ms'] / total * width))
star = ' <-critical' if s['span_id'] in cp_ids else ''
print(f"{s['name']:>12} │{'·'*off}{'█'*w}"
f" {s['duration_ms']:6.1f}ms{star}")
def diagnostics(self, tid):
byid = self.by_id(tid)
issues = []
for s in sorted(self.spans(tid), key=lambda x: x['start_ms']):
pid = s['parent_span_id']
if pid and pid not in byid:
issues.append(f"ORPHAN : '{s['name']}'-র parent {pid} "
f"এই trace-তে নেই (dropped export?)")
continue
p = byid.get(pid)
if p:
if s['start_ms'] < p['start_ms']:
issues.append(f"CLOCK-SKEW: '{s['name']}' parent-এর "
f"{p['start_ms'] - s['start_ms']:.1f}ms আগে শুরু!")
over = (s['start_ms'] + s['duration_ms']) - \
(p['start_ms'] + p['duration_ms'])
if over > 1e-6:
issues.append(f"OVERRUN : '{s['name']}' parent-কে "
f"{over:.1f}ms পেরিয়ে শেষ")
return issues
def sp(trace_id, span_id, parent, name, start, dur):
return dict(trace_id=trace_id, span_id=span_id, parent_span_id=parent,
name=name, start_ms=start, duration_ms=dur)
c = TraceCollector()
T = 'tr-42'
SHUFFLED = [ # ইচ্ছা-করে এলোমেলো order
sp(T, 'i1', 'c1', 'inventory', 15, 503),
sp(T, 'o7', 'zz9', 'lost-span', 400, 10), # orphan!
sp(T, 'a9', 'g1', 'auth-check', -3, 33), # clock-skew!
sp(T, 'g1', '', 'gateway', 0, 562),
sp(T, 'd4', 'c1', 'db-query', 40, 38),
sp(T, 'c1', 'g1', 'cart', 8, 537),
]
for s in SHUFFLED:
c.ingest(s)
print('waterfall (trace tr-42):')
c.waterfall(T)
chain = c.critical_path(T)
cum = sum(s['duration_ms'] for s in chain)
print('\ncritical path:', ' -> '.join(s['name'] for s in chain),
f'(cumulative {cum:.0f}ms)')
print('\ndiagnostics:')
for line in c.diagnostics(T):
print(' ', line)নমুনা output:
waterfall (trace tr-42):
auth-check │███ 33.0ms
gateway │██████████████████████████████████████████████████████████ 562.0ms <-critical
cart │·███████████████████████████████████████████████████████ 537.0ms <-critical
inventory │··████████████████████████████████████████████████████ 503.0ms <-critical
db-query │····████ 38.0ms
lost-span │·········································█ 10.0ms
critical path: gateway -> cart -> inventory (cumulative 1602ms)
diagnostics:
CLOCK-SKEW: 'auth-check' parent-এর 3.0ms আগে শুরু!
ORPHAN : 'lost-span'-র parent zz9 এই trace-তে নেই (dropped export?)তিনটা পাঠ:
- Ingest-order এলোমেলো হলেও tree perfect — order-এর ভরসা timestamp না, parent_edge; production-collector-এর প্রথম design-rule।
- Critical-path cumulative 1602 > wall 565 — overlapping parent-duration double-count-এর সৎ স্বীকারোক্তি; real tools interval-arithmetic-এ refine করে, teaching-version intent-ই দেখায়।
- Diagnostics demo-data-তে দুই bug-class ধরল — dropped-export (orphan), unsynced-clock (skew); তৃতীয় detector (overrun — instrumentation-bug) trigger করতে inventory-র duration ৫৬০ করে চালান।
নিজে বাড়ান:
- Interval-arithmetic critical-path — overlap বাদ দিয়ে true self-time chain
- Per-service latency histogram — হাজার trace-ingest করে p50/p99 রিপোর্ট
- Tail-sampling filter — keep-errors + keep-slowest policy, capture-ratio মাপুন
বাস্তব সিস্টেমে
বাস্তব জগতে
Dapper-থেকে Zipkin। Google-এর 2010 paper concept industrialize করে; Twitter-এর Zipkin ওপেন-সোর্স রূপ — আজ OTel-যুগেও span-model হুবহু Dapper-এরই।
OpenTelemetry adoption। Vendor-রা OTel SDK back করছে — CNCF-র second-most-active project; fragmentation শেষ, এখন backend-competition।
Postmortem culture। Latency incident-এ প্রথম exhibit প্রায়ই comparative- trace: deploy-আগে-পরে waterfall diff — logs-এ ঘণ্টা, traces-এ এক নজর।
Sampling economics। Ad-tech pipelines-এ tail-sampling errors + slow always-keep, baseline 1–5% — storage-bill এক-digit percent। Queue-boundary continuity-ই অনেক org-এর কঠিনতম milestone।
যে ভুলগুলো সবাই করে
“Waterfall-এ span start-time sort করলেই সত্যিকারের ক্রম।”
Sort display-order, truth না — machine-ঘড়ি unsynced, child-parent impossible- ordering routine (build-এর CLOCK-SKEW)। Structure parent_edge থেকে আসে; serious tools skew-কে signal-ও করে।
“Sampling মানে data নষ্ট — 100% trace-ই আসল engineering।”
100%-এর দাম storage (~TB/day), ingestion-cost, আর noise — healthy-request- এর million-copy-র forensic-value শূন্য। Tail-sampling-এর চুক্তি বুদ্ধিমান: errors/outliers 100%, boring baseline সামান্য। Signal-per-byte-ই metric।
“Trace-id ঢোকানো মানেই distributed tracing — debugging সহজ।”
ID শুধু thread; গাছ parent-edge-এ গড়ে, পড়ে critical-path lens-এ। Broken propagation, inconsistent span-name, tag-cardinality explosion — ‘tracing আছে তবু কিছু দেখা যায় না’-র তিন classic কারণ।
“OpenTelemetry নিজেই tracing-backend — install করলেই UI পাব।”
OTel API/SDK/Collector instrumentation আর transport-এর standard; storage, query-language, waterfall-UI সব backend-এর কাজ (Jaeger, Tempo, commercial)। Promise উল্টোটাই: backend-swappable — antenna OTel, receiver আলাদা কিনতে হয়।
বুঝেছেন কি না দেখুন
1Span-এর core-field আর traceparent-এর চার region — মিলিয়ে লিখুন। কোনটা
প্রতি hop-এ বদলায়?
স্মরণ
Span: trace_id, span_id, parent_id, name, start/duration, tags। traceparent: version, trace-id (16B), parent-id (8B), flags (bit0 sampled)। প্রতি hop-এ parent-id বদলায় — immediate-caller-র span_id; trace-id constant।
2Queue-boundary-তে HTTP-header propagation কেন fails, context যায় কোথায়?
Consumer-span-এর parent কে?
যুক্তি
Queue-hop-এ live-connection নেই — header transport নেই। Context যায় message-carrier-এ: producer inject, consumer extract। Consumer-span parent = producer-র publish-span — causality টিকে, timing decoupled।
310,000 req/s × ৬ span × 250 byte। Head 1% হলে দিনে কত GB? Error-rate
0.1%-এ 1% head-sample-এ দিনে কত error-trace বাঁচে — tail keep-all-errors-এ?
প্রয়োগ
Raw 15 MB/s; 1% → ~12.7 GB/দিন (raw ~1.27 TB থেকে)। Errors: 864k/দিন; 1%-এ ≈ 8,640 — burst-day variance বিশাল, minute-এ শূন্যও। Tail keep-all: 864k-ই — দাম buffer+compute।
4Parallel children A 40 ms, B 350 ms, C 90 ms; parent duration 360 ms।
Critical path কোনটা? C-কে অর্ধেক করলে end-to-end? B-কে?
যুক্তি
Path parent→B। C-halving: অপরিবর্তিত ~360 ms — C path-এ ছিলই না। B-halving: নতুন ceiling ≈ parent-self + max(A,C) ≈ 100 ms — প্রায় 3.6× লাভ। Optimize critical-path member-কেই; বাকি optimization দৃশ্যমানই হয় না।
5200 service, 50k req/s platform-এ tracing-rollout-এর প্রথম ৩০ দিন: কোথা থেকে
শুরু, কী sampling, কী success-metric?
ডিজাইন
Week-1: top-revenue-path-এর ৫ service-এ instrumentation + propagation — depth-first। Week-2: collector + tail-sampling (errors/slow 100%, baseline 2%) + log-এ trace_id। Week-3: dependency-map + critical-path dashboards। Week-4: expansion-playbook + span-naming RFC। Metric: postmortem-এ ‘tracing found root-cause’ count — coverage-% না, answer-rate।
এরপর কী
Module wrap — আর সামনের পথ
Level ৯ শেষ — module-arc ফিরে দেখুন: fallacies (‘network reliable, latency zero’) প্রতিটা লেসনের প্রেত থেকে clocks (physical-time অবিশ্বস্ত, happens-before/vector-clock-এ causality), consensus (CAP, quorum, Raft, saga), operations (locks, queues, event-sourcing), শেষে scale আর observability — gossip/CRDT convergence, sharding-yajna, আর আজ tracing-এর আলো, যা না থাকলে বাকি সব অন্ধকারে চলে।
সামনে Level ১০ — Security: environment নিজেই hostile — authN/authZ-গণিত, TLS-handshake-র wire-anatomy (networking-জ্ঞান ফিরবে), threat-modeling, hash/MAC থেকে JWT-session, secret-management, trust-boundary আঁকার শিল্প। Consensus-cluster যত সুন্দর, প্রথম malicious-node-এর কাছে তত মূল্যবান — security-module সেটাই শেখাবে। দেখা হবে Level ১০-এ।
আরও পড়ুন
- Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Benjamin H. Sigelman, Luiz A. Barroso, Mike Burrows, et al. · Google-এর tracing paper — span-tree model আর sampling-strategy-র মূল reference
- W3C Trace Context Recommendation — W3C Distributed Tracing Working Group · traceparent header-র official specification — byte-dissection-এর source of truth