Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 55 — Data Contracts + Schema Evolution

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng một buổi sáng thứ Hai bình thường tại một công ty thương mại điện tử ở TP.HCM. Đội Growth mở dashboard doanh thu để chuẩn bị báo cáo cho ban lãnh đạo, thì phát hiện toàn bộ số liệu tháng này bằng 0. Không phải công ty mất khách. Vấn đề là: hai tuần trước, đội Backend đã đổi tên trường order_total thành total_amount trong bảng đơn hàng để "cho gọn code", và họ đổi luôn kiểu dữ liệu từ VND (số nguyên) sang một object {amount, currency}. Không ai báo đội Data. Pipeline vẫn chạy, nhưng cột order_total giờ toàn null, và mọi dashboard, mọi model dự báo, mọi báo cáo tài chính dựng trên nó đều sập theo dây chuyền.

Đây là câu chuyện tôi đã chứng kiến ở nhiều tổ chức, dưới nhiều biến thể. Người ta gọi hiện tượng này là "fire drill" — cả đội Data phải bỏ hết việc đang làm, chạy đôn chạy đáo tìm nguyên nhân, vá tạm, xin lỗi các bên. Gốc rễ không phải lỗi kỹ thuật đơn lẻ, mà là thiếu một thỏa thuận rõ ràng giữa bên tạo ra dữ liệu (producer) và bên tiêu thụ dữ liệu (consumer).

Trong các bài trước của khóa học, chúng ta đã nói về Data Quality (Bài 12) và Data Observability (Bài 54) — tức là làm sao phát hiện khi dữ liệu hỏng. Bài 55 này đi một bước xa hơn và mang tính phòng ngừa: làm sao để dữ liệu không hỏng ngay từ điểm nguồn, bằng cách biến những giả định ngầm giữa các đội thành một hợp đồng chính thức, có thể kiểm tra tự động. Đó chính là Data Contract (hợp đồng dữ liệu), và người bạn đồng hành của nó — Schema Evolution (tiến hóa lược đồ có kiểm soát). Với một tổ chức đang muốn "data-driven" thực sự, đây là lớp nền tảng để dữ liệu đáng tin ở quy mô lớn.

Khái niệm cốt lõi

Data Contract là gì?

Data Contract là một thỏa thuận chính thức, được viết ra và có thể thực thi tự động, giữa producer và consumer về hình dạng, ý nghĩa và cam kết chất lượng của một tập dữ liệu. Nó giống như hợp đồng API mà các kỹ sư backend đã quen thuộc, nhưng áp dụng cho dữ liệu chảy qua data warehouse, event stream, hay bảng trong database.

Một hợp đồng dữ liệu đầy đủ thường mô tả:

  • Schema (lược đồ): danh sách các trường, kiểu dữ liệu, trường nào bắt buộc, trường nào cho phép null. Ví dụ: order_id là string, bắt buộc, duy nhất; total_amount là số nguyên (đơn vị VND), không âm.
  • Semantics (ngữ nghĩa): ý nghĩa nghiệp vụ của từng trường. order_created_at là thời điểm khách bấm đặt hàng hay thời điểm thanh toán thành công? Sự mơ hồ ở đây gây ra vô số sai lệch báo cáo.
  • Cam kết chất lượng (SLA/SLO): dữ liệu cập nhật mỗi bao lâu (freshness), độ đầy đủ (completeness) tối thiểu, tỷ lệ giá trị hợp lệ. Ví dụ: "bảng cập nhật mỗi 15 phút, không quá 0.1% dòng thiếu customer_id".
  • Quy tắc thay đổi (evolution policy): producer được phép đổi gì mà không cần báo, và đổi gì thì phải qua quy trình thông báo + phiên bản.
  • Ownership (quyền sở hữu): ai là chủ dữ liệu này, liên hệ với ai khi có sự cố.
Điểm mấu chốt khiến Data Contract khác một tài liệu Wiki bình thường: nó có thể được kiểm tra tự động (enforceable). Contract được viết dưới dạng máy đọc được (thường là YAML hoặc JSON Schema), và được cắm vào CI/CD của producer. Khi một kỹ sư định merge code làm đổi schema vi phạm hợp đồng, pipeline CI sẽ báo đỏ và chặn lại — trước khi nó ra production, chứ không phải sau khi dashboard đã sập.

Producer và Consumer — hai vai trong hợp đồng

Trong mô hình này, mỗi tập dữ liệu có một producer (đội tạo ra, ví dụ đội Backend đội sản phẩm) và một hoặc nhiều consumer (đội Data Analytics, đội ML, đội Finance...). Trước khi có hợp đồng, quan hệ này thường là ngầm định và bất đối xứng: producer thay đổi theo nhu cầu của họ, consumer phải tự xoay xở đuổi theo. Data Contract lật ngược điều đó — nó buộc producer nhận trách nhiệm về dữ liệu mình phát ra như một sản phẩm, coi consumer là khách hàng.

Schema Evolution — thay đổi có kiểm soát

Không hợp đồng nào đứng yên mãi. Sản phẩm phát triển, và schema phải thay đổi. Vấn đề không phải là cấm thay đổi, mà là thay đổi có kiểm soát. Người ta phân loại thay đổi thành hai nhóm:

  • Backward-compatible (thay đổi tương thích ngược): consumer cũ vẫn chạy được. Ví dụ: thêm một trường mới có giá trị mặc định, thêm một giá trị enum mới. Các thay đổi này thường an toàn.
  • Breaking change (thay đổi phá vỡ): làm consumer cũ hỏng. Ví dụ: xóa trường, đổi tên trường, đổi kiểu dữ liệu (string → int), thu hẹp phạm vi (cho phép null → bắt buộc). Đây chính là thủ phạm của các fire drill.
Nguyên tắc vàng của Schema Evolution: ưu tiên tuyệt đối các thay đổi tương thích ngược; và mọi breaking change phải đi qua quy trình versioning + deprecation (thông báo ngừng dùng có thời hạn), thay vì đổi thẳng tại chỗ. Ta sẽ nói kỹ về quy trình này ở phần hướng dẫn.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT "ShopViet": trường bị đổi tên âm thầm

ShopViet (tên giả định, quy mô ~2 triệu đơn/tháng) có đội Backend và đội Data tách biệt. Một kỹ sư backend refactor bảng orders, đổi order_total (số nguyên VND) thành total_amount kiểu object {amount, currency}. Anh ấy không biết rằng có 3 dashboard doanh thu, 1 model dự báo tồn kho và 1 báo cáo gửi nhà đầu tư đều đọc trực tiếp trường cũ.

Hậu quả: dashboard hiển thị doanh thu bằng 0 suốt 4 ngày trước khi ai đó phát hiện; báo cáo nhà đầu tư suýt gửi đi với số sai; đội Data mất gần một tuần điều tra và vá lại toàn bộ pipeline.

Diễn giải: Đây là một breaking change kép — vừa đổi tên (xóa trường cũ), vừa đổi kiểu dữ liệu — được đẩy thẳng ra production không phiên bản, không thông báo. Không hề có cơ chế nào chặn nó lại.

Bài học: Nếu bảng orders có một Data Contract khai báo order_total: integer, required và contract này được cắm vào CI của đội Backend, thì commit đổi tên trường sẽ khiến CI báo đỏ ngay lúc pull request, kèm dòng chữ "Breaking change: field order_total removed — vi phạm hợp đồng với consumer: Analytics, ML, Finance". Fire drill 4 ngày sẽ được thay bằng 5 phút thảo luận trong PR.

Ví dụ 2 — Ví điện tử "PayNhanh": kiểu dữ liệu trôi âm thầm

PayNhanh (giả định, dựa trên các tình huống thực của ví điện tử Đông Nam Á) thu event giao dịch qua một event stream dùng schema registry. Đội Mobile phát event transaction, trong đó có trường amount. Ban đầu amount là số nguyên (đơn vị đồng nhỏ nhất). Một bản cập nhật app gửi amount dưới dạng string "50000" cho một số case, do một lỗi serialize.

Vì họ một schema registry với chế độ tương thích (compatibility mode) bật ở mức BACKWARD, event sai kiểu bị registry từ chối ngay tại điểm publish. App nhận lỗi, đội Mobile phát hiện trong vài giờ khi còn ở môi trường staging, sửa trước khi lên production. Không có dòng dữ liệu bẩn nào lọt vào warehouse.

Diễn giải: Đây là ví dụ về Data Contract được thực thi tại lớp truyền tải (schema registry cho streaming) chứ không chỉ tại CI. Contract sống ngay trong đường ống, kiểm tra từng message.

Bài học: Với dữ liệu event/streaming, một schema registry (như Confluent Schema Registry với Avro/Protobuf, hay tự xây với JSON Schema) chính là nơi hợp đồng được thực thi runtime. Bật compatibility mode phù hợp giúp chặn breaking change ngay khi producer cố phát dữ liệu sai — thay vì để consumer phát hiện muộn.

Ví dụ 3 — Fintech "TinCred": nâng cấp có kiểm soát bằng versioning

