এই পর্বে যা শিখবেন
- Monitoring আর observability-র পার্থক্য, আর কোন সংকেত কোন প্রশ্নের উত্তর দেয়
- Structured log, level, sampling, খরচ ও retention; counter, gauge, histogram; label-এর cardinality আর pull বনাম push
- কেন percentile-এর গড় চলে না, histogram-এর bucket থেকে p99 কীভাবে আন্দাজ হয়, আর trace-এর span, traceparent ও critical path
- RED, USE আর চারটি golden signal; SLI থেকে SLO, error budget-এর হিসাব আর error-budget policy
- Multi-window burn-rate alert, alert fatigue, on-call ও runbook, incident response, postmortem আর observability-র খরচ
১ · Monitoring বনাম observability — জানা প্রশ্ন আর অজানা প্রশ্ন
ঈদের আগের রাত। একটি কাল্পনিক mobile banking সেবায় অভিযোগ — “টাকা পাঠাতে অনেক সময় লাগছে।” Dashboard-এ সব সবুজ: CPU ৪০%, error rate স্বাভাবিক, health check ঠিক। তবু কিছু মানুষ কষ্ট পাচ্ছেন। কোথায়?
Monitoring হলো আগে থেকে ঠিক করা কিছু সংখ্যার ওপর নজর রাখা আর সীমা পেরোলে সতর্ক করা — “জানা অজানা” (known unknowns): আমরা জানি কী জিজ্ঞেস করতে হবে, শুধু এই মুহূর্তের উত্তরটি জানি না। Observability হলো বাইরের সংকেত দেখে ভেতরের অবস্থা বোঝার ক্ষমতা, এমন প্রশ্নেও যা আগে কেউ ভাবেনি — “অজানা অজানা” (unknown unknowns): হয়তো সমস্যা শুধু app-এর নতুন সংস্করণে, শুধু একটি অংশীদার ব্যাংকে, যা মোট গড়ে ডুবে যায়।
ডাক্তারের চেম্বারে প্রথমে মাপা হয় জ্বর আর রক্তচাপ — এটি monitoring: জানা কয়েকটি সংখ্যা। রোগ অচেনা হলে ডাক্তার নতুন প্রশ্ন করেন আর নতুন পরীক্ষা দেন; উত্তর মেলে কারণ শরীর আর রোগীর বিবরণে খুঁটিনাটি থাকে। Observability মানে সেই খুঁটিনাটি আগে থেকে রেখে দেওয়া, যাতে বিপদের রাতে নতুন কোড deploy না করেই নতুন প্রশ্ন করা যায়।
শব্দটি এসেছে control theory থেকে — ১৯৬০ সালের দিকে Rudolf Kálmán একটি ব্যবস্থাকে “observable” বলেন, যদি output দেখে ভেতরের অবস্থা পুরো বের করা যায়। দুটি প্রতিযোগী নয়: observability-র তথ্যের ওপরেই monitoring আর alert দাঁড়ায়।
পাঁচ রকম সংকেত
| সংকেত | কী | যে প্রশ্নের উত্তর দেয় | খরচ |
|---|---|---|---|
| Log | একটি ঘটনার সময়-চিহ্নিত বিবরণ | ঠিক কী ঘটেছিল, কোন অনুরোধে, কোন মান নিয়ে? | ঘটনার সংখ্যার সাথে বাড়ে — সবচেয়ে দামি হতে পারে |
| Metric | সময়ের সাথে একটি সংখ্যা (গণনা, মাপ) | কত ঘটছে, বাড়ছে না কমছে, সীমা পেরোল কি? | ঘটনা নয়, ধারার (series) সংখ্যায় বাড়ে — সবচেয়ে সস্তা |
| Trace | একটি অনুরোধের সব ধাপ (span) একটি গাছে | কোন সেবায় কত সময় গেল, ধীর অংশ কোনটি? | অনুরোধ × span; তাই sampling লাগে |
| Profile | কোন ফাংশনে CPU বা memory কতটা খরচ হচ্ছে | কোডের ঠিক কোন লাইন গরম? | নমুনা নিয়ে মাপা, মাঝারি |
| Event | অর্থবহ পরিবর্তন: deploy, config বদল, feature flag | সমস্যা শুরুর ঠিক আগে কী বদলেছিল? | খুব কম, অথচ খুব কাজের |
এই সংকেতগুলো কীভাবে সার্ভিস থেকে বেরিয়ে মানুষের চোখ পর্যন্ত পৌঁছায়, নিচের ছবিতে দেখুন। প্রতিটি সেবার ভেতরে একটি instrumentation লাইব্রেরি সংকেত তৈরি করে; একটি collector সেগুলো জমা করে, ছাঁকে, ভাগ করে আলাদা storage-এ পাঠায়; সেখান থেকে dashboard আর alert.
সংকেতের পথ — সেবা থেকে collector, storage, তারপর dashboard আর alert
Telemetry pipeline: services → collector → stores → dashboards & alerts → on-call
তিনটি সংকেত তিনটি আলাদা দ্বীপ হলে বিপদের রাতে কাজে আসে না। জোড়া লাগায় একটি সাধারণ সুতো: প্রতিটি log লাইনে trace_id, প্রতিটি metric-এর পাশে নমুনা trace-এর লিংক (exemplar), আর প্রতিটি trace-এ সেবা ও সংস্করণের নাম। তখন পথটি হয়: alert → dashboard-এ কোন সেবা → সেই সময়ের ধীর trace → ওই trace id-এর log → কোডের লাইন।
শুরুর রাতের সমস্যাটি gray failure-এর পরিচিত রূপ: health check-এর চোখে সুস্থ, ব্যবহারকারীর চোখে অসুস্থ (পর্ব ৪৪)। প্রতিকার হলো আসল অনুরোধের error আর latency মাপা, আর version, region, অংশীদারের মতো মাত্রা ধরে ভাগ করে দেখতে পারা।
২ · Log — কাঠামোবদ্ধ বিবরণ, level, sampling, খরচ আর গোপনীয়তা
মুদি দোকানের খাতার কথা ভাবুন। একজন দোকানি লেখেন “রহিম ভাই চাল নিলেন, পরে দেবেন।” আরেকজন কলাম টেনে লেখেন: তারিখ │ নাম │ জিনিস │ পরিমাণ │ দাম │ বাকি. মাস শেষে “কার কাছে কত বাকি” বের করতে প্রথম খাতায় পুরো রাত লাগে, দ্বিতীয়টিতে কয়েক মিনিট। Log-এও তাই। মুক্ত বাক্যের log মানুষ পড়তে পারে, কিন্তু যন্ত্র খুঁজতে পারে না; structured log-এ প্রতিটি ঘটনা নির্দিষ্ট নামের ঘরসহ একটি record, সাধারণত JSON.
2026-10-28 21:14:03 ERROR Payment failed for user 01712345678 after 3 retries, bank timeout{
"ts": "2026-10-28T15:14:03.481Z",
"level": "ERROR",
"service": "payment",
"version": "2.14.1",
"region": "dc-a",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"event": "bank_call_failed",
"partner": "bank-07",
"attempt": 3,
"duration_ms": 2004,
"error": "upstream_timeout",
"user_ref": "u_8f3a91",
"msisdn_masked": "017******78"
}দ্বিতীয় রূপে “গত ১৫ মিনিটে partner = bank-07 আর error = upstream_timeout কতবার, কোন version-এ” — এক লাইনের query. খেয়াল করুন: সময় UTC-তে (পর্ব ৩৯), ঘটনার একটি স্থির নাম (event), সংখ্যা সংখ্যা হিসেবে, আর ফোন নম্বর ঢাকা। সবচেয়ে জরুরি ঘর trace_id: এ দিয়েই এই লাইনটি trace আর অন্য সেবার log-এর সাথে জোড়া যায়। আগের পর্বগুলোর request id (পর্ব ১১) আর correlation id (পর্ব ৩২) একই কাজ করে; আধুনিক ব্যবস্থায় সাধারণত trace id-ই সেই ভূমিকা নেয়।
Level — কোন ঘটনা কতটা জরুরি
Level পাঁচটি প্রচলিত: DEBUG (উন্নয়নের খুঁটিনাটি, production-এ সাধারণত বন্ধ), INFO (স্বাভাবিক অর্থবহ ঘটনা), WARN (অস্বাভাবিক কিন্তু সামলানো গেছে, যেমন retry-তে সফল), ERROR (কাজটি ব্যর্থ) আর FATAL (প্রক্রিয়া আর চলতে পারছে না)। সাধারণ ভুল দুটি: সব কিছু ERROR-এ লেখা, আর ব্যবহারকারীর ভুল ইনপুটকে (যেমন ভুল PIN) ERROR বলা।
খরচ — log সবচেয়ে বেশি টাকা খায়
একটি হিসাব করি। ধরা যাক একটি সেবা ১,০০০ req/s পায়, প্রতিটি অনুরোধে গড়ে ৫টি log লাইন, প্রতিটি ৫০০ byte. তাহলে প্রতি সেকেন্ডে ১,০০০ × ৫ × ৫০০ = ২৫ লাখ byte = ২.৫ MB; দিনে ২.৫ × ৮৬,৪০০ = ২,১৬,০০০ MB, অর্থাৎ প্রায় ২১৬ GB; ৩০ দিন রাখলে প্রায় ৬.৪৮ TB — এক সেবার জন্য, compression ছাড়া, আর search-এর index (পর্ব ২৭) ধরলে আরও বেশি।
খরচ কমানোর উপায়গুলো:
- Sampling: সব ERROR আর WARN রাখুন, সফল অনুরোধের INFO-র মাত্র ১০%. ধরা যাক লাইনের ২% ERROR/WARN: তখন প্রতি সেকেন্ডে ৫,০০০ লাইনের মধ্যে থাকে ১০০ + ৪,৯০০ × ০.১ = ৫৯০টি, অর্থাৎ দিনে প্রায় ২৫.৫ GB — আগের প্রায় আট ভাগের এক ভাগ। Trace-এর sampling সিদ্ধান্তের সাথে মিলিয়ে নিলে (অংশ ৫) একটি নমুনা অনুরোধের সব লাইন একসাথে থাকে।
- প্রতি অনুরোধে একটি সমৃদ্ধ লাইন: পাঁচটি টুকরো লাইনের বদলে অনুরোধ শেষে সব গুরুত্বপূর্ণ ঘরসহ একটি “wide event”.
- স্তরভিত্তিক retention: শেষ ৭ দিন দ্রুত খোঁজার storage-এ, তারপর সস্তা object storage-এ, তারপর মুছে ফেলা; সময়সীমা ঠিক করে আইন আর ব্যবসার প্রয়োজন।
- Log-এর হার সীমা: বাগের লুপে লাখো ERROR যেন pipeline না ডোবায়, তাই সেবার ভেতরে log-এর rate limit রাখুন (পর্ব ৪৯)।
Log সাধারণত সরাসরি index-এ যায় না: agent লাইনগুলো batch করে একটি টেকসই log-এ (Kafka-ধরনের, পর্ব ৩৩) পাঠায়, সেখান থেকে indexer পড়ে — হঠাৎ ভিড়েও লাইন হারায় না।
Log-এ কখনো গোপন বা ব্যক্তিগত তথ্য নয়। পাসওয়ার্ড, PIN, OTP, access token, API key, পূর্ণ কার্ড নম্বর — কখনোই log-এ যাবে না। জাতীয় পরিচয়পত্র নম্বর, ফোন নম্বর, ঠিকানার মতো PII হয় বাদ, নয় ঢাকা (017******78) বা একটি অর্থহীন reference-এ বদলানো। নিরাপদ নিয়ম: যে ঘর log হবে তার তালিকা (allowlist) রাখুন, অনুরোধের পুরো body বা header কখনো হুবহু নয়; collector-এ দ্বিতীয় স্তরের ছাঁকনি রাখুন। “কে কখন কী করল”-এর audit log আলাদা জিনিস — তার নিয়ম, অখণ্ডতা আর retention পর্ব ৫২-এ।
৩ · Metric — counter, gauge, histogram আর label-এর বিস্ফোরণ
Log প্রতিটি ঘটনা আলাদা রাখে; metric ঘটনাগুলোকে গুনে বা মেপে একটি সংখ্যায় আনে, আর কিছুক্ষণ পরপর সেই সংখ্যাটি সময়ের সাথে লেখে। ১০ req/s হোক বা ১০ লাখ req/s, একটি counter-এর খরচ প্রায় একই — এজন্যই metric সস্তা, দ্রুত, আর alert-এর মূল ভিত্তি। মৌলিক ধরন তিনটি:
Counter
শুধু বাড়ে: মোট অনুরোধ, মোট error. আসল মানের চেয়ে কাজের তার বাড়ার হার — “গত ৫ মিনিটে প্রতি সেকেন্ডে কতটি”. প্রক্রিয়া restart হলে ০-তে ফেরে; হার হিসাবের সময় এই reset সামলাতে হয় (অনুশীলন ৩)।
http_requests_totalGauge
ওঠে-নামে, এই মুহূর্তের মান: queue-র দৈর্ঘ্য, খোলা connection, memory. হার নয়, মানটিই অর্থবহ।
queue_depthHistogram
মাপগুলো খোপে গোনে: ১০ ms-এর মধ্যে কতটি, ২৫-এর মধ্যে কতটি…। সাথে মোট যোগফল আর সংখ্যা, তাই গড়ও পাওয়া যায়। Latency-র জন্য প্রায় সবসময় এটি।
request_duration_secondsপ্রতিটি metric-এর সাথে থাকে কিছু label — key-value মাত্রা, যেমন route, status, pod. প্রতিটি আলাদা label-সমন্বয় একটি আলাদা time series. Prometheus-ধরনের ব্যবস্থায় সেবা একটি text পাতায় metric দেখায়, এমন:
# TYPE http_requests_total counter
http_requests_total{route="/transfer",method="POST",status="200"} 1849302
http_requests_total{route="/transfer",method="POST",status="503"} 2114
# TYPE queue_depth gauge
queue_depth{queue="sms"} 37
# TYPE request_duration_seconds histogram
request_duration_seconds_bucket{route="/transfer",le="0.1"} 1702111
request_duration_seconds_bucket{route="/transfer",le="0.3"} 1838950
request_duration_seconds_bucket{route="/transfer",le="1"} 1850604
request_duration_seconds_bucket{route="/transfer",le="+Inf"} 1851416
request_duration_seconds_sum{route="/transfer"} 98213.7
request_duration_seconds_count{route="/transfer"} 1851416Histogram-এর bucket-গুলো ক্রমযোজিত (cumulative): le="0.3" মানে ০.৩ সেকেন্ড বা তার কম সময়ে শেষ হওয়া সব অনুরোধ, ০.১-এর নিচেরগুলোসহ। _sum ÷ _count দেয় গড়, এখানে প্রায় ৫৩ ms. লক্ষ করুন, ১৮,৩৮,৯৫০ ÷ ১৮,৫১,৪১৬ ≈ ৯৯.৩৩% অনুরোধ ৩০০ ms-এর মধ্যে — এটি অংশ ৭-এর latency SLI, কোনো percentile আন্দাজ ছাড়াই, সরাসরি।
Cardinality-র বিস্ফোরণ
Time series-এর সংখ্যা হলো সব label-এর মানের সংখ্যার গুণফল। ধরা যাক ২০টি route, ৫টি status, ৩টি method, ৫০টি pod: ২০ × ৫ × ৩ × ৫০ = ১৫,০০০টি series. Histogram-এ ১০টি bucket আর _sum, _count মিলিয়ে প্রতিটির ১২টি — মোট ১,৮০,০০০. এখনো সামলানো যায়। এবার কেউ সরল মনে user_id label জুড়লেন, ১০ লাখ ব্যবহারকারী: সবচেয়ে খারাপ ক্ষেত্রে ১৫,০০০ × ১০,০০,০০০ = ১,৫০০ কোটি series. বাস্তবে সব সমন্বয় ঘটে না, তবু metric database-এর memory ফুরিয়ে যেতে পারে। একে বলে cardinality explosion (cardinality মানে ভিন্ন মানের সংখ্যা)।
নিয়ম: label-এর মান হবে সীমিত আর আগে থেকে জানা। user_id, request_id, email, ID-সহ পূর্ণ URL (/users/8812/orders-এর বদলে route-এর ছাঁচ /users/:id/orders) কখনো label নয় — ওগুলো log বা trace-এর জায়গা। “কতজন আলাদা ব্যবহারকারী” জানতে চাইলে প্রতিটি user-এর series নয়, একটি HyperLogLog (পর্ব ৪৫)।
Pull বনাম push — metric কে কার কাছে নিয়ে যায়
Prometheus-ধরনের ব্যবস্থা pull করে: metric server নিজে প্রতি কয়েক সেকেন্ডে (সাধারণত ১৫–৬০ s, config-নির্ভর) প্রতিটি সেবার /metrics পাতা পড়ে আনে — একে বলে scrape; কোন সেবা কোথায় আছে তা জানে service discovery থেকে। StatsD বা OpenTelemetry-র OTLP-ধরনের ব্যবস্থায় সেবা নিজে push করে পাঠায়।
| দিক | Pull (scrape) | Push |
|---|---|---|
| সেবা মারা গেলে | scrape ব্যর্থ — “লক্ষ্যটি চুপ” নিজেই একটি সংকেত | শুধু নীরবতা; “পাঠাচ্ছে না” আলাদা করে ধরতে হয় |
| ভিড়ের নিয়ন্ত্রণ | metric server নিজের গতিতে টানে | হাজার সেবা একসাথে পাঠালে গ্রাহক চাপে পড়ে |
| স্বল্পায়ু কাজ (batch job, serverless) | scrape-এর আগেই শেষ হয়ে যেতে পারে | শেষ হওয়ার আগে পাঠিয়ে দেয় |
বাস্তবে দুটি মেশে: স্বল্পায়ু job-এর জন্য Prometheus-এর জগতে একটি “pushgateway” আছে, আর অনেক collector দুই ধরনই নেয়। Metric জমা হয় একটি time-series database-এ, যা পাশাপাশি সময়ের মান খুব ভালো compress করে; পুরোনো ডেটা কম ঘন করে রাখা হয় (downsampling), যেমন ১৫ দিন পর ৫ মিনিটের মান।
৪ · Percentile-এর গড় হয় না — histogram-এর bucket থেকে p99
পর্ব ৪-এ দেখেছেন, গড় latency ঠকায়, তাই মাপা হয় percentile (p99), আর কয়েকটি সার্ভারের p99-এর গড় নেওয়া যায় না। Dashboard-এ “গড়” একটি বোতাম চাপলেই আসে বলে এই ভুলটি observability-তে সবচেয়ে বেশি হয়।
কারণটি গাণিতিক। Percentile একটি ক্রমের মান — সব মাপ সাজালে নির্দিষ্ট স্থানে যেটি পড়ে। গড়ের মতো এটি রৈখিক (linear) নয়: দুটি দলের p99 জানলেও মিলিত দলের ক্রমে কোন মান কোথায় পড়বে, তা বলা যায় না, কারণ কোন দলের ধীর অনুরোধ কতগুলো আর কত বড়, সেই তথ্য p99-এর একটি সংখ্যায় হারিয়ে গেছে। মিলিত p99 সবসময় দুই দলের p99-এর মাঝে (ছোটটি থেকে বড়টির মধ্যে) কোথাও থাকে, কিন্তু ঠিক কোথায় — তা বলতে লাগে পুরো বণ্টন, অথবা অন্তত তার একটি মেলানো যায় এমন সারাংশ। Histogram ঠিক সেই সারাংশ: প্রতিটি সার্ভারের একই bucket-এর গণনা যোগ করলেই মিলিত histogram.
নিচের simulator তিনটি সার্ভার বানায়: A বেশিরভাগ অনুরোধ সামলায় আর দ্রুত; C অল্প অনুরোধ পায়, কিন্তু তার ৮% আটকে যায় প্রায় ১.৫ সেকেন্ডে। তারপর ধরে নেয় সার্ভারগুলো শুধু bucket-গণনা পাঠায়, আর Prometheus-এর histogram_quantile-এর মতো bucket-এর ভেতরে সরলরেখায় p99 আন্দাজ করে।
// Three servers behind one service. Is the average of their p99s the service p99?
let seed = 50; function rand() { // mulberry32: seeded PRNG
seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const lat = (base, slowP, slowMs) => // ms: normal body, plus a rare slow tail
rand() < slowP ? slowMs * (0.5 + rand()) : base * (0.5 + 2 * rand() * rand());
const servers = [["A", 7000, 20, 0.002, 400], ["B", 2500, 35, 0.005, 600], ["C", 500, 60, 0.08, 1500]];
const pct = (xs, q) => [...xs].sort((a, b) => a - b)[Math.ceil(q * xs.length) - 1]; // nearest rank
const le = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, Infinity]; // bucket upper bounds, ms
const all = [], p99s = [], merged = le.map(() => 0);
console.log("server requests p50 ms p99 ms");
for (const [name, n, base, slowP, slowMs] of servers) {
const xs = Array.from({ length: n }, () => lat(base, slowP, slowMs));
all.push(...xs); p99s.push(pct(xs, 0.99));
le.forEach((b, k) => merged[k] += xs.filter(x => x <= b).length); // each server exports counts only
console.log(name.padEnd(8) + String(n).padStart(8) + pct(xs, 0.5).toFixed(1).padStart(8) + pct(xs, 0.99).toFixed(1).padStart(8));
}
const avg = p99s.reduce((a, b) => a + b) / p99s.length;
const wavg = servers.reduce((s, sv, i) => s + sv[1] * p99s[i], 0) / all.length;
console.log("average of the three p99s " + avg.toFixed(1) + " ms");
console.log("request-weighted average " + wavg.toFixed(1) + " ms");
console.log("TRUE p99 of all 10000 " + pct(all, 0.99).toFixed(1) + " ms");
console.log("merged buckets (le:count) " + le.map((b, k) => b + ":" + merged[k]).join(" "));
const rank = 0.99 * all.length, i = merged.findIndex(c => c >= rank);
const lo = i ? le[i - 1] : 0, below = i ? merged[i - 1] : 0; // linear inside the bucket
const est = lo + (le[i] - lo) * (rank - below) / (merged[i] - below);
console.log("p99 from merged histogram " + est.toFixed(1) + " ms (inside bucket " + lo + "-" + le[i] + ")");আউটপুট:
server requests p50 ms p99 ms
A 7000 17.7 45.3
B 2500 31.0 80.1
C 500 52.8 2081.1
average of the three p99s 735.5 ms
request-weighted average 155.8 ms
TRUE p99 of all 10000 106.1 ms
merged buckets (le:count) 5:0 10:0 25:6015 50:9270 100:9893 250:9938 500:9951 1000:9962 2500:10000 Infinity:10000
p99 from merged histogram 123.3 ms (inside bucket 100-250)
তিন রকম “গড়”, তিনটিই ভুল। তিনটি p99-এর সাধারণ গড় ৭৩৫.৫ ms — আসল ১০৬.১ ms-এর প্রায় সাত গুণ। এখানে C-এর ২০৮১.১ ms সব টেনে তুলেছে, অথচ C সামলায় মাত্র ৫% অনুরোধ। অনুরোধ-ভারযুক্ত গড় ১৫৫.৮ ms — কাছে, তবু প্রায় ৫০% বেশি। কেন আসল p99 এত কম? ধীরতম ১০০টিই p99 ঠিক করে; C-এর ৪০টি আর A, B-এর ২৩টি ধীর অনুরোধ মিলেও ১০০ হয় না, তাই ১০০তম স্থানে পড়ে একটি মাঝারি অনুরোধ। অন্য সংখ্যায় গড় আসল মানের চেয়ে কম-ও হতে পারে (অনুশীলন ৩) — দিকটাও বলা যায় না।
Histogram-এর আন্দাজ ১২৩.৩ ms. মিলিত গণনায় ৯৯০০তম স্থানটি পড়েছে 100–250 bucket-এ (১০০-এর নিচে ৯,৮৯৩টি, ২৫০-এর নিচে ৯,৯৩৮টি)। Bucket-এর ভেতরে মানগুলো সমানভাবে ছড়ানো ধরে নিয়ে: ১০০ + ১৫০ × (৯,৯০০ − ৯,৮৯৩) ÷ (৯,৯৩৮ − ৯,৮৯৩) = ১০০ + ১৫০ × ৭/৪৫ ≈ ১২৩.৩. আসল ১০৬.১-এর কাছাকাছি; ভুলের সীমা bucket-এর প্রস্থ (১০০–২৫০)।
এ থেকে তিনটি ব্যবহারিক নিয়ম:
- সার্ভার থেকে percentile নয়, bucket পাঠান। আগেই হিসাব করা quantile (“summary”) মেলানো যায় না; bucket যোগ করা যায়।
- SLO-র সীমায় একটি bucket রাখুন। SLO যদি “৩০০ ms-এর মধ্যে”, তাহলে
le="0.3"bucket থাকলে ভালো অনুরোধের অনুপাত সরাসরি গোনা যায়, কোনো interpolation ছাড়াই — যেমন অংশ ৩-এর ৯৯.৩৩%. - সূক্ষ্মতা দরকার হলে sketch. t-digest-এর মতো quantile sketch (পর্ব ৪৫) বা নতুন ব্যবস্থার exponential histogram কম জায়গায় মেলানো যায় এমন, আর প্রান্তে বেশি নির্ভুল সারাংশ রাখে। কোন ব্যবস্থায় কোনটি আছে, তা সংস্করণভেদে আলাদা।
একই ভুলের সময়-রূপও আছে: প্রতি ৫ মিনিটের p99-এর দিনভর গড় দিনের p99 নয়। দিনের p99 চাইলে দিনের সব bucket যোগ করুন। আর মাপ কোথায় নেওয়া হচ্ছে — সার্ভারের ভেতরে, load balancer-এ, নাকি ব্যবহারকারীর ফোনে (RUM) — তাও লিখে রাখুন; একই অনুরোধের তিন জায়গার latency তিন রকম।
৫ · Trace — span, parent-child, traceparent আর sampling
একটি “টাকা পাঠান” অনুরোধ হয়তো পাঁচটি সেবা পেরোয় (পর্ব ৪৭), শেষে latency ২ সেকেন্ড। কোন সেবা দোষী? প্রতিটির metric বলছে “আমার গড় ঠিক আছে”। উত্তর দেয় distributed tracing.
কুরিয়ারে পার্সেল পাঠালে একটি tracking নম্বর পান। ঢাকার হাব, বগুড়ার হাব, রাজশাহীর ডেলিভারি অফিস — প্রতিটি জায়গা স্ক্যান করে সময় লেখে, তাই একটি নম্বরেই দেখা যায় পার্সেল কোথায় কতক্ষণ আটকে ছিল। Trace id হলো সেই tracking নম্বর, আর প্রতিটি হাবের এন্ট্রি একটি span: কোথায়, কখন ঢুকল, কতক্ষণ রইল।
একটি trace হলো একটি অনুরোধের সব কাজের গাছ। গাছের প্রতিটি নোড একটি span: একটি নির্দিষ্ট কাজ (একটি HTTP call, একটি database query), যার আছে trace id (পুরো অনুরোধের এক), নিজের span id, parent span id (কে তাকে ডেকেছে), নাম, শুরুর সময়, স্থায়িত্ব, status, আর কিছু attribute (route, ব্যাংক-অংশীদার, version). যে span-এর parent নেই, সে root span. এক সেবা থেকে আরেক সেবায় trace id আর নিজের span id পৌঁছে দেওয়ার কাজটির নাম context propagation, আর HTTP-তে এর মানক header হলো W3C Trace Context-এর traceparent.
- gateway
span a1root - order
span b2parent a1 - payment
span c3parent b2 - bank-adapter
span d4parent c3
| ঢোকার header | |
| নতুন span | |
| span-এর সময় | |
| sampled flag |
- Gateway — trace-এর জন্ম। App থেকে আসা অনুরোধে traceparent নেই। Gateway নতুন trace id (4bf9…4736) আর root span a1 বানাল, আর ঠিক করল, flag 01 অর্থাৎ এই অনুরোধটি নমুনায় রাখা হবে — এটিই head sampling.
- Order — প্রথম সন্তান। Gateway header পাঠাল
00-4bf9…4736-a1-01: version, trace id, “আমি a1”, sampled. Order span b2 খুলল, parent a1. Trace id বদলায় না — বদলায় শুধু parent-এর ঘর। - Payment — আরও এক ধাপ। Order header-এ নিজের id b2 বসাল; payment-এর span c3, parent b2. প্রতিটি সেবার কাজ এটুকুই: আসা header পড়া, নিজের span খোলা, বাইরে যাওয়ার header-এ নিজের id বসানো।
- Bank-adapter — চতুর্থ সেবা। Parent c3; span d4 ব্যাংকের API ডাকল, লাগল ৭১ ms. Queue হয়ে গেলেও context বার্তার header-এ বসাতে হয় (পর্ব ৩১), নইলে সুতো ছিঁড়ে যায়।
- Span শেষ, collector-এ। ভেতরেরটি আগে শেষ হয়: d4 ৮২, c3 ৮৫, b2 ৮৭, a1 ৮৮ ms-এ। সেবাগুলো span পটভূমিতে batch করে পাঠায়, অনুরোধের পথে অপেক্ষা না করে; collector-এ যেকোনো ক্রমে পৌঁছায়।
- গাছ জোড়া — দোষী খুঁজে পাওয়া। একই trace id-এর span parent id ধরে সাজানো হলো: a1 › b2 › c3 › d4. মোট ৮৮ ms-এর ৭১ ms ব্যাংকের call-এ — মোট গড় দেখে যা বলা যেত না।
GET /v1/accounts/balance HTTP/1.1
Host: payment.internal
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: vendorx=t61rcWkgMzEচারটি অংশ, ড্যাশ দিয়ে আলাদা: 00 version; ৩২ hex অক্ষরের (১৬ byte) trace id; ১৬ hex অক্ষরের (৮ byte) parent id — যে span এই অনুরোধ পাঠাল; আর trace flags, যার শেষ bit 1 মানে trace-টি নমুনায় রাখা হচ্ছে। tracestate ঐচ্ছিক, বিভিন্ন বিক্রেতার নিজস্ব তথ্যের জন্য। W3C এটিকে মানদণ্ড (Recommendation) হিসেবে প্রকাশ করে ২০২০ সালে।
OpenTelemetry এই কাজের বিক্রেতা-নিরপেক্ষ মানদণ্ড: log, metric আর trace-এর API ও SDK, স্বয়ংক্রিয় instrumentation, পাঠানোর protocol OTLP, আর একটি collector. ২০১৯ সালে OpenTracing আর OpenCensus মিলে তৈরি, এখন CNCF-এর অধীনে। কোড একবার instrument করে backend বদলানো যায় শুধু collector-এর config বদলে; তবে ভাষাভেদে SDK-র পরিপক্বতা লেখার সময়ে আলাদা।
Span-এর সময় আর ঘড়ি। প্রতিটি span-এর সময় তার নিজের সার্ভারের ঘড়িতে; ঘড়িতে কয়েক ms পার্থক্য থাকলে (পর্ব ৩৯) সন্তান parent-এর আগে শুরু হচ্ছে দেখাতে পারে। Span-এর স্থায়িত্ব বিশ্বাসযোগ্য, দুই সার্ভারের মাঝের ছোট ফাঁক নয়।
Critical path — কোন অংশ দ্রুত করলে লাভ
Trace-এ সব ধীর span সমান দোষী নয়। Order যদি inventory আর payment-কে একসাথে (parallel) ডাকে, দুটির যেটি পরে শেষ হয় শুধু সেটিই মোট সময় ঠিক করে। যে span-শিকল মোট সময় ঠিক করে, তা critical path. নিচের simulator এলোমেলো ক্রমে আসা span-গুলো parent id ধরে গাছে জোড়ে, তারপর শেষ থেকে পেছনে হেঁটে critical path বের করে: প্রতিটি span-এ সবার শেষে শেষ হওয়া সন্তানে ঢোকে, তারপর তার শুরুর আগের অংশে একই কাজ।
// Spans reach the collector out of order. Rebuild the trace tree, then find the critical path.
let seed = 4; function rand() { // mulberry32: seeded PRNG
seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const hex = n => Array.from({ length: n }, () => (rand() * 16 | 0).toString(16)).join("");
const traceId = hex(32); // one id for the whole request
const ms = (lo, hi) => Math.round(lo + (hi - lo) * rand());
const S = {}; const span = (name, parent, start, dur) =>
(S[name] = { id: hex(16), name, parent: parent && S[parent].id, start, end: start + dur });
span("gateway", null, 0, 0); span("order", "gateway", 1, 0);
span("auth", "order", 2, ms(5, 12));
const t0 = S.auth.end + 1; // after auth: two calls in parallel
span("inventory", "order", t0, ms(15, 30)); span("inv.db", "inventory", t0 + 2, S.inventory.end - t0 - 4);
span("payment", "order", t0, ms(40, 80)); span("bank", "payment", t0 + 3, S.payment.end - t0 - 6);
S.order.end = Math.max(S.inventory.end, S.payment.end) + 2; S.gateway.end = S.order.end + 1;
const arrived = Object.values(S); // Fisher-Yates shuffle: collector order is random
for (let i = arrived.length - 1; i > 0; i--) { const j = rand() * (i + 1) | 0; [arrived[i], arrived[j]] = [arrived[j], arrived[i]]; }
console.log("arrival order: " + arrived.map(s => s.name).join(", "));
const byId = {}, kids = {}; let root;
for (const s of arrived) { byId[s.id] = s; kids[s.id] = []; }
for (const s of arrived) s.parent ? kids[s.parent].push(s) : (root = s);
(function show(s, depth) {
console.log(" ".repeat(depth) + s.name.padEnd(14 - 2 * depth) + String(s.start).padStart(4) +
" ms +" + String(s.end - s.start).padStart(3) + " ms span " + s.id.slice(0, 8));
kids[s.id].sort((a, b) => a.start - b.start).forEach(c => show(c, depth + 1));
})(root, 0);
function crit(s, until) { // walk backwards from the end of s
const path = [s]; let t = Math.min(s.end, until);
for (const c of [...kids[s.id]].sort((a, b) => b.end - a.end))
if (c.end <= t) { path.push(...crit(c, t)); t = c.start; }
return path;
}
console.log("critical path: " + crit(root, Infinity).sort((a, b) => a.start - b.start).map(s => s.name).join(" > "));
console.log("payment -> bank header traceparent: 00-" + traceId + "-" + S.payment.id + "-01");আউটপুট:
arrival order: inventory, gateway, order, inv.db, payment, bank, auth
gateway 0 ms + 88 ms span c1c65809
order 1 ms + 86 ms span a71cf07e
auth 2 ms + 5 ms span 38cd17af
inventory 8 ms + 30 ms span 6fe71101
inv.db 10 ms + 26 ms span e3ab9deb
payment 8 ms + 77 ms span 0c6e80b0
bank 11 ms + 71 ms span f51c1d57
critical path: gateway > order > auth > payment > bank
payment -> bank header traceparent: 00-e531381f9bc5be8b67bef75c4c857078-0c6e80b0e997c64f-01
Span-গুলো এসেছে এলোমেলো ক্রমে (inventory সবার আগে, root gateway দ্বিতীয়, auth সবার শেষে) — collector-এর কাছে ক্রমের কোনো মূল্য নেই, শুধু parent id. গাছে দেখা যাচ্ছে: auth ৫ ms চলার পর ৮ ms-এ inventory আর payment একসাথে শুরু হয়েছে; inventory ৩০ ms-এ শেষ, payment ৭৭ ms ধরে চলেছে, যার ৭১ ms ব্যাংকে। Critical path তাই gateway > order > auth > payment > bank. Inventory এতে নেই: সেটিকে দ্বিগুণ দ্রুত করলেও মোট ৮৮ ms-এর এক ms-ও কমবে না, কারণ payment তখনো চলছে। কিন্তু auth ধারাবাহিক (sequential), তাই সেটি পথে আছে। শেষ লাইনটি payment যে header পাঠিয়েছিল তা দেখায়: trace id পুরো অনুরোধে একই, parent id হলো payment-এর নিজের span id.
Head বনাম tail sampling
প্রতিটি অনুরোধের পুরো trace রাখা ব্যয়বহুল: ২,০০০ req/s, প্রতিটিতে ৩০টি span, প্রতিটি ৫০০ byte হলে প্রতি সেকেন্ডে ৩০ MB, দিনে প্রায় ২.৬ TB. তাই নমুনা রাখা হয়।
| দিক | Head sampling | Tail sampling |
|---|---|---|
| সিদ্ধান্ত কখন | শুরুতে, root span-এ (যেমন ১%), flag-এ নিচে পাঠানো | trace শেষ হওয়ার পর, collector-এ |
| কী রাখে | এলোমেলো ১%; বিরল error প্রায়ই বাদ | বেছে বেছে: সব error, সব ধীর, সাথে অল্প স্বাভাবিক |
| খরচ | খুব কম; বাদ পড়া অনুরোধে span-ই তৈরি হয় না | সব span collector-এ আসে, কয়েক সেকেন্ড memory-তে থাকে |
| নকশার শর্ত | সহজ | একটি trace-এর সব span একই collector-এ যেতে হবে — trace id ধরে consistent hashing (পর্ব ১৬) |
অনেক দল দুটি মেশায়: head sampling-এ বেশিরভাগ বাদ, কিন্তু error হলে span-কে “অবশ্যই রাখা হোক” চিহ্ন; সাথে collector-এ tail নিয়ম। আর exemplar histogram-এর bucket-এর পাশে একটি নমুনা trace id রাখে, যাতে dashboard-এর লাফ থেকে এক ক্লিকে সেই রকম একটি ধীর trace খোলা যায় (সমর্থন সংস্করণভেদে আলাদা)।
৬ · কী মাপবেন — RED, USE আর চারটি golden signal
কোন সংখ্যা সবার আগে দেখবেন? তিনটি পরিচিত, পরস্পরের পরিপূরক কাঠামো:
RED — অনুরোধ-চালিত সেবার জন্য
Rate (প্রতি সেকেন্ডে কত অনুরোধ), Errors (কতগুলো ব্যর্থ), Duration (কত সময় লাগছে — histogram). Tom Wilkie ২০১৫ সালের দিকে এটি জনপ্রিয় করেন।
RED methodUSE — সম্পদের জন্য
Utilization (কতটা ব্যস্ত), Saturation (কাজ কি লাইনে জমছে), Errors. Brendan Gregg-এর পদ্ধতি (২০১২ সালের দিকে প্রকাশিত), প্রতিটি CPU, disk, network, connection pool ধরে প্রশ্ন। পর্ব ১৩-এ প্রথম দেখেছেন।
USE methodচারটি golden signal
Latency, traffic, errors, saturation. Google-এর SRE বই (২০১৬)-এর পরামর্শ: মাত্র চারটি মাপতে পারলে এই চারটি। মোটামুটি RED + saturation.
Golden signalsবাস্তবে নিয়মটি দাঁড়ায়: সেবার জন্য RED, তার নিচের সম্পদের জন্য USE. RED বলে ব্যবহারকারী কষ্ট পাচ্ছেন কি না (লক্ষণ); USE বলে কেন (কারণ)। নিচের টেবিলে কয়েকটি অংশের জন্য উদাহরণ:
| অংশ | ব্যবহারকারীর লক্ষণ (RED) | সম্পদের অবস্থা (USE, saturation) |
|---|---|---|
| API সেবা | route ধরে req/s, 5xx-এর অনুপাত, p50/p99 histogram | CPU, thread pool-এর queue, GC pause |
| Database | query/s, ব্যর্থ query, query latency | connection pool-এ অপেক্ষা, lock wait, replication lag (পর্ব ২২) |
| Message queue | প্রকাশ ও শেষ হওয়ার হার, DLQ-তে যাওয়া | queue depth, সবচেয়ে পুরোনো বার্তার বয়স (পর্ব ৩১), consumer lag (পর্ব ৩৩) |
| Cache | hit ratio, cache-এর পেছনে database-এর QPS | memory, eviction-এর হার, hot key |
তিনটি সূক্ষ্ম ফাঁদ:
- দ্রুত ব্যর্থতা latency-কে “ভালো” দেখায়। একটি সেবা ২ ms-এ 503 ফেরালে গড় latency কমে যায়। সফল আর ব্যর্থ অনুরোধের latency আলাদা মাপুন।
- সব error HTTP status-এ নেই। 200 OK কিন্তু ভুল ব্যালান্স — ধরতে লাগে ব্যবসায়িক metric বা বাইরে থেকে কৃত্রিম অনুরোধ পাঠানো synthetic monitoring.
- Saturation আগে আসে, ব্যর্থতা পরে। Utilisation ৮০% থেকে ৯৫%-এ গেলে queue-তে অপেক্ষা বহুগুণ বাড়ে (পর্ব ৪-এর M/M/1); queue-র দৈর্ঘ্য বা অপেক্ষার সময় তাই সবচেয়ে আগাম সতর্কতা (পর্ব ১৮)।
৭ · SLI থেকে SLO, তারপর error budget আর তার নীতি
পর্ব ৫-এ তিনটি শব্দ আর nines-এর টেবিল দেখেছেন: SLI মাপা সংখ্যা, SLO দলের লক্ষ্য, SLA গ্রাহকের সাথে চুক্তি; আর error budget = ১০০% − SLO. এখানে প্রশ্ন হলো সেগুলো যন্ত্রে বসানো যায় কীভাবে।
একটি ভালো SLI-এর নির্দিষ্ট রূপ
যেমন: (৩০০ ms-এর মধ্যে সফল
/transfer উত্তর) ÷ (সব বৈধ /transfer অনুরোধ)প্রতিটি শব্দ লিখে ঠিক করতে হয়। বৈধ কোনগুলো — ভুল PIN-এর 4xx সাধারণত গোনা হয় না, তবে হঠাৎ 4xx-এর ঢল আলাদা করে দেখা দরকার। কোথায় মাপবেন — load balancer-এ মাপলে যে সার্ভার উত্তরই দেয়নি সেটিও ধরা পড়ে। জানালা — সাধারণত চলমান ৩০ দিন, যাতে মাসের প্রথম দিনে বাজেট হঠাৎ “ভরে” না যায়।
অনুরোধ-ভিত্তিক SLO-তে বাজেট আসলে ব্যর্থ অনুরোধের সংখ্যা; পর্ব ৫-এর মিনিটগুলো পূর্ণ বিভ্রাটের সমতুল্য। নিচের ক্যালকুলেটর দুটিই দেখায়, আর সাথে burn rate: পর্যবেক্ষিত ব্যর্থতার অনুপাত ÷ অনুমোদিত অনুপাত। Burn rate ১ মানে ঠিক ৩০ দিনে বাজেট শেষ।
// Error budget over a 30-day rolling window, steady 1,000 requests per second.
const days = 30, minutes = days * 24 * 60, rps = 1000;
const requests = rps * 86400 * days;
const f = (x, d = 0) => x.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
console.log("window: " + days + " days = " + f(minutes) + " min, " + f(requests) + " requests");
console.log("SLO budget full-outage minutes failed requests allowed per day");
for (const slo of [99, 99.9, 99.95, 99.99]) {
const b = (100 - slo) / 100; // allowed bad fraction
console.log((slo + "%").padEnd(8) + ((100 - slo).toFixed(2) + "%").padStart(7) +
f(b * minutes, 2).padStart(21) + f(Math.round(b * requests)).padStart(25) +
f(Math.round(b * requests / days)).padStart(9));
}
// Burn rate = (observed bad fraction) / (allowed bad fraction). 1.0 = exactly on budget.
const slo = 99.9, b = (100 - slo) / 100;
console.log("\nSLO " + slo + "%: burn error ratio budget empty after budget used in 1 h");
for (const burn of [1, 2, 6, 14.4, 36, 1000]) {
const hoursToEmpty = days * 24 / burn;
const empty = hoursToEmpty >= 72 ? f(hoursToEmpty / 24, 1) + " days"
: hoursToEmpty >= 1 ? f(hoursToEmpty, 1) + " h" : f(hoursToEmpty * 60, 1) + " min";
const used = Math.min(100, burn / (days * 24) * 100); // a budget cannot go below empty
console.log(String(burn).padStart(13) + ((burn * b * 100).toFixed(2) + "%").padStart(13) +
empty.padStart(20) + (used.toFixed(2) + "%").padStart(20));
}আউটপুট:
window: 30 days = 43,200 min, 2,592,000,000 requests
SLO budget full-outage minutes failed requests allowed per day
99% 1.00% 432.00 25,920,000 864,000
99.9% 0.10% 43.20 2,592,000 86,400
99.95% 0.05% 21.60 1,296,000 43,200
99.99% 0.01% 4.32 259,200 8,640
SLO 99.9%: burn error ratio budget empty after budget used in 1 h
1 0.10% 30.0 days 0.14%
2 0.20% 15.0 days 0.28%
6 0.60% 5.0 days 0.83%
14.4 1.44% 50.0 h 2.00%
36 3.60% 20.0 h 5.00%
1000 100.00% 43.2 min 100.00%
ওপরের টেবিল: ৩০ দিনে ২৫৯ কোটি ২০ লাখ অনুরোধ। ৯৯.৯%-এ পূর্ণ বিভ্রাটের অনুমতি ৪৩.২ মিনিট, বা ২৫,৯২,০০০টি ব্যর্থ অনুরোধ — দিনে গড়ে ৮৬,৪০০টি। ৯৯.৯৯%-এ মাত্র ৪.৩২ মিনিট — মানুষ ল্যাপটপ খোলার আগেই শেষ।
নিচের টেবিল (SLO ৯৯.৯%): burn rate ১৪.৪ মানে error ratio ১.৪৪% — শুনতে ছোট, কিন্তু এই গতিতে ৫০ ঘণ্টায় পুরো মাসের বাজেট শেষ, আর প্রতি ঘণ্টায় খরচ হয় বাজেটের ২%. পূর্ণ বিভ্রাটে (burn ১০০০) ৪৩.২ মিনিটেই শেষ।
Error-budget policy — আগে থেকে লেখা চুক্তি
বাজেট শুধু একটি সংখ্যা হলে কাজে আসে না; দরকার একটি error-budget policy — product, development আর operations দল আগে থেকেই একমত হয়ে লেখেন, বাজেট কমলে কী হবে। SRE workbook-এর উদাহরণ নীতির আদলে একটি কাল্পনিক নীতি:
- ৫০%-এর বেশি বাকি: স্বাভাবিক — নতুন feature, পরীক্ষা, সাহসী release.
- ২৫–৫০% বাকি: সতর্ক — প্রতিটি release ধীরে, canary দিয়ে (পর্ব ৫৩), বড় বিভ্রাটের postmortem বাধ্যতামূলক।
- ২৫%-এর কম: feature freeze — শুধু নির্ভরযোগ্যতা আর নিরাপত্তার কাজ; postmortem-এর action item সবার আগে।
- শেষ: সব ঝুঁকিপূর্ণ বদল বন্ধ।
| দিন | |
| ঘটনা | |
| খরচ (ব্যর্থ অনুরোধ) | |
| বাকি বাজেট | |
| নীতি |
- দিন ৫ — স্বাভাবিক ব্যর্থতাও বাজেট খায়। ০.০২% error মানে burn rate ০.২; চলমান জানালায় এটি সবসময় বাজেটের ২০% (৫,১৮,৪০০) খেয়ে রাখে। বাকি ৮০%, নীতি সবুজ।
- দিন ৯ — একটি ভুল config. ৩০ মিনিট ৫০% ব্যর্থ: ৩০ × ৬০,০০০ × ০.৫ = ৯,০০,০০০, বাজেটের ৩৪.৭২%. বাকি ৪৫.২৮% — হলুদ: release শুধু canary দিয়ে, postmortem বাধ্যতামূলক।
- দিন ১৫ — দ্বিতীয় আঘাত। বাইরের নির্ভরতা ২৫ মিনিট বিকল, ৬০% ব্যর্থ: আবার ৯,০০,০০০. বাকি ১০.৫৬% — লাল, feature freeze. সিদ্ধান্ত কারও ব্যক্তিগত নয়; নীতি আগেই লেখা ছিল।
- দিন ২০ — অপেক্ষায় বাজেট ফেরে না। পাঁচ দিন শান্ত, তবু বাকি ১০.৫৬% — পুরোনো দুটি ঘটনা এখনো জানালায়। বাকি ২,৭৩,৬০০টি মানে মাত্র প্রায় ৪.৬ মিনিটের পূর্ণ বিভ্রাট। দল timeout আর retry ঠিক করছে (পর্ব ৪৮)।
- দিন ৩৯ — প্রথম ঘটনা জানালার বাইরে। জানালা এখন দিন ১০–৩৯; বাকি ৪৫.২৮%, হলুদ — সাবধানে release আবার শুরু।
- দিন ৪৫ — আবার সবুজ। দিন ১৫-এর ঘটনাও বাইরে, বাকি ৮০%. চলমান জানালার স্মৃতি ৩০ দিন: একটি বড় বিভ্রাট পুরো এক মাস গতি কমায় — নির্ভরযোগ্যতা আর নতুন জিনিসের গতির মধ্যে স্বয়ংক্রিয় ভারসাম্য।
১০০% SLO ভুল লক্ষ্য। ব্যবহারকারীর ফোন আর mobile network আপনার সেবার চেয়ে অনেক বেশিবার ব্যর্থ হয়; ৯৯.৯৯% আর ১০০%-এর পার্থক্য তিনি টের পান না, অথচ শেষ nine-এর খরচ বিশাল। SLO ঠিক করুন ব্যবহারকারী কতটা সহ্য করেন তা দেখে, বাকিটা বাজেট হিসেবে খরচ করুন।
৮ · Alert — burn rate, দুই জানালা, লক্ষণ আর alert fatigue
সবচেয়ে সরল alert: “গত ৫ মিনিটে error ratio ১%-এর বেশি হলে page.” সমস্যা দুই দিকে। এক মিনিটের ঝাঁকুনিতে এটি রাত ৩টায় মানুষকে জাগায়, অথচ বাজেট প্রায় খরচই হয়নি; আবার ০.৩%-এর ধীর ফুটো সীমা ছোঁয় না, অথচ দিনে বাজেটের ১০% খায়। প্রশ্নটা “error কত?” নয়, প্রশ্ন হলো “এই গতিতে বাজেট কত দ্রুত ফুরোচ্ছে?”
Burn rate থেকে alert-এর সীমা — হিসাবটি
SLO-র জানালা T (৩০ দিন = ৭২০ ঘণ্টা), burn rate b, আর w সময় ধরে চললে বাজেটের যে অংশ খরচ হয়:
“১ ঘণ্টায় বাজেটের ২%” → b = ০.০২ × ৭২০ ÷ ১ = ১৪.৪
একইভাবে “৬ ঘণ্টায় ৫%” → b = ০.০৫ × ৭২০ ÷ ৬ = ৬, আর “৩ দিনে ১০%” → b = ০.১০ × ৩০ ÷ ৩ = ১. Google-এর SRE workbook (২০১৮)-এর “Alerting on SLOs” অধ্যায় এই তিনটিকে সূচনা-বিন্দু হিসেবে সুপারিশ করে: প্রথম দুটিতে page (এখনই মানুষ দরকার), তৃতীয়টিতে ticket (কর্মদিবসে দেখলেই চলে)। সংখ্যাগুলো সূচনা-বিন্দু, আইন নয়।
কেন দুটি জানালা? শুধু ১ ঘণ্টার জানালা হলে বিভ্রাট থামার পরও প্রায় এক ঘণ্টা alert বন্ধ হয় না। তাই একটি ছোট জানালাও লাগে — সাধারণত লম্বাটির ১/১২, যেমন ৫ মিনিট — আর দুটিতেই সীমা পেরোলে তবেই page: লম্বা জানালা বলে “ক্ষতিটা অর্থবহ”, ছোটটি বলে “এখনো চলছে”। একে বলে multi-window, multi-burn-rate alert. ধরার সময়ও হিসাব করা যায়: error ratio e হলে ১ ঘণ্টার জানালা ১৪.৪-এ পৌঁছায় প্রায় ১৪.৪ × ০.০০১ × ৬০ ÷ e মিনিটে — পূর্ণ বিভ্রাটে (e = ১) প্রায় ০.৮৬ মিনিট, ৮% error-এ প্রায় ১০.৮ মিনিট।
নিচের simulator ৩০ দিনের প্রতি মিনিটের গণনা বানায় (১,০০০ req/s): স্বাভাবিক ০.০২% error, এলোমেলো কিছু এক মিনিটের ঝাঁকুনি (৮%), দিন ৯-এর ১৪:০০-তে ২০ মিনিটের একটি আসল বিভ্রাট (৮%), আর দিন ২২ থেকে ০.৩%-এর ধীর ফুটো। তারপর তিনটি নিয়ম পরীক্ষা করে: সরল সীমা, দুই জানালার page, আর ৩ দিনের ticket.
// 30 days of per-minute error counts, SLO 99.9% (allowed bad ratio 0.001). Who pages, and when?
let seed = 9; function rand() { // mulberry32: seeded PRNG
seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const D = 1440, M = 30 * D, REQ = 60000, B = 0.001; // 1,000 req/s = 60,000 per minute
const inc0 = 9 * D + 14 * 60, leak0 = 22 * D; // incident day 9 14:00 (20 min), leak from day 22
const bad = [], kind = [];
for (let m = 0; m < M; m++) {
let r = 0.0002, k = "ok"; // healthy: 0.02% errors
if (rand() < 0.0007) { r = 0.08; k = "blip"; } // 1-minute blip (pod restart), harmless
if (m >= leak0) { r = Math.max(r, 0.003); k = k === "blip" ? k : "leak"; } // slow leak: 0.3%
if (m >= inc0 && m < inc0 + 20) { r = 0.08; k = "incident"; } // real outage: 8%
bad.push(REQ * r * (0.5 + rand())); kind.push(k);
}
const pre = [0]; bad.forEach((x, m) => pre.push(pre[m] + x));
const burn = (m, w) => (pre[m + 1] - pre[Math.max(0, m + 1 - w)]) / (Math.min(w, m + 1) * REQ * B);
const rules = [["threshold: 5m error ratio > 1%", m => burn(m, 5) * B > 0.01],
["page: (1h & 5m > 14.4) or (6h & 30m > 6)", m => (burn(m, 60) > 14.4 && burn(m, 5) > 14.4) ||
(burn(m, 360) > 6 && burn(m, 30) > 6)],
["ticket: 3d & 6h burn > 1", m => burn(m, 3 * D) > 1 && burn(m, 360) > 1]];
const when = m => "day " + Math.floor(m / D) + " " + String(Math.floor(m % D / 60)).padStart(2, "0") +
":" + String(m % 60).padStart(2, "0");
const real = (m, ticket) => (m >= inc0 && m < inc0 + 80) || (ticket && m >= leak0);
for (const [name, rule] of rules) {
let on = false; const fires = [];
for (let m = 0; m < M; m++) { const x = rule(m); if (x && !on) fires.push(m); on = x; }
const good = fires.filter(m => real(m, name.startsWith("ticket")));
console.log(name + "\n fired " + fires.length + "x, false alarms " + (fires.length - good.length) +
", real: " + (good.map(when).join(", ") || "none"));
}
let m = 0; while (m < M && pre[m + 1] < B * REQ * M) m++;
console.log("blips: " + kind.filter(k => k === "blip").length + ", 30-day budget used: " +
(pre[M] / (B * REQ * M) * 100).toFixed(1) + "%, budget empty at " + (m < M ? when(m) : "never"));আউটপুট:
threshold: 5m error ratio > 1%
fired 24x, false alarms 23, real: day 9 14:00
page: (1h & 5m > 14.4) or (6h & 30m > 6)
fired 1x, false alarms 0, real: day 9 14:12
ticket: 3d & 6h burn > 1
fired 1x, false alarms 0, real: day 22 18:23
blips: 24, 30-day budget used: 102.4%, budget empty at day 29 18:25
সরল সীমা: মাসে ২৪ বার page, তার ২৩টি মিথ্যা — প্রতিটি এক মিনিটের ঝাঁকুনি, যা নিজে থেকেই সেরে গেছে। আসল বিভ্রাট ধরেছে সঙ্গে সঙ্গে (১৪:০০)। কিন্তু দিন ২২-এর ধীর ফুটো একবারও ধরেনি, অথচ সেটিই বাজেট শেষ করেছে: ৩০ দিনে খরচ ১০২.৪%, বাজেট শূন্য দিন ২৯-এর ১৮:২৫-এ।
দুই জানালার page: মাসে একবারই, এবং আসল — ১৪:১২-তে, শুরুর ১২ মিনিট পরে (সূত্রের ~১০.৮ মিনিটের কাছে; error-এর ওঠানামায় একটু বেশি)। ঝাঁকুনি ১ ঘণ্টার burn-কে ১৪.৪-এর ধারেকাছেও তোলে না। কয়েক মিনিট দেরির বিনিময়ে ২৩টি মিথ্যা page বাদ। ধীর ফুটোর ticket খুলেছে দিন ২২-এর ১৮:২৩-এ — শুরুর ১৮ ঘণ্টা ২৩ মিনিট পর, যখন বাজেট ফুরোতে আরও প্রায় সাত দিন বাকি।
লক্ষণে alert, কারণে নয়
“CPU ৯০%”, “pod restart”, “disk ৮০%” — এগুলো কারণ। CPU ৯০%-এও ব্যবহারকারী হয়তো কিছু টের পান না; আবার CPU ৩০%-এ থেকেও একটি deadlock সব অনুরোধ আটকে দিতে পারে। লক্ষণভিত্তিক alert page দেয় শুধু যখন ব্যবহারকারী কষ্ট পাচ্ছেন বা শিগগির পাবেন — SLO-র burn rate তার পরিষ্কার রূপ; কারণের সংকেত থাকে dashboard-এ। ব্যতিক্রম: “এই হারে disk ৪ ঘণ্টায় ভরবে” — ভবিষ্যৎ লক্ষণের নির্ভরযোগ্য পূর্বাভাস।
Alert fatigue বাস্তব বিপদ: যে page প্রায়ই মিথ্যা, মানুষ তা উপেক্ষা করতে শেখেন — আসল দিনেও। প্রতিটি page হবে জরুরি, পদক্ষেপযোগ্য, আর মানুষের বুদ্ধি দরকার এমন; প্রতিবার একই যান্ত্রিক কাজ লাগলে সেটি স্বয়ংক্রিয় করুন।
On-call, runbook আর dashboard
On-call মানে পালা করে একজন নির্দিষ্ট সময়ে page-এর দায়িত্বে থাকেন; টেকসই রাখতে পালায় যথেষ্ট মানুষ আর page সীমিত রাখতে হয় — SRE বই একটি ১২ ঘণ্টার পালায় দুটির বেশি ঘটনা না রাখার মতো লক্ষ্যের কথা বলে। প্রতিটি alert-এর সাথে একটি runbook: অর্থ, প্রভাব, কোন dashboard দেখবেন, সাধারণ কারণ, নিরাপদ প্রতিকার (rollback, traffic সরানো, feature flag বন্ধ), আর কাকে ডাকবেন।
Dashboard-এর ভালো ছাঁচ ওপর থেকে নিচে: SLO আর বাকি বাজেট; প্রতিটি সেবার RED; নির্ভরতা আর সম্পদের USE; আর সব গ্রাফে deploy ও config বদলের চিহ্ন, কারণ অনেক বিভ্রাট শুরু হয় একটি বদলের ঠিক পরে।
৯ · Incident response, postmortem আর observability-র খরচ
Alert বেজেছে। বড় বিভ্রাটে ভূমিকা আলাদা করা হয়: incident commander সমন্বয় আর সিদ্ধান্ত নেন (নিজে কোড ঠিক করেন না), একজন আসল পরিবর্তন করেন, আরেকজন গ্রাহক ও অন্য দলকে খবর দেন। মূলমন্ত্র: আগে প্রশমন (mitigation), পরে মূল কারণ।
| সময় | |
| সংকেত | |
| পদক্ষেপ |
- ১৪:১২ — page. দুই জানালার burn-rate alert বাজল (১ ঘণ্টায় ১৫.৭, ৫ মিনিটে ৬২.৬)। On-call দুই মিনিটে acknowledge করে একটি incident চ্যানেল খুললেন, যাতে সব সিদ্ধান্ত সময়সহ এক জায়গায় থাকে।
- ১৪:১৫ — dashboard: কোথায়? Runbook-এর প্রথম ধাপ: RED আর সাম্প্রতিক বদল। শুধু payment-এর 5xx ৮%-এ, বাকিরা স্বাভাবিক; deploy-এর চিহ্ন ১৩:৫৮-তে, payment v2.15.
- ১৪:১৭ — trace আর log: কেন? Exemplar থেকে একটি ধীর trace: payment থেকে ব্যাংকের span ২,০০৪ ms-এ timeout. সেই trace id-এর log-এ “pool exhausted (max=20)” — নতুন সংস্করণে pool-সীমা কমেছে বলে আন্দাজ, প্রশমনের জন্য যথেষ্ট।
- ১৪:১৯ — প্রশমন আগে। Runbook-এর ধাপ ৩: সন্দেহজনক release হলে rollback. v2.14-এ ফেরার পর error ০.০২%-এ ফিরল; গভীর তদন্ত পরে।
- ১৪:২৩ — alert বন্ধ, তারপর incident শেষ। ৫ মিনিটের জানালা ১৪.৪-এর নিচে নামতেই alert বন্ধ — ছোট জানালার সুফল। পর্যবেক্ষণের পর incident শেষ ঘোষণা, status পাতা হালনাগাদ। খরচ প্রায় ৯০,০০০ ব্যর্থ অনুরোধ, বাজেটের প্রায় ৩.৫%.
- পরের সপ্তাহ — blameless postmortem. টাইমলাইন, প্রভাব, অবদান রাখা কারণ (পর্যালোচনায় pool-সীমা চোখ এড়িয়েছে, canary ছিল না, pool-এর saturation metric ছিল না), আর মালিক ও তারিখসহ action item. প্রশ্ন ছিল ব্যবস্থা কেন একজন মানুষকে এই ভুল করতে দিল, কে ভুল করল তা নয়।
Blameless postmortem-এর যুক্তি সরল: মানুষকে শাস্তি দিলে পরের বার তাঁরা ঘটনা লুকান, আর প্রতিষ্ঠান শেখে না। বিমান চলাচলের নিরাপত্তা-সংস্কৃতির এই চিন্তা সফটওয়্যারে জনপ্রিয় হয় John Allspaw-এর ২০১২ সালের একটি লেখার মতো আলোচনায়। Postmortem, failover-এর অনুশীলন আর দুর্যোগ থেকে ফেরা — পর্ব ৫১; এই metric দিয়েই canary analysis নতুন সংস্করণকে পুরোনোটির পাশে তুলনা করে — পর্ব ৫৩।
Observability-র খরচ — নকশার অংশ
Observability বিনামূল্যে নয়: storage, network, index আর instrumentation-এর CPU — বড় প্রতিষ্ঠানে এর বিল অবকাঠামোর উল্লেখযোগ্য অংশ হতে পারে।
| সংকেত | খরচ বাড়ে | হাতল |
|---|---|---|
| Log | ঘটনার সংখ্যা × লাইনের আকার × retention | level, sampling, প্রতি অনুরোধে একটি লাইন, স্তরভিত্তিক retention, index না করা ঘর |
| Metric | series-এর সংখ্যা (cardinality) × scrape-এর ঘনত্ব | label-এর সীমা, histogram-এর কম bucket, downsampling, কম দরকারি metric বাদ |
| Trace | অনুরোধ × span × sampling-এর হার | head/tail sampling, span কম, attribute ছোট |
ছয় মাসে কেউ না খোলা dashboard আর কোনো প্রশ্নে না লাগা metric বাদ দিন।
আরও গভীরে: Google-এর Dapper paper (Sigelman ও সহকর্মীরা, ২০১০) দেখায়, খুব কম sampling হারেও (কিছু সেবায় হাজারে এক) বড় ব্যবস্থার latency-র কারণ বোঝা যায়, আর instrumentation-কে সাধারণ RPC লাইব্রেরির ভেতরে রাখলে application কোড না বদলেই প্রায় সব সেবা ঢেকে যায়। Burn-rate alert-এর বিকল্পগুলোর precision, recall, ধরার আর reset সময়ের তুলনা আছে SRE workbook-এর “Alerting on SLOs” অধ্যায়ে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- ৩০ দিনে ৯৯.৯৫% SLO-তে error budget কত মিনিট? ০.০০০৫ × ৪৩,২০০ = ২১.৬ মিনিট।
- দশটি সার্ভারের p99-এর গড় কি সেবার p99? না — percentile রৈখিক নয়; bucket-এর গণনা যোগ করে তারপর percentile.
- Burn rate ১৪.৪ কোথা থেকে? ১ ঘণ্টায় বাজেটের ২%: ০.০২ × ৭২০ ÷ ১ = ১৪.৪; সাথে ৫ মিনিটের ছোট জানালা দ্রুত reset-এর জন্য।
- Metric-এ user_id label কেন নয়? প্রতিটি মান একটি নতুন series — cardinality বিস্ফোরণ; এটি log বা trace-এর attribute.
- Head আর tail sampling-এর পার্থক্য? শুরুতে এলোমেলো সিদ্ধান্ত (সস্তা, বিরল error হারায়) বনাম trace শেষে collector-এ বাছাই (error রাখে, কিন্তু trace id ধরে routing লাগে)।
১০ · AI-এর যুগে — observability-র নকশা যাচাই
Monitoring-এর নকশা চাইলে AI প্রায়ই একটি তালিকা দেয়: “Prometheus, Grafana, ELK, Jaeger বসান; CPU ৮০%-এ alert; সব log রাখুন।” SLO নেই, কে কখন জাগবেন বলা নেই, খরচের হিসাব নেই, আর CPU-র alert লক্ষণ নয়। খসড়া AI দিতে পারে; কোনটি মাপবেন আর কেন — সেই বিচার আপনার।
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| সার্ভারগুলোর p99-এর গড় দিয়ে সেবার p99 বা SLO | Bucket যোগ করে নিজে হিসাব; অংশ ৪-এর simulator-এর মতো ছোট পরীক্ষা |
| Error budget-এর ভুল গুণ (যেমন ৯৯.৯৫%-এ “৪৩ মিনিট”) | (১ − SLO) × জানালার মিনিট নিজে করুন; অনুরোধে আর মিনিটে দুটিই |
| কারণে page: CPU, memory, pod restart | প্রতিটি page-এর জন্য জিজ্ঞেস করুন: ব্যবহারকারী কি এখন কষ্ট পাচ্ছেন? |
| user_id, request_id-কে metric label বানানো | Series = label-মানের গুণফল; হিসাব করুন |
| Log-এ পুরো request body বা token লেখার কোড | Allowlist, masking, আর গোপন তথ্যের জন্য স্বয়ংক্রিয় স্ক্যান |
| পণ্যের সীমা বা দাম বানিয়ে বলা | অফিসিয়াল ডকুমেন্টেশন আর আপনার সংস্করণ; দাম বিক্রেতার পাতায় |
AI-কে নকশাকার নয়, সমালোচক আর হিসাবের সহকারী হিসেবে ব্যবহার করুন, আর অনুমান স্পষ্ট করতে বাধ্য করুন:
AI-এর observability নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১১০০% নির্ভরযোগ্যতা কেন ভুল লক্ষ্য, আর error budget কীভাবে development আর operations-এর পুরোনো টানাপোড়েন মেটায়?
বহু বছর ধরে সফটওয়্যার প্রতিষ্ঠানে দুটি দলের লক্ষ্য ছিল উল্টো। Development দলের পুরস্কার নতুন feature দ্রুত ছাড়ায়; operations দলের পুরস্কার কিছু না ভাঙায় — আর সবচেয়ে নিশ্চিত উপায় হলো কিছুই না বদলানো।
Google-এ Ben Treynor Sloss ২০০৩ সালের দিকে যে Site Reliability Engineering দল গড়েন, তার একটি মূল ধারণা — যা পরে ২০১৬ সালের SRE বইয়ে বিস্তারিত — হলো নির্ভরযোগ্যতাকে একটি পরিমাপযোগ্য বাজেট বানানো। প্রথম ধাপ: ১০০% অর্থহীন। ব্যবহারকারী আপনার সেবা পান তাঁর ফোন, mobile network, বাড়ির Wi-Fi আর ISP পেরিয়ে; সেগুলোর নিজের ব্যর্থতা আপনার ৯৯.৯৯%-এর চেয়ে অনেক বেশি। ৯৯.৯৯% থেকে ১০০%-এ গেলে তিনি পার্থক্য টের পান না, কিন্তু আপনার খরচ বাড়ে বহুগুণ — বাড়তি region, বাড়তি মানুষ, আর প্রায় শূন্য পরিবর্তন।
দ্বিতীয় ধাপ: SLO-র নিচের ফাঁকটুকু (৯৯.৯%-এ ০.১%) “খরচ করার” জিনিস। বাজেট বাকি থাকলে development দল নিজেরাই ঝুঁকি নিতে পারেন — কারও অনুমতি লাগে না। বাজেট শেষ হলে release থামে — এটিও কারও ব্যক্তিগত সিদ্ধান্ত নয়, আগে লেখা নীতি। ফলে তর্কটা মতামত থেকে সংখ্যায় সরে যায়: “আপনার release ঝুঁকিপূর্ণ” নয়, বরং “এই মাসে বাজেটের ৮০% গেছে”।
সীমাবদ্ধতাও আছে। SLO যদি ভুল জিনিস মাপে (যেমন শুধু load balancer-এর 5xx, অথচ app-এ ভুল ব্যালান্স দেখাচ্ছে), বাজেট মিথ্যা নিরাপত্তা দেয়। আর ছোট দলে কঠোর freeze ব্যবসার ক্ষতি করতে পারে; নীতি তাই আলোচনা করে বানাতে হয়।
মূল উপলব্ধি: নির্ভরযোগ্যতা “যত বেশি তত ভালো” নয়, এটি একটি পণ্য-সিদ্ধান্ত। SLO ঠিক করে কতটা যথেষ্ট, আর error budget সেই সিদ্ধান্তকে দুই দলের ভাগ করা, সংখ্যায় লেখা একটি চুক্তিতে পরিণত করে।
প্র ০২Dapper থেকে OpenTelemetry — distributed tracing-এর ইতিহাস কেন শেষ পর্যন্ত একটি বিক্রেতা-নিরপেক্ষ মানদণ্ডে পৌঁছাল?
Google ২০১০ সালে Dapper নামের নিজের tracing ব্যবস্থার বর্ণনা প্রকাশ করে (Benjamin Sigelman ও সহকর্মীরা)। এর তিনটি নকশা-সিদ্ধান্ত প্রভাবশালী হয়: trace id আর parent id-এর গাছ; খুব কম sampling হার, যাতে production-এ খরচ নগণ্য থাকে; আর instrumentation-কে সাধারণ RPC ও threading লাইব্রেরির ভেতরে বসানো, যাতে প্রতিটি দলকে নিজের কোড বদলাতে না হয়। শেষেরটিই সবচেয়ে গুরুত্বপূর্ণ — trace-এর মূল্য তখনই, যখন পথের প্রতিটি সেবা অংশ নেয়; একটি সেবা বাদ পড়লে সুতো সেখানেই ছিঁড়ে যায়।
এরপর open-source জগতে এল Twitter-এর Zipkin (২০১২ সালে উন্মুক্ত), পরে Uber-এর Jaeger. সমস্যা দেখা দিল অন্য জায়গায়: প্রতিটি ব্যবস্থার নিজের header, নিজের লাইব্রেরি, নিজের ডেটা-আকার। ফলে লাইব্রেরির লেখকেরা প্রায়ই কোনোটির জন্যই instrumentation লিখতেন না, আর প্রতিষ্ঠান একবার একটি বিক্রেতা বেছে নিলে বদলানো মানে সব সেবার কোড বদলানো — vendor lock-in.
দুটি প্রতিযোগী মানদণ্ডের চেষ্টা হয় — OpenTracing (শুধু API) আর Google-সমর্থিত OpenCensus (API + বাস্তবায়ন + metric) — আর দুই শিবিরে ভাগ হওয়া সম্প্রদায় ২০১৯ সালে মিলে বানায় OpenTelemetry. প্রায় একই সময়ে W3C Trace Context (২০২০ সালে Recommendation) header-টিকে মানক করে, যাতে ভিন্ন বিক্রেতার সেবাও একই trace-এ যুক্ত হতে পারে। এখন একটি লাইব্রেরি একবার OpenTelemetry API-তে instrument করলেই যেকোনো backend-এ ডেটা যায়; বিক্রেতা বদলানো মানে collector-এর config বদলানো।
এটি একটি পুরোনো নকশা-শিক্ষার পুনরাবৃত্তি: M জন উৎপাদক আর N জন গ্রাহকের মাঝে একটি সাধারণ interface বসালে M × N সংযোগ M + N-এ নামে। তবে মানদণ্ডেরও দাম আছে: বড় specification আর ভাষাভেদে অসম পরিপক্বতা।
মূল উপলব্ধি: Tracing-এর মূল্য নেটওয়ার্ক-প্রভাবের মতো — যত বেশি সেবা একই সুতো বহন করে, তত বেশি কাজের। তাই এখানে প্রযুক্তির চেয়ে বড় বিজয় ছিল মানদণ্ডে একমত হওয়া।
প্র ০৩Metric সস্তা কিন্তু “অন্ধ”, log দামি কিন্তু বিস্তারিত — উচ্চ cardinality-র প্রশ্নের উত্তর তাহলে কোথায় রাখবেন?
Metric-এর শক্তি তার সংক্ষেপে: লাখো ঘটনা একটি সংখ্যায়। দুর্বলতাও সেখানে — যে মাত্রা label হিসেবে আগে থেকে রাখা হয়নি, তা দিয়ে পরে ভাগ করা যায় না। আর label-এ যা রাখা যায়, তা সীমিত-মানের হতে হয়: route, status, region. অথচ বাস্তবের কঠিন প্রশ্নগুলো প্রায়ই উচ্চ-cardinality: “কোন ব্যবহারকারী?”, “কোন merchant-এর লেনদেন?”, “কোন build hash?”. Metric-এ এগুলো রাখলে series-এর বিস্ফোরণ (অংশ ৩-এর ১,৫০০ কোটি)।
প্রচলিত উত্তর ছিল log খোঁজা, কিন্তু তা দামি আর ধীর: “গত এক ঘণ্টায় merchant ধরে p99” বের করতে কোটি লাইন পড়তে হয়।
গত কয়েক বছরে কিছু প্রকৌশলী — Charity Majors-এর মতো কয়েকজন যাঁর লেখায় এই চিন্তা জনপ্রিয় — একটি তৃতীয় পথের কথা বলেন: প্রতি অনুরোধে একটি “wide event”, অর্থাৎ অনেক ঘরের (কয়েক ডজন থেকে শত) একটি structured record, যা column store-এ রাখা হয় (পর্ব ১৯)। Column store যেকোনো ঘর ধরে দ্রুত group আর filter করতে পারে, তাই উচ্চ-cardinality প্রশ্নও সেকেন্ডে চলে; আর metric, এমনকি trace-এর অনেকটা, এই event থেকেই বের করা যায়। এই দৃষ্টিভঙ্গিকে কেউ কেউ “observability 2.0” বলেন; নামটি বিপণনেরও, তাই সাবধানে নিন।
দামের হিসাব এখানেও আছে। প্রতিটি অনুরোধের wide event রাখা ব্যয়বহুল, তাই এখানেও নমুনা রাখা হয়। আর সতর্কতার জন্য সস্তা, স্থির, নির্ভুল গণনা — অর্থাৎ metric — এখনো সেরা: alert নমুনার ওপর দাঁড়ালে বিরল ঘটনা ফসকে যেতে পারে।
মূল উপলব্ধি: প্রশ্নের ধরন দিয়ে সংকেত বাছুন। জানা প্রশ্ন আর alert — metric. অজানা, উচ্চ-cardinality প্রশ্ন — structured event বা trace-এর attribute, column store-এ. আর সব জায়গায় একই trace id, যাতে এক থেকে আরেকে লাফ দেওয়া যায়।
প্র ০৪“লক্ষণে alert, কারণে নয়” — নিয়মটি কোথা থেকে এল, আর কখন এটি ভাঙা ঠিক?
Google-এর SRE Rob Ewaschuk ২০১৩ সালের দিকে “My Philosophy on Alerting” নামে একটি লেখায় নিয়মটি স্পষ্টভাবে তোলেন, যা পরে SRE বইয়ের monitoring অধ্যায়ের ভিত্তি হয়। মূল যুক্তি: একটি ব্যবস্থায় সম্ভাব্য কারণ অসংখ্য — প্রতিটি disk, প্রতিটি process, প্রতিটি নির্ভরতা — কিন্তু ব্যবহারকারীর লক্ষণ মাত্র কয়েকটি: সেবা পাওয়া যাচ্ছে না, ধীর, ভুল ফল। কারণের জন্য alert বানালে দুই রকম ভুল একসাথে হয়: অনেক কারণ লক্ষণ ঘটায় না (CPU ৯০% কিন্তু সব ঠিক — মিথ্যা page), আর অনেক লক্ষণের কারণ আগে কেউ ভাবেনি (যার কোনো alert নেই — ফসকে যাওয়া বিভ্রাট)। লক্ষণে alert দিলে দুটিই কমে।
Alert fatigue-এর ঝুঁকি শুধু সফটওয়্যারে নয়। হাসপাতালের যন্ত্রের অবিরাম বিপ-বিপ শব্দে চিকিৎসাকর্মীদের সতর্কবার্তার প্রতি অসাড় হয়ে পড়া একটি পরিচিত নিরাপত্তা-সমস্যা হিসেবে আলোচিত; সফটওয়্যারের on-call-এও একই মনস্তত্ত্ব। প্রতিটি মিথ্যা page পরের আসল page-এর বিশ্বাসযোগ্যতা কমায়।
তবে নিয়মটি অন্ধভাবে মানার নয়। তিনটি জায়গায় কারণ-ভিত্তিক সতর্কতা যুক্তিসঙ্গত: (১) নিশ্চিত ভবিষ্যৎ লক্ষণ — disk-এর বাড়ার হার দেখে “৪ ঘণ্টায় ভরে যাবে”, বা TLS certificate “৭ দিনে মেয়াদ শেষ”; ব্যবহারকারী এখনো কষ্ট পাচ্ছেন না, কিন্তু পাবেন নিশ্চিত। (২) Redundancy হারানো — তিনটি replica-র একটি মরলে সেবা ঠিক আছে, কিন্তু আর একটি মরলে বিপদ; এটি ticket, page নয়। (৩) লক্ষণ মাপা অসম্ভব বা দেরিতে আসে এমন ক্ষেত্রে, যেমন রাতের batch job — সেখানে “কাজটি সময়মতো শুরু হয়নি” নিজেই লক্ষণ।
মূল উপলব্ধি: Page হলো একজন মানুষের ঘুম আর মনোযোগ খরচ করা। সেটি খরচ করুন শুধু তখন, যখন ব্যবহারকারী কষ্ট পাচ্ছেন বা নিশ্চিত পাবেন; বাকি সব dashboard বা ticket-এর জায়গা।
প্র ০৫Log কি শুধু সহায়ক তথ্য, নাকি নিজেই একটি ঝুঁকি? গোপনীয়তা, নিরাপত্তা আর retention নিয়ে নকশায় কী ভাববেন?
সাধারণভাবে log-কে নিরীহ ভাবা হয় — শুধু লেখা, পড়া হয় কালেভদ্রে। কিন্তু তিনটি কারণে log একটি বড় ঝুঁকির জায়গা।
এক, ফাঁস। Log অনেক মানুষ পড়েন (developer, support, বাইরের বিক্রেতা), অনেক জায়গায় কপি হয় (index, backup, laptop-এ ডাউনলোড), আর মূল database-এর চেয়ে প্রায়ই কম সুরক্ষিত। “debug-এর জন্য” পুরো অনুরোধ log করলে পাসওয়ার্ড, token, OTP, কার্ড নম্বর সেখানে চলে যায়। প্রতিরক্ষা: allowlist (কোন ঘর log হবে তা লিখে রাখা), গোপন তথ্যের ধরন চিনে collector-এ মুছে ফেলা, আর log-এ প্রবেশাধিকারের কড়া নিয়ন্ত্রণ (পর্ব ৫২)।
দুই, log-এর লাইব্রেরিও আক্রমণের পথ। ২০২১ সালের ডিসেম্বরে Java-র বহুল ব্যবহৃত logging লাইব্রেরি Log4j-এ “Log4Shell” নামে একটি গুরুতর দুর্বলতা প্রকাশ পায়: কিছু সংস্করণে log করা লেখার ভেতরের একটি বিশেষ ছাঁচ লাইব্রেরিকে বাইরের সার্ভার থেকে কোড আনতে প্ররোচিত করতে পারত। শিক্ষা: log লাইব্রেরিও নিরাপত্তা-হালনাগাদের তালিকায় রাখতে হয়, আর বাইরের ইনপুটকে log-এ সবসময় সাধারণ লেখা হিসেবেই রাখতে হয়।
তিন, retention মানে দায়। যত বেশিদিন রাখবেন, ফাঁস হলে ক্ষতি তত বড়, আর ব্যক্তিগত তথ্যের আইন (যেমন ইউরোপের GDPR-এর data minimization নীতি) প্রয়োজনের বেশি রাখাকেই সমস্যা বলে। অন্যদিকে কিছু ক্ষেত্রে আইন নির্দিষ্ট সময় রাখতে বাধ্য করে, যেমন আর্থিক লেনদেনের audit. তাই সমাধান সব রেখে দেওয়া বা কিছুই না রাখা নয়: debug log অল্প কয়েক দিন, metric বহু মাস (তাতে ব্যক্তিগত তথ্য থাকে না), audit log আইন অনুযায়ী, আলাদা ও পরিবর্তন-রোধী জায়গায়। কোন খাতে কী রাখা বাধ্যতামূলক, তা নিয়ন্ত্রকের নিয়মে দেখুন।
মূল উপলব্ধি: প্রতিটি log লাইন এমনভাবে লিখুন যেন সেটি একদিন ভুল মানুষের হাতে পড়তে পারে। কম তথ্য, ঢাকা তথ্য, কম সময় — observability আর গোপনীয়তা একসাথে রক্ষা করার এটিই পথ।
অনুশীলন
খাতা-কলম নিয়ে বসুন; হিসাবগুলো নিজে করে তারপর উত্তর মেলান।
-
Error budget-এর হিসাব: একটি কাল্পনিক টিকিট-বুকিং API গড়ে ৫০০ req/s পায়, SLO ৩০ দিনের চলমান জানালায় ৯৯.৯৫%. (ক) বাজেট অনুরোধে আর পূর্ণ-বিভ্রাটের মিনিটে কত? (খ) ১২ মিনিট ধরে ৩০% অনুরোধ ব্যর্থ হলে বাজেটের কত অংশ গেল? (গ) “৬ ঘণ্টায় বাজেটের ৫%” নিয়মের burn rate আর error ratio-র সীমা কত?
(ক) ৩০ দিনে অনুরোধ = ৫০০ × ৮৬,৪০০ × ৩০ = ১,২৯,৬০,০০,০০০ (১২৯ কোটি ৬০ লাখ)। অনুমোদিত ব্যর্থ অংশ ১ − ০.৯৯৯৫ = ০.০০০৫, তাই বাজেট = ৬,৪৮,০০০টি অনুরোধ। মিনিটে: ০.০০০৫ × ৪৩,২০০ = ২১.৬ মিনিট — অনুরোধের হারের ওপর নির্ভর করে না।
(খ) ১২ মিনিটে অনুরোধ = ১২ × ৬০ × ৫০০ = ৩,৬০,০০০; তার ৩০% = ১,০৮,০০০টি ব্যর্থ। বাজেটের অংশ = ১,০৮,০০০ ÷ ৬,৪৮,০০০ = ১/৬ ≈ ১৬.৬৭%.
(গ) b = ০.০৫ × ৭২০ ÷ ৬ = ৬. Error ratio-র সীমা = ৬ × ০.০৫% = ০.৩%. অর্থাৎ ৬ ঘণ্টার গড় error ০.৩%-এর বেশি (আর ছোট ৩০ মিনিটের জানালাতেও) হলে page.
-
একটি alert-তালিকার সমালোচনা: একটি দলের page-তালিকা: (১) যেকোনো সার্ভারে CPU > ৮০% ৫ মিনিট; (২) যেকোনো ERROR log লাইন; (৩) p99 > ৩০০ ms, ১ মিনিটের জানালায়; (৪) disk > ৯০%; (৫) metric
login_attempts_total{user_id, ip}-এর ওপর “একজন ব্যবহারকারী ১০ বারের বেশি”. দলটি সপ্তাহে প্রায় ৬০টি page পায়। কী কী সমস্যা, আর আপনার নকশা কী?- (১) CPU কারণ, লক্ষণ নয় — dashboard-এ (USE), page নয়।
- (২) প্রতিটি ERROR লাইন — সবচেয়ে বড় শব্দদূষণ; বদলে ব্যর্থ অনুরোধের অনুপাতের SLI.
- (৩) p99 > ৩০০ ms, ১ মিনিট — ১ মিনিটের p99 খুব ওঠানামা করে, বাজেটের সাথে যুক্তও নয়। বদলে latency SLI (“৩০০ ms-এর মধ্যে সফল উত্তরের অনুপাত”, histogram-এর bucket থেকে) আর তার burn rate.
- (৪) disk ৯০% — বড় disk-এ সপ্তাহ চলতে পারে, ছোটটিতে মিনিট। বদলে “বর্তমান হারে ৪ ঘণ্টায় ভরবে” ধরনের পূর্বাভাস; কয়েক দিনের হলে ticket.
- (৫) user_id আর ip label — cardinality বিস্ফোরণ, metric database-ই বিপদে পড়বে। ব্রুট-ফোর্স ঠেকানো নিরাপত্তা আর rate limiting-এর কাজ (পর্ব ৪৯, পর্ব ৫২); metric-এ শুধু মোট ব্যর্থ login-এর হার, বিস্তারিত log-এ।
নতুন নকশা: page শুধু দুই SLO-র (availability ও latency) multi-window burn rate-এ — ১ ঘণ্টা ও ৫ মিনিটে ১৪.৪, ৬ ঘণ্টা ও ৩০ মিনিটে ৬; ticket ৩ দিন ও ৬ ঘণ্টায় ১, আর disk/certificate-এর পূর্বাভাসে। প্রতিটি page-এর runbook.
-
আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড দুই লাইনে ঠিক কী ছাপবে, আর কেন।
const a = [10, 10, 10, 10, 1000]; // server A latencies, ms const b = [20, 20, 20, 20, 20]; // server B latencies, ms const p90 = xs => [...xs].sort((x, y) => x - y)[Math.ceil(0.9 * xs.length) - 1]; console.log(p90(a), p90(b), (p90(a) + p90(b)) / 2, p90([...a, ...b])); const c = [100, 160, 220, 15, 75]; // a counter, scraped every 60 s let inc = 0; for (let i = 1; i < c.length; i++) inc += c[i] >= c[i - 1] ? c[i] - c[i - 1] : c[i]; console.log((c[4] - c[0]) / 240, inc / 240);1000 20 510 20 -0.10416666666666667 0.8125প্রথম লাইন: ৫টি মানের p90-এর স্থান ⌈০.৯ × ৫⌉ = ৫, অর্থাৎ সবচেয়ে বড়টি — A-তে 1000, B-তে 20. দুটির গড় 510. কিন্তু মিলিত ১০টি মানে স্থান ⌈০.৯ × ১০⌉ = ৯; সাজালে নবমটি 20 (একটিমাত্র 1000 দশম স্থানে)। আসল p90 = 20, গড় বলছে 510 — পঁচিশ গুণ ভুল।
দ্বিতীয় লাইন: চতুর্থ scrape-এ counter 220 থেকে 15-এ নেমেছে — প্রক্রিয়া restart হয়েছে। সরল হিসাব (শেষ − প্রথম) ÷ ২৪০ s ঋণাত্মক হার দেয়, যা অর্থহীন। Reset বুঝে হিসাব: ৬০ + ৬০ + ১৫ (নতুন শুরু থেকে) + ৬০ = ১৯৫, ÷ ২৪০ = 0.8125 প্রতি সেকেন্ডে। Prometheus-ধরনের
rate()এভাবেই reset সামলায়। -
Trace sampling-এর খরচ: একটি সেবায় ২,০০০ req/s, প্রতিটি trace-এ গড়ে ৩০টি span, প্রতিটি span ৫০০ byte. (ক) সব রাখলে দিনে কত? (খ) ১% head sampling-এ? (গ) Tail sampling-এ সব error (০.৫%), সব ধীর (১%) আর স্বাভাবিকের ০.১% রাখলে (ধরে নিন তিন দল আলাদা), দিনে কত, আর collector-কে কী সামলাতে হবে যদি সিদ্ধান্তের আগে ১০ সেকেন্ড অপেক্ষা করতে হয়?
(ক) ২,০০০ × ৩০ × ৫০০ = ৩,০০,০০,০০০ byte = ৩০ MB প্রতি সেকেন্ডে; দিনে ৩০ × ৮৬,৪০০ = ২৫,৯২,০০০ MB ≈ ২.৫৯ TB.
(খ) ১%: দিনে প্রায় ২৫.৯২ GB. কিন্তু ০.৫% error-এর মাত্র ১% রাখা হয় — প্রতি সেকেন্ডের ১০টি error-trace থেকে গড়ে ০.১টি, অর্থাৎ প্রায় ১০ সেকেন্ডে একটি।
(গ) রাখার অংশ ০.৫% + ১% + ০.১% = ১.৬%: দিনে ২.৫৯২ TB × ০.০১৬ ≈ ৪১.৫ GB — head-এর চেয়ে মাত্র ৬০% বেশি, অথচ সব error আর ধীর trace সহ। দাম অন্য জায়গায়: সব span (৩০ MB/s) collector পর্যন্ত আসে, আর ১০ সেকেন্ড ধরে রাখতে প্রায় ৩০০ MB memory লাগে (overhead বাদে)। একটি trace-এর সব span একই collector-এ পৌঁছাতে হবে, তাই সামনে trace id ধরে consistent hashing-এর একটি load balancing স্তর (পর্ব ১৬)।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি incident-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Burn-rate alert বাজে, on-call acknowledge করেন
- Dashboard-এ RED আর সাম্প্রতিক বদল দেখে কোন সেবা, তা খোঁজা
- Trace আর log দিয়ে সম্ভাব্য কারণ বোঝা
- প্রশমন — rollback বা traffic সরানো
- পর্যবেক্ষণের পর incident শেষ ঘোষণা
- Blameless postmortem আর action item
তিনটি সার্ভারের p99 যথাক্রমে ৪৫ ms, ৮০ ms আর ২,০৮১ ms. পুরো সেবার p99 সম্পর্কে কোন কথাটি নিশ্চিতভাবে সত্য?
- তিনটির গড়, প্রায় ৭৩৫ ms
- ৪৫ আর ২,০৮১ ms-এর মাঝে কোথাও; ঠিক মান জানতে bucket-গণনা লাগবে
- ঠিক ২,০৮১ ms, কারণ সবচেয়ে ধীর সার্ভারই ঠিক করে
- অনুরোধ-সংখ্যা দিয়ে ভারযুক্ত গড়, যা সবসময় নির্ভুল
৩০ দিনের চলমান জানালায় SLO ৯৯.৯%, সেবা স্থিরভাবে ১,০০০ req/s পায়। Error budget অনুরোধের হিসাবে কত?
- ২,৫৯,২০০টি
- ১২,৯৬,০০০টি
- ২৫,৯২,০০০টি
- ২,৫৯,২০,০০০টি
৩০ দিনের SLO-তে “১ ঘণ্টায় বাজেটের ২% খরচ হলে page” নিয়মের burn rate সীমা কত, আর SLO ৯৯.৯% হলে কোন error ratio-তে?
- ১৪.৪, অর্থাৎ ১.৪৪% error
- ২, অর্থাৎ ০.২% error
- ৬, অর্থাৎ ০.৬% error
- ৭২০, অর্থাৎ ৭২% error
একটি metric-এ label route (২০টি মান), status (৫টি), pod (৫০টি)। একজন developer user_id label (১০ লাখ ব্যবহারকারী) যোগ করতে চান। সবচেয়ে বড় সমস্যা কী?
- Scrape একটু ধীর হবে, তার বেশি কিছু নয়
- শুধু গোপনীয়তার সমস্যা, প্রযুক্তিগতভাবে ঠিক আছে
- Counter আর বাড়তে পারবে না
- Time series-এর সংখ্যা বহুগুণ বেড়ে cardinality বিস্ফোরণ
HTTP header traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-এ 00f067aa0ba902b7 অংশটি কী?
- পুরো অনুরোধের trace id
- পাঠানো span-এর id, অর্থাৎ নতুন span-এর parent
- Sampling-এর সিদ্ধান্ত
- Header-এর version
কোন alert-টি “লক্ষণে alert” নীতির সবচেয়ে কাছাকাছি?
- যেকোনো সার্ভারে CPU ৯০%-এর বেশি
- একটি pod restart হলে
- Checkout-এর availability SLO-র burn rate ১ ঘণ্টা ও ৫ মিনিট দুই জানালায় ১৪.৪-এর বেশি
- Log-এ যেকোনো ERROR লাইন দেখা দিলে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Monitoring জানা প্রশ্নের উত্তর দেয়; observability খুঁটিনাটি রেখে অজানা প্রশ্নও করতে দেয়। Log, metric, trace (সাথে profile, event) — সবগুলোকে জোড়ে একই trace id.
- Log structured, level আর sampling-সহ, খরচ আর retention হিসাব করে; গোপন তথ্য আর খোলা PII কখনো নয়. Metric-এ counter, gauge, histogram; label সীমিত — series = label-মানের গুণফল.
- Percentile-এর গড় হয় না; bucket যোগ করে percentile, আর SLO-র সীমায় একটি bucket. Trace-এ span-এর গাছ,
traceparent-এ propagation, critical path, head বনাম tail sampling. - সেবায় RED, সম্পদে USE; SLI = ভালো ÷ বৈধ; ৩০ দিনে ৯৯.৯% = ৪৩.২ মিনিট; বাজেটের নিয়ম আগে থেকে লেখা policy.
- Alert লক্ষণে, burn rate-এ, দুই জানালায় (১ ঘণ্টা ও ৫ মিনিটে ১৪.৪); আগে প্রশমন, পরে কারণ; তারপর blameless postmortem.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫১ · Failover আর disaster recovery — backup, RPO-RTO আর multi-regionপরবর্তী পর্ববড় বিপর্যয় থেকে ফেরার পরিকল্পনা, অনুশীলন আর postmortem.
- পর্ব ০৫ · Availability আর reliability — nines, redundancy, single point of failure আর MTTRভিত্তিNines-এর টেবিল আর SLI, SLO, SLA-র প্রথম পরিচয়।
- পর্ব ০৪ · Latency, throughput আর percentile — p99, tail latency আর Little-এর সূত্রভিত্তিPercentile, histogram আর tail latency-র গণিত।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Operating Systems · পারফরম্যান্স আর observabilityগভীরে যানএকটি সার্ভারের ভেতরে top, vmstat, perf দিয়ে USE-ধরনের তদন্ত।
- Computer Networks · Network troubleshootingপাশের ট্র্যাকLatency আর packet loss-এর উৎস network-এ খোঁজার পদ্ধতি।
- DBMS ও SQL · Performance tuningপাশের ট্র্যাকধীর query খোঁজা আর মাপা — database-এর দিক থেকে observability.