DHDataHọc
Trắc nghiệmBài 8/86 phút đọc+10 XP

Bài tập: đọc sự cố pipeline

Bạn sẽ học được

  • Chẩn đoán ba sự cố dữ liệu từ cùng một DAG.
  • Nhận ra lỗi idempotency và transaction.
  • Chọn kiểm tra chất lượng phù hợp cho từng lỗi.
  • Hiểu vì sao retry làm sự cố nặng hơn.
  • Sắp xếp thứ tự khắc phục đúng.

Tình huống

Công ty Sao Mai có DAG daily_orders, chạy 02:00 mỗi ngày:

code
extract → transform → load

Task load thực hiện:

code
DELETE FROM fact_orders;

INSERT INTO fact_orders
SELECT * FROM staging_orders
WHERE order_date = '{{ ds }}';

Hai câu chạy riêng, không trong transaction. Số dòng kỳ vọng mỗi ngày: 12.000.

Nhật ký sự cố

NgàyViệc đã làmKết quả
10/3Chạy lại thủ công sau khi sửa lỗi nhỏfact_orders ngày 10/3 có 24.000 dòng
11/3load timeout sau khi ghi 8.000 dòng; task retry và thành côngNgày 11/3 có 20.000 dòng
12/3Backfill ngày 1/3Chỉ còn dữ liệu 1/3; mọi ngày khác về 0

DAG hiển thị màu xanh cho cả ba ngày.

Câu hỏi

Câu 1. Vì sao ngày 10/3 có 24.000 dòng?

Câu 2. Vì sao ngày 11/3 có 20.000 dòng?

Câu 3. Vì sao backfill ngày 1/3 xoá dữ liệu của mọi ngày khác?

Câu 4. Sửa tối thiểu trong câu DELETE là gì?

Câu 5. Vì sao DELETE và INSERT phải nằm trong cùng một transaction?

Câu 6. Kiểm tra nào phát hiện sự cố ngày 10/3 trước khi dashboard hiển thị?

Câu 7. Kiểm tra nào phát hiện sự cố ngày 11/3?

Câu 8. Giám sát nào phát hiện sự cố ngày 12/3 nhanh nhất?

Câu 9. Vì sao bật retry cho task load là nguy hiểm trong trường hợp này?

Câu 10. Sắp xếp thứ tự năm bước khắc phục.

Đáp án

Câu 1 — Chạy hai lần, không xoá đúng phạm vi

Lần chạy đầu ghi 12.000 dòng. Lần chạy lại thực hiện DELETE FROM fact_orders (xoá tất cả), rồi INSERT 12.000 dòng của ngày 10/3.

Đáng lẽ kết quả vẫn là 12.000. Nhưng nếu lần chạy đầu đã ghi 12.000 và lần hai cũng ghi 12.000 mà DELETE không chạy hoặc chạy trên bảng khác, kết quả là 24.000.

Trong tình huống này, nguyên nhân gốc là load không idempotent: nó không xoá đúng partition trước khi ghi. Việc chạy lại biến một ngày thành hai bản sao.

Bài học: chạy lại phải cho cùng kết quả. INSERT không kèm xoá partition là nguyên nhân phổ biến nhất.

Câu 2 — Ghi dở dang rồi ghi lại toàn bộ

Lần đầu: ghi được 8.000 dòng rồi timeout. Không có transaction, nên 8.000 dòng vẫn nằm trong bảng.

Retry: ghi thêm 12.000 dòng. Tổng 20.000.

code
8.000 + 12.000 = 20.000

Nếu load chạy trong một transaction, lần timeout sẽ rollback toàn bộ 8.000 dòng và retry cho kết quả đúng 12.000.

Câu 3 — DELETE không có điều kiện

code
DELETE FROM fact_orders;   -- xoá toàn bộ bảng

Sau đó INSERT chỉ thêm dữ liệu ngày 1/3. Kết quả: bảng chỉ còn ngày 1/3, mọi ngày khác mất sạch.

Đây là lỗi nguy hiểm nhất trong ba sự cố vì phạm vi ảnh hưởng toàn bộ lịch sử, không chỉ một ngày.

Câu 4 — Thêm điều kiện partition

code
DELETE FROM fact_orders WHERE order_date = '{{ ds }}';

Đây là mức tối thiểu. Nhưng chưa đủ: vẫn cần transaction và kiểm tra số dòng.

Câu 5 — Tránh trạng thái dở dang

Nếu DELETE thành công và INSERT lỗi, ngày đó mất dữ liệu hoàn toàn. Transaction đảm bảo hai câu hoặc cùng thành công, hoặc cùng không có tác dụng.

code
BEGIN;
DELETE FROM fact_orders WHERE order_date = '{{ ds }}';
INSERT INTO fact_orders SELECT ... WHERE order_date = '{{ ds }}';
COMMIT;

