Bỏ qua để đến nội dung
Knowledge Hub

Lỗi hay gặp khi viết ADR, runbook và README Trung cấp

Thời gian đọc ước tính: 9 phút

Trang này gom các lỗi hay gặp khi review ADR, runbook và README. Mỗi lỗi có bản hay gặp, bản nên viết, và hậu quả cụ thể nếu để nguyên.

Nhóm Lỗi
ADR Sửa đè ADR cũ, Chỉ có hệ quả tốt, Tiêu đề danh từ, không có phương án, ADR trong wiki, ADR cho mọi thứ, Trùng số
Vận hành Runbook mơ hồ, Alert không có runbook
README README trộn bốn loại

Sửa ADR-0012 cho khớp hiện trạng sau khi chuyển sang CDC

PR hay gặp

# 0012. Phát event đơn hàng qua transactional outbox
Một relay riêng claim dòng pending bằng FOR UPDATE SKIP LOCKED...
Chúng ta chưa dùng CDC. Xem lại khi throughput vượt khả năng polling.
Debezium đọc WAL của bảng outbox và publish lên Kafka.

PR nên làm

# 0012. Phát event đơn hàng qua transactional outbox
-- Status: accepted
+- Status: Superseded by ADR-0031
# 0031. Chuyển relay outbox sang Debezium CDC (file mới)
+- Supersedes: ADR-0012
## Context
Throughput outbox vượt khả năng polling; platform team đã có Kafka Connect
(đúng điều kiện "xem lại khi..." của ADR-0012).

Vì sao

  • Sửa đè xoá mất lý do vì sao từng chọn polling. Khi CDC gặp sự cố replication slot, không ai biết phương án cũ đã được cân nhắc thế nào.
  • Nygard: quyết định cũ được giữ lại và đánh dấu superseded. Chỉ được sửa status và link.

Consequences như một bài bán hàng

ADR tồi

## Consequences
- Không bao giờ mất event
- Hệ thống scale tốt hơn
- Kiến trúc hiện đại, chuẩn microservices

ADR tốt

## Consequences
Tốt:
- Không còn đơn paid mà không phát event khi process chết giữa chừng.
Xấu:
- Vẫn at-least-once: relay có thể publish trùng. Consumer mới BẮT BUỘC idempotent theo eventId.
- Event trễ thêm tối đa một chu kỳ poll.
- Bảng outbox chỉ tăng: cần job dọn và alert cho dòng pending cũ.

Vì sao

  • Hệ quả xấu không ghi vẫn xảy ra. Team mới viết consumer không idempotent, relay publish trùng, Shipping tạo hai vận đơn cho một đơn hàng.
  • Người sau gặp nó như bất ngờ và kết luận quyết định là sai, rồi đảo ngược mù quáng. Nygard yêu cầu ghi mọi hệ quả, không chỉ cái tích cực.

Tiêu đề là danh từ, không có phương án bị loại

Phần tiêu đề “Tiêu đề là danh từ, không có phương án bị loại”

ADR không trả lời được 'sao không làm X'

ADR tồi

# ADR 15: Isolation level
## Decision
Dùng READ COMMITTED.

ADR tốt

# 0015. Trừ tồn kho bằng READ COMMITTED + SELECT ... FOR UPDATE thay vì SERIALIZABLE
## Context
Hai checkout đồng thời có thể cùng đọc stock = 1 rồi cùng trừ (lost update).
Phương án đã cân nhắc:
1. SERIALIZABLE toàn transaction: đúng, nhưng phải retry khi serialization failure,
và mọi code path checkout phải có vòng retry.
2. UPDATE ... SET stock = stock - 1 WHERE stock >= 1: rẻ nhất, nhưng không dùng được
khi cần đọc nhiều dòng rồi quyết định.
3. READ COMMITTED + SELECT ... FOR UPDATE trên các dòng sẽ trừ.
## Decision
Chúng ta sẽ khoá các dòng inventory bằng SELECT ... FOR UPDATE theo thứ tự product_id tăng dần.

Vì sao

  • Tiêu đề danh từ không cho biết đã chọn gì; index ADR thành danh sách vô nghĩa.
  • Không có phương án bị loại thì sáu tháng sau có người đề xuất “chuyển sang SERIALIZABLE cho an toàn”, và cuộc tranh luận bắt đầu lại từ đầu.
  • MADR bắt buộc Considered Options chính vì lý do này.

Code trỏ tới wiki, không ai thấy khi review

Hay gặp

// Xem quyết định ở https://wiki.example.com/pages/viewpage.action?pageId=884213
await this.outbox.insert(tx, event);

Nên viết

docs/adr/0012-phat-event-qua-transactional-outbox.md
// ADR-0012: event đi qua outbox, KHÔNG gọi producer.send() trực tiếp ở đây.
await this.outbox.insert(tx, event);

Vì sao

  • Wiki tách repo: PR đổi code không đụng tới ADR, reviewer không thấy nó khi review. Link vỡ khi công ty đổi công cụ wiki.
  • ADR trong docs/adr/ đi cùng version code, grep được, và được review trong cùng PR.

Decision log thành bãi rác

Decision log tồi

