SLI, SLO, error budget và alert không làm phiền
Tiếp đi: SLO và alert. Hiện tại mình alert khi CPU > 80%, tuần trước bị gọi dậy 6 lần mà mở ra thì chẳng có gì hỏng.
Đó chính là bệnh kinh điển: alert theo nguyên nhân thay vì triệu chứng. Người dùng không quan tâm CPU, họ quan tâm checkout có thành công và có nhanh không. Bài này đổi cách nghĩ: đo trải nghiệm người dùng, đặt mục tiêu, và chỉ đánh thức người khi mục tiêu bị đe dọa. Phần cơ bản về p95/p99 và alert fatigue đã có ở High availability; ở đây mình làm thật cho shop-app.
1. Bài toán
Alert hiện tại của shop-app:
- CPU > 80% 5 phút (bắn lúc batch đêm chạy, user không sao)
- Memory > 85% (JVM heap luôn gần đầy, đó là bình thường)
- Pod restart > 0 (HPA scale, rollout cũng bắn)
- Có dòng ERROR trong log (thẻ bị từ chối cũng bắn)
Kết quả: 40 alert/tuần, team mute kênh Slack. Tuần trước payment lỗi 30% trong 20 phút thì không ai thấy vì không có alert nào đo đúng thứ đó.
2. Vấn đề
- Nguyên nhân có hàng trăm loại, không liệt kê hết; triệu chứng thì chỉ vài cái (lỗi, chậm).
- Alert nguyên nhân bắn cả khi user không bị ảnh hưởng → alert fatigue.
- Không có mục tiêu chung: "chậm" là bao nhiêu? 99.9% hay 99.99%? Dev muốn ship nhanh, ops muốn ổn định, cãi nhau mãi.
3. Giải pháp: SLI → SLO → error budget → alert
| Khái niệm | Nghĩa | Ví dụ shop-app |
|---|---|---|
| SLI (indicator) | Tỷ lệ sự kiện tốt / tổng sự kiện | % request POST /orders trả về không phải 5xx và nhanh hơn 800ms |
| SLO (objective) | Mục tiêu nội bộ cho SLI trong một cửa sổ | 99.5% trong 28 ngày |
| SLA (agreement) | Cam kết với khách, có phạt | 99% / tháng, vi phạm hoàn tiền. Luôn lỏng hơn SLO |
| Error budget | 1 − SLO: lượng lỗi được phép | 0.5% × 28 ngày ≈ 3.4 giờ "hỏng" mỗi 28 ngày |
Chọn SLI theo phương pháp:
- RED cho service (request-driven): Rate, Errors, Duration. Áp cho order, payment, inventory.
- USE cho tài nguyên: Utilization, Saturation, Errors. Áp cho CPU, connection pool, thread pool, disk. Dùng để chẩn đoán, không để page.
- Google gọi gộp là 4 golden signals: latency, traffic, errors, saturation.
Error budget biến chuyện cãi nhau thành con số: còn budget thì ship tính năng; hết budget thì đóng băng release, ưu tiên ổn định.
4. Code từng bước
Bước 1: Bật histogram cho request (order-service)
management:
endpoints.web.exposure.include: health,prometheus
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 300ms,800ms,2s # thêm bucket đúng ngưỡng SLO
observations:
key-values:
env: ${ENV:dev}
Bước 2: Recording rule định nghĩa SLI
# slo-rules.yaml (PrometheusRule, kube-prometheus-stack)
groups:
- name: checkout-slo
rules:
- record: sli:checkout_requests:rate5m
expr: sum(rate(http_server_requests_seconds_count{app="order-service",uri="/orders",method="POST"}[5m]))
- record: sli:checkout_good:rate5m
expr: |
sum(rate(http_server_requests_seconds_bucket{app="order-service",uri="/orders",
method="POST",outcome!="SERVER_ERROR",le="0.8"}[5m]))
- record: sli:checkout_error_ratio:rate5m
expr: 1 - (sli:checkout_good:rate5m / sli:checkout_requests:rate5m)
# Lặp lại với [30m], [1h], [6h] cho alert đa cửa sổ
Bước 3: Burn-rate alert
Burn rate = tốc độ tiêu budget so với mức "vừa đủ hết đúng cuối kỳ". Burn rate 1 = hết budget sau đúng 28 ngày. Burn rate 14.4 = hết 2% budget trong 1 giờ.
- alert: CheckoutErrorBudgetFastBurn
expr: |
sli:checkout_error_ratio:rate1h > (14.4 * 0.005)
and
sli:checkout_error_ratio:rate5m > (14.4 * 0.005)
labels: { severity: page }
annotations:
summary: "Checkout đang đốt error budget nhanh (hết trong ~2 ngày)"
runbook_url: https://wiki.shop.local/runbooks/checkout-slo
dashboard: https://grafana.shop.local/d/checkout
- alert: CheckoutErrorBudgetSlowBurn
expr: |
sli:checkout_error_ratio:rate6h > (6 * 0.005)
and
sli:checkout_error_ratio:rate30m > (6 * 0.005)
labels: { severity: ticket } # tạo ticket, không gọi dậy
Cửa sổ dài (1h) đảm bảo không bắn vì một spike ngắn; cửa sổ ngắn (5m) giúp alert tự tắt nhanh khi đã hết lỗi.
Bước 4: Định tuyến bằng Alertmanager và thử
route:
receiver: slack-shop
group_by: [alertname, app]
routes:
- matchers: [ severity="page" ]
receiver: pagerduty-shop
receivers:
- name: pagerduty-shop
pagerduty_configs: [ { routing_key: "${PAGERDUTY_KEY}" } ]
- name: slack-shop
slack_configs: [ { api_url: "${SLACK_WEBHOOK}", channel: "#shop-alerts" } ]
# Thử: bắn lỗi giả rồi xem alert pending → firing
kubectl port-forward svc/prometheus 9090 -n monitoring
curl -s 'localhost:9090/api/v1/query?query=sli:checkout_error_ratio:rate5m'
curl -s localhost:9090/api/v1/alerts | jq '.data.alerts[].labels.alertname'
5. Cách hoạt động
Error budget 28 ngày = 100%
│████████████████████████████████████████│ ngày 0
│██████████████████████████████░░░░░░░░░░│ ngày 10: sự cố payment đốt 25%
│██████████████████████░░░░░░░░░░░░░░░░░░│ ngày 15: deploy lỗi đốt 20%
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ hết budget → freeze release, làm reliability
Burn rate Hết budget trong Cửa sổ dài/ngắn Hành động
14.4 ~2 ngày 1h / 5m page ngay
6 ~5 ngày 6h / 30m page hoặc ticket
1 28 ngày 3d / 6h ticket
Phân tầng alert:
Triệu chứng (SLO burn) ──► PAGE (người dùng đang đau)
Nguyên nhân (USE) ──► dashboard (dùng khi điều tra)
Sắp hỏng (disk 90%, cert hết hạn 7 ngày) ──► ticket giờ hành chính
6. So sánh: alert nguyên nhân vs triệu chứng
| Alert nguyên nhân (CPU, memory, restart) | Alert triệu chứng (SLO burn rate) | |
|---|---|---|
| Số lượng | Nhiều, khó liệt kê đủ | Vài cái cho mỗi user journey |
| False positive | Cao (CPU cao mà user ổn) | Thấp |
| Bỏ sót | Có (lỗi kiểu mới không có alert) | Ít: hỏng kiểu gì cũng làm lỗi/chậm |
| Hành động | Mơ hồ | Rõ: user đang bị ảnh hưởng, cần mitigate |
| Vai trò đúng | Dashboard chẩn đoán | Page on-call |
7. Khi nào nên dùng / không nên dùng
- Đặt SLO cho vài user journey quan trọng (checkout, xem sản phẩm), không phải cho mọi endpoint.
- Đừng đặt 100% hay 99.99% khi phụ thuộc bên thứ ba 99.9%: không bao giờ đạt được.
- Mỗi alert page phải có runbook và phải cần người hành động ngay. Không hành động được → hạ xuống ticket hoặc xóa.
- Ngoại lệ hợp lý cho alert nguyên nhân: thứ sắp gây sự cố mà SLO chưa thấy (disk sắp đầy, certificate sắp hết hạn).
rate, _bucket, le nghĩa là gì? Bài sau: Prometheus và Grafana.Sao không dùng latency trung bình cho SLO cho đơn giản? Dashboard của mình đang hiện average 120ms, nhìn rất ổn mà.
Chính vì "nhìn rất ổn" nên nó nguy hiểm. Average che mất đuôi phân bố:
1000 request: 990 cái 50ms, 10 cái 7s
average = (990×50 + 10×7000)/1000 = 119.5ms ← "ổn"
p99 = 7s ← 1% user chờ 7 giây
Và 1% đó không ngẫu nhiên: thường là khách có giỏ hàng lớn nhất (nhiều item → nhiều lần gọi inventory), tức khách giá trị nhất. Một trang gọi 5 service thì xác suất dính ít nhất một p99 là 1 − 0.995 ≈ 5%.
Cách định nghĩa SLI như ở trên ("% request nhanh hơn 800ms") còn tốt hơn cả p99: nó là tỷ lệ đếm được, cộng trừ được, và tính ra error budget trực tiếp. Còn percentile thì không lấy trung bình được giữa các Pod (xem bài 19.3).
Team có 4 người thay nhau on-call. Làm sao để on-call không thành cực hình?
Câu hỏi rất thật. On-call bền vững cần cả kỹ thuật lẫn quy trình:
- Đo chính alert: mỗi tuần đếm số page, % page có hành động thật. Mục tiêu tham khảo: ≤ 2 sự cố mỗi ca 12 giờ. Page không có hành động → xóa hoặc sửa ngưỡng trong tuần đó.
- Mọi page có runbook: ý nghĩa, dashboard, 3 bước kiểm tra đầu tiên, cách mitigate (rollback, scale, tắt feature flag).
- Gom nhóm:
group_by,inhibit_rulestrong Alertmanager. Postgres chết thì đừng bắn thêm 3 alert "order/payment/inventory lỗi". - Phân cấp: page (gọi điện) chỉ cho SLO burn nhanh; còn lại vào Slack/ticket giờ hành chính.
- Rotation công bằng: có primary và secondary, bàn giao ca có ghi chú, được nghỉ bù sau đêm bị gọi.
- Sửa gốc: sự cố lặp lại → action item trong postmortem (bài 19.6), không chỉ restart Pod cho qua.
# Ức chế alert con khi gốc đã kêu
inhibit_rules:
- source_matchers: [ alertname="PostgresDown" ]
target_matchers: [ severity="page", namespace="shop" ]
equal: [ namespace ]
Phỏng vấn hỏi về SLO và alerting thì trả lời sao?
Q1. SLI, SLO, SLA khác nhau thế nào?
🇻🇳 Tiếng Việt:
SLI là thước đo, thường là tỷ lệ sự kiện tốt trên tổng, ví dụ % request checkout thành công và nhanh hơn 800ms. SLO là mục tiêu nội bộ cho SLI trong một cửa sổ, ví dụ 99.5% trong 28 ngày. SLA là cam kết hợp đồng với khách hàng, có hậu quả tài chính, và luôn lỏng hơn SLO để có khoảng an toàn. Phần còn lại 1 − SLO là error budget.
🇬🇧 English:
An SLI is the measurement, usually good events over total events, for example the percentage of checkout requests that succeed in under 800ms. An SLO is the internal target for that SLI over a window, like 99.5% over 28 days. An SLA is a contractual promise to customers with financial consequences, and it's always looser than the SLO to leave a safety margin. One minus the SLO is the error budget.
Q2. Error budget dùng để làm gì?
🇻🇳 Tiếng Việt:
Nó biến tranh luận giữa tốc độ release và độ ổn định thành con số. Còn budget thì team được ship nhanh, thử nghiệm; tiêu hết budget thì dừng release tính năng và dành thời gian cho reliability. Nó cũng là cơ sở cho alert: em alert theo tốc độ đốt budget thay vì theo ngưỡng tĩnh.
🇬🇧 English:
It turns the argument between shipping speed and stability into a number. While budget remains, the team can ship fast and experiment; once it's spent, feature releases pause and the team works on reliability. It also drives alerting: I alert on how fast the budget is burning rather than on static thresholds.
Q3. Burn-rate alert là gì, sao tốt hơn ngưỡng tĩnh?
🇻🇳 Tiếng Việt:
Burn rate là tốc độ tiêu error budget so với tốc độ "vừa đủ hết cuối kỳ". Em dùng alert đa cửa sổ: page khi burn rate trên 14.4 ở cả cửa sổ 1 giờ và 5 phút, tạo ticket khi burn rate trên 6 ở cửa sổ 6 giờ. Cửa sổ dài tránh spike ngắn, cửa sổ ngắn giúp alert tắt nhanh khi đã hồi phục. So với "error rate > 1%", nó gắn trực tiếp với tác động lên người dùng và mức khẩn cấp.
🇬🇧 English:
Burn rate is how fast you consume the error budget relative to the pace that would exactly exhaust it at the end of the window. I use multi-window alerts: page when burn rate exceeds 14.4 over both 1 hour and 5 minutes, and open a ticket when it exceeds 6 over 6 hours. The long window ignores short spikes and the short window lets the alert resolve quickly after recovery. Compared to "error rate above 1%", it ties directly to user impact and urgency.
Q4. RED và USE khác nhau thế nào?
🇻🇳 Tiếng Việt:
RED là Rate, Errors, Duration, dùng cho service xử lý request như order-service, nó đo trải nghiệm người dùng nên hợp để làm SLI và page. USE là Utilization, Saturation, Errors, dùng cho tài nguyên như CPU, connection pool, thread pool, giúp chẩn đoán nguyên nhân khi RED đã báo có vấn đề. Em page theo RED và dùng USE trên dashboard.
🇬🇧 English:
RED is Rate, Errors, Duration, for request-driven services like order-service; it reflects user experience, so it's good for SLIs and paging. USE is Utilization, Saturation, Errors, for resources like CPU, connection pools and thread pools; it helps diagnose the cause once RED shows a problem. I page on RED and keep USE on dashboards.
Cách nhớ ngắn gọn
- SLI đo, SLO mục tiêu, SLA hợp đồng; budget = 1 − SLO.
- Page triệu chứng (user đau), không page nguyên nhân (CPU).
- Burn rate đa cửa sổ: 14.4 (1h & 5m) page, 6 (6h & 30m) ticket.
- RED cho service, USE cho tài nguyên.
- Mỗi page có runbook và có hành động; không thì xóa.
Tự kiểm tra
SLO 99.9% trong 30 ngày, error budget là bao nhiêu phút?
0.1% × 43 200 phút ≈ 43 phút.
Vì sao SLA phải lỏng hơn SLO?
Để team phát hiện và sửa khi vi phạm SLO trước khi vi phạm hợp đồng và bị phạt.
Alert "JVM heap > 85%" nên là page không?
Không. Đó là nguyên nhân, thường bình thường với JVM. Để trên dashboard; page theo SLO.
Vì sao burn-rate alert cần cả cửa sổ ngắn?
Để alert tự tắt nhanh sau khi hồi phục, thay vì kêu thêm cả giờ vì cửa sổ dài còn chứa lỗi cũ.
Average 120ms có đủ để kết luận latency tốt?
Không. Average che đuôi; cần p99 hoặc tỷ lệ request dưới ngưỡng.