07 Kiểm thử, thất bại và sửa lỗi
Ghi chú giảng viên cho module 07 — thời lượng, nội dung dẫn giảng, lỗi thường gặp và các bước khôi phục.
Tài liệu đồng hành của người điều phối cho bài học 07 Kiểm thử, thất bại và sửa lỗi.
Thời lượng
Khoảng 20 phút. Đây là phòng lab sự cố; hãy bảo vệ đủ khoảng chạy để phần chẩn đoán đi đến kết luận trước module 08 và phần tổng kết. Phòng lab chạy fault 01 (chẩn đoán 5-15 phút). Fault 02 (5-10 phút) và 03 (10-20 phút, và chỉ với bộ dữ liệu đầy đủ khoảng 30 triệu dòng) vẫn có sẵn như các sự cố bổ sung tuỳ chọn cho một phòng học chạy nhanh. Hãy đặt một mốc dừng cứng trong buổi tổng duyệt để phần tổng kết không bị bó hẹp.
Nội dung dẫn giảng
- Đây là phần thành quả: dùng mọi thứ đã xây được đến giờ để xử lý một sự cố thật.
- Hãy mô tả triệu chứng, không mô tả nguyên nhân, và để các agent tự hội tụ từ telemetry.
- Cân nhắc trình chiếu chẩn đoán của vài người tham dự cạnh nhau trên máy chiếu.
- Phòng lab chạy fault 01. Nếu thời gian cho phép thêm một vòng, hãy thêm fault 02 (và chỉ thêm fault 03 khi bộ dữ liệu đầy đủ đã được nạp mồi trước); dùng bước khôi phục stash-and-switch dùng chung bên dưới giữa các fault.
Đáp án
Đừng chia sẻ phần này với học viên; nó chỉ tồn tại trong playbook và không bao giờ được
commit vào repo của ứng dụng. Mỗi fault là một thay đổi nhỏ trên nhánh riêng của nó, tách ra từ
build-workshop-v1;
cách sửa là revert nó. Hãy chạy từng fault một. Sự cố của phòng lab là fault 01 (5-15 phút,
đòn bất ngờ — backend vô can). Các phần bổ sung tuỳ chọn cho một phòng học chạy nhanh: fault 02 (5-10 phút,
khởi động, trace nói hết mọi thứ) và fault 03 chỉ khi có bộ dữ liệu đầy đủ
(10-20 phút, suy luận sâu hơn về ClickHouse).
Bước khôi phục dùng chung cho mọi fault (lệnh stash bảo toàn các chỉnh sửa của người tham dự và tránh việc chuyển nhánh bị chặn):
git stash push --include-untracked -m "module-07-fix"
git switch build-workshop-v1
docker compose --env-file .env.workshop \
-f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --buildFault 02 — zone stats trả về 500 (fault/02-zone-stats-500)
Thông điệp commit mà người tham dự thấy: "backend: align zone stats column names with API params"
(chỉ chạm vào zone_stats_sql trong backend/app/query_builders.py). Nó group by
pickup_zone_id, một cột không tồn tại trên taxi_trips, thay vì
pickup_location_id.
- Triệu chứng: bản đồ choropleth trống (các polygon vẫn vẽ ra, nhưng mọi zone đều nằm trong
nhóm màu nhạt nhất) và chú thích "Query Nms" biến mất; hàng loạt lỗi 500 trên
GET /api/metrics/zone_stats(React Query thử lại ba lần). Mọi card khác vẫn ổn. - Nơi tín hiệu nằm ở đó: một span
clickhouse.querytrong ClickStack vớierror=True,error.category="query_failed", vàdb.statementchứapickup_zone_id AS zone_id; ngoại lệ được ghi lại là ClickHouse Code 47UNKNOWN_IDENTIFIER. Log ERROR tương ứng: "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=...". - Đường chẩn đoán: tìm span lỗi 500 -> đọc
db.statement->UNKNOWN_IDENTIFIERtrênpickup_zone_id->DESCRIBE taxi_trips(hoặc so sánh với các query builder tương tự) -> cột thật làpickup_location_id. - Cách sửa: đổi
pickup_zone_idtrở lại thànhpickup_location_idtrongzone_stats_sql(hoặcgit revertcommit gây lỗi); build lại backend. - Khôi phục: dùng bước khôi phục dùng chung ở trên.
Fault 03 — dashboard chậm (fault/03-slow-dashboard)
Thông điệp commit: "backend: window the trend series by bucket in HAVING" (chạm vào
timeseries_sql). Nó chuyển vị từ điều kiện cửa sổ thời gian từ WHERE sang một HAVING trên
alias của bucket, nên WHERE trở thành 1. ClickHouse không còn có thể lược bỏ theo primary key
(car_type, pickup_datetime) nữa và quét toàn bộ taxi_trips với hai
trạng thái quantileTDigest, làm nổ mức max_execution_time 5s.
- Triệu chứng: chỉ card xu hướng "What's happening now?" thất bại — nó quay vòng, rồi hiện "504 Query timed out...". Mọi card cùng cấp vẫn nhanh.
- Nơi tín hiệu nằm ở đó: một span
clickhouse.queryvớierror.category="timeout",db.elapsed_mskhoảng 5000, vàdb.statementhiệnWHERE 1 ... GROUP BY ts HAVING ts >= ...; ngoại lệ là Code 159TIMEOUT_EXCEEDED. Sự tương phản với các span 200 nhanh trên các endpoint khác chính là dấu hiệu nhận biết. - Đường chẩn đoán: một card chậm giữa các card cùng cấp nhanh -> mở span bị timeout -> đọc
câu SQL -> bộ lọc thời gian nằm trong
HAVINGvàWHEREkhông có khoảngpickup_datetime-> một anti-pattern về lược bỏ theo primary key / đẩy điều kiện xuống dưới (các card cùng cấp đặt cửa sổ thời gian trongWHERE). - Cách sửa: đưa cửa sổ thời gian trở lại
WHERE(revert commit gây lỗi); build lại backend. - Khôi phục: dùng bước khôi phục dùng chung ở trên.
- Lưu ý cho giảng viên: mức nghiêm trọng tỉ lệ với khối lượng dữ liệu và kích thước service. Trên bộ dữ liệu
đầy đủ đã nạp mồi (~30 triệu dòng) với một service ở mức workshop-tier (có thể đang thức dậy từ trạng thái nghỉ),
mốc timeout 5s nổ ra một cách đáng tin cậy; còn trên bộ dữ liệu mồi nhỏ thì nó chỉ chậm hơn, không phải một lỗi 504 dứt khoát.
send_receive_timeoutphía client vàmax_execution_timephía server đều là 5s, nên một cuộc đua timeout ở tầng socket có thể đôi khi hiện ra thành lỗi 500 thay vì lỗi 504 gọn gàng — đó là đặc tính của cấu hình cơ sở, không phải của fault.
Fault 01 — bản đồ không tải được (fault/01-map-not-loading)
Thông điệp commit: "frontend: serve map geojson from /static asset path" (chạm vào
đường dẫn fetch trong frontend/src/ui/ZoneMap.tsx). Nó fetch /static/taxi_zones.geojson, thứ
không tồn tại. Điểm tinh tế then chốt: cơ chế fallback SPA của nginx (try_files ... /index.html) trả về HTTP 200
kèm tài liệu HTML thay vì lỗi 404, nên r.ok vẫn vượt qua và r.json() ném ra một
SyntaxError kiểu Unexpected token '<'.
- Triệu chứng: card bản đồ đứng chết — các tile nền vẫn vẽ nhưng không có polygon NYC hay choropleth nào, và một dòng lỗi màu đỏ hiện ngay tại chỗ. Mọi thứ khác vẫn ổn.
- Nơi tín hiệu nằm ở đó: không có gì trong ClickStack phía backend — asset đó chưa bao giờ đến được
FastAPI. Console của trình duyệt hiện lỗi phân tích JSON có nêu tên
/static/taxi_zones.geojson; tab Network hiện request đó trả vềtext/htmlvới trạng thái 200; access log của nginx hiện cơ chế fallback. - Đường chẩn đoán: các trace ở backend đều sạch -> chuyển sang console / tab Network của trình duyệt
-> một request
.geojsontrả về 200text/html-> nhận ra cái bẫy fallback của SPA -> file thực ra nằm ở web root,/taxi_zones.geojson. - Cách sửa: revert đường dẫn fetch (hai dòng) hoặc
git revertcommit gây lỗi; build lại frontend. - Khôi phục: dùng bước khôi phục dùng chung ở trên.
- Điểm giảng dạy: không phải mọi thất bại đều hiện ra trong các trace ở backend, và một lỗi 404 có thể giả dạng thành 200 phía sau một cơ chế fallback của SPA — vậy nên hãy đọc chính nội dung phản hồi và content-type, không chỉ mã trạng thái.
- Ghi chú (SDK trình duyệt): với SDK trình duyệt của HyperDX đã bật (overlay của module 05),
frontend giờ hiện lỗi này ngay trong ClickStack — lỗi phân tích của
ZoneMapđược ghi lại thành một spanconsole.errordướiServiceName=nyc-taxi-frontend, đi kèm với span resource của phản hồi 200text/htmlđó. Nhờ vậy agent có thể định vị nó từ telemetry mà không cần rời khỏi ClickStack; console / tab Network của trình duyệt là phương án dự phòng, không phải con đường duy nhất.
Ví dụ phản hồi của agent (fault 01, qua ClickStack MCP):