docs/adr/
0041-doi-ten-bien-orderSvc-thanh-orderService.md
0042-nang-lodash-4-17-20-len-4-17-21.md
0043-them-index-orders-created-at.md
0044-dung-prettier-print-width-100.md
0045-giu-modular-monolith-chua-tach-shipping.md

Decision log tốt

docs/adr/
0045-giu-modular-monolith-chua-tach-shipping.md
# Phần còn lại: commit message, migration, config linter
git log --oneline
a1c3e9f refactor: đổi tên orderSvc thành orderService
7b20d4e chore(deps): nâng lodash 4.17.21 (patch bảo mật)
e91f0aa perf: thêm index orders(created_at) cho báo cáo ngày

Vì sao

  • 300 file về tên biến làm ADR quan trọng chìm mất; không ai đọc index nữa.
  • Lọc bằng hai câu hỏi: khó đảo ngược không? người khác sẽ hỏi lại không? Không thì commit message là đủ.

Runbook “kiểm tra log và xử lý phù hợp”

Phần tiêu đề “Runbook “kiểm tra log và xử lý phù hợp””

Runbook không dùng được lúc 3 giờ sáng

Runbook tồi

# Outbox alert
Kiểm tra log của relay và xử lý phù hợp.
Nếu cần thì xoá các dòng lỗi trong bảng outbox.

Runbook tốt

# Runbook: OutboxBacklogHigh
Owner: team Checkout. Cập nhật lần cuối: 2026-09-12
## Ảnh hưởng
Shipping nhận OrderPaid chậm. Thanh toán KHÔNG bị ảnh hưởng.
## Chẩn đoán
1. kubectl get pods -l app=outbox-relay
2. SELECT id, event_type, created_at FROM outbox
WHERE published_at IS NULL ORDER BY id LIMIT 20;
## Xử lý
- Dòng độc: KHÔNG xoá. Chuyển sang outbox_parked, mở ticket.

Vì sao

  • Không có lệnh, không có ngưỡng: người on-call (thường không viết code này) phải đoán, MTTR kéo dài.
  • “Xoá các dòng lỗi” là mất event vĩnh viễn: đơn đã paid nhưng Shipping không bao giờ nhận. Runbook tốt ghi rõ việc không được làm.

Alert kêu nhưng không chỉ đường

Alert rule tồi

- alert: OutboxBacklogHigh
expr: outbox_oldest_pending_age_seconds > 60
labels:
severity: page

Alert rule tốt

- alert: OutboxBacklogHigh
expr: outbox_oldest_pending_age_seconds > 60
for: 5m
labels:
severity: page
annotations:
summary: Outbox có dòng pending cũ hơn 60s trong 5 phút
runbook_url: https://git.example.com/order-service/-/blob/main/docs/runbooks/outbox-backlog.md

Vì sao

  • Không có for, alert fire ngay ở lần evaluate đầu tiên vượt ngưỡng, kể cả spike ngắn lúc deploy: on-call bị đánh thức vô ích.
  • Không có link runbook, người nhận page phải đi tìm docs giữa đêm. Prometheus mô tả annotations chính là chỗ để mô tả alert và link runbook.

README là kho thay vì cửa vào

README tồi

# order-service
## Lịch sử
Năm 2023 team chọn Kafka thay vì RabbitMQ vì... (3 trang)
## Cài đặt
Nếu bạn dùng Mac M1 thì..., nếu dùng Windows thì..., nếu dùng Postgres 14 thì...
## Env vars
DB_URL, KAFKA_BROKERS, OUTBOX_POLL_MS (=500) <- viết tay, code đã đổi default thành 200

README tốt

# order-service
Quản lý vòng đời đơn hàng (bounded context Ordering). KHÔNG xử lý vận chuyển.
## Quickstart
docker compose up -d && npm ci && npm run migrate && npm run dev
## Tài liệu
- Kiến trúc (C4): docs/architecture.md
- Quyết định: docs/adr/ (vì sao Kafka: ADR-0004)
- Runbook: docs/runbooks/
- Env vars: sinh từ schema config, xem docs/reference/config.md
Owner: team Checkout, #team-checkout

Vì sao

  • Theo Diátaxis, “vì sao Kafka” là explanation (thuộc ADR), bảng env var là reference (nên sinh từ code).
  • Bảng env var viết tay lệch default: dev mới cấu hình theo README, chạy được nhưng sai hành vi so với production.
  • Quickstart rẽ nhánh khắp nơi thì người mới không chạy nổi service trong ngày đầu.

Hai nhánh song song cùng tạo 0013

Hay gặp

docs/adr/
0013-dung-redis-cho-idempotency-key.md # merge từ nhánh A
0013-tach-schema-reporting.md # merge từ nhánh B

Nên làm

Terminal window
# CI: fail nếu hai file ADR trùng tiền tố số
dupes=$(ls docs/adr | grep -oE '^[0-9]{4}' | sort | uniq -d)
if [ -n "$dupes" ]; then
echo "ADR trùng số: $dupes"
exit 1
fi

Vì sao

  • “Xem ADR-0013” giờ trỏ tới hai quyết định khác nhau; link trong code và commit message trở nên mơ hồ.
  • Người merge sau đổi số trước khi merge; CI chặn nếu quên. Không cần đổi sang đánh số theo ngày.

Tiếp theo: quay về Tổng quan cụm ADR và tài liệu, hoặc xem cụm Code review.