Part 03 · High Level Design
Observability: Logs, Metrics and Traces
User বলার আগেই জেনে যাওয়া যে কিছু ভেঙেছে: structured log, কাজের metric, distributed trace, CPU না দেখে SLO দেখে alert, আর এমন health check যেটার উপর load balancer ভরসা করতে পারে।
⏱ 2-3 ঘণ্টা · Station 31 / 80
Customer রা আগে জানল
একটা online pharmacy র on-call phone সারারাত বাজে: "CPU 80% এর উপরে", "CPU 80% এর উপরে", "disk 75% ভরা"। Engineer প্রতিবার দেখে, user দের কোনো সমস্যা পায় না, আবার ঘুমিয়ে পড়ে। কয়েক সপ্তাহ পর ওই alert গুলো আর কেউ মন দিয়ে পড়ে না।
তারপর এক বৃহস্পতিবার বিকেলে 1.5% checkout fail করতে শুরু করল, কারণ নতুন একটা fraud-check rule timeout হচ্ছে। CPU কম, কারণ fail করা request সস্তা। Disk ঠিক আছে। একটাও alert বাজল না। Team জানল দুই ঘণ্টা পর, রাগী Facebook post থেকে। Dashboard এর গড় latency স্বাভাবিক দেখাচ্ছে, কারণ 98.5% request দ্রুত। আর যখন শেষ পর্যন্ত খুঁজতে বসল, log হলো বারোটা service এর free-text line, কোন line গুলো একই customer এর checkout এর সেটা বোঝার কোনো উপায় নেই।
Data এর অভাব ছিল না, তবু গুরুত্বপূর্ণ দুইটা প্রশ্নের উত্তর দিতে পারল না: এই মুহূর্তে কি user দের ক্ষতি হচ্ছে, আর ঠিক কোথায় ভাঙছে? এটাই observability: বাইরে থেকে তোমার system কী করছে সেটা এত দ্রুত বুঝতে পারা যাতে ব্যবস্থা নেওয়া যায়। এই chapter এ আছে তিনটা signal (log, metric, trace), গড় কেন মিথ্যা বলে, CPU না দেখে SLO দেখে alert, আর এমন health check যেটার উপর load balancer ভরসা করতে পারে।
সমস্যা: অনেক data, কোনো উত্তর নেই
| কী হয় | কেন |
|---|---|
| Team এর আগে user রা সমস্যা টের পায় | Alert machine এর resource দেখে, user এর অভিজ্ঞতা না |
| On-call alert উপেক্ষা করে | এত alert যেগুলোয় কিছু করার নেই (alert fatigue) |
| "Latency ঠিক আছে" অথচ কিছু user কয়েক সেকেন্ড অপেক্ষা করে | গড় slow tail লুকিয়ে রাখে; কেউ p99 দেখে না |
| Service জুড়ে একটা request কেউ অনুসরণ করতে পারে না | কোনো trace ID নেই, distributed tracing নেই |
| Log খোঁজা slow আর আন্দাজে চলে | কাঠামো বা একই রকম field ছাড়া free-text log |
| Monitoring এর bill ফুলে ওঠে | Metric এ high-cardinality label (user ID), প্রতিটা debug log চিরকাল রাখা |
হাসপাতালের একটা ward এর কথা ভাবো
Ward এর nurse রোগীর আজকের প্রতিটা কথা পড়ে রোগীকে বোঝেন না। তিনটা হাতিয়ার আছে, প্রত্যেকটা আলাদা প্রশ্নের জন্য।
বিছানার পাশের chart সময়ের সাথে সংখ্যা দেখায়: প্রতি ঘণ্টায় তাপমাত্রা, pulse, রক্তচাপ। এক নজরেই বোঝা যায় সব স্বাভাবিক নাকি ভুল দিকে যাচ্ছে। এগুলো metric: সস্তা, সংক্ষেপ করা, কিছু একটা ভুল সেটা ধরতে আর alarm এর জন্য ভালো।
Note এ লেখা থাকে কী ঘটল, খুঁটিনাটি সহ: "১৪:২০, রোগী বুকে ব্যথার কথা বললেন, aspirin দেওয়া হলো"। এগুলো log: আলাদা আলাদা ঘটনার বিস্তারিত, কোথায় দেখতে হবে জানার পর অপরিহার্য।
রোগীকে X-ray, lab আর surgery ঘুরিয়ে নেওয়ার সময় case file প্রতিটা বিভাগে সময় লিখে রোগীর সাথে চলে, তাই দেখা যায় ছয় ঘণ্টার চার ঘণ্টাই গেছে একটা lab এর ফলের জন্য অপেক্ষায়। এটা trace: অনেক service এর মধ্য দিয়ে একটা request এর যাত্রা, সময় কোথায় গেল দেখায়।
আর যে alarm ডাক্তারকে ঘুম থেকে তোলে সেটা রোগীর অবস্থার সাথে বাঁধা ("oxygen 90% এর নিচে"), monitor এর processor কতটা ব্যস্ত তার সাথে না। এটাই SLO দিয়ে লক্ষণ দেখে alert।
তবে একটা জায়গায় মেলে না। Nurse রোগীকে দেখতে পারেন। চলমান একটা system এর ভেতরে তুমি দেখতে পারো না; শুধু সেটুকুই দেখো যেটা জানানোর জন্য সেটা বানানো। Observability incident এর আগেই design এ রাখতে হয়, incident এর সময় যোগ করা যায় না।
1,000টা request: 980টায় 50 থেকে 100 ms লাগে আর 20টায় 2 সেকেন্ড। Dashboard এর কোন সংখ্যা সমস্যাটা দেখায়?
Observability র পেছনের নিয়মগুলো
- Metric হলো সময়ের সাথে সংখ্যা, একত্র করা আর রাখতে সস্তা: request এর হার, error এর হার, latency র বিন্যাস (service এর জন্য RED method), আর resource এর জন্য utilisation, saturation আর error (USE method)। Dashboard আর alert এগুলোর উপর চলে।
- গড় না, percentile দেখো। Latency কে histogram হিসেবে রেকর্ড করো আর p50, p95 আর p99 দেখো। User রা বিরক্ত হয় tail এ, আর অনেক call করে এমন একটা page এ একটা slow call পুরো page কে slow করে দেয়।
- Log হলো খুঁটিনাটি সহ ঘটনা। Structured ভাবে লেখো (একই রকম field সহ JSON: timestamp, level, service, trace ID, user বা order ID, message), যাতে grep না করে filter আর group করতে পারো। Incident এ যা লাগবে সেটা log করো; production এ হইচই করা debug log sample করো বা বাদ দাও।
- Trace service জুড়ে একটা request অনুসরণ করে। প্রতিটা request একটা trace ID পায়, header এ পাঠানো হয় (W3C
traceparent); প্রতিটা কাজের একক একটা span, শুরু, সময়কাল আর একটা parent সহ। Trace ঠিক দেখায় কোন call slow বা fail ছিল। খরচ নিয়ন্ত্রণে trace sample করো (ধরো 1 থেকে 10%, আর সব error আর slow গুলো রেখে দাও)। - তিনটাকে জুড়ে দাও। প্রতিটা log line এ trace ID রাখো আর exemplar সহ metric ব্যবহার করো, যাতে "p99 লাফিয়েছে" থেকে একটা উদাহরণ trace এ, সেখান থেকে ঠিক ওই request এর log এ যেতে পারো।
- কারণ না, user যে লক্ষণ টের পায় তার উপর alert দাও। SLO ভাঙলে page করো: আসল request এর error rate আর latency। বেশি CPU, ভরা queue বা একটা মরা instance dashboard আর ticket এ দেখার জিনিস, মানুষকে জাগানোর না, যদি না সেগুলো user এর ক্ষতি করতে যাচ্ছে।
- Error-budget এর burn rate ব্যবহার করো। SLO যদি মাসে 0.1% error দেয়, burn rate 1 মানে budget ঠিক মাস জুড়ে খরচ হবে। Page করো যখন ছোট window এ burn বেশি (যেমন এক ঘণ্টা ধরে 14.4x, যেটা মাসের budget এর 2% পুড়িয়ে ফেলে), আর ticket দাও যখন long window এ মাঝারি (যেমন তিন দিন ধরে 1x)। বড় আগুনে দ্রুত, ছোটখাটোতে চুপ।
- প্রতিটা alert এ কিছু করার থাকতে হবে। একজন owner, একটা runbook এর link আর স্পষ্ট "কী করতে হবে" লাগে। কোনো alert নিয়মিত বাজলে আর কেউ কিছু না করলে, ঠিক করো বা মুছে ফেলো।
- Health check: liveness বনাম readiness। Liveness উত্তর দেয় "process কি আটকে গেছে? restart দাও"। Readiness উত্তর দেয় "এখন কি traffic নিতে পারবে?" (dependency তে পৌঁছানো যায়, warm up হয়েছে)। Load balancer readiness ব্যবহার করে। দুইটাই হালকা রাখো, আর liveness কে অন্য service এর উপর নির্ভরশীল কোরো না, নাহলে database এর একটা ঝাঁকুনিতে প্রতিটা pod restart হবে।
Page থেকে কারণ পর্যন্ত একটা incident
Team কীভাবে দশ মিনিটে fail করা fraud check খুঁজে পায়
ধাপ 1/5 Burn-rate alert page করে
গত এক ঘণ্টায় checkout এর error rate 1.5%, 99.9% SLO এর বিপরীতে burn rate 15x। Alert service এর নাম বলে আর runbook এর link দেয়।
- SLO
- 99.9% checkout সফল
- burn
- 15x
- page
- 14:07
ধাপে ধাপে একটা service কে observable বানানো
- User যা করে তার জন্য SLI আর SLO ঠিক করো: "30 দিনে 99.9% request এ checkout 1 s এর মধ্যে সফল হয়"।
- প্রতি endpoint এ RED metric দাও (হার, error, histogram হিসেবে সময়) আর গুরুত্বপূর্ণ resource এর জন্য USE metric।
- Structured log লেখো, trace ID আর যে business ID দিয়ে খুঁজবে সেগুলো সহ।
- OpenTelemetry দিয়ে tracing যোগ করো: HTTP, gRPC আর message header দিয়ে context পাঠাও।
- প্রতি service এ একটা dashboard বানাও, যেটা শুরু হয় SLO দিয়ে, তারপর RED, তারপর dependency।
- Burn rate এ alert দাও (দ্রুত আর ধীর window); বাকি signal ticket বা dashboard এ পাঠাও।
- প্রতিটা page এর জন্য একটা runbook লেখো, আর প্রতিটা incident এর পর alert গুলো পর্যালোচনা করো।
নিজে বলো: কাজের signal নাকি হইচই?
Incident এর সময় এটা কি কাজে আসবে?
1.5% checkout fail করলে 14:07 এ বাজে; বাকি সময় চুপ
চালিয়ে দেখার মতো percentile, trace আর burn rate
তিনটা অংশ। এক হাজার request এর latency, যার 2% একটা slow পথে পড়ে, গড় আর percentile দিয়ে সংক্ষেপ করা। একটা slow checkout span এর tree হিসেবে, indentation সহ print করা, আর সবচেয়ে বেশি সময় নেওয়া span খুঁজে বের করা। আর ছয় ঘণ্টার error rate আর CPU, 99.9% SLO এর জন্য একটা CPU alert আর একটা error-budget burn-rate alert মিলিয়ে দেখা। প্রতিটা language integer ব্যবহার করে আর একই output দেয়।
package main
import (
"fmt"
"sort"
"strings"
)
// 1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s.
func latency() {
lat := make([]int, 1000)
sum := 0
for i := range lat {
lat[i] = 50 + i*37%51
if i%50 == 7 {
lat[i] = 2000
}
sum += lat[i]
}
sort.Ints(lat)
fmt.Println("1,000 requests, 2% of them hit a slow path:")
fmt.Printf(" average %d ms, p50 %d ms, p95 %d ms, p99 %d ms, max %d ms\n", sum/1000, lat[499], lat[949], lat[989], lat[999])
}
// 2. A trace: one request as a tree of timed spans.
type span struct {
name string
ms int
children []span
}
func printSpan(s span, depth int, slowest *span) {
fmt.Printf(" %s%-*s %4d ms\n", strings.Repeat(" ", depth), 22-2*depth, s.name, s.ms)
if len(s.children) == 0 && s.ms > slowest.ms {
*slowest = s
}
for _, c := range s.children {
printSpan(c, depth+1, slowest)
}
}
func trace() {
root := span{"GET /checkout", 420, []span{
{"auth", 12, nil},
{"load cart (db)", 40, nil},
{"payment", 330, []span{
{"fraud check", 280, nil},
{"charge card", 45, nil},
}},
{"render", 30, nil},
}}
fmt.Println("trace of one slow checkout:")
slowest := span{}
printSpan(root, 0, &slowest)
fmt.Printf(" most of the time: %s, %d ms of %d (%d%%)\n", slowest.name, slowest.ms, root.ms, slowest.ms*100/root.ms)
}
// 3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
// Error rates are in hundredths of a percent (150 = 1.50%).
func alerts() {
errs := []int{5, 8, 150, 160, 20, 6}
cpu := []int{85, 88, 40, 35, 60, 82}
const budget = 10 // 0.10% in hundredths of a percent
fmt.Println("six hours of errors and CPU; SLO 99.9% (budget 0.1%):")
used := 0
for h := range errs {
burn := errs[h] * 10 / budget // tenths: 150 -> 150 (15.0x the allowed rate)
page := "-"
if burn >= 144 { // 14.4x for an hour = 2% of the month's budget gone
page = "PAGE"
}
cpuAlert := "-"
if cpu[h] > 80 {
cpuAlert = "alert"
}
fmt.Printf(" hour %d: errors %d.%02d%%, burn %2d.%dx -> %-4s | CPU %d%% -> %s\n",
h+1, errs[h]/100, errs[h]%100, burn/10, burn%10, page, cpu[h], cpuAlert)
used += errs[h]
}
month := budget * 30 * 24 // the whole month's budget, in the same units
fmt.Printf(" error budget used in these 6 hours: %d.%d%% of the month's\n", used*1000/month/10, used*1000/month%10)
}
func main() {
latency()
trace()
alerts()
}from dataclasses import dataclass, field
def latency() -> None:
"""1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s."""
lat = [2000 if i % 50 == 7 else 50 + i * 37 % 51 for i in range(1000)]
total = sum(lat)
lat.sort()
print("1,000 requests, 2% of them hit a slow path:")
print(f" average {total // 1000} ms, p50 {lat[499]} ms, p95 {lat[949]} ms, p99 {lat[989]} ms, max {lat[999]} ms")
@dataclass
class Span:
"""2. A trace: one request as a tree of timed spans."""
name: str
ms: int
children: list["Span"] = field(default_factory=list)
def print_span(s: Span, depth: int, slowest: list[Span]) -> None:
print(f" {' ' * depth}{s.name:<{22 - 2 * depth}} {s.ms:4} ms")
if not s.children and s.ms > slowest[0].ms:
slowest[0] = s
for c in s.children:
print_span(c, depth + 1, slowest)
def trace() -> None:
root = Span("GET /checkout", 420, [
Span("auth", 12),
Span("load cart (db)", 40),
Span("payment", 330, [Span("fraud check", 280), Span("charge card", 45)]),
Span("render", 30),
])
print("trace of one slow checkout:")
slowest = [Span("", 0)]
print_span(root, 0, slowest)
s = slowest[0]
print(f" most of the time: {s.name}, {s.ms} ms of {root.ms} ({s.ms * 100 // root.ms}%)")
def alerts() -> None:
"""3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
Error rates are in hundredths of a percent (150 = 1.50%)."""
errs = [5, 8, 150, 160, 20, 6]
cpu = [85, 88, 40, 35, 60, 82]
budget = 10 # 0.10% in hundredths of a percent
print("six hours of errors and CPU; SLO 99.9% (budget 0.1%):")
used = 0
for h, (e, c) in enumerate(zip(errs, cpu)):
burn = e * 10 // budget # tenths: 150 -> 150 (15.0x the allowed rate)
page = "PAGE" if burn >= 144 else "-" # 14.4x for an hour = 2% of the month's budget gone
cpu_alert = "alert" if c > 80 else "-"
print(f" hour {h + 1}: errors {e // 100}.{e % 100:02}%, burn {burn // 10:2}.{burn % 10}x -> {page:<4} | CPU {c}% -> {cpu_alert}")
used += e
month = budget * 30 * 24 # the whole month's budget, in the same units
print(f" error budget used in these 6 hours: {used * 1000 // month // 10}.{used * 1000 // month % 10}% of the month's")
latency()
trace()
alerts()const div = (a: number, b: number) => Math.floor(a / b);
/** 1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s. */
function latency() {
const lat = Array.from({ length: 1000 }, (_, i) => (i % 50 === 7 ? 2000 : 50 + ((i * 37) % 51)));
const sum = lat.reduce((a, b) => a + b, 0);
lat.sort((a, b) => a - b);
console.log("1,000 requests, 2% of them hit a slow path:");
console.log(` average ${div(sum, 1000)} ms, p50 ${lat[499]} ms, p95 ${lat[949]} ms, p99 ${lat[989]} ms, max ${lat[999]} ms`);
}
/** 2. A trace: one request as a tree of timed spans. */
type Span = { name: string; ms: number; children: Span[] };
const span = (name: string, ms: number, children: Span[] = []): Span => ({ name, ms, children });
function printSpan(s: Span, depth: number, slowest: { s: Span }) {
console.log(` ${" ".repeat(depth)}${s.name.padEnd(22 - 2 * depth)} ${String(s.ms).padStart(4)} ms`);
if (s.children.length === 0 && s.ms > slowest.s.ms) slowest.s = s;
for (const c of s.children) printSpan(c, depth + 1, slowest);
}
function trace() {
const root = span("GET /checkout", 420, [
span("auth", 12),
span("load cart (db)", 40),
span("payment", 330, [span("fraud check", 280), span("charge card", 45)]),
span("render", 30),
]);
console.log("trace of one slow checkout:");
const slowest = { s: span("", 0) };
printSpan(root, 0, slowest);
const s = slowest.s;
console.log(` most of the time: ${s.name}, ${s.ms} ms of ${root.ms} (${div(s.ms * 100, root.ms)}%)`);
}
/** 3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
* Error rates are in hundredths of a percent (150 = 1.50%). */
function alerts() {
const errs = [5, 8, 150, 160, 20, 6];
const cpu = [85, 88, 40, 35, 60, 82];
const budget = 10; // 0.10% in hundredths of a percent
console.log("six hours of errors and CPU; SLO 99.9% (budget 0.1%):");
let used = 0;
errs.forEach((e, h) => {
const burn = div(e * 10, budget); // tenths: 150 -> 150 (15.0x the allowed rate)
const page = burn >= 144 ? "PAGE" : "-"; // 14.4x for an hour = 2% of the month's budget gone
const cpuAlert = cpu[h]! > 80 ? "alert" : "-";
console.log(
` hour ${h + 1}: errors ${div(e, 100)}.${String(e % 100).padStart(2, "0")}%, burn ${String(div(burn, 10)).padStart(2)}.${burn % 10}x -> ${page.padEnd(4)} | CPU ${cpu[h]}% -> ${cpuAlert}`,
);
used += e;
});
const month = budget * 30 * 24; // the whole month's budget, in the same units
console.log(` error budget used in these 6 hours: ${div(div(used * 1000, month), 10)}.${div(used * 1000, month) % 10}% of the month's`);
}
latency();
trace();
alerts();/// 1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s.
fn latency() {
let mut lat: Vec<i32> = (0..1000).map(|i| if i % 50 == 7 { 2000 } else { 50 + i * 37 % 51 }).collect();
let sum: i32 = lat.iter().sum();
lat.sort();
println!("1,000 requests, 2% of them hit a slow path:");
println!(" average {} ms, p50 {} ms, p95 {} ms, p99 {} ms, max {} ms", sum / 1000, lat[499], lat[949], lat[989], lat[999]);
}
/// 2. A trace: one request as a tree of timed spans.
#[derive(Clone)]
struct Span {
name: &'static str,
ms: i32,
children: Vec<Span>,
}
fn span(name: &'static str, ms: i32, children: Vec<Span>) -> Span {
Span { name, ms, children }
}
fn print_span(s: &Span, depth: usize, slowest: &mut Span) {
println!(" {}{:<w$} {:4} ms", " ".repeat(depth), s.name, s.ms, w = 22 - 2 * depth);
if s.children.is_empty() && s.ms > slowest.ms {
*slowest = s.clone();
}
for c in &s.children {
print_span(c, depth + 1, slowest);
}
}
fn trace() {
let root = span("GET /checkout", 420, vec![
span("auth", 12, vec![]),
span("load cart (db)", 40, vec![]),
span("payment", 330, vec![span("fraud check", 280, vec![]), span("charge card", 45, vec![])]),
span("render", 30, vec![]),
]);
println!("trace of one slow checkout:");
let mut slowest = span("", 0, vec![]);
print_span(&root, 0, &mut slowest);
println!(" most of the time: {}, {} ms of {} ({}%)", slowest.name, slowest.ms, root.ms, slowest.ms * 100 / root.ms);
}
/// 3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
/// Error rates are in hundredths of a percent (150 = 1.50%).
fn alerts() {
let errs = [5, 8, 150, 160, 20, 6];
let cpu = [85, 88, 40, 35, 60, 82];
let budget = 10; // 0.10% in hundredths of a percent
println!("six hours of errors and CPU; SLO 99.9% (budget 0.1%):");
let mut used = 0;
for (h, (&e, &c)) in errs.iter().zip(cpu.iter()).enumerate() {
let burn = e * 10 / budget; // tenths: 150 -> 150 (15.0x the allowed rate)
let page = if burn >= 144 { "PAGE" } else { "-" }; // 14.4x for an hour = 2% of the month's budget gone
let cpu_alert = if c > 80 { "alert" } else { "-" };
println!(" hour {}: errors {}.{:02}%, burn {:2}.{}x -> {page:<4} | CPU {c}% -> {cpu_alert}", h + 1, e / 100, e % 100, burn / 10, burn % 10);
used += e;
}
let month = budget * 30 * 24; // the whole month's budget, in the same units
println!(" error budget used in these 6 hours: {}.{}% of the month's", used * 1000 / month / 10, used * 1000 / month % 10);
}
fn main() {
latency();
trace();
alerts();
}import java.util.Arrays;
import java.util.List;
public class Main {
/** 1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s. */
static void latency() {
int[] lat = new int[1000];
int sum = 0;
for (int i = 0; i < 1000; i++) {
lat[i] = i % 50 == 7 ? 2000 : 50 + i * 37 % 51;
sum += lat[i];
}
Arrays.sort(lat);
System.out.println("1,000 requests, 2% of them hit a slow path:");
System.out.printf(" average %d ms, p50 %d ms, p95 %d ms, p99 %d ms, max %d ms%n", sum / 1000, lat[499], lat[949], lat[989], lat[999]);
}
/** 2. A trace: one request as a tree of timed spans. */
record Span(String name, int ms, List<Span> children) {
Span(String name, int ms) { this(name, ms, List.of()); }
}
static Span printSpan(Span s, int depth, Span slowest) {
System.out.printf(" %s%-" + (22 - 2 * depth) + "s %4d ms%n", " ".repeat(depth), s.name(), s.ms());
if (s.children().isEmpty() && s.ms() > slowest.ms()) slowest = s;
for (Span c : s.children()) slowest = printSpan(c, depth + 1, slowest);
return slowest;
}
static void trace() {
Span root = new Span("GET /checkout", 420, List.of(
new Span("auth", 12),
new Span("load cart (db)", 40),
new Span("payment", 330, List.of(new Span("fraud check", 280), new Span("charge card", 45))),
new Span("render", 30)));
System.out.println("trace of one slow checkout:");
Span s = printSpan(root, 0, new Span("", 0));
System.out.printf(" most of the time: %s, %d ms of %d (%d%%)%n", s.name(), s.ms(), root.ms(), s.ms() * 100 / root.ms());
}
/** 3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
* Error rates are in hundredths of a percent (150 = 1.50%). */
static void alerts() {
int[] errs = {5, 8, 150, 160, 20, 6};
int[] cpu = {85, 88, 40, 35, 60, 82};
final int budget = 10; // 0.10% in hundredths of a percent
System.out.println("six hours of errors and CPU; SLO 99.9% (budget 0.1%):");
int used = 0;
for (int h = 0; h < errs.length; h++) {
int burn = errs[h] * 10 / budget; // tenths: 150 -> 150 (15.0x the allowed rate)
String page = burn >= 144 ? "PAGE" : "-"; // 14.4x for an hour = 2% of the month's budget gone
String cpuAlert = cpu[h] > 80 ? "alert" : "-";
System.out.printf(" hour %d: errors %d.%02d%%, burn %2d.%dx -> %-4s | CPU %d%% -> %s%n",
h + 1, errs[h] / 100, errs[h] % 100, burn / 10, burn % 10, page, cpu[h], cpuAlert);
used += errs[h];
}
int month = budget * 30 * 24; // the whole month's budget, in the same units
System.out.printf(" error budget used in these 6 hours: %d.%d%% of the month's%n", used * 1000 / month / 10, used * 1000 / month % 10);
}
public static void main(String[] args) {
latency();
trace();
alerts();
}
}#include <algorithm>
#include <cstdio>
#include <numeric>
#include <string>
#include <vector>
// 1. Latency: 1,000 requests. Most take 50-100 ms; 2% hit a slow path and take 2 s.
void latency() {
std::vector<int> lat(1000);
for (int i = 0; i < 1000; i++) lat[i] = i % 50 == 7 ? 2000 : 50 + i * 37 % 51;
int sum = std::accumulate(lat.begin(), lat.end(), 0);
std::sort(lat.begin(), lat.end());
std::printf("1,000 requests, 2%% of them hit a slow path:\n");
std::printf(" average %d ms, p50 %d ms, p95 %d ms, p99 %d ms, max %d ms\n", sum / 1000, lat[499], lat[949], lat[989], lat[999]);
}
// 2. A trace: one request as a tree of timed spans.
struct Span {
std::string name;
int ms;
std::vector<Span> children;
};
void printSpan(const Span& s, int depth, const Span*& slowest) {
std::printf(" %s%-*s %4d ms\n", std::string(2 * depth, ' ').c_str(), 22 - 2 * depth, s.name.c_str(), s.ms);
if (s.children.empty() && s.ms > slowest->ms) slowest = &s;
for (const auto& c : s.children) printSpan(c, depth + 1, slowest);
}
void trace() {
Span root{"GET /checkout", 420, {
{"auth", 12, {}},
{"load cart (db)", 40, {}},
{"payment", 330, {{"fraud check", 280, {}}, {"charge card", 45, {}}}},
{"render", 30, {}},
}};
std::printf("trace of one slow checkout:\n");
Span none{"", 0, {}};
const Span* slowest = &none;
printSpan(root, 0, slowest);
std::printf(" most of the time: %s, %d ms of %d (%d%%)\n", slowest->name.c_str(), slowest->ms, root.ms, slowest->ms * 100 / root.ms);
}
// 3. Alerts. SLO: 99.9% of requests succeed over 30 days, so the error budget is 0.1%.
// Error rates are in hundredths of a percent (150 = 1.50%).
void alerts() {
const int errs[] = {5, 8, 150, 160, 20, 6};
const int cpu[] = {85, 88, 40, 35, 60, 82};
const int budget = 10; // 0.10% in hundredths of a percent
std::printf("six hours of errors and CPU; SLO 99.9%% (budget 0.1%%):\n");
int used = 0;
for (int h = 0; h < 6; h++) {
int burn = errs[h] * 10 / budget; // tenths: 150 -> 150 (15.0x the allowed rate)
const char* page = burn >= 144 ? "PAGE" : "-"; // 14.4x for an hour = 2% of the month's budget gone
const char* cpuAlert = cpu[h] > 80 ? "alert" : "-";
std::printf(" hour %d: errors %d.%02d%%, burn %2d.%dx -> %-4s | CPU %d%% -> %s\n", h + 1, errs[h] / 100, errs[h] % 100,
burn / 10, burn % 10, page, cpu[h], cpuAlert);
used += errs[h];
}
int month = budget * 30 * 24; // the whole month's budget, in the same units
std::printf(" error budget used in these 6 hours: %d.%d%% of the month's\n", used * 1000 / month / 10, used * 1000 / month % 10);
}
int main() {
latency();
trace();
alerts();
}ছয়টাই হুবহু এটা print করে:
1,000 requests, 2% of them hit a slow path:
average 113 ms, p50 76 ms, p95 99 ms, p99 2000 ms, max 2000 ms
trace of one slow checkout:
GET /checkout 420 ms
auth 12 ms
load cart (db) 40 ms
payment 330 ms
fraud check 280 ms
charge card 45 ms
render 30 ms
most of the time: fraud check, 280 ms of 420 (66%)
six hours of errors and CPU; SLO 99.9% (budget 0.1%):
hour 1: errors 0.05%, burn 0.5x -> - | CPU 85% -> alert
hour 2: errors 0.08%, burn 0.8x -> - | CPU 88% -> alert
hour 3: errors 1.50%, burn 15.0x -> PAGE | CPU 40% -> -
hour 4: errors 1.60%, burn 16.0x -> PAGE | CPU 35% -> -
hour 5: errors 0.20%, burn 2.0x -> - | CPU 60% -> -
hour 6: errors 0.06%, burn 0.6x -> - | CPU 82% -> alert
error budget used in these 6 hours: 4.8% of the month's
Output টা পড়ো:
- latency: গড় 113 ms আর p95 99 ms, দুইটাই "ঠিক আছে"। p99 2,000 ms: প্রতি পঞ্চাশ জনে একজন user দুই সেকেন্ড অপেক্ষা করে। শুধু tail এর percentile এটা দেখায়।
- trace: request এ লেগেছে 420 ms, যার 280 ms গেছে payment এর ভেতরের fraud check এ। Tree ছাড়া তুমি শুধু জানতে "checkout slow"।
- ঘণ্টা 1, 2 আর 6: CPU alert বাজে, অথচ error 0.05 থেকে 0.08%, budget এর অনেক ভেতরে। কাউকে জাগানোর দরকার ছিল না।
- ঘণ্টা 3 আর 4: error 1.5 আর 1.6%, burn rate 15x আর 16x। Burn-rate alert page করে; CPU কম (40%, 35%), কারণ fail করা request সস্তা, তাই আসল incident এর সময় CPU alert চুপ থাকে।
- খরচ হওয়া budget: এই ছয় ঘণ্টায় মাসের error budget এর 4.8% খরচ হয়েছে, বেশিরভাগটাই দুই ঘণ্টায়।
- কোন language এ কী বদলালো: indent বাড়ার সাথে width কমে এমনভাবে tree print করা। Go আর C++ width টা argument হিসেবে পাঠায় (
%-*s), Rust একটা নাম দেওয়া width ব্যবহার করে ({:<w$}), Python format spec এর ভেতরে বসায় ({name:<{w}}), TypeScriptpadEnd(w)call করে, আর Java format string টা বানিয়ে নেয়। Java রrecordspan তৈরির পর বদলানো যায় না, তাইprintSpanএকটা update না করে সবচেয়ে slow span টা ফেরত দেয়।
আসল project এ observability
- OpenTelemetry হলো code থেকে trace, metric আর log বানানোর standard উপায়, এই course এর প্রতিটা language এর SDK সহ। এগুলো পাঠাও Prometheus আর Grafana তে, log এর জন্য Loki বা Elasticsearch এ, trace এর জন্য Tempo বা Jaeger এ, বা একটা hosted service এ (Datadog, Honeycomb, New Relic, Grafana Cloud)।
- সব জায়গায় context পাঠাও: HTTP আর gRPC header, আর queue র জন্য message header, যাতে async সীমানায় trace ভেঙে না যায়।
- খরচ নিয়ন্ত্রণ: trace sample করো (error আর slow গুলো রাখো), debug log বাদ দাও বা sample করো, প্রতিটা signal এর retention আলাদা ঠিক করো, আর metric এর cardinality তে চোখ রাখো।
- Dashboard: প্রতিটা service এর dashboard শুরু করো তার SLO আর error budget দিয়ে, তারপর RED metric, তারপর dependency। Deploy marker যোগ করো, কারণ বেশিরভাগ incident এর প্রথম প্রশ্ন হলো "কী বদলেছে?"।
- On-call এর স্বাস্থ্য: প্রতিটা page এর runbook থাকবে; হইচই করা alert প্রতি সপ্তাহে পর্যালোচনা করো; প্রতি shift এ কয়টা page হলো গোনো; incident এর পরে জিজ্ঞেস করো "কোন signal আগে জানাতে পারত?"।
- Synthetic check: তোমার network এর বাইরে থেকে প্রতি মিনিটে login আর checkout করা একটা robot এমন সমস্যা ধরে যেগুলো ভেতরের metric মিস করে, যেমন DNS বা certificate এর failure।
যে design গুলো পরে problem create করে
Observability র ভুলটা ধরো
database 20 s এর জন্য ঝাঁকুনি খেল; Kubernetes প্রতিটা pod restart করল
একটু থামো, ভাবো
তোমার SLO হলো 30 দিনে 99.9% request সফল। একটা bug এর কারণে আজ থেকে স্থিরভাবে 0.2% request fail করছে। এক ঘণ্টায় 14.4x burn এ page আর তিন দিনে 1x burn এ ticket থাকলে কোন alert বাজবে, আর কখন? এটা কি ঠিক আচরণ?
উত্তর দেখো
0.1% budget এর বিপরীতে 0.2% error মানে burn rate 2x। এটা 14.4x এর অনেক নিচে, তাই page কখনো বাজে না। এটা 1x এর উপরে, তাই তিন দিনের window তে এটা দেখা গেলেই ticket alert বাজে, স্থিরভাবে পুড়তে থাকলে মোটামুটি এক দিনের মধ্যে। এটাই উদ্দেশ্য: 2x এ মাসের budget প্রায় 15 দিনে শেষ হবে, যেটা গুরুতর, কিন্তু "রাত ৩টায় কাউকে জাগাও" এর মতো গুরুতর না। এই সপ্তাহেই, কাজের সময়ে ঠিক করতে হবে। দ্রুত-burn এর page রাখা থাকে সেই সমস্যার জন্য যেগুলো কয়েক ঘণ্টা বা কয়েক দিনে budget শেষ করে দেবে।
ছোট্ট quiz
1. Log, metric আর trace এর পার্থক্য কী?
উত্তর
Metric হলো সময়ের সাথে একত্র করা সংখ্যা (হার, error, latency)। Log হলো আলাদা আলাদা ঘটনার বিস্তারিত রেকর্ড। Trace সময় দেওয়া span এর একটা tree হিসেবে service জুড়ে একটা request অনুসরণ করে।
2. গড়ের বদলে p99 কেন দেখবে?
উত্তর
গড় slow tail লুকিয়ে রাখে: কয়েকটা খুব slow request তাকে প্রায় নাড়ায়ই না। p99 বলে সবচেয়ে slow 1% কতটা slow, আর বিরক্ত user রা সেটাই অনুভব করে।
3. CPU না দেখে SLO এর burn rate দেখে কেন alert দেবে?
উত্তর
Burn rate কথা দেওয়া reliability র বিপরীতে user এর মুখোমুখি failure মাপে। User ঠিক থাকলেও CPU বেশি হতে পারে, আর তারা কষ্ট পেলেও কম হতে পারে (fail করা request সস্তা)। লক্ষণ দেখে page করা মানে কম মিথ্যা alarm আর কোনো incident মিস না হওয়া।
4. Liveness বনাম readiness: কোনটা কী কাজে?
উত্তর
Liveness আটকে যাওয়া বা নষ্ট process ধরে যাতে restart দেওয়া যায়। Readiness বলে একটা instance এখন traffic পাবে কিনা। Load balancer readiness ব্যবহার করে; liveness অন্য service এর উপর নির্ভর করা উচিত না।
তোমার challenge: tail যখন মোটা হয়
Slow পথটা বদলে 10% request এ করো, 2% এর বদলে (i % 50 == 7 এর জায়গায় i % 10 == 7), আর latency অংশটা আবার চালাও। চালানোর আগে আন্দাজ করো: গড়, p50, p95 আর p99 এর কোনগুলো অনেক বদলাবে?
- শুধু ওই শর্তটা বদলাও।
- পুরনো আর নতুন latency line মেলাও।
তুমি পেরেছো যখন latency line টা হবে average 267 ms, p50 78 ms, p95 2000 ms, p99 2000 ms, max 2000 ms। এখন p95 ও সমস্যা দেখাচ্ছে, আর গড় দ্বিগুণের বেশি, অথচ মধ্যমা প্রায় নড়েই না। তোমার SLO যদি হতো "p95 300 ms এর নিচে", 2% এর version পাস করত আর 10% এর version fail করত: কোন percentile এর কথা দিচ্ছো সেটা একটা product এর সিদ্ধান্ত।
এক লাইনে
System কে নিজেকে বোঝাতে দাও: হার, error আর latency histogram হিসেবে metric দাও (আর গড় না, percentile পড়ো), trace ID সহ structured log লেখো, প্রতিটা service আর queue জুড়ে request trace করো, page করো শুধু SLO burn rate এ যেটা মানে user কষ্ট পাচ্ছে, প্রতিটা alert কে একজন owner আর একটা runbook দাও, আর liveness আর readiness check হালকা আর সৎ রাখো।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →