0

Bài 12: Kafka Transactions — Mảnh ghép cuối cùng cho Exactly-Once Semantics (EOS)

Ở phần trước, chúng ta đã thấy Idempotent Producer giải quyết rất tốt bài toán chống trùng lặp. Tuy nhiên, nó có một giới hạn lớn: nó chỉ đảm bảo Exactly-once trong phạm vi 1 Producer gửi tới 1 Partition của 1 Topic.

Trong thực tế, các ứng dụng (như Kafka Streams, microservices) thường hoạt động theo chu trình Consume-Transform-Produce:

  1. Đọc dữ liệu từ Topic A (Consume).
  2. Xử lý logic/Tính toán (Transform).
  3. Ghi kết quả sang Topic B (Produce).
  4. Xác nhận đã đọc xong ở Topic A bằng cách commit offset.

Bài toán: Điều gì xảy ra nếu service của bạn bị sập ngay sau bước 3, tức là đã ghi xong kết quả sang Topic B nhưng chưa kịp commit offset ở Topic A?

Khi service khởi động lại, nó sẽ đọc lại tin nhắn cũ từ Topic A và tiến hành ghi thêm một bản sao nữa sang Topic B. Dữ liệu bị nhân đôi! Idempotent Producer không thể cứu bạn ở đây vì khi restart, chu kỳ Produce là một session hoàn toàn mới.

Đây là lúc Kafka Transactions bước vào để hoàn thiện bức tranh.

image.png

1. Kafka Transactions là gì?

Được giới thiệu từ bản 0.11, Kafka Transactions API cho phép bạn gom nhóm nhiều thao tác — bao gồm việc ghi dữ liệu vào nhiều Topic/Partition khác nhau và cả việc commit offset — thành một khối Atomic (nguyên tử) duy nhất.

Triết lý của nó rất đơn giản: Hoặc tất cả các thao tác cùng thành công, hoặc không có gì thay đổi.


2. Các thành phần cốt lõi của hệ thống Giao dịch

Để quản lý luồng dữ liệu phức tạp này, Kafka sử dụng các cơ chế nội bộ sau:

  • transactional.id: Khác với Producer ID (bị reset mỗi khi ứng dụng khởi động lại), transactional.id là một chuỗi định danh cố định do bạn tự cấu hình (ví dụ: order-processing-service-1). Nhờ ID này, Kafka nhận diện được Producer ngay cả khi nó vừa bị crash và restart, từ đó phục hồi hoặc hủy bỏ các giao dịch đang dang dở.
  • Transaction Coordinator: Một module chạy ngầm bên trong Broker, đóng vai trò như một "nhà cái" quản lý trạng thái của các giao dịch (Ongoing, PrepareCommit, CompleteCommit...).
  • Transaction Log: Một topic nội bộ siêu nhẹ (__transaction_state) dùng để Transaction Coordinator ghi lại nhật ký giao dịch, giúp hệ thống không bị quên trạng thái ngay cả khi Broker sập.

3. Vòng đời của một luồng Exactly-once

Để đảm bảo không có dữ liệu nào bị lọt hoặc lặp trong chu trình Consume-Transform-Produce, Kafka thực hiện các bước sau:

  1. Khởi tạo: Producer đăng ký transactional.id với Transaction Coordinator để lấy quyền điều hành.
  2. Bắt đầu (Begin): Ứng dụng đánh dấu bắt đầu một transaction.
  3. Xử lý: Ứng dụng đọc dữ liệu từ Topic A và thực hiện tính toán.
  4. Ghi kết quả (Produce): Producer gửi kết quả sang Topic B. Các tin nhắn này được Broker lưu vào disk nhưng bị gắn cờ là "uncommitted" (chưa hoàn tất).
  5. Gửi Offset: Thay vì Consumer tự gọi lệnh commit offset như thông thường, Producer sẽ gửi thông tin offset của Topic A trực tiếp cho Transaction Coordinator. Điều này cực kỳ quan trọng vì nó gộp bước "đánh dấu đã đọc" vào chung một giao dịch với bước "ghi kết quả".
  6. Xác nhận (Commit/Abort):
    • Thành công: Ứng dụng gọi lệnh Commit. Coordinator ghi một marker Commit vào Topic B và chính thức cập nhật offset cho Topic A.
    • Lỗi (Crash/Exception): Ứng dụng (hoặc Coordinator khi bị timeout) gọi lệnh Abort. Coordinator ghi marker Abort. Toàn bộ dữ liệu vừa ghi sẽ bị đánh dấu là đồ bỏ đi.

4. Chốt chặn cuối cùng: Consumer Isolation Level

Dù Producer đã đóng gói dữ liệu vào Transaction rất cẩn thận, nhưng nếu Consumer ở đầu ra (đọc từ Topic B) cứ thấy có tin nhắn mới là lấy ra dùng, thì mọi nỗ lực đều vô nghĩa. Nó sẽ đọc nhầm cả những tin nhắn thuộc về các giao dịch đang dang dở hoặc đã bị hủy.

Để hoàn tất Exactly-once, Consumer đầu ra phải được cấu hình tham số:

isolation.level = read_committed (Mặc định của Kafka là read_uncommitted).

Với cấu hình này, Consumer sẽ tự động lọc và bỏ qua các tin nhắn thuộc về các giao dịch bị Abort, đồng thời chờ đợi các giao dịch đang mở. Nó chỉ trả về cho ứng dụng những tin nhắn đã có marker Commit.


🛠️ Tổng hợp cấu hình Exactly-once toàn tập

Nếu bạn đang xây dựng hệ thống tài chính hoặc ví điện tử, đây là combo cấu hình bắt buộc:

Thành phần Cấu hình Tác dụng
Producer enable.idempotence = true Bật cơ chế chống trùng lặp cơ bản.
Producer transactional.id = "your-app-id" Kích hoạt Transactions, đảm bảo tính nguyên tử xuyên suốt quá trình restart.
Producer acks = all Đảm bảo dữ liệu không bị mất ở mức phần cứng.
Consumer (Output) isolation.level = read_committed Chỉ đọc các dữ liệu đã được xác nhận thành công trọn vẹn.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí