Data Mesh 4 — Data Contracts
Data Mesh 4 — Data Contracts
Ở bài dữ liệu như một sản phẩm chúng ta đã nói data product phải có "bảo hành" — cam kết chất lượng với khách hàng. Data contract chính là văn bản cam kết đó, được viết ra dưới dạng máy đọc được (machine-readable) và được thực thi tự động. Nếu data product là một API cung cấp dữ liệu, thì data contract là đặc tả interface của API đó: nó nói rõ producer hứa gì và consumer được quyền trông cậy vào điều gì.
Đây là nguyên tắc dễ nói khó làm nhất trong Data Mesh, vì nó động chạm tới thói quen lâu đời: team nguồn đổi bảng của mình như đổi việc nhà của họ, mặc kệ ai đang dùng.
Vấn đề: silent breakage
Hãy hình dung một kịch bản kinh điển ở ngân hàng. Domain Thẻ xuất bảng card_txn cho domain Rủi ro dùng để chấm điểm gian lận (fraud scoring) và giám sát giao dịch. Một sáng thứ Hai, kỹ sư bên Thẻ refactor: đổi cột amount (đơn vị VND) thành amount_minor (đơn vị đồng nhỏ nhất — tức nhân 100), và đổi txn_ts từ giờ Việt Nam sang UTC. Với họ đó là cải tiến. Không ai báo bên Rủi ro.
Chuyện gì xảy ra? Pipeline bên Rủi ro không lỗi — cột vẫn là số, vẫn là timestamp, code vẫn chạy. Nhưng mọi ngưỡng cảnh báo gian lận bỗng lệch 100 lần, và cửa sổ thời gian giám sát lệch 7 tiếng. Mô hình fraud báo động giả tràn lan hoặc bỏ sót giao dịch bất thường. Ba tuần sau, khi khối lượng cảnh báo bất thường được điều tra, người ta mới lần ra nguyên nhân. Đây là silent breakage (vỡ âm thầm): dữ liệu vẫn "chảy", pipeline vẫn "xanh", nhưng ngữ nghĩa đã sai — thứ nguy hiểm nhất vì không có gì báo động.
Silent breakage có nhiều biến thể:
- Đổi kiểu / đơn vị / múi giờ — như ví dụ trên; hại nhất vì không gãy về kỹ thuật.
- Xoá / đổi tên cột — downstream gãy ngay, nhưng ít ra là gãy ồn ào (loud), dễ phát hiện.
- Nới nullable — cột trước không bao giờ null, nay thỉnh thoảng null → làm hỏng join, sai phép tính tổng.
- Đổi cardinality / miền giá trị — thêm một mã trạng thái mới mà consumer chưa xử lý; hoặc
currencyxuất hiện giá trị lạ. - Trễ dữ liệu (freshness) — bảng đáng lẽ cập nhật mỗi giờ nay trễ 6 tiếng; report ra số cũ mà không ai biết.
Data contract sinh ra để biến những thay đổi này thành có kiểm soát: hoặc bị chặn ở CI trước khi lên production, hoặc bắt buộc đi kèm thông báo và một phiên bản mới tương thích.
Data contract là gì
Data contract là một thoả thuận rõ ràng, chính thức và có phiên bản giữa producer (bên tạo/xuất dữ liệu) và consumer (bên tiêu thụ) về hình dạng, ngữ nghĩa và chất lượng của một tập dữ liệu — được viết dưới dạng máy đọc được để có thể kiểm tra và thực thi tự động.
Ba từ khoá cần nhấn:
- Rõ ràng (explicit): những giả định ngầm ("cột này chắc không bao giờ null đâu") được viết ra thành cam kết. Ẩn ý là nguồn gốc mọi silent breakage.
- Có phiên bản (versioned): contract thay đổi theo thời gian; mỗi thay đổi có số phiên bản và quy tắc tương thích, chứ không phải sửa đè âm thầm.
- Thực thi được (enforceable): contract không phải tài liệu Word treo trên wiki, mà là file cấu hình được máy kiểm tra trong CI/CD và pipeline.
Điểm mấu chốt: contract dịch chuyển trách nhiệm về đúng chỗ. Trước đây consumer phải "phòng thủ" bằng đủ loại kiểm tra ở cuối chuỗi. Với contract, producer cam kết trước và hệ thống chặn vi phạm ngay tại nguồn.
Các thành phần của một data contract
Một contract đầy đủ không chỉ là schema. Nó gồm năm nhóm:
1. Schema — hình dạng cấu trúc
Danh sách cột kèm: tên, kiểu dữ liệu, có nullable hay không, khoá (primary/unique key), quan hệ (foreign key). Đây là phần dễ hình dung nhất và cũng dễ tự động kiểm tra nhất.
2. Semantics — ngữ nghĩa
Phần quan trọng nhất mà hay bị bỏ quên. Một cột số tên amount không nói lên gì nếu thiếu: đơn vị (VND hay đồng nhỏ nhất?), múi giờ của timestamp, định nghĩa nghiệp vụ ("giao dịch" tính từ lúc authorize hay lúc settle?), enum hợp lệ cho các cột phân loại. Ví dụ silent breakage ở đầu bài chính là vi phạm phần semantics, không phải phần schema.
3. Chất lượng & SLO — cam kết định lượng
Contract phải nêu các Service Level Objective đo được:
| Chiều chất lượng | Ví dụ cam kết |
|---|---|
| Freshness (độ tươi) | Dữ liệu trễ tối đa 15 phút so với thời điểm giao dịch |
| Completeness (đầy đủ) | account_no không null 100%; số dòng/ngày ≥ 95% trung bình 7 ngày |
| Uniqueness / tính hợp lệ | txn_id là duy nhất; amount ≥ 0; currency ∈ {VND, USD, EUR} |
| Accuracy (chính xác) | Tổng số tiền theo ngày khớp hệ nguồn trong sai số 0.01% |
| Availability (khả dụng) | Output port đạt uptime 99.5% trong giờ nghiệp vụ |
4. Ownership & versioning
Ai là owner (team, người liên hệ, kênh hỗ trợ), phiên bản hiện tại của contract, lịch sử thay đổi, và chính sách deprecation (báo trước bao lâu khi khai tử một phiên bản). Không có owner rõ ràng thì contract chỉ là giấy.
5. Chính sách truy cập & PII
Phân loại nhạy cảm từng cột (ví dụ full_name, số thẻ là PII), yêu cầu masking/mã hoá, ai được cấp quyền đọc mức nào. Đây là móc nối với governance — xem bài chất lượng dữ liệu và access control.
Schema evolution & tương thích
Trái tim kỹ thuật của data contract là quản lý thay đổi schema theo quy tắc tương thích (compatibility). Ba mức tương thích chuẩn (mượn từ thế giới schema registry):
- Backward compatible — consumer viết theo schema mới vẫn đọc được dữ liệu ghi theo schema cũ. Điều này an toàn khi bạn muốn nâng consumer trước. Cho phép: thêm cột có giá trị mặc định, xoá cột optional.
- Forward compatible — consumer viết theo schema cũ vẫn đọc được dữ liệu ghi theo schema mới. An toàn khi bạn muốn nâng producer trước, consumer nâng sau. Cho phép: thêm cột optional, xoá cột có default.
- Full compatible — vừa backward vừa forward. Chỉ cho phép thêm/xoá cột optional. Đây là mức nghiêm ngặt nhất và an toàn nhất cho môi trường có nhiều consumer nâng cấp không đồng bộ.
Quy tắc thực dụng phân loại thay đổi thành an toàn và phá vỡ (breaking):
| Thay đổi | Ảnh hưởng |
|---|---|
| Thêm cột nullable / có default | An toàn (additive) — consumer cũ bỏ qua được |
| Nới lỏng ràng buộc (int → bigint, cho phép giá trị enum mới đã lường trước) | Thường an toàn, cần cân nhắc |
| Đổi tên cột | Phá vỡ — coi như xoá + thêm |
| Xoá cột đang được dùng | Phá vỡ |
| Đổi kiểu (string → int) hoặc đổi đơn vị/múi giờ | Phá vỡ (loại nguy hiểm: gãy ngữ nghĩa mà không gãy kỹ thuật) |
| Siết nullable → not null (nếu dữ liệu có null) | Phá vỡ ghi |
| Thu hẹp miền giá trị enum | Phá vỡ |
Nguyên tắc vàng: chỉ thêm, đừng đổi tại chỗ (additive-only). Muốn "đổi" một cột thì thêm cột mới (amount_minor bên cạnh amount), giữ cột cũ đến hết chu kỳ deprecation, rồi mới xoá ở một major version mới của contract kèm thông báo. Đây là kỹ thuật expand–contract (mở rộng rồi thu gọn).
Enforcement: contract phải được máy kiểm tra
Contract chỉ có giá trị khi được thực thi tự động, ở hai tuyến:
Tuyến 1 — CI/CD: kiểm ở thời điểm thay đổi định nghĩa
Khi ai đó sửa file schema/model của producer, pipeline CI so sánh contract mới với contract hiện hành:
- Nếu thay đổi là additive → cho merge, tăng minor version.
- Nếu thay đổi là breaking → chặn merge/deploy, yêu cầu bump major version + quy trình thông báo consumer đã đăng ký.
Điều này biến "đổi schema âm thầm" thành bất khả thi về mặt kỹ thuật: pull request không qua được cổng. Đây là ý tưởng shift-left — đẩy việc phát hiện vi phạm về càng sớm càng tốt trong vòng đời, tốt nhất là ở lúc code review chứ không phải lúc report sai trên bàn sếp.
Tuyến 2 — Pipeline runtime: kiểm ở thời điểm dữ liệu chảy
Ngay cả khi schema đúng, dữ liệu vẫn có thể vi phạm cam kết chất lượng (null bất thường, trùng khoá, giá trị ngoài miền). Vì vậy pipeline gắn data tests chạy trên dữ liệu thật:
- dbt tests — khai báo test
not_null,unique,accepted_values,relationshipsngay trong file YAML của model. Xem bài dbt. - Great Expectations — bộ "expectation" phong phú (phân phối, khoảng giá trị, tỉ lệ null...), sinh cả data docs. Phù hợp cho kiểm định phức tạp.
Nguyên lý circuit breaker: nếu dữ liệu vi phạm contract nghiêm trọng, chặn không cho nó lan xuống downstream (fail pipeline, cách ly bản ghi lỗi) thay vì để dữ liệu bẩn trôi vào báo cáo.
Streaming: schema registry
Với luồng sự kiện thời gian thực (Kafka), enforcement được đặt ngay tầng serialization qua schema registry dùng Avro hoặc Protobuf. Producer đăng ký schema, registry kiểm tra tương thích (backward/forward/full theo cấu hình subject) trước khi cho phép ghi message với schema mới. Consumer tra registry để deserialize đúng. Nhờ vậy, một producer không thể "đẩy" một schema phá vỡ vào topic. Kiến trúc chi tiết xem bài Kafka.
Công cụ & định dạng
Contract thường được biểu diễn dưới dạng YAML (dễ đọc cho người, dễ parse cho máy) và versioned trong Git cạnh code của data product. Một số chuẩn đang phổ biến:
- Open Data Contract Standard (ODCS) — chuẩn mở (do Bitol/Linux Foundation quản lý) định nghĩa cấu trúc YAML cho contract: schema, quality, SLA, roles, custom properties. Mục tiêu là một định dạng liên thông giữa các công cụ.
- dbt contracts — dbt hỗ trợ khai báo
contract: {enforced: true}cho model để ép kiểu và ràng buộc cột. - Schema Registry (Confluent/Apicurio) cho Avro/Protobuf trong streaming.
Ví dụ phác thảo một contract YAML (minh hoạ, không phải cú pháp chuẩn của một tool cụ thể):
dataProduct: card_txn
version: 1.1.0
owner: { team: domain-the, contact: "#the-data" }
schema:
- name: txn_id { type: string, unique: true, nullable: false }
- name: account_no { type: string, nullable: false, pii: false }
- name: amount_minor{ type: long, nullable: false, unit: "đồng (minor)" }
- name: currency { type: string, enum: [VND, USD, EUR] }
- name: txn_ts_utc { type: timestamp, timezone: UTC }
quality:
- column: txn_id, test: unique
- column: account_no, test: not_null
- column: amount_minor, test: "value >= 0"
slo:
freshness: "<= 15m"
completeness: ">= 99.9%"
Contract là "API" của data product
Trong Data Mesh, mỗi data product tự trị (autonomous) và giao tiếp với thế giới bên ngoài chỉ qua output port có contract. Nội bộ nó tự do làm gì tuỳ ý — đổi công nghệ lưu trữ, refactor pipeline — miễn là giữ đúng contract. Đây chính là encapsulation (đóng gói) kiểu microservice, áp cho dữ liệu: contract là interface ổn định, còn phần triển khai bên trong ẩn đi.
Nhờ vậy Data Mesh mới scale được: hàng chục domain phát triển độc lập, không cần đồng bộ hoá về mặt tổ chức, mà vẫn tích hợp được với nhau vì mọi liên kết đều đi qua các contract được máy kiểm. Federated governance (bài sau) đặt ra chuẩn contract chung (mọi product phải có contract, phải khai báo PII, phải publish SLO), còn từng domain tự viết nội dung contract của mình. Contract vì thế là điểm gặp giữa quyền tự trị của domain và kỷ luật toàn cục.
Use case thực tế
Bối cảnh. Domain Thẻ của NCB xuất data product card_txn (giao dịch thẻ), consumer chính là domain Rủi ro dùng cho giám sát giao dịch và fraud scoring theo thời gian gần thực. Trước đây họ vận hành không contract, và trong 12 tháng đã có 3 lần sự cố silent breakage, mỗi lần trung bình mất ~11 ngày để phát hiện và khắc phục, gây một đợt phải chấm điểm lại (re-score) toàn bộ cảnh báo trong kỳ.
Triển khai contract. Team đặt ra contract v1.0 gồm: khoá txn_id unique, account_no not-null, amount phải >= 0, currency ∈ {VND, USD, EUR}, freshness <= 15 phút, completeness >= 99.9%. Contract được commit dạng YAML cạnh dbt model, gắn not_null/unique/accepted_values làm dbt tests, và một CI gate so schema mỗi PR.
Kết quả sau 2 quý.
- Lần refactor "đổi đơn vị
amount" tái diễn — nhưng lần này CI gate chặn PR ngay vì đó là breaking change; kỹ sư buộc phải làm expand–contract (amount_minorcạnhamount, bump v2.0, báo trước 30 ngày). - Một sự cố nguồn khiến 4% giao dịch bị null
account_no— dbt testnot_nullfail pipeline, dữ liệu bẩn bị chặn không xuống Rủi ro; cảnh báo tới owner trong ~8 phút thay vì 11 ngày. - Thời gian phát hiện vi phạm trung bình giảm từ ~11 ngày xuống dưới 15 phút; không còn đợt re-score toàn kỳ nào trong 2 quý.
Kiểm thử contract minh hoạ trên sandbox. Trên môi trường ngân hàng thật, dữ liệu giao dịch nằm trong bảng card_txn. Ở sandbox học tập, ta minh hoạ chính xác kiểu kiểm tra mà một contract yêu cầu — không null trên khoá, uniqueness, giá trị hợp lệ — bằng một câu gộp trên các bảng có sẵn:
-- ▶ Chạy được
SELECT
COUNT(*) AS tong_dong,
COUNT(*) FILTER (WHERE t.account_id IS NULL) AS vi_pham_not_null_account,
COUNT(*) - COUNT(DISTINCT t.id) AS vi_pham_unique_txn_id,
COUNT(*) FILTER (WHERE t.amount < 0) AS vi_pham_amount_am,
COUNT(*) FILTER (WHERE a.currency NOT IN ('VND','USD','EUR')) AS vi_pham_currency
FROM transactions t
JOIN accounts a ON a.id = t.account_id;
Nếu contract được tuân thủ, cả bốn cột vi_pham_* phải bằng 0. Đây đúng là logic mà dbt test hay Great Expectations tự động hoá và chạy mỗi lần dữ liệu cập nhật.
Ghi nhớ
- Data contract = thoả thuận rõ ràng, có phiên bản, máy đọc được giữa producer & consumer về schema, ngữ nghĩa và chất lượng dữ liệu — được thực thi tự động.
- Nó giải quyết silent breakage: schema/đơn vị/múi giờ nguồn đổi âm thầm làm vỡ downstream mà pipeline vẫn "xanh". Nguy hiểm nhất là vỡ ngữ nghĩa chứ không vỡ kỹ thuật.
- Một contract gồm 5 nhóm: schema, semantics, chất lượng & SLO (freshness/completeness/validity), ownership & versioning, chính sách truy cập/PII.
- Schema evolution theo mức tương thích backward / forward / full. Nguyên tắc: additive-only — thêm cột an toàn; đổi/xoá/đổi tên là phá vỡ. Muốn đổi thì dùng expand–contract + deprecation + major version.
- Enforcement hai tuyến: CI/CD chặn merge khi schema đổi phá vỡ (shift-left); pipeline chạy data tests (dbt tests, Great Expectations) trên dữ liệu thật. Streaming dùng schema registry (Avro/Protobuf) — xem Kafka.
- Định dạng phổ biến: YAML, chuẩn Open Data Contract Standard; trong dbt là
contract: {enforced: true}. - Trong Data Mesh, contract là "API" của data product: nội bộ tự do, giao tiếp bên ngoài chỉ qua output port có contract — nền tảng để nhiều domain scale độc lập mà vẫn tích hợp.
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.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đả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.