มีคืนหนึ่งผมโดน page ตีสอง เปิดเครื่องมาดู alert ชื่อ HighCPUUsage บน node ตัวหนึ่ง เปิด Grafana latency ปกติ error rate เป็นศูนย์ ผู้ใช้ไม่รู้สึกอะไรเลย ผมนั่งจ้อง dashboard อยู่ยี่สิบนาทีแล้วก็ acknowledge ทิ้ง ไปนอนต่อ แล้วเช้ามาก็เจอคำถามจากตัวเองว่า "alert ตัวนี้มีไว้ทำไมวะ"

คำตอบคือไม่มีใครรู้ มันถูกเขียนไว้ตั้งแต่ก่อนผมเข้าทีม และไม่มีใครกล้าลบ เพราะกลัวว่าวันหนึ่งมันจะมีประโยชน์ ซึ่งนี่แหละคือต้นตอของ alert fatigue เกือบทุกที่: เราเพิ่ม alert ง่ายมาก แต่ลบยากมาก

บทความนี้เล่าวิธีที่ผมใช้ทำ "alert audit" แบบเบาๆ ทำได้ในครึ่งวัน ไม่ต้องมี tool พิเศษ มีแค่ Prometheus กับ Alertmanager (หรือระบบเทียบเท่า) และมีกฎข้อเดียวที่ต้องยึด: alert ที่ปลุกคนได้ ต้องเป็นเรื่องที่คนต้องลุกขึ้นมาทำอะไรสักอย่างตอนนี้เลย ถ้าตื่นมาแล้วทำได้แค่ "ดูเฉยๆ" มันไม่ควรเป็น page

ตัวอย่างทั้งหมดในบทความเป็นของสมมติ ไม่ได้มาจากระบบของบริษัทไหน

ทำไม alert ถึงเน่าได้เอง

alert ไม่ได้เสียตอนเขียน มันเสียทีหลัง เหตุผลที่เจอบ่อยมีสามอย่าง

อย่างแรกคือระบบเปลี่ยนแต่ threshold ไม่เปลี่ยน CPU 80% เคยหมายถึง "ใกล้เต็ม" ตอนที่ใช้ instance เล็ก พอย้ายมา autoscaling แล้ว 80% กลายเป็นสถานะปกติของช่วงเช้า

อย่างที่สองคือคนเขียนย้ายทีมไปแล้ว เหลือแต่ rule ที่ไม่มี owner พอมันดังก็ไม่มีใครรู้ว่าต้องทำอะไร ก็เลย ack แล้วปล่อย

อย่างที่สามคือ alert ที่วัด "สาเหตุ" แทนที่จะวัด "อาการ" CPU สูง, disk 70%, pod restart หนึ่งครั้ง พวกนี้เป็นสาเหตุที่อาจจะนำไปสู่ปัญหาหรือไม่ก็ได้ แต่ผู้ใช้ไม่ได้รู้สึกอะไรจนกว่าจะมีอาการจริง (error พุ่ง, latency ช้า) หลักที่ SRE ทั่วไปแนะนำคือ page จากอาการที่ผู้ใช้รู้สึก ส่วนสาเหตุให้ไปอยู่ในหน้า dashboard หรือ ticket

ขั้นที่ 1: ดึงข้อมูลว่า alert ไหนดังบ่อยจริง

อย่าเริ่มจากความรู้สึก เริ่มจากตัวเลข Prometheus เก็บ metric ALERTS ไว้ให้อยู่แล้ว ลองนับว่าใน 30 วันที่ผ่านมา alert แต่ละชื่อเปลี่ยนสถานะเป็น firing กี่ครั้ง

# จำนวนครั้งที่ alert แต่ละตัวเข้าสถานะ firing ใน 30 วัน sort_desc( sum by (alertname) ( changes(ALERTS{alertstate="firing"}[30d]) ) ) 

ตัวเลขจะประมาณการ (เพราะ changes นับการเปลี่ยนค่าของ series ไม่ใช่จำนวน incident เป๊ะๆ) แต่พอใช้จัดอันดับได้ ผมมักเจอว่า alert ห้าอันดับแรกกินสัดส่วนของ page ทั้งหมดเกินครึ่ง ซึ่งแปลว่าแก้แค่ห้าตัวก็เห็นผลทันที

ถ้าใช้ Alertmanager คู่กับระบบ paging (PagerDuty, Opsgenie, หรืออะไรก็ตาม) ให้ export รายการ page ย้อนหลัง 30 วันออกมาด้วย แล้วจับคู่กับชื่อ alert คอลัมน์ที่อยากได้คือ: ชื่อ alert, จำนวนครั้งที่ page, กี่ครั้งที่คน ack แล้ว "ไม่ได้ทำอะไรเลย"

คอลัมน์สุดท้ายนั้นหายากหน่อย ต้องถามทีมหรือไล่ดู incident note แต่ถ้าหาได้ มันคือตัวชี้วัดที่ตรงที่สุด

