DataOps nâng cao 11 — Môi trường, dữ liệu test & Write-Audit-Publish

13 thg 7, 2026 2 lượt xem
#data-engineering
#dataops
#write-audit-publish
#test-data
#environments

Vì sao "môi trường & dữ liệu" là gốc rễ của độ tin cậy

Ta đã bàn nhiều về CI/CD (DataOps nâng cao 4), data quality & contract (DataOps nâng cao 5), và platform self-service (DataOps nâng cao 7). Nhưng có hai câu hỏi nền tảng thường bị né tránh: Dev test bằng dữ liệu gì?Làm sao đảm bảo dữ liệu xấu không bao giờ lọt ra người dùng cuối?

Trong ngân hàng, hai câu hỏi này có ràng buộc pháp lý gắt: không được copy dữ liệu khách hàng thật vào môi trường non-prod (Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân — gọi tắt NĐ13). Đồng thời báo cáo tuân thủ gửi NHNN thì tuyệt đối không được hiển thị số liệu chưa kiểm. Bài này giải quyết cả hai bằng ba trụ: tách môi trường đúng cách, sản xuất dữ liệu test an toàn, và mẫu Write-Audit-Publish (WAP).

Tách môi trường: không chỉ là ba cái tên

"Có dev/staging/prod" chưa đủ. Tách môi trường đúng nghĩa là tách bốn thứ: mã, cấu hình, dữ liệu, và quyền truy cập. Nhiều đội tách được code (ba branch Git) nhưng vẫn để dev query thẳng bảng production — đó là tách giả.

Chiều táchSai lầm phổ biếnĐúng chuẩn ngân hàng
StorageCùng database, khác prefix bảngDatabase/schema riêng, tốt hơn là account/warehouse riêng cho prod
Dữ liệuDev đọc trực tiếp bảng prodNon-prod chỉ chứa dữ liệu đã masking/synthetic
QuyềnCùng service account cho mọi envIAM/role riêng; dev không có quyền đọc PII prod
Cấu hìnhHardcode connection stringBiến môi trường + secret manager theo env (Sec 5)

Với warehouse, mô hình khuyến nghị là tách theo schema/database và profile dbt theo target:

# profiles.yml — cấu hình, không đánh dấu chạy được
ncb_dwh:
  target: dev
  outputs:
    dev:
      schema: dbt_{{ env_var('USER') }}   # mỗi dev một schema riêng
      database: NCB_NONPROD
    prod:
      schema: analytics
      database: NCB_PROD                    # account/role riêng, PII thật

Nguyên tắc vàng: luồng quyền chỉ đi một chiều — prod có thể "đẩy" dữ liệu đã masking xuống non-prod qua một job kiểm soát, nhưng non-prod không bao giờ có credential đọc prod. Bất kỳ chiều ngược lại nào cũng là lỗ hổng NĐ13.

Bài toán dữ liệu test: cái bẫy kép

Đây là nghịch lý cốt lõi của mọi đội dữ liệu ngân hàng:

  • Không thể test tốt nếu dev không có dữ liệu giống thật. Bảng 10 dòng giả không lộ ra lỗi skew, null bất thường, hay giao dịch mồ côi mà chỉ xuất hiện ở quy mô hàng chục triệu dòng thật.
  • Nhưng KHÔNG được copy dữ liệu khách thật xuống non-prod — vi phạm NĐ13, và một lần rò rỉ từ môi trường dev (vốn kiểm soát lỏng hơn) là đủ để mất giấy phép.

Không có một giải pháp duy nhất. Ta phối hợp bốn kỹ thuật, chọn theo nhu cầu:

1. Data subsetting — lấy mẫu đại diện

Thay vì copy toàn bộ, lấy một lát cắt nhất quán tham chiếu (referentially consistent): chọn ~1% khách hàng, rồi kéo theo đúng tài khoản và giao dịch của họ để giữ khóa ngoại còn nguyên. Lát cắt nhỏ (vài GB thay vì vài TB) chạy nhanh trên laptop dev nhưng vẫn giữ hình dạng phân phối. Lưu ý: subset vẫn là dữ liệu thật ⇒ bắt buộc masking sau khi cắt trước khi đưa vào non-prod.

2. Synthetic data — sinh dữ liệu giả giống phân phối thật

Sinh dữ liệu hoàn toàn nhân tạo nhưng giữ đặc tính thống kê: phân bố số dư, tỷ lệ loại giao dịch, tần suất theo giờ. Ưu điểm lớn nhất: không chứa một byte PII thật nào ⇒ an toàn NĐ13 tuyệt đối, chia sẻ được cho vendor/CI công khai. Nhược điểm: khó tái tạo tương quan phức tạp và các "long tail" hiếm gặp (gian lận, edge case). Công cụ: Faker (giả lập trường), SDV/Synthea (giữ phân phối đa biến).

3. Data masking / anonymization cho non-prod

Khi buộc phải dùng dữ liệu gốc, anonymize trước khi hạ xuống dev: hash CCCD, tokenize số tài khoản, dịch chuyển ngày sinh, thay tên bằng bút danh nhất quán. Chi tiết kỹ thuật masking (deterministic vs random, format-preserving, k-anonymity) xem Governance 5 — mã hóa & masking. Quan trọng: masking phải nhất quán referential — cùng một customer_id phải map ra cùng bút danh ở mọi bảng, nếu không JOIN sẽ vỡ.

4. Zero-copy clone — bản sao logic tức thì

Lakehouse hiện đại (Snowflake, Databricks/Delta, Iceberg) cho phép clone không copy dữ liệu vật lý: tạo một bảng/schema mới trỏ tới cùng file nền, chỉ ghi phần thay đổi (copy-on-write). Clone 5 TB xong trong vài giây, gần như miễn phí storage cho tới khi ghi đè. Đây là nền tảng của môi trường ephemeral và của cả mẫu WAP bên dưới. Kết hợp với time travel để lấy snapshot tại một thời điểm — xem Databricks 2 — Delta Lake về CLONEVERSION AS OF.

-- Snowflake — KHÔNG đánh dấu chạy được (phương ngữ khác, DDL)
CREATE SCHEMA dev_pr_142 CLONE analytics;   -- bản sao logic tức thì
-- Databricks Delta
CREATE TABLE dev.accounts SHALLOW CLONE prod.accounts;

dbt defer/state — dev không build lại toàn bộ

Bổ trợ cho clone ở tầng transform: dbt build --defer --state <prod_manifest> cho phép dev build chỉ những model đã đổi, còn các model chưa đổi thì tham chiếu (defer) sang bản prod thay vì phải build lại toàn bộ DAG hàng trăm model. Kết hợp --select state:modified+ để chỉ chạy model thay đổi và hạ lưu của nó — dev vòng lặp phút thay vì giờ. Nền tảng của CI ephemeral rẻ.

Write-Audit-Publish: chống lộ dữ liệu xấu

Đây là trái tim của bài. Vấn đề: pipeline chạy xong, ghi thẳng vào bảng production. Nếu logic sai hay dữ liệu nguồn hỏng, người dùng thấy ngay số liệu xấu — báo cáo đối soát NHNN hiển thị dư nợ lệch, không thể rút lại.

Write-Audit-Publish đảo ngược thứ tự: ghi trước, kiểm sau, chỉ công bố nếu đạt.

Ba bước:

  1. Write — pipeline ghi kết quả vào một vùng staging tách biệt: một bảng tạm, một audit branch, hoặc một clone. Người dùng không nhìn thấy vùng này.
  2. Audit — chạy bộ DQ check trên staging: control total (tổng kiểm soát), tính bất biến (invariant), so với ngày trước, kiểm null/unique/orphan. Đây chính là các test ta đã dựng ở DataOps nâng cao 5.
  3. Publishchỉ khi audit đạt thì đổi con trỏ (swap) để người dùng thấy dữ liệu mới. Nếu fail: giữ nguyên bản cũ, bỏ staging, cảnh báo. Người dùng không bao giờ thấy trạng thái trung gian.

Hiện thực WAP: bốn cách

Kỹ thuậtCơ chế "publish"Nền tảng
Bảng tạm + swapALTER TABLE ... RENAME hoặc swap partitionMọi warehouse, đơn giản nhất
Branch (data version control)merge audit branch → mainNessie, lakeFS, Iceberg branch
Delta/Iceberg atomic overwritecommit atomic một transactionDelta Lake, Iceberg
Blue-green datahai bản đầy đủ, view trỏ luân phiênWarehouse, chi phí storage 2x

