Alert Best Practices: สร้าง Alert ที่ Actionable และลด False Positive

ปัญหาที่พบบ่อยที่สุดของทีม oncall คือ notification ท่วมท้นจน engineer เริ่มเพิกเฉย ส่วนใหญ่เกิดจากการตั้งเงื่อนไขแจ้งเตือนที่ไม่ actionable — ปลุกคนกลางดึกโดยไม่มีอะไรต้องทำ, แจ้งซ้ำ ๆ จนกลายเป็น noise, หรือไม่มีบริบทพอที่จะเริ่ม troubleshoot การออกแบบกฎแจ้งเตือนที่ดีจึงเป็นศิลปะที่สำคัญไม่แพ้การเขียนโค้ด

บทความนี้สรุปหลักการออกแบบ notification ที่มีคุณภาพ ตั้งแต่ SLI/SLO-based approach, golden signals, การเขียน runbook, การหลีกเลี่ยง flappy alert ไปจนถึง anti-pattern ที่ควรหลีกเลี่ยง เพื่อให้ทีมได้รับข้อความที่ทำให้เกิดการตัดสินใจ ไม่ใช่แค่รับรู้

Actionable คืออะไร

การแจ้งเตือนที่ actionable ต้องผ่านเกณฑ์ 3 ข้อ — (1) Urgent ปัญหาที่ต้องแก้ทันที ไม่สามารถรอได้ (2) Important มีผลกระทบต่อผู้ใช้หรือธุรกิจจริง ๆ (3) Actionable คนที่ได้รับสามารถลงมือแก้ได้ทันที ถ้าขาดข้อใดข้อหนึ่ง ไม่ควรปลุกคน — ควรเป็นแค่ ticket หรือ dashboard เท่านั้น

SLI, SLO, SLA: พื้นฐานการวัด

แนวคิด SRE จาก Google เสนอให้วัดคุณภาพระบบผ่าน 3 แนวคิด Service Level Indicator (SLI) เช่น request latency, success rate, Service Level Objective (SLO) เป้าหมายที่ยอมรับได้ เช่น 99.9% ของ request ต้องเสร็จใน 500ms และ Service Level Agreement (SLA) สัญญากับลูกค้าที่ต่ำกว่า SLO

แจ้งเตือนเมื่อ SLO กำลังจะถูกละเมิด ดีกว่าแจ้งเมื่อ metric เกิน threshold เพียงครั้งเดียว เพราะสะท้อน user impact จริง ๆ

# Error Budget Burn Rate Alert
- alert: HighErrorBudgetBurn
  expr: |
    (
      sum(rate(http_requests_total{status=~"5.."}[1h]))
      /
      sum(rate(http_requests_total[1h]))
    ) > (14.4 * 0.001)
  for: 2m
  labels:
    severity: critical
  annotations:
    summary: "Error budget burning 14.4x faster than normal"
    runbook: "https://wiki.example.com/runbooks/error-budget"

14.4 คือ burn rate ที่จะใช้ error budget หมดภายใน 2 วัน ถ้า SLO = 99.9% (error budget = 0.1%) multiplier นี้แปลว่าเสียเร็วกว่าปกติ 14.4 เท่า

Golden Signals: 4 ตัวชี้วัดที่ต้อง monitor

Google SRE book แนะนำให้ทุก service track 4 ตัวชี้วัดหลัก ที่ครอบคลุม 90% ของปัญหาที่เกิดขึ้น

  • Latency — เวลาในการประมวลผล request แยก success และ failure
  • Traffic — ปริมาณ request/second หรือ concurrent connection
  • Errors — อัตรา error จริง ๆ (5xx) หรือ failed transaction
  • Saturation — ความอิ่มตัวของทรัพยากร เช่น CPU, memory, queue depth

สำหรับระบบที่ใช้ infrastructure เช่น database เพิ่ม USE Method (Utilization, Saturation, Errors) ของ Brendan Gregg ที่เน้น hardware resource

Severity Level: จัดลำดับความสำคัญ