TinCred (giả định) là startup chấm điểm tín dụng, có bảng credit_features mà đội ML dùng để train model. Đội Data Platform cần đổi cách tính trường income_verified — từ boolean sang một enum ba trạng thái (verified, pending, rejected) để phản ánh nghiệp vụ mới. Đây rõ ràng là breaking change.

Thay vì đổi thẳng, họ làm theo quy trình versioning: tạo credit_features_v2 với trường mới, giữ nguyên v1 chạy song song. Họ thông báo deprecation cho đội ML với thời hạn 60 ngày, hỗ trợ migrate, rồi mới tắt v1. Model không hề gián đoạn train, và khi cần audit, họ vẫn truy được logic cũ.

Diễn giải: Breaking change không thể tránh, nhưng nó được quản lý như một dự án nhỏ chứ không phải một cú đổi lén.

Bài học: Với dữ liệu quan trọng, "expand and contract" (mở rộng rồi co lại) + deprecation window là mẫu hình an toàn nhất: thêm cái mới, cho hai bên cùng chạy, rồi mới bỏ cái cũ.

Hướng dẫn từng bước

Dưới đây là lộ trình thực tế để đưa Data Contract vào một tổ chức, từ chỗ chưa có gì.

Bước 1 — Xác định các "dữ liệu vàng" cần hợp đồng trước. Đừng cố làm hợp đồng cho tất cả cùng lúc. Hãy bắt đầu từ 3–5 tập dữ liệu quan trọng nhất và hay gây sự cố nhất: thường là bảng đơn hàng, event người dùng, bảng khách hàng. Nguyên tắc: nơi nào một thay đổi có thể làm sập báo cáo ban lãnh đạo hoặc model production, nơi đó cần hợp đồng trước.

Bước 2 — Viết hợp đồng dưới dạng máy đọc được. Dùng một định dạng khai báo như YAML. Một hợp đồng tối giản có thể trông như sau:

dataset: orders
owner: team-backend-commerce
version: 1.2.0
fields:
  - name: order_id
    type: string
    required: true
    unique: true
  - name: total_amount
    type: integer      # đơn vị: VND
    required: true
    constraints: { min: 0 }
  - name: order_status
    type: enum
    values: [created, paid, shipped, cancelled]
sla:
  freshness: 15m
  completeness: 99.9%

Bước 3 — Thống nhất với consumer. Ngồi lại với các đội đang dùng dữ liệu, xác nhận: họ thực sự phụ thuộc vào trường nào, kỳ vọng freshness ra sao. Chính bước đối thoại này đã ngăn được nửa số sự cố, vì nó phơi bày những phụ thuộc ngầm mà producer không hề biết.

Bước 4 — Cắm contract vào CI/CD của producer. Đây là bước biến tài liệu thành hàng rào. Khi producer thay đổi schema, một bước kiểm tra so sánh schema mới với contract; nếu là breaking change chưa được phê duyệt, CI fail. Với streaming, tương đương là bật compatibility check ở schema registry.

Bước 5 — Thiết lập quy trình cho breaking change. Định nghĩa rõ: thêm trường mới có default = tự động cho phép; xóa/đổi tên/đổi kiểu = bắt buộc bump major version + thông báo consumer + deprecation window (ví dụ 30–60 ngày) + expand-and-contract. Ghi quy trình này thành chính sách chung, không để tùy hứng.

Bước 6 — Giám sát tuân thủ trong production. Contract cũng nên được kiểm tra sau khi dữ liệu vào (kết nối với Data Observability ở Bài 54): mỗi ngày chạy validate xem dữ liệu thực có đúng schema, có đạt SLA không, và cảnh báo owner nếu lệch.

Bước 7 — Nhân rộng dần và gắn vào văn hóa. Khi vài hợp đồng đầu chứng minh được giá trị (số fire drill giảm), mở rộng ra các dataset khác. Quan trọng hơn cả công cụ: biến "sản xuất dữ liệu như một sản phẩm" thành chuẩn mực làm việc.

Lỗi thường gặp & mẹo

Lỗi 1 — Viết contract nhưng không thực thi. Rất nhiều đội dừng ở việc viết một trang Confluence mô tả schema rồi coi đó là "data contract". Không có CI check, không có registry, tài liệu đó nhanh chóng lỗi thời và vô dụng. Mẹo: nếu một thay đổi vi phạm không làm gì đó tự động báo đỏ, thì bạn chưa có contract — bạn chỉ có ghi chú.

Lỗi 2 — Làm quá nặng ngay từ đầu. Cố viết hợp đồng chi tiết cho 200 bảng trong quý đầu là công thức chắc chắn thất bại. Mẹo: bắt đầu nhỏ, chọn dataset đau nhất, chứng minh giá trị, rồi mới lan rộng.

