Data Engineering 12 — CDC & Kiến trúc dữ liệu thời gian thực
Vì sao cần dữ liệu thời gian thực?
Trong suốt chuỗi bài trước, phần lớn pipeline của chúng ta vận hành theo kiểu batch: gom dữ liệu cả ngày, đến nửa đêm (hoặc cuối ngày — End Of Day, EOD) thì chạy một mẻ lớn để nạp vào kho dữ liệu. Mô hình này đơn giản, dễ kiểm soát và đủ tốt cho hầu hết báo cáo quản trị. Nhưng nó có một giới hạn cố hữu: độ trễ.
Hãy hình dung một ngân hàng. Nếu báo cáo giao dịch chỉ được cập nhật vào 1 giờ sáng hôm sau, thì:
- Bộ phận chống gian lận (fraud) chỉ phát hiện một chuỗi rút tiền bất thường sau khi tiền đã ra khỏi hệ thống — quá muộn.
- Dashboard điều hành luôn hiển thị số liệu của ngày hôm qua, không phản ánh tình hình đang diễn ra.
- Các mô hình chấm điểm rủi ro tín dụng dùng dữ liệu cũ 24 giờ, bỏ lỡ tín hiệu vừa phát sinh.
Dữ liệu thời gian thực (real-time / near-real-time) rút ngắn độ trễ từ hàng giờ xuống còn vài giây đến vài chục giây. Thay vì hỏi "hôm qua đã xảy ra gì?", ta hỏi "ngay lúc này đang xảy ra gì?". Đây là nền tảng cho phát hiện gian lận tức thời, dashboard sống, cảnh báo vận hành, và cá nhân hóa theo hành vi.
Nhưng đổi lại, kiến trúc realtime phức tạp hơn batch rất nhiều. Bài này sẽ đi từ gốc rễ: làm sao bắt được thay đổi từ cơ sở dữ liệu nguồn mà không làm nó "ngộp thở", rồi đẩy dòng thay đổi đó qua streaming để đồng bộ vào kho dữ liệu một cách đáng tin cậy.
Change Data Capture (CDC) — bắt thay đổi từ nguồn
Vấn đề: làm sao biết dữ liệu nguồn vừa đổi?
Cách ngây thơ nhất là polling: cứ vài phút lại chạy SELECT * FROM transactions WHERE updated_at > :last_run. Cách này có nhiều nhược điểm nghiêm trọng:
- Tạo tải lớn lên CSDL nguồn (mà CSDL nguồn thường là hệ thống core đang phục vụ giao dịch — không được phép chậm).
- Bỏ sót bản ghi bị xóa (DELETE không để lại dấu vết trong kết quả query).
- Phụ thuộc vào cột
updated_atchính xác — mà không phải bảng nào cũng có. - Không bắt được các thay đổi trung gian (một dòng đổi 3 lần giữa hai lần poll thì chỉ thấy trạng thái cuối).
Change Data Capture (CDC) là kỹ thuật bắt từng thay đổi (insert, update, delete) trên CSDL nguồn và phát ra dưới dạng một dòng sự kiện. Ý tưởng then chốt: đọc transaction log thay vì query bảng.
Vì sao đọc transaction log?
Mọi CSDL quan hệ hiện đại đều ghi lại mọi thay đổi vào một nhật ký giao dịch trước khi áp dụng vào bảng — để phục hồi sau sự cố và để replication. PostgreSQL gọi đó là WAL (Write-Ahead Log), MySQL gọi là binlog, SQL Server có transaction log, Oracle có redo log.
Đọc trực tiếp log này mang lại:
- Tải gần như bằng 0 lên CSDL nguồn (chỉ đọc một file tuần tự, không động vào bảng).
- Đầy đủ mọi thay đổi theo đúng thứ tự, kể cả DELETE.
- Giá trị trước và sau của bản ghi (before/after image).
Ba phương pháp CDC
| Phương pháp | Cách hoạt động | Độ trễ | Tải lên nguồn | Bắt được DELETE? | Hạn chế |
|---|---|---|---|---|---|
| Log-based | Đọc WAL/binlog/redo log | Rất thấp (giây) | Rất thấp | Có | Cần quyền đọc log, cấu hình DB; phụ thuộc loại DB |
| Trigger-based | Tạo trigger ghi thay đổi vào bảng audit | Thấp | Cao (mỗi write thêm 1 write) | Có | Tăng tải ghi, phải sửa schema nguồn |
| Timestamp/Query-based | Poll cột updated_at | Cao (chu kỳ poll) | Trung bình–cao | Không | Bỏ sót delete, cần cột tin cậy |
Log-based là lựa chọn ưu tiên cho hệ thống production tải cao như core banking, vì gần như không tác động đến nguồn. Trigger-based dùng khi không thể bật log hoặc DB không hỗ trợ. Timestamp-based chỉ hợp cho dữ liệu chỉ-thêm (append-only) và yêu cầu độ trễ thấp không cao.
Debezium + Kafka Connect
Debezium là nền tảng CDC mã nguồn mở phổ biến nhất, chạy như một connector trong Kafka Connect. Nó hỗ trợ log-based CDC cho PostgreSQL, MySQL, SQL Server, Oracle, MongoDB...
Luồng hoạt động:
- Debezium kết nối CSDL nguồn và đọc transaction log.
- Mỗi thay đổi được chuyển thành một sự kiện CDC dạng JSON/Avro, gồm:
op(c=create, u=update, d=delete),before,after, và metadata (bảng, vị trí log, timestamp). - Sự kiện được đẩy vào một Kafka topic (thường mỗi bảng một topic).
- Các hệ thống tiêu thụ (consumer) đọc topic để đồng bộ tiếp.
Một ví dụ sự kiện update đơn giản hóa:
{
"op": "u",
"before": { "id": 101, "balance": 5000000 },
"after": { "id": 101, "balance": 4500000 },
"source": { "table": "accounts", "lsn": 982374, "ts_ms": 1719720000123 }
}
Khi Debezium khởi động lần đầu, nó thường chạy snapshot (đọc toàn bộ trạng thái hiện tại) rồi mới chuyển sang đọc log liên tục (streaming). Nhờ vậy consumer có được cả ảnh chụp ban đầu lẫn dòng thay đổi tiếp theo.
Streaming ETL/ELT — biến đổi trên luồng
Trong batch, ta gom dữ liệu rồi transform theo mẻ. Trong streaming, dữ liệu chảy liên tục nên ta phải biến đổi từng sự kiện (hoặc cửa sổ sự kiện) ngay khi nó đi qua.
Các thao tác phổ biến trên luồng:
- Lọc & định tuyến: bỏ sự kiện không liên quan, chia topic theo loại.
- Làm giàu (enrichment): ghép sự kiện giao dịch với dữ liệu tham chiếu (ví dụ ghép
account_idvới thông tin khách hàng). - Tổng hợp theo cửa sổ (windowed aggregation): ví dụ "tổng số tiền rút trong 5 phút gần nhất theo từng tài khoản" — phục vụ phát hiện gian lận.
- Phát hiện mẫu: nhiều giao dịch liên tiếp ở các địa điểm xa nhau.
Công cụ xử lý dòng phổ biến: Kafka Streams, Apache Flink, Spark Structured Streaming. Flink mạnh về xử lý trạng thái (stateful) và windowing chính xác theo thời gian sự kiện (event time); Kafka Streams nhẹ và gắn chặt với Kafka.
Điểm khác biệt ETL vs ELT vẫn áp dụng: ta có thể biến đổi trên đường đi (streaming ETL) rồi ghi kết quả sạch vào kho, hoặc đổ thô vào kho/lakehouse rồi transform sau bằng SQL (ELT). Thực tế hiện đại thường lai: làm sạch/làm giàu nhẹ trên luồng, để các biến đổi nặng cho lớp warehouse.
Lambda vs Kappa — hai trường phái kiến trúc
Khi cần cả số liệu lịch sử chính xác lẫn tốc độ realtime, có hai kiến trúc kinh điển.
Lambda Architecture
Lambda chạy song song hai lớp:
- Batch layer: xử lý lại toàn bộ dữ liệu lịch sử theo mẻ, cho kết quả chính xác, đầy đủ nhưng trễ.
- Speed layer (streaming): xử lý dữ liệu mới đến tức thời, cho kết quả gần đúng nhưng nhanh.
- Serving layer: hợp nhất kết quả của hai lớp khi truy vấn.
Ưu điểm: vừa chính xác (nhờ batch tính lại), vừa nhanh (nhờ speed). Nhược điểm lớn nhất: phải duy trì hai code base cho cùng một logic — dễ lệch nhau, tốn công bảo trì.
Kappa Architecture
Kappa loại bỏ batch layer: chỉ một đường streaming duy nhất. Mọi dữ liệu đều đi qua log sự kiện (Kafka), và khi cần tính lại lịch sử thì replay (phát lại) toàn bộ log qua cùng một pipeline streaming.
Ưu điểm: một code base, đơn giản hơn về vận hành logic. Yêu cầu: log phải lưu đủ lâu (hoặc lưu vĩnh viễn / tiered storage) để replay được.
So sánh
| Tiêu chí | Lambda | Kappa |
|---|---|---|
| Số đường xử lý | Hai (batch + speed) | Một (chỉ streaming) |
| Code base logic | Trùng lặp ở hai lớp | Duy nhất |
| Tính lại lịch sử | Chạy lại batch | Replay log sự kiện |
| Độ phức tạp vận hành | Cao | Trung bình |
| Phù hợp khi | Cần batch chính xác + streaming, logic batch khác streaming | Logic streaming đủ tái dùng cho cả lịch sử |
| Rủi ro chính | Hai lớp lệch logic | Cần log lưu lâu, replay tốn tài nguyên |
Khi nào dùng gì? Nếu tổ chức đã có hệ batch trưởng thành và logic phức tạp khó đưa hết lên streaming, Lambda an toàn hơn. Nếu xây mới và muốn tránh trùng lặp, Kappa gọn hơn — đặc biệt khi Kafka làm "nguồn sự thật" (source of truth) và có thể replay.
Đảm bảo exactly-once & idempotency
Đây là phần khó nhất của streaming. Khi đồng bộ realtime, ta phải trả lời: điều gì xảy ra nếu một sự kiện được giao hai lần?
Có ba ngữ nghĩa giao nhận (delivery semantics):
- At-most-once: có thể mất sự kiện, không bao giờ trùng. (Không chấp nhận được với dữ liệu tài chính.)
- At-least-once: không mất, nhưng có thể trùng. (Phổ biến, mặc định ở nhiều hệ thống.)
- Exactly-once: không mất, không trùng. (Lý tưởng nhưng tốn kém.)
Trong thực tế, hệ thống thường cho at-least-once (vì khi consumer crash sau khi xử lý nhưng trước khi commit offset, sự kiện sẽ được đọc lại). Cách chống trùng tốt nhất không phải là ép exactly-once tuyệt đối ở mọi tầng, mà là làm cho thao tác ghi trở nên idempotent — chạy nhiều lần cũng cho kết quả như chạy một lần.
Các kỹ thuật idempotency khi nạp vào warehouse:
- UPSERT theo khóa nghiệp vụ: dùng
MERGE/INSERT ... ON CONFLICTtheo primary key, để cùng một bản ghi giao lại chỉ ghi đè chứ không nhân đôi. - Khóa de-dup: lưu một mã sự kiện duy nhất (ví dụ
table + primary key + log position) và bỏ qua nếu đã xử lý. - Dùng vị trí log (LSN/offset) làm mốc: ghi đè theo thứ tự, không áp dụng sự kiện cũ hơn trạng thái hiện tại.
Kafka cũng hỗ trợ transactions và idempotent producer để đạt exactly-once trong nội bộ Kafka (read-process-write). Nhưng khi sink ra một hệ ngoài (như warehouse), exactly-once đầu-cuối thường được đảm bảo bằng idempotent sink + at-least-once delivery, đơn giản và bền hơn là cố ép two-phase commit khắp nơi.
Xử lý schema change
Schema nguồn sẽ thay đổi — thêm cột, đổi kiểu, đổi tên. Nếu không xử lý, pipeline sẽ vỡ.
Các nguyên tắc:
- Schema Registry: dùng một registry (ví dụ Confluent Schema Registry) lưu phiên bản schema cho từng topic, ràng buộc tương thích (compatibility):
- Backward compatible: consumer mới đọc được dữ liệu cũ.
- Forward compatible: consumer cũ đọc được dữ liệu mới.
- Thêm cột là an toàn nhất: cột mới nên có giá trị mặc định / nullable, consumer cũ bỏ qua được.
- Đổi kiểu / xóa cột là phá vỡ (breaking): cần phối hợp triển khai, thường qua versioning topic hoặc giai đoạn chuyển tiếp.
- Debezium phát kèm schema trong sự kiện và cập nhật khi DDL thay đổi, giúp consumer thích ứng.
Ở tầng warehouse, schema evolution (cho phép bảng đích tự thêm cột mới) giúp giảm ma sát — nhưng vẫn cần giám sát để tránh "trôi schema" âm thầm làm sai báo cáo.
Ví dụ: Đồng bộ Core Banking → Data Warehouse near-real-time
Hãy ghép tất cả lại trong bối cảnh ngân hàng. Mục tiêu: dashboard giao dịch cập nhật trong vài giây và phát hiện gian lận tức thời, mà không đụng vào hiệu năng của core banking.
Diễn giải luồng:
- Core Banking DB ghi mọi giao dịch vào WAL như bình thường.
- Debezium đọc WAL (log-based CDC), không query bảng → tải lên core gần như bằng 0.
- Mỗi thay đổi thành sự kiện đẩy vào Kafka topics (mỗi bảng một topic, ví dụ
transactions,accounts). - Stream Processor (Flink/Kafka Streams) làm giàu (ghép thông tin khách hàng), tính tổng hợp theo cửa sổ (tổng rút 5 phút), và định tuyến.
- Kết quả ghi vào Data Warehouse bằng UPSERT idempotent → dashboard luôn có số liệu mới nhất.
- Đồng thời, một nhánh chảy sang Fraud Engine để chấm điểm rủi ro tức thời và chặn giao dịch đáng ngờ.
Lưu ý vận hành quan trọng:
- Idempotent UPSERT theo
transaction_idđảm bảo nếu Debezium replay sau sự cố, warehouse không bị nhân đôi giao dịch — cực kỳ quan trọng với dữ liệu tiền. - Thứ tự trong một bảng/khóa được giữ nhờ Kafka phân vùng theo key (ví dụ phân vùng theo
account_id). - Bảo mật: dữ liệu nhạy cảm (số tài khoản, CMND) cần mã hóa/khử nhận dạng (masking) ngay trên luồng trước khi tới các consumer ít quyền.
Đánh đổi độ phức tạp
Realtime không miễn phí. Trước khi chọn nó, hãy cân nhắc:
- Vận hành: phải chạy và giám sát Kafka, Connect, stream processor, schema registry — nhiều thành phần stateful, nhiều thứ có thể hỏng lúc 2 giờ sáng.
- Gỡ lỗi khó hơn: lỗi trong dòng dữ liệu liên tục khó tái hiện hơn một job batch chạy lại được.
- Chi phí: hạ tầng streaming chạy 24/7 tốn hơn job batch chạy một lần mỗi đêm.
- Tính đúng đắn: xử lý đúng event time, dữ liệu đến muộn (late data), và exactly-once đòi hỏi kỹ năng cao.
Quy tắc thực dụng: chỉ đưa realtime vào nơi độ trễ thực sự tạo ra giá trị kinh doanh (gian lận, cảnh báo, vận hành). Phần còn lại — báo cáo tháng, đối soát — cứ để batch. Một kiến trúc lành mạnh thường lai (hybrid): streaming cho cái gấp, batch cho cái nặng và chính xác. Đừng "realtime hóa" mọi thứ chỉ vì nghe hấp dẫn.
Tóm tắt
- Dữ liệu thời gian thực giảm độ trễ từ hàng giờ (batch EOD) xuống vài giây, mở khóa phát hiện gian lận và dashboard sống.
- CDC bắt từng thay đổi ở nguồn; log-based (đọc WAL/binlog qua Debezium + Kafka Connect) là cách ít tải nhất, bắt được cả DELETE.
- Streaming ETL biến đổi/làm giàu/tổng hợp dữ liệu trên luồng bằng Flink, Kafka Streams hoặc Spark Structured Streaming.
- Lambda dùng hai lớp batch + speed (chính xác nhưng trùng code); Kappa chỉ một đường streaming và replay log (gọn nhưng cần log lưu lâu).
- Exactly-once đầu-cuối thực tế đạt được bằng at-least-once + idempotent UPSERT, không phải ép two-phase commit khắp nơi.
- Quản lý schema change bằng schema registry và quy tắc tương thích; chỉ realtime hóa nơi độ trễ tạo giá trị thật.
Tự kiểm tra
- Vì sao log-based CDC tốt hơn polling
updated_atkhi đồng bộ một bảng giao dịch tải cao? Nó bắt được loại thay đổi nào mà polling bỏ sót? - Một sự kiện CDC của Debezium chứa những trường nào, và
before/afterdùng để làm gì? - So sánh Lambda và Kappa: điểm yếu lớn nhất của mỗi kiến trúc là gì, và khi nào bạn chọn Kappa?
- Tại sao trong thực tế ta thường chấp nhận at-least-once và dựa vào idempotency thay vì ép exactly-once tuyệt đối? Cho ví dụ một UPSERT idempotent với giao dịch ngân hàng.
- Một cột mới được thêm vào bảng nguồn. Vì sao đây thường là thay đổi "an toàn", còn xóa cột thì không?
- Trong ví dụ Core Banking → Warehouse, điều gì giữ cho hiệu năng của core không bị ảnh hưởng, và điều gì ngăn giao dịch bị nhân đôi khi replay?
Đọc tiếp
- Quay lại tổng quan toàn cảnh ở Data Engineering 1 để định vị CDC/streaming trong bức tranh lớn.
- Tiếp theo, dữ liệu realtime này phục vụ ai? Hãy xem cách lớp BI & trực quan hóa biến luồng giao dịch thành dashboard, và cách các mô hình AI/ML (chấm điểm gian lận, rủi ro tín dụng) tiêu thụ feature từ chính pipeline streaming mà ta vừa xây.
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.