Bảng tạm + swap là cách phổ biến nhất và không cần công cụ đặc biệt: ghi vào accounts_staging, audit, rồi RENAME staging thành bảng chính trong một transaction — thao tác atomic swap, người dùng không thấy bảng nửa vời.

Branch/clone (Nessie/lakeFS/Iceberg) đưa mô hình Git vào dữ liệu: pipeline ghi lên một branch, chạy audit trên branch đó, chỉ merge vào main nếu đạt. Đẹp ở chỗ WAP trở thành pull-request-cho-dữ-liệu, và rollback = trỏ lại commit cũ.

Blue-green data: duy trì hai bản đầy đủ ("blue" đang phục vụ, "green" vừa build). Sau khi green pass audit, đổi view/synonym trỏ sang green — swap tức thì, rollback tức thì bằng cách trỏ lại blue. Đổi lại là tốn gấp đôi storage (zero-copy clone làm chi phí này gần bằng 0).

Bước AUDIT: SQL control-total chạy được trên sandbox

Đây là ví dụ thật của bước Audit — kiểm control total trên vùng staging trước khi publish. Ta đối chiếu tổng số dư và số tài khoản theo từng thành phố, kèm hai cột reference (số nhân viên / số phòng ban làm mẫu số quy mô tổ chức để đội vận hành soi bất thường), ép kiểu ::numeric khi ROUND. Đội báo cáo so con số này với tổng từ nguồn; lệch ⇒ chặn publish, giữ bản cũ:

-- ▶ Chạy được
SELECT c.city,
       COUNT(DISTINCT a.id)                          AS so_tk,
       COUNT(DISTINCT c.id)                          AS so_kh,
       ROUND(SUM(a.balance)::numeric, 2)             AS tong_du,
       ROUND(COALESCE(SUM(t.amount), 0)::numeric, 2) AS tong_gd,
       ROUND((SUM(a.balance) / NULLIF(COUNT(DISTINCT a.id), 0))::numeric, 2) AS du_tb_moi_tk,
       ref.so_nhan_vien,
       ref.so_phong_ban
FROM customers c
JOIN accounts a          ON a.customer_id = c.id
LEFT JOIN transactions t ON t.account_id = a.id
CROSS JOIN (
    SELECT COUNT(DISTINCT e.id)            AS so_nhan_vien,
           COUNT(DISTINCT d.id)            AS so_phong_ban
    FROM employees e
    JOIN departments d ON d.id = e.department_id
) ref
GROUP BY c.city, ref.so_nhan_vien, ref.so_phong_ban
HAVING SUM(a.balance) > 0
ORDER BY tong_du DESC;

Câu này chạm cả 5 bảng sandbox trong đúng một SELECT. Trong pipeline WAP thật, một job wrapper chạy câu tương tự trên bảng staging, so kết quả với ngưỡng bất biến (ví dụ: tổng dư không lệch quá 0.01% so với snapshot hôm qua, không có du_tb_moi_tk âm). Nếu mọi bất biến đạt ⇒ gọi bước publish (swap). Nếu không ⇒ raise, giữ nguyên bản cũ.

Mẫu jinja hoá bước audit-then-publish trong dbt/orchestrator (minh hoạ, không đánh dấu chạy được):

-- pseudocode SQL/jinja — minh hoạ WAP, KHÔNG chạy được
{% set drift = run_query("SELECT ABS(SUM(balance)-{{ prev_total }})/{{ prev_total }} FROM accounts_staging") %}
{% if drift[0][0] | float < 0.0001 %}
    ALTER TABLE accounts        RENAME TO accounts_old;   -- swap atomic
    ALTER TABLE accounts_staging RENAME TO accounts;
    DROP TABLE accounts_old;
{% else %}
    {{ raise_compiler_error("AUDIT FAIL: drift " ~ drift[0][0] ~ " > 0.01% — giữ bản cũ") }}
{% endif %}

Môi trường ephemeral cho mỗi PR

