·

Tiếng Việt: Hands On Diagnosing A Production Incident

Hands On Diagnosing A Production Incident

Một buổi tối thứ Sáu cao điểm, checkout bắt đầu rớt hàng loạt trên năm service khác nhau — đây là cách một on-call engineer dùng AI để lần ra root cause thật sự trong 40 phút thay vì 4 giờ.

Bối cảnh sự cố

19:42 tối thứ Sáu, giờ cao điểm mua sắm cuối tuần. Hệ thống thương mại điện tử của công ty (tạm gọi là ShopFlow) đang phục vụ khoảng 3.200 request/phút vào luồng checkout. Đột nhiên PagerDuty đổ chuông: alert từ Datadog báo checkout-api p99 latency tăng từ 220ms lên 4.8s, đồng thời tỷ lệ lỗi 5xx nhảy từ 0.1% lên 11%.

Kiến trúc liên quan gồm năm thành phần chính:

  • api-gateway — Kong, nhận request từ frontend, route tới các service phía sau.
  • order-service — Node.js/Express, tạo đơn hàng, gọi inventory-servicepayment-service.
  • inventory-service — Go, giữ tồn kho realtime, dùng Redis làm cache layer trước Postgres.
  • payment-service — Java/Spring Boot, xử lý thanh toán qua Stripe, publish event lên RabbitMQ sau khi charge thành công.
  • order-events-consumer — consumer đọc từ RabbitMQ, cập nhật trạng thái đơn hàng và gửi email xác nhận.

Triệu chứng ban đầu quan sát được trên Grafana dashboard "Checkout Health":

  • order-serviceinventory-service gọi qua HTTP bắt đầu timeout sau 3s (giới hạn cấu hình), tỷ lệ timeout tăng dần theo thời gian chứ không phải spike đột ngột.
  • RabbitMQ queue order.created có queue depth tăng từ ~50 message lên hơn 12.000 message chỉ trong 25 phút.
  • Một số khách hàng report trên Slack support channel: "đã bị trừ tiền nhưng đơn hàng hiện trạng thái Pending mãi không xong", một vài trường hợp báo bị charge 2 lần.
  • CPU và memory của inventory-service pod vẫn ở mức bình thường (45-60%), không có OOM kill, không có restart nào trong 30 phút gần nhất — loại trừ ngay giả thuyết "service bị crash".

Về mặt kinh doanh: đây là khung giờ chiếm ~18% doanh thu trong ngày của ShopFlow. Ước tính mỗi phút downtime checkout tương đương khoảng 2.400 USD doanh thu bị mất, chưa kể rủi ro double-charge ảnh hưởng uy tín thương hiệu. SLA nội bộ cam kết checkout p99 < 500ms và error rate < 1%.

Đội hình ứng cứu: Minh (on-call engineer, người đầu tiên nhận page), và Lan (incident commander — IC, được escalate sau 8 phút khi mức độ nghiêm trọng được xác nhận là SEV-1). Team quyết định mở incident channel #inc-2026-0709-checkout trên Slack, tạo Datadog incident, và bắt đầu thu thập bằng chứng song song với việc cân nhắc rollback.

Deploy gần nhất: inventory-service v2.14.0 được rollout lúc 19:15, cách thời điểm alert nổ ra khoảng 27 phút — một chi tiết quan trọng sẽ quay lại ở phần sau.

Mẹo: Ghi lại mốc thời gian bắt đầu triệu chứng (không phải thời điểm alert nổ) ngay từ đầu — chênh lệch giữa "sự cố thực sự bắt đầu" và "lúc hệ thống monitoring phát hiện ra" thường là manh mối định vị root cause quan trọng nhất.

Xây dựng gói ngữ cảnh sự cố

Trước khi mở AI assistant, việc đầu tiên Minh làm là gom "bằng chứng hiện trường" thành một context package có tổ chức — vì ném một câu hỏi mơ hồ như "checkout đang bị lỗi, sao vậy?" vào AI mà không kèm dữ liệu cụ thể chỉ tạo ra phỏng đoán chung chung, vô dụng trong tình huống SEV-1.

Các artifact cần thu thập, theo thứ tự ưu tiên:

  1. Recent deploys & diff — lịch sử deploy 2 giờ gần nhất từ CI/CD (GitHub Actions), đặc biệt là diff của inventory-service v2.14.0 so với v2.13.2.
  2. Alert history — timeline các alert từ Datadog/PagerDuty, sắp theo thứ tự thời gian.
  3. Metrics dashboards — screenshot hoặc export số liệu từ Grafana: p50/p95/p99 latency, error rate, queue depth, connection pool utilization.
  4. Logs — log lỗi từ order-serviceinventory-service trong khung giờ 19:15-19:45, lọc theo level ERROR/WARN.
  5. Distributed traces — vài trace ID tiêu biểu từ Datadog APM cho các request bị timeout, để thấy breakdown thời gian giữa các service.
  6. On-call chat transcript — tóm tắt các quan sát ban đầu, giả thuyết đã bị loại trừ (như "không phải OOM").

Minh tổ chức toàn bộ thành một file markdown duy nhất, dán trực tiếp vào Claude Code (đã có sẵn quyền truy cập repo qua terminal). Cấu trúc context package thực tế:


## Timeline
- 19:15 — Deploy inventory-service v2.14.0 (PR #4821, "optimize stock lookup query")
- 19:38 — Datadog alert: inventory-service HTTP p95 > 2000ms (WARN)
- 19:42 — PagerDuty page: checkout-api p99 4.8s, error rate 11% (CRITICAL)
- 19:50 — IC Lan joins #inc-2026-0709-checkout, declares SEV-1

## Symptoms
- order-service -> inventory-service HTTP calls timing out at 3s ceiling
  (timeout rate climbing gradually, not a sudden step change)
- RabbitMQ queue `order.created` depth: 50 -> 12,400 in 25 minutes
- Support reports: orders stuck "Pending", 3 confirmed double-charge cases
- inventory-service pod CPU 45-60%, memory stable, zero restarts, zero OOM kills

## Recent diff (PR #4821, inventory-service v2.13.2 -> v2.14.0)
```diff
- func (r *StockRepo) GetAvailable(sku string) (int, error) {
-     return r.redis.Get(ctx, "stock:"+sku).Int()
- }
+ func (r *StockRepo) GetAvailable(sku string) (int, error) {
+     rows, err := r.db.QueryContext(ctx,
+         "SELECT quantity FROM stock_ledger WHERE sku=$1 ORDER BY id DESC LIMIT 1", sku)
+     ...
+ }

order-service error logs (sample, 19:40-19:45)

2026-07-09T19:41:03+07:00 ERROR order-service: inventory lookup failed sku=SKU-88213
context deadline exceeded (client.Timeout exceeded while awaiting headers)
[... 340 similar lines in this window ...]

Datadog APM trace excerpt (trace_id=8f2a91cc)

order-service -> inventory-service: 3002ms (TIMEOUT)
inventory-service internal: db.query stock_ledger: 2890ms
postgres.acquire_connection: 2650ms (waiting on pool)

Metrics

  • inventory-service DB connection pool: max=20, active=20, waiting=47 (since 19:20)
  • Postgres primary CPU: 38% (not saturated)
  • Redis (previously used for stock cache): connections=2 (was ~180 before deploy)

Việc đưa cả **diff của PR gây ra thay đổi**, **log lỗi thực tế**, **trace breakdown theo từng hop**, và **số liệu connection pool** vào cùng một chỗ giúp AI (hoặc bất kỳ đồng nghiệp nào đọc sau) nhìn thấy toàn cảnh thay vì chỉ một triệu chứng bề mặt. Đây chính là nguyên tắc "context window là chiến trường" — càng đưa đúng, đủ, có cấu trúc thì AI reasoning càng chính xác, càng đưa thừa noise (toàn bộ log 2 tiếng, full stack trace không liên quan) thì càng loãng tín hiệu.

> **Mẹo:** Luôn dán kèm diff của deploy gần nhất trong context package, kể cả khi bạn nghĩ nó "không liên quan" — AI RCA session gần như luôn bắt đầu bằng câu hỏi "cái gì thay đổi gần đây nhất", và bạn không muốn mất thời gian round-trip đi tìm nó giữa chừng.

## Phiên RCA có cấu trúc cùng AI

Với context package đã sẵn sàng, Minh mở Claude Code ngay trong terminal (đã kết nối repo `shopflow-backend`) và bắt đầu một phiên RCA có cấu trúc theo ba pha, thay vì hỏi lan man.

### Phase 1: Phân tích ban đầu

Minh dán toàn bộ context package ở trên kèm prompt:

I'm the on-call engineer handling a SEV-1 production incident. Here is the
full incident context package (timeline, symptoms, recent diff, logs, trace,
metrics) pasted above.

Please do an initial analysis:
1. Summarize what you understand is happening, in plain terms.
2. List every signal in this data that looks abnormal.
3. Do NOT jump to a fix yet — I need a broad initial read before we narrow down.


AI (Claude Code) phản hồi với một bản tóm tắt có cấu trúc, đại ý:

- **Điều đang xảy ra**: `order-service` gọi `inventory-service` để check tồn kho, nhưng call này đang timeout ở ngưỡng 3s ngày càng nhiều, khiến toàn bộ luồng checkout bị chặn (order-service đang gọi đồng bộ, không có fallback/circuit breaker), kéo theo RabbitMQ queue phình to vì `payment-service` vẫn charge được nhưng downstream xử lý event bị nghẽn do backpressure từ order-service.
- **Các tín hiệu bất thường được liệt kê**:
  - Trace cho thấy 2890ms trong tổng 3002ms nằm ở bước `db.query stock_ledger`, và phần lớn trong đó (2650ms) là thời gian **chờ connection pool**, không phải thời gian query thực thi.
  - Connection pool của `inventory-service`: `active=20/max=20, waiting=47` — pool đã bão hòa hoàn toàn kể từ 19:20, tức là **5 phút sau deploy**, sớm hơn nhiều so với lúc alert nổ ra lúc 19:38.
  - Redis connections rớt từ ~180 xuống còn 2 — dấu hiệu code mới không còn dùng Redis nữa.
  - PR #4821 đổi hàm `GetAvailable` từ đọc Redis cache sang query trực tiếp Postgres `stock_ledger`, dùng `ORDER BY id DESC LIMIT 1` — một pattern có thể gây full/partial scan nếu thiếu index phù hợp.
  - Postgres CPU chỉ 38% — nghĩa là DB server không hề quá tải, vấn đề nằm ở tầng connection pool/application, không phải hạ tầng DB.
- AI chủ động note: "Timeout rate climbing gradually" khớp với giả thuyết connection pool bị rò rỉ hoặc bị giữ quá lâu dần theo thời gian, chứ không phải một sự cố tức thời như DB down.

### Phase 2: Xếp hạng và kiểm chứng giả thuyết

Minh tiếp tục, ép AI đưa ra danh sách giả thuyết có xếp hạng kèm cách kiểm chứng cụ thể:

Good. Now give me a ranked list of hypotheses for the root cause, ordered by
likelihood given the evidence. For each hypothesis:
- State the hypothesis in one sentence.
- State the specific evidence that supports it.
- Propose ONE concrete, fast test I can run right now (a query, a log grep,
a dashboard check, a kubectl command) to confirm or rule it out.

We are still in an active incident — tests must be read-only / safe to run
against production.


AI trả về ba giả thuyết xếp hạng:

**#1 (khả năng cao nhất) — Missing index gây mỗi query full-scan trên `stock_ledger`, khiến mỗi connection giữ lâu hơn nhiều so với trước, làm cạn pool dần.**
Bằng chứng: query mới thay Redis O(1) lookup bằng Postgres query có `ORDER BY ... LIMIT 1` trên bảng ledger (append-only, tăng trưởng liên tục) — nếu không có index đúng trên `(sku, id)`, mỗi query phải scan toàn bộ lịch sử ledger của SKU đó.
Test đề xuất: chạy `EXPLAIN ANALYZE` trực tiếp câu query trong PR trên một SKU có nhiều bản ghi lịch sử, xem có `Seq Scan` không.

**#2 — Connection không được release đúng cách (leak) do thiếu `defer rows.Close()` hoặc lỗi xử lý context trong code mới.**
Bằng chứng: pool bão hòa dần theo thời gian kể cả khi throughput không tăng đột biến — pattern kinh điển của connection leak.
Test đề xuất: `grep -n "rows.Close\|defer" ` trong file thay đổi của PR #4821 để kiểm tra resource cleanup.

**#3 (khả năng thấp hơn) — Pool size bị giảm nhầm trong config khi deploy.**
Bằng chứng: yếu, chỉ dựa trên việc pool bão hòa nhanh; chưa có bằng chứng trực tiếp.
Test đề xuất: `kubectl get configmap inventory-service-config -o yaml | grep -i pool` so sánh với version trước.

Minh chạy song song cả ba test. Kết quả: `EXPLAIN ANALYZE` cho ra `Seq Scan on stock_ledger (cost=0.00..48213.00 rows=1)` — xác nhận giả thuyết #1 gần như chắc chắn. Đồng thời, code review nhanh cho thấy có `defer rows.Close()` đầy đủ — loại bỏ giả thuyết #2. Config pool size không đổi — loại bỏ giả thuyết #3.

### Phase 3: Xác nhận root cause

Minh quay lại AI với bằng chứng thu được, yêu cầu xác nhận và đóng gói thành root cause statement rõ ràng:

Confirmed via EXPLAIN ANALYZE: the new query does a Seq Scan on stock_ledger,
cost ~48,213. Ruled out hypothesis #2 (rows.Close present) and #3 (pool config
unchanged). Please:
1. Confirm this is consistent as the root cause end-to-end — walk me through
the full causal chain from this query to the checkout outage.
2. Tell me what evidence would make this a FALSE positive, if any.
3. Recommend immediate mitigation vs. proper fix, separately.


AI xác nhận và trình bày chuỗi nhân quả đầy đủ:

1. PR #4821 thay lookup tồn kho từ Redis (O(1), ~1ms) sang Postgres query thiếu index phù hợp trên `stock_ledger` (append-only, đã tích lũy hàng triệu dòng lịch sử) → mỗi query mất trung bình 2.6-2.9s do full scan.
2. Mỗi request giữ một connection trong pool suốt thời gian đó → pool (`max=20`) bão hòa chỉ sau ~5 phút traffic cao điểm.
3. Request mới phải xếp hàng chờ connection → độ trễ cộng dồn, vượt ngưỡng timeout 3s của `order-service`.
4. `order-service` gọi đồng bộ, không có circuit breaker/fallback → toàn bộ request checkout bị block chờ inventory check, kéo p99 lên 4.8s và trả 5xx khi timeout.
5. `payment-service` không biết inventory check đã fail phía trên, trong một số trường hợp do retry logic ở tầng gateway/client, request thanh toán bị gửi lại → double-charge.
6. Event `order.created` publish lên RabbitMQ vẫn thành công nhưng consumer downstream xử lý chậm do cùng chia sẻ tài nguyên hạ tầng bị nghẽn → queue depth phình to.

AI cũng chỉ rõ điều kiện để bác bỏ giả thuyết này: nếu `EXPLAIN ANALYZE` cho thấy planner dùng index scan bình thường (không phải seq scan) thì phải tìm nguyên nhân khác — nhưng bằng chứng thực tế đã loại khả năng này.

Về hành động, AI tách rõ hai tầng:
- **Mitigation ngay lập tức** (khôi phục dịch vụ trong vài phút): rollback `inventory-service` về v2.13.2 — vì code cũ dùng Redis, không đụng tới `stock_ledger` theo cách gây scan.
- **Fix triệt để** (sau khi đã ổn định): thêm composite index `(sku, id DESC)` trên `stock_ledger`, viết migration, test lại query plan trước khi re-deploy, đồng thời thêm circuit breaker ở `order-service` để tránh một dependency chậm kéo sập toàn bộ checkout trong tương lai.

> **Mẹo:** Luôn tách rõ hai câu hỏi với AI trong pha xác nhận: "root cause là gì" và "làm sao dừng máu ngay bây giờ" — gộp chung dễ khiến team vội áp dụng một fix "đúng về lâu dài" (như thêm index) ngay giữa incident, trong khi rollback mới là hành động an toàn, nhanh, và có thể đảo ngược.

## Hands-On: Đi qua trọn vẹn một incident

Đây là quy trình đầy đủ, bạn có thể áp dụng lại nguyên vẹn cho một service của mình:

**Bước 1 — Xác nhận mức độ nghiêm trọng và mở incident.** Ngay khi alert nổ, kiểm tra business impact (ở đây: SEV-1 vì ảnh hưởng doanh thu trực tiếp + double-charge). Mở incident channel, escalate IC nếu cần. Đừng debug một mình khi mức độ đã là SEV-1 — có người điều phối giúp bạn tập trung vào kỹ thuật.

**Bước 2 — Đóng băng biến số.** Trước khi thử bất kỳ thay đổi nào, xác định: có deploy nào gần đây không? (Có — v2.14.0 lúc 19:15). Đây luôn là nghi phạm số một.

**Bước 3 — Gom context package** như mô tả ở phần 2: timeline, diff, log, trace, metrics — dán vào một chỗ có cấu trúc.

**Bước 4 — Chạy phiên RCA ba pha với AI**: Phase 1 hỏi tổng quan không kết luận vội, Phase 2 ép ra danh sách giả thuyết kèm test cụ thể an toàn để chạy trên production, Phase 3 xác nhận root cause bằng bằng chứng thật, tách mitigation khỏi fix triệt để.

**Bước 5 — Áp dụng mitigation trước.** Team quyết định rollback `inventory-service` về v2.13.2 lúc 20:04. Lệnh thực tế:

kubectl rollout undo deployment/inventory-service -n prod --to-revision=41
kubectl rollout status deployment/inventory-service -n prod


**Bước 6 — Verify.** Theo dõi Grafana trong 5 phút tiếp theo: p99 latency giảm từ 4.8s xuống 240ms lúc 20:09, error rate về 0.2%, connection pool active giảm về 3-6, RabbitMQ queue depth bắt đầu drain (từ 12.400 xuống 1.200 trong 10 phút nhờ consumer bắt kịp). IC Lan xác nhận "all clear" lúc 20:20, đóng SEV-1, chuyển sang theo dõi (monitoring mode) thêm 30 phút trước khi resolve incident chính thức.

**Bước 7 — Xử lý hậu quả nghiệp vụ.** Team support truy vấn Stripe dashboard, đối chiếu với log `payment-service` trong khung 19:38-20:04 để xác định chính xác danh sách order bị double-charge, xử lý hoàn tiền tự động cho khoảng 14 khách hàng bị ảnh hưởng.

**Bước 8 — Fix triệt để, không vội.** Sáng thứ Hai, engineer phụ trách viết migration thêm index:

```sql
CREATE INDEX CONCURRENTLY idx_stock_ledger_sku_id
  ON stock_ledger (sku, id DESC);

Chạy EXPLAIN ANALYZE lại trên staging với dữ liệu production-scale (dùng snapshot), xác nhận planner chuyển sang Index Scan, cost giảm từ ~48.000 xuống dưới 5. Đồng thời thêm circuit breaker (dùng thư viện gobreaker) ở order-service cho call tới inventory-service, với fallback trả về "kiểm tra tồn kho tạm thời chậm, vui lòng thử lại" thay vì để toàn bộ checkout treo.

Bước 9 — Postmortem. Viết postmortem blameless trong vòng 48 giờ, gồm: timeline đầy đủ, root cause, tại sao index thiếu không bị bắt ở review/staging (staging dùng dataset nhỏ, không đủ dữ liệu để stock_ledger đủ lớn bộc lộ vấn đề), action items cụ thể có owner và deadline: (1) thêm index — done, (2) thêm circuit breaker — done, (3) yêu cầu bắt buộc EXPLAIN ANALYZE cho mọi PR đổi query trên bảng lớn, (4) nạp production-scale synthetic data vào staging cho các bảng append-only.

Mẹo: Khi viết postmortem, luôn dán lại chính đoạn hội thoại RCA ba pha với AI vào phần "Investigation" — nó vừa là bằng chứng minh bạch cho quá trình điều tra, vừa là tài liệu huấn luyện cực tốt cho on-call engineer thế hệ sau gặp pattern tương tự.

Những điểm chính

  • Luôn xác định mốc thời gian triệu chứng bắt đầu thực sự (không phải lúc alert nổ) và kiểm tra deploy gần nhất đầu tiên — đây là nghi phạm hàng đầu trong đa số production incident.
  • Một context package có cấu trúc (timeline, diff, log, trace, metrics) trong một prompt duy nhất giúp AI reasoning chính xác hơn nhiều so với hỏi rời rạc, mơ hồ.
  • Chia phiên RCA thành ba pha rõ ràng: Phase 1 (phân tích tổng quan, không kết luận vội) → Phase 2 (danh sách giả thuyết xếp hạng, kèm test cụ thể an toàn để chạy trên production) → Phase 3 (xác nhận root cause bằng bằng chứng thật, kèm điều kiện bác bỏ).
  • Luôn tách rõ mitigation (dừng máu ngay, thường là rollback) khỏi fix triệt để (thêm index, refactor, thêm circuit breaker) — đừng để áp lực incident khiến bạn deploy một "proper fix" chưa được test kỹ giữa lúc production đang cháy.
  • Latency tăng dần (không phải spike đột ngột) thường là dấu hiệu của resource pool bão hòa dần (connection pool, thread pool, file descriptor) chứ không phải lỗi tức thời — hãy nhìn vào các chỉ số "waiting/active" thay vì chỉ CPU/memory.
  • Trace phân tán (distributed tracing) với breakdown thời gian theo từng hop là công cụ xác định chính xác "thời gian mất ở đâu" — ở đây là 2650ms chờ connection pool, không phải bản thân câu query.
  • Sự cố ở một service (inventory-service chậm) có thể lan thành hậu quả nghiêm trọng hơn ở service khác (double-charge ở payment) khi thiếu circuit breaker/fallback — luôn thiết kế để một dependency chậm không kéo sập toàn hệ thống.
  • Postmortem blameless với action items cụ thể, có owner/deadline, và bao gồm cả bản ghi phiên RCA với AI, biến một sự cố đau đớn thành tài sản tri thức lâu dài cho cả team.