DataOps nâng cao 29 — Mẫu & phản mẫu pipeline

13 thg 7, 2026 2 lượt xem
#data-engineering
#best-practices
#dataops
#design-patterns
#anti-patterns

Vì sao cần một "cẩm nang mẫu"

Sau nhiều bài về kiến trúc, dbt, quality, môi trường, cost — điều còn thiếu là một tham chiếu nhanh để khi ngồi thiết kế một pipeline mới, ta có sẵn danh mục các cách làm đã được kiểm chứng và các cạm bẫy cần né. Bài này đóng vai trò đó: một cẩm nang mẫu (pattern)phản mẫu (anti-pattern) cho pipeline dữ liệu, viết theo cấu trúc dễ tra: vấn đề → giải pháp → khi nào dùng.

Pattern không phải luật cứng. Chúng là các giải pháp lặp đi lặp lại cho những vấn đề lặp đi lặp lại. Giá trị lớn nhất của việc gọi tên chúng là: khi review một thiết kế, ta chỉ cần nói "chỗ này thiếu idempotency" thay vì giải thích nửa trang. Trong ngân hàng, nơi một con số sai trong báo cáo NHNN có thể thành sự cố tuân thủ, việc áp đúng mẫu và tránh đúng phản mẫu là ranh giới giữa pipeline tin cậy và pipeline gây khủng hoảng lúc 2 giờ sáng cuối tháng.

Nhóm mẫu tốt (patterns nên áp)

1. Idempotency — chạy lại an toàn

Vấn đề: Job fail giữa chừng, ta retry, và bản ghi bị nhân đôi. Hoặc backfill một ngày đã chạy rồi khiến số liệu sai.

Giải pháp: Mỗi lần chạy với cùng input phải cho cùng kết quả cuối, bất kể chạy 1 hay 5 lần. Kỹ thuật cốt lõi: MERGE/upsert theo khoá tự nhiên thay vì INSERT thuần; hoặc delete-insert theo partition (xoá đúng ngày đang xử lý rồi ghi lại). Tránh INSERT ... SELECT không có chốt chặn.

Khi nào dùng: Luôn luôn. Idempotency là điều kiện tiên quyết cho mọi thứ khác — retry, backfill, replay đều dựa vào nó.

2. Incremental có backfill

Vấn đề: Full-refresh mỗi lần quét lại toàn bộ lịch sử — chậm, tốn compute, và tăng tuyến tính theo thời gian.

Giải pháp: Chỉ xử lý dữ liệu mới kể từ mốc lần chạy trước (theo created_at/updated_at hoặc high-watermark), nhưng luôn thiết kế kèm cơ chế backfill để xử lý lại một khoảng ngày khi cần sửa lỗi. Xem chi tiết ở DataOps nâng cao 13 — Cost & Performance.

Khi nào dùng: Bảng fact lớn, tăng trưởng đều. Không dùng cho bảng nhỏ (< vài triệu dòng) — chi phí phức tạp không đáng.

3. Medallion / multi-hop (bronze → silver → gold)

Vấn đề: Trộn logic ingest, làm sạch, và tính toán nghiệp vụ trong một bước khiến khó debug và không tái sử dụng được.

Giải pháp: Chia dữ liệu qua các tầng: bronze (raw, gần như nguyên trạng nguồn), silver (đã làm sạch, chuẩn hoá, dedup), gold (đã tổng hợp theo nghiệp vụ, sẵn cho BI). Mỗi tầng có hợp đồng rõ ràng.

Khi nào dùng: Gần như mọi platform hiện đại. Với pipeline rất nhỏ có thể gộp silver/gold.

4. Staging — tách nguồn khỏi logic

Vấn đề: Model nghiệp vụ tham chiếu thẳng bảng nguồn; nguồn đổi tên cột là vỡ hàng loạt.

Giải pháp: Một lớp staging mỏng đứng giữa: đổi tên cột về chuẩn nội bộ, ép kiểu, không có logic nghiệp vụ. Đây là mẫu chuẩn của dbt — xem dbt 01 — Tổng quanDataOps nâng cao 2 — dbt & ELT.

Khi nào dùng: Luôn, khi có nhiều nguồn hoặc nguồn hay đổi.

5. Write-Audit-Publish (WAP)

Vấn đề: Dữ liệu xấu ghi thẳng ra bảng người dùng đang đọc.

Giải pháp: Write vào bảng tạm/staging → Audit chạy kiểm tra chất lượng → chỉ khi PASS mới Publish (swap/promote) ra bảng chính. Chi tiết ở DataOps nâng cao 11 — Môi trường & WAP.

Khi nào dùng: Bất kỳ bảng nào phục vụ báo cáo tuân thủ hoặc dữ liệu ra khách hàng.

6. Circuit breaker — chặn dữ liệu xấu

Vấn đề: Nguồn đổ ra dữ liệu bất thường (0 dòng, số âm, spike gấp 10 lần) và pipeline vẫn chạy tiếp, lan lỗi xuống hạ nguồn.

Giải pháp: Đặt các "cầu dao" kiểm tra ngưỡng (row count, control total, phân phối) — nếu vi phạm thì dừng pipeline thay vì publish. Liên hệ DataOps nâng cao 5 — Quality & Contracts.

Khi nào dùng: Trước mọi bước publish quan trọng.

7. Dead-letter / quarantine

Vấn đề: Một vài dòng lỗi làm hỏng cả batch; ta buộc phải chọn giữa fail toàn bộ hoặc bỏ qua âm thầm.

Giải pháp: Dòng không parse được / vi phạm contract được tách ra bảng quarantine (dead-letter) kèm lý do, phần còn lại vẫn chạy. Dữ liệu quarantine được review và replay sau.

Khi nào dùng: Ingest dữ liệu bên ngoài, chất lượng không kiểm soát được (file đối tác, API bên thứ ba).

8. Data contract ở ranh giới

Vấn đề: Team nguồn đổi schema mà không báo, hạ nguồn vỡ.

Giải pháp: Định nghĩa hợp đồng (schema, kiểu, ràng buộc, SLA) ở ranh giới giữa các team; kiểm tra tự động trong CI. Xem DataOps nâng cao 5.

Khi nào dùng: Ranh giới giữa các team/hệ thống, đặc biệt khi nguồn do đội khác sở hữu.

9. Config / metadata-driven

Vấn đề: 50 pipeline gần giống nhau, mỗi cái copy-paste; đổi một quy tắc phải sửa 50 chỗ.

Giải pháp: Một engine đọc cấu hình (YAML/bảng metadata) mô tả nguồn, đích, quy tắc — và sinh/chạy pipeline từ đó. Thêm nguồn mới = thêm một dòng config.

Khi nào dùng: Nhiều pipeline đồng dạng (ví dụ nạp 40 bảng từ core banking). Không dùng khi chỉ có vài pipeline khác biệt lớn — sẽ over-engineering.

10. Small-files compaction

Vấn đề: Streaming/micro-batch tạo hàng nghìn file nhỏ, làm query chậm thảm hại trên data lake.

Giải pháp: Job compaction định kỳ gộp file nhỏ thành file lớn (target 128–512 MB), tối ưu layout (OPTIMIZE/VACUUM trên định dạng bảng như Iceberg/Delta).

Khi nào dùng: Mọi kiến trúc lakehouse có ghi tần suất cao.

11. Slowly Changing Dimension (SCD)

Vấn đề: Thuộc tính khách (thành phố, phân khúc) thay đổi theo thời gian; ghi đè làm mất lịch sử.

Giải pháp: SCD type 2 — mỗi thay đổi tạo một dòng mới với valid_from/valid_to, giữ lịch sử để phân tích "tại thời điểm đó". Type 1 (ghi đè) khi không cần lịch sử.

Khi nào dùng: Dimension cần truy vết lịch sử (phân khúc rủi ro tín dụng theo thời gian).

12. Late-arriving data handling

Vấn đề: Giao dịch của ngày hôm qua đến sau khi batch hôm qua đã đóng.

Giải pháp: Cửa sổ re-process (ví dụ luôn xử lý lại 3 ngày gần nhất) hoặc watermark cho phép trễ. Kết hợp idempotency để re-process an toàn.

Khi nào dùng: Nguồn có độ trễ (giao dịch liên ngân hàng, đối soát).

13. Retry với backoff

Vấn đề: Lỗi tạm thời (mạng, timeout) làm fail cả pipeline không cần thiết.

Giải pháp: Retry tự động với exponential backoff + jitter, giới hạn số lần; chỉ retry lỗi transient, không retry lỗi logic. Đòi hỏi bước phải idempotent.

Khi nào dùng: Mọi bước gọi hệ thống ngoài (API, extract từ core).

14. DAG data-aware & immutable raw zone

Vấn đề: DAG chạy theo giờ cố định, chạy cả khi nguồn chưa sẵn; và raw zone bị sửa làm mất khả năng replay.

Giải pháp: Data-aware scheduling — job hạ nguồn kích hoạt khi dataset thượng nguồn cập nhật (Airflow Datasets), không phụ thuộc giờ. Immutable raw zone — vùng raw chỉ ghi thêm, không sửa/xoá, để mọi thứ đều replay được từ nguồn sự thật.

Khi nào dùng: Pipeline có phụ thuộc phức tạp; raw zone thì luôn nên immutable.

15. Blue-green deploy cho data

Vấn đề: Deploy phiên bản model mới, phát hiện sai, không rollback nhanh được.

Giải pháp: Chạy song song bảng "green" (mới) bên cạnh "blue" (đang phục vụ), verify, rồi swap con trỏ/view. Rollback = swap ngược.

Khi nào dùng: Thay đổi lớn về logic của bảng gold quan trọng.

Nhóm phản mẫu (anti-patterns cần tránh)

Phản mẫuVì sao nguy hiểmThay bằng
Full-refresh vô íchTốn compute, chậm, chi phí tăng theo thời gianIncremental + backfill
SELECT * & bảng rộng vô tội vạKéo cột thừa, vỡ khi nguồn đổi, tốn I/OChọn đúng cột cần
Hardcode secret/đường dẫn/ngàyRò rỉ bí mật, không portable giữa envEnv var + secret manager, param hoá ngày
Copy-paste không DRYSửa một quy tắc phải sửa N chỗ, dễ sótMacro/module/metadata-driven
Pipeline không idempotentRetry/backfill làm sai số liệuMERGE/upsert, delete-insert partition
Big ball of mud SQL 1000 dòngKhông ai đọc nổi, không test đượcChia model/CTE, dbt refs
Không test / không tài liệuSai lặng lẽ, tri thức mất theo ngườiTest tối thiểu + doc (dbt 04)
Silent failure (nuốt lỗi)try/except: pass — dữ liệu thiếu mà xanhFail loud, log, dead-letter
One giant DAGMột task fail đọng cả DAG, khó debugChia DAG theo domain, data-aware
Sửa dữ liệu tay trên prodKhông tái lập được, mất audit trailFix ở code + backfill có ghi log
Thiếu môi trường tách biệtTest đè lên prod, PII lộTách dev/staging/prod (adv 11)
Over-engineering (real-time khi không cần)Phức tạp, tốn tiền, khó vận hànhBatch đủ dùng; real-time chỉ khi có nhu cầu thật
No ownershipPipeline "mồ côi", không ai sửa khi hỏngGán owner + SLA rõ ràng

Trong đó ba phản mẫu gây sự cố số liệu nhiều nhất ở ngân hàng là: không idempotent (retry đêm cuối tháng làm nhân đôi giao dịch), silent failure (một nguồn về thiếu mà báo cáo vẫn xanh), và sửa dữ liệu tay trên prod (fix một số rồi tháng sau tái diễn vì gốc chưa sửa).

Ví dụ before/after

Trước (anti-pattern — không idempotent, full logic một cục):

-- Minh hoạ PHẢN MẪU (không đánh dấu chạy được):
-- INSERT thuần → retry là nhân đôi; SELECT * kéo cột thừa
INSERT INTO gold_daily_txn
SELECT * FROM staging_txn WHERE 1=1;   -- không lọc theo ngày, không dedup

Sau (pattern — incremental, dedup, chỉ cột cần, idempotent bằng delete-insert): xem hai block chạy được ngay dưới đây.

SQL minh hoạ: MẪU vs PHẢN MẪU

Hai block dưới đây chạy được trên sandbox PostgreSQL read-only (5 bảng chuẩn). Chúng minh hoạ phần SELECT của mẫu tốt — phần MERGE/INSERT được mô tả bằng lời (sandbox chỉ đọc).

(a) Incremental đúng: lọc theo created_at + dedup bằng ROW_NUMBER

Đây là mẫu incremental + idempotent: chỉ lấy giao dịch trong cửa sổ gần đây (như thể backfill lại 30 ngày), và khử trùng lặp để retry không tạo dòng lặp. Trong pipeline thật, kết quả này sẽ được MERGE vào bảng gold theo khoá — nên chạy lại nhiều lần cho cùng một kết quả.

-- ▶ Chạy được
WITH recent AS (
  SELECT
    t.id,
    t.account_id,
    a.customer_id,
    t.amount,
    t.kind,
    t.created_at,
    ROW_NUMBER() OVER (
      PARTITION BY t.id
      ORDER BY t.created_at DESC
    ) AS rn
  FROM transactions t
  JOIN accounts a ON a.id = t.account_id
  WHERE t.created_at >= NOW() - INTERVAL '30 days'
)
SELECT
  account_id,
  customer_id,
  COUNT(*)                              AS txn_count,
  ROUND(SUM(amount)::numeric, 2)        AS total_amount
FROM recent
WHERE rn = 1                            -- giữ 1 bản ghi/khoá → chống nhân đôi
GROUP BY account_id, customer_id
ORDER BY total_amount DESC;

(b) Thay SELECT * bằng truy vấn tổng hợp gọn — chỉ cột cần

Phản mẫu là SELECT * từ join nhiều bảng rồi để BI tự lọc. Mẫu tốt: tính đúng thứ cần ở tầng gold, chỉ trả cột phục vụ báo cáo — ít I/O, ổn định khi nguồn thêm cột.

-- ▶ Chạy được
SELECT
  c.city,
  a.currency,
  COUNT(DISTINCT c.id)                      AS customers,
  COUNT(a.id)                               AS accounts,
  ROUND(SUM(a.balance)::numeric, 2)         AS total_balance,
  ROUND(AVG(a.balance)::numeric, 2)         AS avg_balance
FROM customers c
JOIN accounts a ON a.customer_id = c.id
GROUP BY c.city, a.currency
ORDER BY total_balance DESC;

Bản đồ các mẫu trong pipeline

Sơ đồ dưới đây định vị các mẫu chính theo vị trí trong một pipeline medallion, với idempotency và WAP là hai trụ xuyên suốt.

Nguyên tắc chọn mẫu theo bối cảnh

Pattern có chi phí phức tạp. Chọn theo bối cảnh, không áp máy móc:

  • Đơn giản trước. Batch idempotent + incremental giải quyết 90% nhu cầu ngân hàng. Đừng dựng streaming/real-time nếu SLA cho phép trễ 1 giờ — đó là over-engineering.
  • Cân theo rủi ro. Bảng phục vụ NHNN thì bắt buộc WAP + circuit breaker + data contract; bảng phân tích nội bộ thì nhẹ hơn.
  • Cân theo quy mô. Metadata-driven đáng giá khi có >10 pipeline đồng dạng; dưới ngưỡng đó thì code tường minh dễ đọc hơn.
  • Idempotency là không thương lượng. Đây là mẫu duy nhất nên áp mọi lúc, vì mọi cơ chế phục hồi (retry, backfill, replay) đều dựa vào nó.

Use case thực tế

Bối cảnh: Pipeline "Số dư huy động cuối ngày" của NCB nạp ~3 triệu giao dịch/ngày từ core banking, tổng hợp thành bảng gold phục vụ báo cáo NHNN. Trước cải tổ, pipeline là một job full-refresh quét lại toàn bộ lịch sử, dùng INSERT thuần và SELECT *.

Sự cố: Một đêm cuối tháng, job timeout ở phút thứ 40; đội trực retry. Vì INSERT không idempotent, giao dịch ngày đó bị nhân đôi → số dư huy động báo cáo lệch +1.900 tỷ. Sáng hôm sau team nghiệp vụ phát hiện, phải chặn báo cáo và fix tay trên prod (thêm một phản mẫu nữa). Root cause: thiếu idempotency + không có circuit breaker chặn con số bất thường.

Cải tổ theo cẩm nang, kết quả sau 1 quý:

Chỉ sốTrướcSau
Kiểu nạpFull-refreshIncremental (30 ngày) + backfill
IdempotencyKhông (INSERT)MERGE theo khoá + dedup ROW_NUMBER
Thời gian chạy~42 phút~6 phút
Chi phí compute/tháng~100% (mốc)~28%
Chặn số bất thườngKhôngCircuit breaker (lệch >2% → dừng) + WAP
Sự cố số liệu/quý30

Ba mẫu tạo ra khác biệt lớn nhất: idempotency (retry không còn nhân đôi), incremental (nhanh + rẻ), và WAP + circuit breaker (số sai không bao giờ lên báo cáo). Việc bỏ phản mẫu sửa tay trên prod — thay bằng backfill có log — giúp mọi thay đổi đều tái lập và audit được.

Ghi nhớ

  • Pattern là ngôn ngữ chung khi review thiết kế: gọi tên "thiếu idempotency" ngắn gọn hơn giải thích nửa trang.
  • Idempotency là mẫu không thương lượng — áp mọi lúc bằng MERGE/upsert hoặc delete-insert theo partition; mọi cơ chế phục hồi dựa vào nó.
  • Bộ mẫu lõi cho ngân hàng: incremental + backfill, medallion, staging, WAP + circuit breaker + data contract cho bảng phục vụ tuân thủ.
  • Dead-letter/quarantine để một vài dòng xấu không làm vỡ cả batch; retry với backoff cho lỗi transient (đòi hỏi bước idempotent).
  • Ba phản mẫu gây sự cố số liệu nhiều nhất: không idempotent, silent failure, sửa dữ liệu tay trên prod — né cả ba.
  • Tránh SELECT *, hardcode, copy-paste không DRY, big-ball-of-mud SQL, one giant DAG, và over-engineering real-time khi batch đủ dùng.
  • Chọn mẫu theo rủi ro và quy mô, ưu tiên đơn giản; đừng áp mẫu phức tạp cho pipeline nhỏ.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5