Kết hợp zero-copy clone + dbt defer, ta dựng được môi trường tạm cho mỗi Pull Request: khi dev mở PR #142, CI tự CREATE SCHEMA dev_pr_142 CLONE ..., build chỉ model đã đổi (defer phần còn lại), chạy test, đăng kết quả lên PR, rồi huỷ schema khi PR merge/close. Chi phí gần 0 nhờ clone, và mỗi PR được kiểm trên dữ liệu giống prod (đã masking) mà không đụng bảng thật.

Seed dữ liệu ổn định cho CI: với các test cần kết quả tất định (deterministic), dùng một bộ seed cố định (dbt seeds/, hoặc snapshot synthetic đã version) thay vì clone dữ liệu động — để COUNT(*) hôm nay bằng hôm qua, test không "flaky". Nguyên tắc: dùng clone masking cho test hình dạng/hiệu năng, dùng seed cố định cho test logic chính xác.

Use case thực tế

Bối cảnh. Đội DWH NCB build mart mart_du_no_theo_chi_nhanh phục vụ báo cáo gửi NHNN mỗi sáng 6h. Trước đây pipeline ghi thẳng vào bảng prod; một hôm nguồn CDC bị trễ khiến 40% giao dịch của ngày thiếu, mart vẫn ghi đè và báo cáo hiển thị dư nợ hụt 1.200 tỷ. Đội nghiệp vụ gửi số sai lên NHNN trước khi kịp phát hiện.

Áp dụng WAP. Đội tái cấu trúc pipeline theo Write-Audit-Publish:

  1. Write — pipeline ghi vào mart_du_no_staging (một clone rỗng của mart), không ai thấy.
  2. Audit — chạy 4 bất biến: (a) control total tổng dư lệch < 0.01% so hôm qua; (b) số chi nhánh = 89 (đủ mạng lưới); (c) không có dư nợ âm; (d) đếm giao dịch hôm nay ≥ 95% trung bình 7 ngày (bắt được đúng ca CDC trễ). Câu SQL control-total ở trên là chốt (a)+(b).
  3. Publish — chỉ khi cả 4 đạt mới RENAME swap staging thành mart chính, atomic.

Kết quả sau 6 tháng. Ca CDC trễ tái diễn 3 lần — cả 3 lần chốt (d) fail, pipeline giữ nguyên mart hôm trước và alert on-call lúc 5h50, đội xử lý nguồn xong trước 6h30. Người dùng không lần nào thấy số sai. Chi phí thêm: một clone (gần 0 storage) + ~90 giây audit mỗi sáng. Song song, môi trường dev chuyển hẳn sang subset 1% đã masking + synthetic cho long-tail gian lận, gỡ bỏ toàn bộ quyền dev đọc PII prod — vượt kiểm toán NĐ13 nội bộ.

Ghi nhớ

  • Tách môi trường = tách 4 thứ: code, config, dữ liệu, quyền. Non-prod không bao giờ có credential đọc PII prod; luồng dữ liệu chỉ đi một chiều prod → (masking) → non-prod.
  • Không copy dữ liệu khách thật xuống non-prod (NĐ13). Bốn công cụ: subsetting (mẫu đại diện, vẫn phải masking), synthetic (an toàn nhất, khó tái tạo long-tail), masking (khi buộc dùng gốc, phải nhất quán referential), zero-copy clone (bản sao logic tức thì).
  • dbt defer/state cho dev build chỉ model đã đổi — vòng lặp phút thay vì giờ; nền tảng CI ephemeral rẻ.
  • Write-Audit-Publish: ghi vào staging → audit DQ → chỉ publish (swap atomic) nếu đạt. Người dùng không bao giờ thấy trạng thái trung gian hay dữ liệu xấu.
  • Hiện thực WAP: bảng tạm + RENAME swap (đơn giản nhất), branch/merge (Nessie/lakeFS/Iceberg), atomic overwrite (Delta/Iceberg), blue-green (2x storage, rẻ nhờ clone).
  • Ephemeral env mỗi PR = clone + defer, huỷ sau khi merge, chi phí ~0. Dùng seed cố định cho test logic tất định, clone masking cho test hình dạng/hiệu năng.
  • Ngân hàng: WAP đảm bảo báo cáo NHNN chỉ hiển thị số đã kiểm; audit control-total + bất biến drift là chốt chặn cuối trước publish.

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