ข้ามไปยังเนื้อหา

SLO, Error Budget และ Burn Rate Alert

Datadog SLO แปลง target แบบ “99.9% ดีตลอด 30 วัน” ให้กลายเป็น error budget ที่ track ได้ และ Datadog มี SLO alert สองแบบที่ต่างกันเพราะ “budget เหลือเท่าไหร่” กับ “กำลังหมดเร็วแค่ไหน” คือคนละปัญหากัน

Metric-based — อัตราส่วนระหว่าง metric query สองตัว good event หารด้วย total event

slos/checkout-availability.json
{
"name": "Checkout API availability",
"type": "metric",
"query": {
"numerator": "sum:trace.http.request.hits{service:checkout-api,!http.status_code:5*}.as_count()",
"denominator": "sum:trace.http.request.hits{service:checkout-api}.as_count()"
},
"thresholds": [
{ "timeframe": "30d", "target": 99.9 }
]
}

Monitor-based — สร้างจาก uptime history ของ monitor ที่มีอยู่แล้วหนึ่งตัวหรือมากกว่า เวลา “ดี” ของ SLO คือช่วงที่ monitor พวกนั้น ไม่ได้ trigger

slos/checkout-monitor-based.json
{
"name": "Checkout API monitor-based SLO",
"type": "monitor",
"monitor_ids": [111111, 222222],
"thresholds": [
{ "timeframe": "30d", "target": 99.9 }
]
}

Time Slice — ประเมิน query ทีละ time slice ขนาดคงที่ทั่วทั้ง window เช่น “p99 latency ต่ำกว่า 300ms ในทุก slice ขนาด 5 นาทีหรือไม่”

slos/checkout-p99-latency.json
{
"name": "Checkout API p99 latency SLO",
"type": "time_slice",
"sli_specification": {
"time_slice": {
"query": {
"formula": "query1",
"queries": ["p99:trace.http.request.duration{service:checkout-api}"]
},
"comparator": "<",
"threshold": 300,
"query_interval_seconds": 300
}
},
"thresholds": [
{ "timeframe": "30d", "target": 99.9 }
]
}

target แบบ 99.9% ตลอด window 30 วัน มีนัยว่ายอมให้เวลาหรือ event ที่ “แย่” ได้จำนวนหนึ่ง — ส่วนที่ยอมได้นั้นคือ error budget

Target: 99.9% over 30d
30d = 43200 minutes
Allowed bad time (error budget) = 0.1% x 43200 = 43.2 minutes

ทุก event ที่แย่หรือทุกนาทีที่แย่กิน budget 43.2 นาทีนั้นไปทีละส่วน พอ budget เหลือ 0% window นั้นก็ตกต่ำกว่า target 99.9% ไปแล้ว

นี่คือความต่างที่สำคัญที่สุดในบทเรียนนี้ และ Datadog มี SLO alert สองแบบแยกกันสำหรับเรื่องนี้

Error budget alert — ใช้ threshold บนเปอร์เซ็นต์สะสมของ budget ที่ถูกใช้ไปแล้วใน window เช่น alert ตอน budget ถูกใช้ไป 75% และตั้ง warn เร็วกว่านั้นที่ 50% ก็ได้

monitors/checkout-error-budget-alert.json
{
"name": "checkout-api error budget consumption",
"type": "slo alert",
"query": "error_budget(\"<slo_id>\").over(\"30d\") > 75",
"message": "@slack-checkout-oncall 75% of the 30d error budget for checkout-api is already consumed."
}

Burn rate alert — notify เมื่อ อัตรา การใช้ error budget เกิน threshold และค้างอยู่ต่อเนื่องตลอดช่วงเวลาหนึ่ง query shape จริงต้องการสามส่วน ไม่ใช่สอง: .over(), .long_window(), และ .short_window():

monitors/checkout-burn-rate-alert.json
{
"name": "checkout-api SLO burn rate (fast)",
"type": "slo alert",
"query": "burn_rate(\"<slo_id>\").over(\"30d\").long_window(\"6h\").short_window(\"30m\") > 10",
"message": "@pagerduty-checkout-critical Burn rate is 10x normal - at this rate the entire 30d budget is gone in about 3 days."
}

เอกสารของ Datadog เองให้ตัวอย่างจริงที่ทำงานสมบูรณ์หนึ่งอันสำหรับ SLO target 30 วัน: burn rate 14.4, long_window("1h"), short_window("5m") แล้วระบุว่า “theoretical error budget consumed: 2%” ตลอด long window นั้น หน้าเดียวกันให้สูตรที่อยู่เบื้องหลัง (burn rate = (ความยาว SLO target เป็นชั่วโมง × % ของ budget ที่ใช้ไป) / (long window เป็นชั่วโมง × 100)) และวิธีคิดอีกแบบที่เทียบเท่ากัน (ความยาว SLO target / burn rate = เวลาที่ error budget จะหมดถ้า burn ที่อัตรานี้ต่อเนื่อง) ทั้งสองสูตรตรงกับตัวอย่างของบทนี้เอง burn_rate(...).over("30d").long_window("6h") > 10:

Approach 1 (time to exhaustion): 30d / burn_rate(10) = 3 days
Approach 2 (budget consumed over one long_window):
(long_window_hours x burn_rate x 100) / (SLO_target_hours)
= (6 x 10 x 100) / (30 x 24) = 6000 / 720 = 8.33%

ตัวเลข 8.33%-ต่อ-6-ชั่วโมงนั้นแหละคือเหตุผลว่าทำไม message ใน fence ข้างบนถึงเขียนว่า “gone in about 3 days” — สูตรทั้งสองตรงกัน ตัวอย่างจริงของ docs.datadoghq.com เองก็ใช้สูตรเดียวกันแค่ตัวเลขต่างไป (long_window=1h, burn_rate=14.4, over=30d → (1 x 14.4 x 100)/720 = 2% ตรงกับที่หน้าเอกสารระบุ “2%” เป๊ะ)

ขยายสูตรเดียวกันเป็นตาราง multi-window เล็ก ๆ (short window คงอัตราส่วนที่ Google แนะนำไว้ที่ ~1/12 ของ long window ซึ่งหน้าเอกสารระบุตรง ๆ):

| Sensitivity | burn_rate | long_window | short_window (~1/12) | over | budget consumed / long_window | time to exhaust budget |
|-------------|-----------|-------------|-----------------------|------|--------------------------------|--------------------------|
| Fast page | 14.4 | 1h | 5m | 30d | 2.00% (docs' own example) | 30d / 14.4 = ~2.08 days |
| This lesson | 10 | 6h | 30m | 30d | 8.33% | 30d / 10 = 3 days |
| Slow ticket | 1 | 3d (72h) | 6h | 30d | 10.00% | 30d / 1 = 30 days |

burn rate ที่สูงคู่กับ long_window สั้น จับ incident รุนแรงที่เคลื่อนไหวเร็วได้ภายในไม่กี่นาที ส่วน burn rate ใกล้ 1 คู่กับ long_window หลายวัน คือสัญญาณ “budget กำลังจะหมดพอดีตามกำหนด” — ควรเป็น ticket ไม่ใช่ page การรันสองหรือสามตัวนี้คู่กันกับ SLO เดียวกัน (ตัวเร็ว/page หนึ่งตัว ตัวช้า/ticket หนึ่งตัว) คือ pattern มาตรฐาน — burn-rate monitor ตัวเดียวบอกได้แค่ความไวที่มันถูก tune ไว้เท่านั้น

alert สองแบบนี้แก้คนละปัญหากัน error budget alert บอกว่า “budget กำลังจะหมด” — ดูที่ยอดสะสมที่ใช้ไปแล้ว เหมาะกับการตัดสินใจว่าสัปดาห์นี้ ship ของที่มีความเสี่ยงได้ปลอดภัยไหม ส่วน burn rate alert บอกว่า “budget กำลังจะหมด เร็วมาก ตอนนี้เลย” — ดูที่ความเร็ว ไม่ใช่ยอดรวม incident ที่ burn เร็วต่อเนื่องสามารถเผา budget 30 วันทั้งก้อนหมดได้ในไม่กี่ชั่วโมง และ burn rate alert จับสัญญาณนั้นได้ตั้งแต่ชั่วโมงแรก ก่อนที่ threshold แบบสะสมของ error budget alert จะข้ามไปด้วยซ้ำ

flowchart LR
  SLO[SLO target 99.9% over 30d] --> Budget[Error budget]
  Budget --> EBA{Error budget alert: percent consumed above threshold}
  Budget --> BRA{Burn rate alert: consumption rate above threshold, sustained}
  EBA -->|e.g. 75 percent consumed| Notify1[Notify: budget running out]
  BRA -->|e.g. 10x burn rate for 1h| Notify2[Notify: fast incident right now]
alert จาก error budget เทียบกับ alert จาก burn rate

🐛 หา bug ในโค้ดนี้

// AI comment: "Page the on-call when the error budget is burning 10x too fast"
{
"name": "checkout-api SLO burn rate (fast)",
"type": "slo alert",
"query": "burn_rate(\"<slo_id>\").over(\"1h\").long_window(\"6h\") > 10",
"message": "@pagerduty-checkout-critical Burn rate is 10x normal."
}
เฉลย

นี่คือ JSON ที่ valid และผ่าน Monitor(**data, _check_type=True) model validation ได้สะอาด — พิสูจน์แล้วข้างบนว่า client เช็คแค่ว่า query เป็น str เท่านั้น ไม่เช็ค Datadog query grammar ข้างในเลย มีบั๊กจริงสองตัวซ่อนอยู่ในสตริงนั้น อย่างแรก .over("1h") — .over() ต้องเป็น window ที่ config ไว้ของ SLO เอง (7d/30d/90d) ไม่ใช่ alerting window เลย "1h" ไม่ใช่ความยาว SLO target ที่ valid ด้วยซ้ำ อย่างที่สอง ไม่มี .short_window() อยู่เลย ซึ่ง query grammar ของ burn-rate ต้องการ เอกสารของ Datadog (docs.datadoghq.com/service_management/service_level_objectives/burn_rate/) ปฏิเสธ query ที่ขาดส่วนใดส่วนหนึ่งไป การแก้ ให้ตรงกับ SLO ที่ define ไว้ก่อนหน้าในบทนี้: burn_rate("<slo_id>").over("30d").long_window("6h").short_window("30m") > 10 ทั้ง comment ของ AI และตัว JSON-shape validator ไม่มีทางจับสิ่งนี้ได้เลย — ต้องอ่าน query grammar จริงหรือ real API call เท่านั้นถึงจะจับได้

SLO คือมาตรวัดระยะทางที่มีเกจน้ำมันติดมาด้วย มาตรวัดระยะทาง (query metric/monitor/time-slice ของ SLO เอง) แค่วัด “ดี” กับ “แย่” ต่อเนื่อง เกจน้ำมัน (error budget) แปลงมันเป็น “เรายอมให้มีเวลาแย่ได้เท่าไหร่ แล้วเหลือเท่าไหร่” error budget alert อ่านค่าที่เกจโดยตรง — threshold บนระดับน้ำมัน burn rate alert ดูว่าเข็มขยับเร็วแค่ไหน — threshold บนอัตราการเปลี่ยนแปลงที่ค้างอยู่ตลอด window รถอาจมีถังเต็ม (budget แทบไม่ถูกใช้เลย) แต่เผาน้ำมันในอัตราที่ถ้าคงไว้แบบนี้จะหมดถังภายในชั่วโมงเดียว และ burn rate alert คือตัวเดียวในสองแบบนี้ที่สังเกตเห็นทันเวลา

  • Metric-based เทียบกับ monitor-based เทียบกับ time-slice SLO — metric-based ยืดหยุ่นที่สุด (อัตราส่วนของ query สองตัวแบบไหนก็ได้) แต่ต้องมีการแบ่ง “ดี”/“ทั้งหมด” เป็น metric query แยกกันอยู่แล้ว; monitor-based ใช้ monitor ที่มีอยู่แล้วซ้ำ แต่สืบทอด threshold ที่ monitor พวกนั้นถูก tune ไว้; time-slice เป็นแบบเดียวในสามแบบที่สื่อ “ต้องคงอยู่เหนือ/ใต้ threshold ต่อเนื่อง” แทนที่จะเป็น “เฉลี่ย” ได้ แลกกับ query shape ที่ซับซ้อนขึ้นนิดหน่อย (พิสูจน์แล้วข้างบน: sli_specification.time_slice ซ้อน formula/query กับ comparator/threshold เข้าด้วยกัน)
  • Error budget alert เทียบกับ burn rate alert — error budget alert คิดง่ายกว่า (เปอร์เซ็นต์สะสมตัวเดียว) แต่ fire หลังจากความเสียหายจริงเกิดขึ้นแล้วเท่านั้น; burn rate alert จับปัญหาได้เร็วกว่า แต่ต้อง tune ตัวเลขสองตัวให้ถูก (long_window, short_window) และพิสูจน์แล้วข้างบนว่า .over() ต้องตรงกับ window ของ SLO เองเป๊ะ ไม่งั้น query จะไม่มีความหมาย
  • burn-rate monitor ตัวเดียว เทียบกับคู่เร็ว+ช้า — burn-rate monitor ตัวเดียวที่ tune สำหรับ incident เร็ว (พิสูจน์แล้วข้างบน: 14.4/1h/5m, ~2% budget ต่อ window) พลาด leak ที่ช้าแต่ต่อเนื่องซึ่งจะเผา budget เดียวกันหมดในไม่กี่วัน รัน monitor ตัวที่สองที่ช้ากว่า (1/3d/6h ในตารางของบทนี้) จับ case นั้นได้ด้วย แลกกับต้องดูแลและ tune สอง monitor ต่อหนึ่ง SLO แทนที่จะเป็นตัวเดียว
  • “.over() ใน burn rate query คือ alerting window (burn ต้องค้างนานแค่ไหน)” ไม่จริง — พิสูจน์แล้วข้างบน อ้างจากเอกสารตรง ๆ: .over() คือ window target ที่ config ไว้ของ SLO เอง (7d/30d/90d) ส่วนความนานที่ต้องค้างคือ .long_window()
  • “burn rate query ที่ไม่มี .short_window() ยัง valid อยู่ แค่แม่นยำน้อยลง” ไม่จริง — ตัวอย่างจริงในเอกสารและ burn-rate query ทุกตัวต้องมีทั้ง .long_window() และ .short_window(); query ที่ไม่มี .short_window() (พิสูจน์แล้วข้างบน ดราฟต์เก่าของบทนี้) ผิดรูป ไม่ใช่แค่แม่นยำน้อยลง
  • “JSON model validation (datadog_api_client) จะจับ burn_rate() query string ที่ผิดได้” ไม่จริง — พิสูจน์แล้วข้างบนด้วยการรันทั้ง query ที่แก้แล้วและที่ผิดผ่าน Monitor model จริงที่ pin ไว้: ทั้งคู่ได้ {"ok": true} client เช็คแค่ type ของ field (query ต้องเป็น str) ไม่เช็ค Datadog query grammar ข้างในสตริงนั้น
  • “SLO ที่ไม่มี thresholds ยังเป็น SLO document ที่ใช้ได้” ไม่จริง — พิสูจน์แล้วข้างบน: ServiceLevelObjective(**data, _check_type=True) ต้องการ thresholds สำหรับ SLO ทุกแบบใน client version ที่ pin นี้ document ที่ไม่มี field นี้ construct ไม่ได้เลย
Metric-based SLO วัดอะไร
Time Slice SLO ที่เช็คว่า "p99 latency ต่ำกว่า 300ms ในทุก window 5 นาที" ประเมินเงื่อนไขของตัวเองอย่างไร
SLO alert ที่ตั้งให้ notify เมื่อ error budget 30 วันถูกใช้ไป 75% คือตัวอย่างของ
shape ที่ถูกต้องจริงของ burn rate alert query สำหรับ SLO ที่ define ไว้ตลอด window 30 วัน alert เมื่อ burn 10 เท่าค้างอยู่วัดตลอด 6 ชั่วโมงคืออะไร
monitor JSON ที่มี query เป็นตัวเก่าที่ผิด burn_rate("<slo_id>").over("1h").long_window("6h") > 10 (ไม่มี short_window, .over() ผิด) ถูกรันผ่าน datadog_api_client's Monitor(**data, _check_type=True) จะเกิดอะไรขึ้น
ทำไม burn rate alert ถึงจับ incident ได้เร็วกว่า error budget alert