Chẩn đoán mong đợi: agent gắn thất bại này với request geojson trả về 200 text/html (fallback của SPA), lỗi JSON.parse phát sinh được ghi lại dưới nyc-taxi-frontend, và khuyến nghị phát hành asset vào /static/ hoặc bọc bảo vệ cho bước phân tích .json().
Lỗi thường gặp
- Không đủ đường cơ sở telemetry để agent định vị được lỗi; module 05 phải đã chạy từ sớm.
- Người tham dự nhảy ngay vào cách sửa trước khi xác nhận nguyên nhân gốc từ bằng chứng.
- Triệu chứng của fault chưa hiện ra vì stack vừa mới được dựng lên — hoặc, với fault 03, vì có quá ít dữ liệu lịch sử được nạp. Đã đo trong buổi tổng duyệt: với bộ dữ liệu mồi một tháng mặc định (~3,17 triệu dòng), fault WHERE-vs-HAVING không quan sát được — lần quét toàn bảng chạy trong khoảng 300-560ms, không phân biệt được với đường cơ sở. Hãy nạp mồi vài tháng trước khi demo fault 03, hoặc cứ dùng fault 01 (tái hiện ngay lập tức) và coi fault 03 là một minh hoạ ở quy mô lớn.
- Fault 01 không sinh ra trace nào ở backend cả, và request lỗi trả về 200
text/htmlqua cơ chế fallback của SPA thay vì lỗi 404; người tham dự có thể tìm mãi trong các trace. Hãy nhắc họ chuyển sang console / tab Network của trình duyệt, nơi lỗi phân tích JSON và phản hồitext/htmlhiện ra. - Quên
--buildsau khi checkout một nhánh fault, nên image cũ vẫn tiếp tục chạy.
Các bước khôi phục
- Bước khôi phục stash-and-switch dùng chung phục hồi ứng dụng hoàn chỉnh mà không bỏ đi bản sửa mà người tham dự đã thử.
- Tiêm lại một fault bằng cách checkout một trong các nhánh
fault/01-map-not-loading/fault/02-zone-stats-500/fault/03-slow-dashboardvà build lại với các file compose của workshop cộng với của otel. - Triệu chứng, tín hiệu và cách sửa của từng fault nằm trong phần Đáp án ở trên. Hãy giữ đáp án chỉ trong playbook này; nó không bao giờ được commit vào repo của ứng dụng.