แบ่งระดับ severity ตาม impact และความเร่งด่วน เพื่อกำหนด routing และ escalation ที่เหมาะสม

Severityตัวอย่างการตอบสนอง
P1 / Criticalservice ล่มทั้งหมด, ข้อมูลเสียหายปลุก oncall ทันที 24/7
P2 / Highfeature หลักไม่ทำงาน, degraded performanceปลุก oncall ในเวลาทำงาน
P3 / Warningtrend ไม่ดี, capacity ใกล้เต็มสร้าง ticket, ดูใน business hours
P4 / Infodeploy, config change, routine eventlog อย่างเดียว ไม่ notify

หลีกเลี่ยง Flappy Alert

Flappy alert คือการแจ้งเตือนที่เปลี่ยนสถานะ firing/resolved ซ้ำ ๆ ในเวลาสั้น ๆ ทำให้ทีมสับสน วิธีแก้คือใช้ for: clause ให้เงื่อนไขขอ trigger ต่อเนื่องอย่างน้อย N นาทีก่อน fire

# ไม่ดี — alert fire ทุกครั้งที่ CPU spike
- alert: HighCPU
  expr: cpu_usage > 80

# ดี — ต้อง sustained อย่างน้อย 5 นาที
- alert: HighCPU
  expr: cpu_usage > 80
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "CPU usage sustained above 80% for 5 minutes"
    description: "Instance {{ $labels.instance }} CPU = {{ $value }}%"

สำหรับ metric ที่ noisy มาก ใช้ avg_over_time หรือ rate แทน snapshot value เพื่อให้ทนต่อ spike ชั่วคราว

เขียน Runbook ที่มีประโยชน์

Runbook คือเอกสารที่บอกวิธีตอบสนองต่อการแจ้งเตือนแต่ละประเภท ควรลิงก์ไว้ใน annotation ของ rule ทุกตัว เนื้อหาที่ดีควรประกอบด้วย

  • Summary — ปัญหานี้หมายความว่าอะไร มีผลกระทบระดับใด
  • Diagnostic steps — command ที่ควรรัน, dashboard ที่ควรดู, log ที่ควรเช็ค
  • Remediation — ขั้นตอนการแก้ไข เรียงลำดับจากง่ายไปยาก
  • Escalation — ถ้าแก้ไม่ได้ใน X นาที ติดต่อใคร
  • Related alerts — การแจ้งเตือนที่อาจเกิดร่วมกัน
- alert: DatabaseConnectionPoolExhausted
  expr: db_pool_active / db_pool_max > 0.9
  for: 2m
  labels:
    severity: critical
    team: database
  annotations:
    summary: "DB connection pool > 90% utilized"
    description: |
      Pool {{ $labels.pool }} on {{ $labels.instance }}
      has {{ $value | humanizePercentage }} connections in use.
    runbook: "https://wiki.example.com/runbooks/db-pool-exhausted"
    dashboard: "https://grafana.example.com/d/db-pool"

Symptom-Based vs Cause-Based

แนวคิดสำคัญจาก Google SRE — แจ้งเตือนที่อาการ (symptom) ไม่ใช่สาเหตุ (cause) เพราะสาเหตุเปลี่ยนได้ แต่อาการที่กระทบผู้ใช้สำคัญเสมอ

  • Symptom-based (ดี) — “API error rate > 1%” / “checkout latency p99 > 2s” / “user login success rate < 99%”
  • Cause-based (ระวัง) — “CPU > 80%” / “memory > 90%” / “disk queue depth > 10” — อาจไม่กระทบผู้ใช้จริง

Cause-based ใช้เป็น warning หรือ trend alert ได้ แต่ critical alert ควรเป็น symptom-based เสมอ เพื่อให้มั่นใจว่าสิ่งที่ปลุกคนจริง ๆ กระทบ user

วัดคุณภาพของ Alert เอง

ระบบ oncall ที่ดีควร audit ตัวเองเป็นประจำ metric สำคัญที่ควร track

  • Actionability rate — กี่ % ของ alert ที่คนรับแล้วต้องทำอะไรจริง ๆ ต่ำกว่า 50% = มี noise เยอะ
  • Time to acknowledge (TTA) — เวลาเฉลี่ยที่คนรับการแจ้งเตือน ถ้าสูงมาก = alert fatigue
  • Alert volume per shift — จำนวน page ต่อกะ ไม่ควรเกิน 2 ครั้งเฉลี่ย
  • Top 10 noisiest alerts — review รายเดือนเพื่อปรับปรุงหรือลบ

Anti-Patterns ที่ควรหลีกเลี่ยง

  • “Just in case” alerts — แจ้งเตือนที่เพิ่มไว้เผื่อ ไม่มีคนรู้ว่าต้องทำอะไรเมื่อ fire
  • Cascading noise — service A ล่มทำให้ B, C, D, E แจ้งเตือนพร้อมกัน ใช้ inhibit rules แก้
  • Too aggressive thresholds — ตั้ง threshold ต่ำเกินจนมี false positive ทุกวัน
  • Missing context — annotation แค่ “Something wrong” โดยไม่บอกว่า service/instance/metric ไหน
  • No runbook — คนใหม่ในทีมไม่รู้ต้องทำอะไร
  • Duplicate alerts — rule ต่างกันแต่ detect ปัญหาเดียวกัน ควร merge
  • Stale alerts — rule ที่ไม่ fire มาเป็นปี อาจเป็นเพราะโค้ดเปลี่ยนหรือ metric name เปลี่ยน

ตัวอย่าง Alert ที่ดีเทียบกับที่ไม่ดี

# ❌ ไม่ดี — ไม่มีบริบท, cause-based, ไม่มี runbook
- alert: HighMemory
  expr: memory_used > 8000000000
  annotations:
    summary: "Memory high"

# ✅ ดี — symptom-based, มีบริบท, มี runbook
- alert: APILatencyHigh
  expr: |
    histogram_quantile(0.99,
      sum by (le, service) (rate(http_duration_seconds_bucket[5m]))
    ) > 2
  for: 10m
  labels:
    severity: critical
    team: platform
  annotations:
    summary: "{{ $labels.service }} p99 latency > 2s"
    description: |
      Service {{ $labels.service }} p99 latency is
      {{ $value | printf "%.2f" }}s for the past 10 minutes.
      Users are likely experiencing slow responses.
    runbook: "https://wiki.example.com/runbooks/api-latency"
    dashboard: "https://grafana.example.com/d/api-overview?var-service={{ $labels.service }}"

วัฒนธรรมทีม: Blameless Postmortem

ทุก incident ที่ผ่าน critical alert ควรมี postmortem แบบ blameless — เน้นหาสาเหตุเชิงระบบไม่ใช่หาคนผิด ในเอกสารควรมี timeline, root cause, impact, action items ที่ชัดเจน เพื่อป้องกันไม่ให้เกิดซ้ำ การ review notification บ่อย ๆ ใน retrospective ช่วยให้ทีมร่วมกันปรับปรุงคุณภาพของระบบ

สรุป

การออกแบบระบบ notification ที่ actionable ไม่ใช่เรื่องเทคนิคล้วน ๆ แต่เป็นเรื่องของวัฒนธรรมและการตัดสินใจ — ยึดหลัก symptom-based แทน cause-based, ใช้ SLO-driven approach, เขียน runbook ทุกครั้ง, หลีกเลี่ยง flappy alert ด้วย for: clause และ audit คุณภาพเป็นประจำ

เป้าหมายสุดท้ายคือให้ทีม oncall ได้นอนหลับ ไม่ต้องลุกขึ้นมารับ notification ที่ไม่ต้องทำอะไร ระบบที่สมบูรณ์จะปลุกคนเฉพาะเมื่อ user กำลังเดือดร้อนจริง ๆ และคนที่รับสามารถแก้ได้ทันทีด้วยข้อมูลที่ครบถ้วน