Lỗi 3 — Đối xử mọi thay đổi như nhau. Chặn cả những thay đổi tương thích ngược (như thêm trường mới) sẽ khiến producer ghét quy trình và tìm cách né. Mẹo: phân biệt rõ backward-compatible (cho qua tự động) và breaking (mới cần quy trình nặng). Sự linh hoạt này giữ cho producer hợp tác.

Lỗi 4 — Quên ngữ nghĩa, chỉ lo kiểu dữ liệu. Contract đúng kiểu nhưng sai ý nghĩa vẫn phá báo cáo. revenue là trước hay sau hoàn tiền? Mẹo: mỗi trường quan trọng nên có mô tả nghiệp vụ ngắn gọn ngay trong contract.

Lỗi 5 — Đổi thẳng tại chỗ thay vì versioning. Cám dỗ lớn nhất khi cần breaking change là "sửa nhanh cho xong". Mẹo: luôn expand-and-contract với deprecation window; coi việc tắt schema cũ là một task riêng có deadline.

Mẹo văn hóa: Data Contract chỉ sống nếu producer coi consumer là khách hàng. Hãy làm cho việc tuân thủ dễ hơn việc phá vỡ — công cụ tốt, thông báo tự động, template sẵn — thay vì chỉ dựng rào và trách phạt.

Bài tập thực hành

  • Chọn dataset đau nhất: Trong tổ chức (hoặc dự án giả định) của bạn, liệt kê 3 tập dữ liệu mà một thay đổi bất ngờ ở nguồn từng gây hoặc có thể gây sự cố nghiêm trọng. Xếp hạng theo mức thiệt hại. Chọn 1 để làm bài tập.
  • Viết một Data Contract tối giản: Với dataset đã chọn, viết một file YAML gồm: danh sách trường + kiểu + trường bắt buộc, ít nhất một constraint, một trường enum, và một sla (freshness + completeness). Thêm mô tả nghiệp vụ cho 2 trường dễ gây hiểu nhầm nhất.
  • Phân loại thay đổi: Cho 5 thay đổi sau, đánh dấu backward-compatible hay breaking, và nêu cách xử lý: (a) thêm trường discount_code cho phép null; (b) đổi amount từ int sang float; (c) xóa trường legacy_id; (d) thêm giá trị refunded vào enum trạng thái; (e) đổi trường email từ optional sang required.
  • Thiết kế deprecation plan: Với thay đổi (c) và (e) ở trên, viết một kế hoạch expand-and-contract có mốc thời gian: thông báo, thời hạn deprecation, cách migrate consumer, ngày tắt phiên bản cũ.
  • Suy ngẫm về thực thi: Viết 3–4 câu mô tả chính xác điểm nào trong pipeline của bạn sẽ chặn một breaking change (CI của producer? schema registry? validate sau ingest?). Nếu hiện tại không có điểm nào, đó chính là lỗ hổng đầu tiên cần vá.

Tóm tắt

Data Contract biến những giả định ngầm giữa đội tạo dữ liệu và đội dùng dữ liệu thành một thỏa thuận chính thức, máy đọc được và thực thi tự động — bao gồm schema, ngữ nghĩa, cam kết chất lượng và quy tắc thay đổi. Nó giải quyết tận gốc nỗi đau kinh điển: source đổi schema âm thầm, làm sập consumer ở hạ nguồn, kéo cả đội vào fire drill.

Người bạn song hành là Schema Evolution: thay đổi là tất yếu, nhưng phải có kiểm soát. Ưu tiên tuyệt đối các thay đổi tương thích ngược (thêm trường, thêm enum), và bắt buộc mọi breaking change (xóa, đổi tên, đổi kiểu) phải đi qua versioning + deprecation window theo mẫu expand-and-contract.

Về mặt thực thi, hợp đồng cần được cắm vào CI/CD của producer (chặn breaking change ngay lúc pull request) và/hoặc vào schema registry cho dữ liệu streaming (chặn message sai ngay tại điểm publish), cộng thêm giám sát tuân thủ sau ingest. Nhưng công nghệ chỉ là một nửa — nửa còn lại là văn hóa: producer phải coi dữ liệu mình phát ra là một sản phẩm và consumer là khách hàng. Hãy bắt đầu nhỏ từ vài dataset quan trọng nhất, chứng minh rằng số fire drill giảm hẳn, rồi nhân rộng. Đó là cách một tổ chức data-driven xây được nền dữ liệu đáng tin ở quy mô lớn.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi