0

Bài 4: Consumer Offsets & Cơ chế Commit — "Trí nhớ" của hệ thống phân tán

Bạn hãy nhớ lại Bài 2: Offset giống như số thứ tự của tin nhắn trong một Partition. Khi Consumer đọc dữ liệu nó sẽ tuần tự lấy tin nhắn từ Offset 0, 1, 2... Nhưng làm thế nào để Kafka biết Consumer đã đọc xong và không đẩy lại tin nhắn cũ?

1. Topic Đặc biệt: __consumer_offsets

Kafka không lưu trạng thái đã đọc trên bộ nhớ tạm (RAM) của Consumer. Thay vào đó, bản thân Kafka sử dụng chính sức mạnh của nó để giải quyết vấn đề này.

Kafka ngầm tạo ra một Topic nội bộ cực kỳ quan trọng tên là consumer_offsets. Nó hoạt động như một cuốn sổ nhật ký:

  • Khi một Consumer trong một Consumer Group đọc xong một số tin nhắn và xác nhận nó đã xử lý ổn thỏa, nó sẽ gửi một bản ghi vào Topic __consumer_offsets.
  • Bản ghi này nói rằng "Báo cáo Consummer Group A đã xử lý xong dữ liệu đến Offset số 100 tại Partition số 2 của Topic Orders"
  • Nếu server của Consummer bị sập (crash) và khởi động lại, đầu tiên nó làm là hỏi Kafka "Lần trước nhóm của tôi đọc đến đâu rồi?". Kafka sẽ tra sổ consumer_offsets và trả lời: "Đọc đến 100 rồi, hãy bắt đầu lấy tiếp từ 101".

Nhờ cơ chế này, hệ thống dù có bị gián đoạn vẫn có thể tự động khôi phục đúng vị trí mà không mất dữ liệu.

2. Cơ chế Commit: Khi nào thì được phép "Đánh dấu đã đọc"?

Hành động Consumer gửi báo cáo tiến độ lên Kafka gọi là Commit Offset. Đây là bước quyết định sự an toàn của dữ liệu. Nếu bạn thiết kế sai thời điểm commit, hệ thống sẽ gặp thảm họa. Có 3 cách tiếp cận:

Cách 1: At-most-once (Chỉ một lần, mất cũng chịu)

  • Cách hoạt động: Consumer vừa lấy tin nhắn từ Kafka về (chưa thèm xử lý logic, chưa lưu vào database) là lập tức gửi thông báo "Đã đọc" (Commit) cho Kafka.
  • Rủi ro: Nếu Consumer đang ghi dữ liệu vào database mà bị sập, tin nhắn đó coi như "bay màu" mãi mãi vì Kafka tưởng Consumer đã xử lý xong.
  • Sử dụng khi: Chấp nhận mất dữ liệu, ưu tiên tốc độ (ví dụ: Log tracking, telemetry).

Cách 2: At-least-once (Ít nhất một lần, thà trùng còn hơn sót)

  • Cách hoạt động: Consumer lấy tin nhắn về -> Xử lý logic -> Ghi vào DB thành công -> Mới gửi thông báo Commit cho Kafka. Đây là cấu hình mặc định và được dùng nhiều nhất.
  • Rủi ro: Giả sử Consumer xử lý xong, ghi DB thành công, nhưng chuẩn bị Commit thì bị rớt mạng hoặc sập nguồn. Lúc này Kafka chưa nhận được thông báo "Đã đọc". Khi Consumer khởi động lại, Kafka sẽ đẩy lại chính tin nhắn đó. Bạn sẽ bị xử lý đúp (Duplicate Processing).
  • Giải pháp bắt buộc: Hàm xử lý (Consumer logic) của bạn phải được thiết kế có tính Idempotent (Dù chạy 1 lần hay 100 lần thì kết quả cuối cùng trong DB vẫn giống nhau - ví dụ: Dùng INSERT IGNORE hoặc kiểm tra transaction_id trước khi xử lý).

Cách 3: Exactly-once (Chính xác một lần)

Đây là trạng thái hoàn hảo nhất nhưng cũng phức tạp và ảnh hưởng hiệu năng nhiều nhất. Kafka hỗ trợ Transactional API để đảm bảo tin nhắn được xử lý và commit trong một transaction duy nhất. Thường chỉ dùng trong các hệ thống Stream Processing khắt khe (như chuyển tiền).

Tóm lại cho Backend Developer: Bạn hãy luôn nhớ cấu hình Consumer của mình dùng chế độ At-least-once. Tuyệt đối không để tự động commit ngay khi vừa nhận tin nhắn. Hãy luôn thiết kế Database và Logic Code để chịu được việc tin nhắn bị đẩy lại nhiều lần (Idempotency).


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í