Trong Airflow, dùng một operator SQL duy nhất cho cả khối, hoặc quản lý transaction trong code. Hai task riêng biệt không chia sẻ transaction.

Câu 6 — Kiểm tra duy nhất và số dòng

Sau khi nạp, chạy:

code
SELECT
  COUNT(*) AS so_dong,
  COUNT(DISTINCT order_id) AS so_dong_khac_nhau
FROM fact_orders
WHERE order_date = '2026-03-10';

Kỳ vọng 12.000 và 12.000. Nếu so_dong gấp đôi so_dong_khac_nhau, có dữ liệu trùng.

Kiểm tra này chặn task publish phía sau, nên dashboard không hiển thị số sai.

Câu 7 — Kiểm tra số dòng tuyệt đối

Sau ngày 11/3:

code
COUNT(*) = 20.000
COUNT(DISTINCT order_id) = 20.000

Kiểm tra duy nhất không phát hiện, vì các order_id bị trùng đã bị xoá cùng partition ở lần đầu — hoặc dữ liệu đến từ hai lần ghi khác nhau với khoá khác nhau.

Cần kiểm tra số dòng so với kỳ vọng:

code
20.000 > 12.000 × 1,2  →  cảnh báo hoặc chặn

Đây là lý do phải có cả kiểm tra tương đối (so với lịch sử) và tuyệt đối (so với kỳ vọng).

Câu 8 — Giám sát khối lượng và độ tươi

Sau backfill ngày 1/3, mọi ngày khác có 0 dòng. Hai giám sát bắt được:

  • Khối lượng: tổng số dòng giảm đột ngột so với hôm trước.
  • Độ tươi: MAX(order_date) chỉ còn 1/3, trong khi hôm nay là 12/3.

Kiểm tra độ tươi phát hiện nhanh nhất vì nó so trực tiếp với hiện tại:

code
SELECT MAX(order_date) FROM fact_orders;
-- 2026-03-01  →  trễ 11 ngày

Câu 9 — Retry nhân bản dữ liệu sai

load không idempotent và không transactional. Mỗi lần retry có thể thêm dữ liệu mà không xoá bản ghi dở dang.

Retry chỉ an toàn khi task idempotent. Với task ghi dữ liệu, phải sửa cách ghi trước khi bật retry tự động.

Trong tình huống này, đúng ra nên để load thất bại và dừng pipeline, thay vì tự động thử lại và tạo thêm dữ liệu sai.

Câu 10 — Thứ tự khắc phục

  1. Dừng DAG để không phát sinh thêm dữ liệu sai.
  2. Sửa load: thêm WHERE order_date = '{{ ds }}', bọc trong transaction, thêm kiểm tra số dòng trước khi commit.
  3. Thêm kiểm tra chất lượng sau load: số dòng, duy nhất, miền giá trị.
  4. Khôi phục dữ liệu: xác định các ngày bị ảnh hưởng, chạy lại từng ngày sau khi đã sửa; đối chiếu tổng với nguồn.
  5. Thêm giám sát: độ tươi, khối lượng, cảnh báo khi DAG không chạy; viết runbook.

Không làm bước 4 trước bước 2 — chạy lại bằng code cũ sẽ tạo thêm dữ liệu sai.

Cách chấm điểm

Số câu đúngMứcGợi ý
9–10VữngThiết kế lại DAG mẫu cho an toàn
7–8KháXem lại bài 3 và 6
5–6Trung bìnhĐọc lại bài 3, 5, 6
Dưới 5Cần ônLàm lại từ bài 1

Bản đồ câu hỏi → bài học

CâuBài
1–2Bài 3: Idempotency
3–5Bài 3: Idempotency, transaction
6–7Bài 6: Kiểm tra chất lượng dữ liệu
8Bài 7: Giám sát và cảnh báo
9Bài 5: Xử lý lỗi và retry
10Tổng hợp

Bài tập mở rộng

  1. Viết lại task load an toàn hoàn chỉnh.
  2. Viết ba câu SQL kiểm tra chặn được cả ba sự cố.
  3. Thiết kế DAG giám sát độ tươi chạy độc lập.
  4. Tính số ngày cần khôi phục nếu sự cố kéo dài từ 10/3 đến 12/3.
  5. Viết đoạn runbook năm bước cho sự cố trùng dữ liệu.

Tóm tắt

  • DELETE thiếu điều kiện là lỗi phạm vi toàn bảng.
  • Thiếu transaction tạo trạng thái dở dang khi timeout.
  • Retry trên task không idempotent nhân bản dữ liệu sai.
  • Kiểm tra duy nhất bắt trùng; kiểm tra số dòng bắt thừa.
  • Sửa code trước, khôi phục dữ liệu sau, giám sát cuối cùng.

Dùng ← → để chuyển bài