# CDC với Debezium: lấy thay đổi từ database của khách mà không quét lại cả bảng

> Đọc transaction log thay vì truy vấn bảng giúp bạn bắt được từng insert, update, delete, nhưng bạn phải hiểu snapshot, watermark và cái replication slot có thể lặng lẽ làm đầy ổ đĩa production của khách.

Bản gốc: https://fdetimes.net/vi/bach-khoa/change-data-capture-debezium-dong-bo-du-lieu-khach/

Khách hàng muốn agent của bạn trả lời theo trạng thái đơn hàng mới nhất. Cách nhanh nhất ai cũng nghĩ tới là một cron job chạy `SELECT * FROM orders` mỗi đêm rồi so với bản cũ.

Tuần đầu chạy ổn. Rồi bảng phình ra, DBA của khách hỏi vì sao 2 giờ sáng nào database cũng nóng lên, còn bạn phát hiện mình chưa từng thấy được dòng nào bị xoá.

Đây là tình huống quen thuộc với FDE: bạn cần dữ liệu sống từ một hệ thống mình không sở hữu, không được sửa code ứng dụng của khách, và không được làm chậm database production. Change data capture (CDC) giải đúng bài toán này, và Debezium là một công cụ mã nguồn mở được làm ra cho việc đó.

Hiểu CDC đến nơi đến chốn giúp bạn nói chuyện với DBA của khách như một đồng nghiệp, thay vì thành một rủi ro họ phải canh chừng. Các phần dưới giải thích cách Debezium hoạt động, kèm một ví dụ cấu hình cụ thể và những cái bẫy vận hành cần chặn trước khi chúng thành sự cố.

## Đừng hỏi bảng, hãy đọc nhật ký

Debezium tự mô tả là một nền tảng phân tán mã nguồn mở cho change data capture. Nó cho phép ứng dụng phản ứng với mọi insert, update, delete mà không phải đụng vào ứng dụng đang ghi dữ liệu. Với khách hàng, điểm đó đáng giá nhất: không ai phải sửa hệ thống đang chạy.

Mẹo nằm ở chỗ Debezium không truy vấn bảng. Connector cho PostgreSQL đọc các thay đổi đã commit từ transaction log qua logical decoding, dùng chính giao thức streaming replication của Postgres. Với MySQL, connector đọc binlog và phát ra sự kiện ở mức từng dòng. Một dòng bị xoá cũng thành một sự kiện, thứ mà cron job so sánh bảng không bao giờ thấy.

Có một chi tiết làm buổi nói chuyện với DBA dễ hơn hẳn. Plug-in `pgoutput` có sẵn trong PostgreSQL 10 trở lên, nên bạn không phải xin cài thêm thư viện nào lên server của khách. Ở nhiều doanh nghiệp, mỗi gói cài thêm trên production là một vòng phê duyệt, và tránh được nó có thể tiết kiệm cho bạn cả tuần.

## Nếu đã đọc log, sao vẫn cần snapshot?

Câu hỏi này gần như chắc chắn sẽ xuất hiện. Câu trả lời là log không phải lịch sử đầy đủ. Tài liệu Debezium nói rõ phần lớn server Postgres không giữ toàn bộ lịch sử database trong WAL, vì WAL bị dọn sau một thời gian. MySQL cũng thường được cấu hình để xoá binlog sau một khoảng nhất định.

Bài báo DBLog của Netflix nêu đúng vấn đề này: transaction log thường không chứa toàn bộ lịch sử thay đổi, nên chỉ bắt log thì không đồng bộ được cả database. Vì thế lần đầu kết nối, connector Postgres chụp một snapshot nhất quán của các schema, rồi mới chuyển sang đọc log từ điểm đó trở đi.

Mô hình snapshot cũ có hai nhược điểm khi gặp bảng lớn. Streaming bị chặn tới khi snapshot xong, và nếu snapshot đứt giữa chừng thì không chạy tiếp được. Incremental snapshot của Debezium sửa chuyện này: bảng được đọc theo từng chunk trong khi streaming vẫn chạy, và bạn kích hoạt nó bằng cách ghi tín hiệu vào một bảng signal riêng.

Để đối chiếu dữ liệu đọc theo chunk với các thay đổi đang diễn ra, Debezium mượn ý tưởng watermark của DBLog: chèn mốc thấp và mốc cao vào transaction log quanh mỗi lần đọc chunk.

Theo bài báo DBLog, cách này không khoá bảng nên tác động lên database nguồn rất nhỏ. Đó là câu bạn nên nói với DBA khi họ hỏi snapshot có làm treo hệ thống không.

## Ví dụ: bảng orders của một công ty giao hàng

Thử hình dung khách là một công ty giao hàng chạy Postgres 15, với bảng `public.orders` mà agent của bạn cần theo dõi. Bạn xin DBA một user có quyền replication và quyền đọc các bảng cần theo dõi, rồi tạo bảng signal.

Bảng signal là kênh để bạn ra lệnh cho Debezium: muốn chụp lại một bảng, bạn chỉ cần một câu INSERT vào đó. Vì thế nên thống nhất với DBA ngay từ đầu ai được phép ghi tín hiệu vào bảng này, thay vì đi xin quyền vào đúng hôm cần backfill. Cấu hình connector chỉ cần vài dòng:

```json
{
  "name": "orders-cdc",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "db.customer.internal",
    "database.port": "5432",
    "database.user": "cdc_user",
    "database.dbname": "logistics",
    "topic.prefix": "customer_a",
    "plugin.name": "pgoutput",
    "table.include.list": "public.orders,public.debezium_signal",
    "signal.data.collection": "public.debezium_signal"
  }
}
```

Khi connector khởi động, nó snapshot `orders` rồi chuyển sang đọc WAL. Một đơn đổi trạng thái từ "đang giao" sang "đã giao" thành một sự kiện update. Một đơn bị huỷ và xoá thành một sự kiện delete. Hệ thống của bạn chỉ nhận đúng những dòng đã đổi, không phải cả bảng.

Ba tháng sau, khách thêm cột và bạn cần chụp lại `orders` để backfill dữ liệu cũ. Thay vì dừng pipeline, bạn ghi một tín hiệu:

```sql
INSERT INTO public.debezium_signal (id, type, data)
VALUES ('resnap-orders-1', 'execute-snapshot',
        '{"data-collections": ["public.orders"]}');
```

Debezium đọc bảng theo chunk, streaming vẫn chạy song song, và watermark lo phần đối chiếu. Agent của khách không mất một phút dữ liệu mới nào.

## Cái bẫy nằm ở replication slot

Gunnar Morling, tác giả một bài viết chuyên về rủi ro này, mô tả nó rất rõ. Khi connector dừng, slot vẫn còn đó nhưng không hoạt động, và Postgres giữ lại mọi đoạn WAL từ vị trí slot đứng yên. Đĩa đầy dần cho tới khi database của khách hết chỗ.

Bạn có thể đã tắt connector vì kết thúc thử nghiệm, nhưng slot thì vẫn nằm trên server của khách. Morling khuyên đặt cảnh báo khi một slot giữ quá 100 MB WAL, đồng thời theo dõi dung lượng đĩa trống. Truy vấn kiểm tra rất ngắn:

```sql
SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retained
FROM pg_replication_slots;
```

MySQL có rủi ro theo chiều ngược lại. Nếu connector dừng quá lâu và binlog ở vị trí của nó đã bị xoá, connector không tiếp tục được và phải snapshot lại.

Connector MySQL cũng ghi lại mọi câu lệnh DDL kèm vị trí binlog để dựng lại cấu trúc bảng ở bất kỳ thời điểm nào, nên lịch sử schema đó phải được lưu bền vững chứ không phải để tạm.

**Điểm mấu chốt:** Mỗi replication slot bạn tạo trên database của khách là một khoản nợ, phải có người canh.

## Làm từ đầu theo thứ tự nào?

Bắt đầu bằng customer discovery với DBA: phiên bản Postgres hay MySQL, WAL hoặc binlog được giữ bao lâu, ai có quyền tạo user replication, ai trực khi đĩa đầy. Câu trả lời cuối cùng quyết định cảnh báo của bạn gửi tới đâu.

Tiếp theo, thống nhất danh sách bảng cụ thể thay vì lấy cả database, tạo bảng signal và chốt ai được ghi tín hiệu vào đó, rồi chạy thử trên staging với lượng dữ liệu sát với thật.

Chỉ khi cảnh báo slot đã chạy và đã được kiểm tra bằng cách cố tình dừng connector, bạn mới bật lên production. Cuối cùng, viết runbook bàn giao gồm cách xoá slot khi kết thúc dự án.

## Những lỗi nào hay gặp nhất?

Lỗi phổ biến nhất là coi slot như tài nguyên của riêng mình rồi quên nó khi dọn môi trường thử nghiệm. Cũng hay gặp không kém là giả định log có đủ lịch sử, bỏ qua snapshot, rồi thắc mắc vì sao thiếu những đơn hàng cũ. Không ít người còn dùng snapshot kiểu cũ trên bảng lớn và để streaming bị chặn cho tới khi snapshot chạy xong.

Còn một lỗi thuộc về giao tiếp: không nói trước với DBA rằng connector dừng nghĩa là WAL tích lại. Khi họ tự phát hiện điều đó lúc 3 giờ sáng, niềm tin vào cả dự án sẽ khó lấy lại.

## Kỹ năng này hiện ra trong CV thế nào?

Khi đọc JD của các vị trí FDE hay data engineer, hãy để ý các từ khoá như CDC, Debezium, Kafka Connect, logical replication. Trong CV, đừng chỉ ghi "dùng Debezium". Hãy ghi rằng bạn đã thiết kế pipeline CDC với incremental snapshot và cảnh báo WAL theo slot, vì đó là dấu hiệu bạn từng vận hành nó thật.

Một pipeline CDC tốt là thứ khách gần như quên mất là đang chạy. Việc của bạn là bảo đảm thứ duy nhất họ nhớ tới là dữ liệu luôn mới, chứ không phải cái ổ đĩa đầy.

**Thử ngay tuần này:**

- Dựng Postgres local bằng Docker, bật logical replication, chạy connector Debezium với pgoutput trên một bảng orders mẫu, rồi xem sự kiện cho insert, update và delete.
- Dừng connector một giờ trong lúc vẫn ghi dữ liệu, chạy truy vấn pg_replication_slots để xem lượng WAL bị giữ lại tăng ra sao, rồi viết cảnh báo ở ngưỡng 100 MB.
- Tạo bảng debezium_signal, gửi tín hiệu execute-snapshot cho một bảng và xác nhận streaming không bị dừng trong lúc snapshot chạy.

## Nguồn

- [Debezium](https://debezium.io/)

- [Debezium connector for PostgreSQL](https://debezium.io/documentation/reference/stable/connectors/postgresql.html)

- [Debezium connector for MySQL](https://debezium.io/documentation/reference/stable/connectors/mysql.html)

- [Incremental Snapshots in Debezium](https://debezium.io/blog/2021/10/07/incremental-snapshots)

- [DBLog: A Watermark Based Change-Data-Capture Framework](https://ar5iv.labs.arxiv.org/html/2010.12597)

- [The Insatiable Postgres Replication Slot](https://morling.dev/blog/insatiable-postgres-replication-slot)