ขั้นที่ 2: ถาม 5 ข้อกับทุก alert

ไล่ทีละ alert ในรายการ (เริ่มจากที่ดังบ่อยสุด) แล้วถามห้าข้อนี้

checklist 5 ข้อ alert audit และโครง runbook

ข้อ 1: ผู้ใช้รู้สึกไหม ถ้า alert นี้ดังแล้วผู้ใช้ไม่เห็นผลกระทบ (ยัง) มันไม่ควรเป็น page ให้ลดระดับเป็น ticket หรือแจ้ง Slack ในเวลางาน

ข้อ 2: คน on-call ต้องทำอะไรสักอย่างตอนนี้เลยไหม ถ้าคำตอบคือ "ดูเฉยๆ" หรือ "รอดูว่าหายเองไหม" ให้เอาออกจาก paging ทันที alert ที่หายเองได้ส่วนใหญ่ควรมี for: ที่ยาวพอ หรือไม่ควรมีเลย

ข้อ 3: มี runbook ไหม และอ่านแล้วทำตามได้จริงไหม ลองสุ่มเปิด runbook ของ alert ที่ไม่ได้เขียนเอง ให้เพื่อนร่วมทีมที่ไม่เคยเจอเรื่องนี้ลองอ่าน ถ้าอ่านแล้วต้องไปถามคนอื่นต่อ แปลว่า runbook ยังไม่ผ่าน

ข้อ 4: มี owner ที่ยังอยู่ในทีมไหม ใส่ label team กับ owner ใน rule ทุกตัว alert ที่ไม่มีเจ้าของให้เลือกสองทาง: หาเจ้าของใหม่ภายในสัปดาห์นี้ หรือลบ

ข้อ 5: ใน 90 วันที่ผ่านมามันเคยจับปัญหาจริงไหม ถ้าไม่เคยเลยและดังแต่ false positive ให้ตัดสินใจกล้าๆ ได้เลย ถ้ามันสำคัญจริงเดี๋ยวก็มีคนมาเรียกคืน และตอนนั้นเราจะรู้ว่าต้องเขียนให้ดีกว่าเดิมยังไง

กฎการตัดสินที่ผมใช้ง่ายมาก ตอบ "ไม่" ตั้งแต่สองข้อขึ้นไป = ลดระดับหรือลบ ตอบ "ไม่" ข้อ 1 หรือ 2 ข้อเดียวก็พอให้ถอดออกจาก paging แล้ว

ขั้นที่ 3: ลดระดับ ไม่ใช่ลบทิ้งทุกอย่าง

ผมมักเจอคนกลัวว่าลด alert แล้วจะพลาดเหตุการณ์จริง วิธีที่ช่วยให้สบายใจคือแยกเป็นสามชั้น แล้ว route ด้วย label severity ใน Alertmanager

# alertmanager.yml (ตัวอย่างสมมติ) route: receiver: slack-ops-info group_by: [alertname, service] routes: - matchers: [severity="page"] receiver: pager-oncall repeat_interval: 1h - matchers: [severity="ticket"] receiver: ticket-queue repeat_interval: 24h # ที่เหลือ (severity="info") ไป Slack ช่องข้อมูล ไม่ปลุกใคร receivers: - name: pager-oncall - name: ticket-queue - name: slack-ops-info 

ชั้น page สงวนไว้ให้อาการที่ผู้ใช้รู้สึกเท่านั้น (เช่น error rate เกิน SLO, ระบบหลักตอบไม่ได้) ชั้น ticket สำหรับเรื่องที่ต้องแก้แต่ไม่รีบ เช่น disk จะเต็มภายในสามวัน ชั้น info คือทุกอย่างที่เหลือ ไปอยู่ใน dashboard หรือ Slack ให้ดูตอนมีเวลา

CPU 85% ที่ปลุกผมคืนนั้น ย้ายไปอยู่ชั้น info ทันที ส่วนสิ่งที่มา page แทนคือ alert จาก SLO ว่า error budget กำลังไหม้เร็วผิดปกติ ซึ่งเป็นอาการที่ผู้ใช้รู้สึกจริง

อีกอย่างที่ช่วยลดเสียงรบกวนได้มากคือ inhibit_rules ถ้าระบบทั้งก้อนล่มอยู่แล้ว ไม่จำเป็นต้องมี alert ย่อยสิบตัวดังตามกันมา ให้ตัวใหญ่กดตัวเล็กไว้

inhibit_rules: - source_matchers: [alertname="ServiceDown"] target_matchers: [severity="page"] equal: [service] 

ขั้นที่ 4: โครง runbook 1 หน้าที่ตีสองอ่านรู้เรื่อง

ตอนตีสอง สมองเราทำงานได้ประมาณครึ่งเดียว runbook ที่ดีจึงไม่ใช่เอกสารยาว แต่เป็นหน้าเดียวที่ไล่ตามได้โดยไม่ต้องคิดเยอะ ผมใช้โครงนี้

  1. อาการคืออะไร หนึ่งประโยค เช่น "อัตรา 5xx ของ API หลักเกิน 2% ติดต่อกัน 5 นาที"
  2. ผลกระทบต่อผู้ใช้ ใครเดือดร้อนและเดือดร้อนแค่ไหน ช่วยให้ตัดสินใจว่าต้องปลุกคนอื่นไหม
  3. เช็คอะไรก่อน (3 อย่างพอ) ลิงก์ dashboard ตรงๆ ไม่ใช่ "ไปดู Grafana" เพราะตอนนั้นไม่มีใครจำได้ว่า folder อยู่ไหน
  4. แก้ยังไงเร็วที่สุด คำสั่งที่ copy ไปรันได้เลย พร้อมบอกว่าผลที่คาดหวังคืออะไร (rollback, scale, restart ตัวที่ตายแล้ว)
  5. ถ้าไม่หายใน X นาที ต้อง escalate หาใคร ใส่ชื่อทีมกับช่องทาง ไม่ใช่เบอร์ส่วนตัวใน repo
  6. วันที่ตรวจล่าสุดและใครตรวจ runbook ที่ไม่ได้อัปเดตเกินหกเดือนให้ถือว่าอาจใช้ไม่ได้

อย่าเขียน runbook เป็น "ทฤษฎี" ห้ามมีย่อหน้าอธิบายว่า Kubernetes scheduler ทำงานยังไง เอาไปไว้ในเอกสารอื่น runbook มีหน้าที่เดียวคือพาคนที่งัวเงียไปถึงจุดที่ระบบกลับมา

ใน rule ให้ใส่ลิงก์ไปที่ runbook ผ่าน annotation เพื่อให้เห็นใน page เลย ไม่ต้องไปหา

- alert: ApiErrorBudgetBurn expr: | ( sum(rate(http_requests_total{job="api",code=~"5.."}[5m])) / sum(rate(http_requests_total{job="api"}[5m])) ) > 0.02 for: 5m labels: severity: page team: platform annotations: summary: "5xx ของ API เกิน 2% นาน 5 นาที" runbook_url: "https://runbooks.example.internal/api-5xx" 

(ตัวอย่างนี้ตั้งใจให้อ่านง่าย ของจริงถ้าใช้ SLO ควรใช้ burn rate หลายหน้าต่างเวลาแทน threshold ตรงๆ แบบนี้ แต่โครง label กับ annotation เหมือนกัน)

ขั้นที่ 5: ทำให้เป็นนิสัย ไม่ใช่โปรเจกต์ครั้งเดียว

audit ครั้งเดียวช่วยได้สักพัก แล้ว alert ก็เน่าอีก สิ่งที่ผมทำให้มันยั่งยืนมีสามอย่างเล็กๆ

  • นับ page ต่อ shift ใส่ตัวเลขใน retro ของทีมทุกสัปดาห์ ถ้า on-call หนึ่งคนโดน page เกินสองสามครั้งต่อคืนเป็นประจำ ถือว่าระบบ alert ป่วย
  • ทุก page ที่ไม่ต้องลงมือทำอะไร = ต้องมี action item ใครโดน page แล้ว ack ทิ้ง ให้เปิด ticket สั้นๆ ว่าจะลด ย้าย หรือแก้ alert นั้นยังไง ไม่งั้นมันจะวนกลับมาอีก
  • ตั้งปฏิทิน audit ทุกไตรมาส ใช้เวลาครึ่งวัน ไล่เฉพาะห้าถึงสิบ alert ที่ดังบ่อยสุดก็พอ

สรุปสั้นๆ

alert fatigue ไม่ได้แก้ด้วยการซื้อเครื่องมือใหม่ แต่แก้ด้วยการกล้าถามว่า "alert ตัวนี้ทำให้ใครต้องทำอะไรจริงๆ ไหม" แล้วยอมลดหรือลบเมื่อคำตอบคือไม่ ระบบ monitoring ที่ดีไม่ใช่ระบบที่ดังเยอะ แต่คือระบบที่พอดังแล้วทุกคนรู้ทันทีว่าต้องลุกขึ้นมาทำอะไร

ลองเริ่มจาก query ด้านบน ดึงห้าอันดับแรกมาดู แล้วถามห้าข้อกับแต่ละตัว ผมเดาว่าอย่างน้อยหนึ่งตัวในนั้นจะเป็นแบบ HighCPUUsage ของผม ที่ไม่มีใครรู้ว่ามีไว้ทำไม 555

หมายเหตุ: ตัวอย่าง config และชื่อ alert ทั้งหมดเป็นของสมมติเพื่อประกอบการอธิบาย ไม่ได้อ้างอิงระบบของบริษัทหรือลูกค้าใด ควรปรับ threshold และ routing ให้เหมาะกับระบบและ SLO ของทีมคุณเอง