Mở đầu — vì sao bài này quan trọng
Nếu bạn đã đi qua Bài 20 về Event-Driven Architecture và Message Queue, hẳn bạn đã quen với ý tưởng "hệ thống nói chuyện với nhau qua thông điệp" thay vì gọi trực tiếp. Bài 21 này đào sâu vào một cái tên mà bạn sẽ nghe đi nghe lại trong mọi cuộc họp kiến trúc của các công ty công nghệ tại Việt Nam hiện nay: Apache Kafka.
Vì sao một Technical BA cần hiểu Kafka? Bởi vì Kafka không còn là chuyện riêng của đội backend. Khi MoMo, VNPay, Tiki hay một ngân hàng số như Cake hay Timo thiết kế luồng "đơn hàng hoàn tất gửi sang đội tích điểm, đội gửi email, đội phân tích dữ liệu", thì cách họ định nghĩa thông điệp gì chạy qua đâu, theo thứ tự nào, ai được đọc lại từ điểm nào chính là một bài toán nghiệp vụ. Và người dịch nhu cầu nghiệp vụ đó thành spec kỹ thuật chính là bạn.
Mục tiêu của bài này không phải biến bạn thành Kafka engineer. Mục tiêu là để khi ngồi họp, bạn không gật gù cho qua khi nghe "cái này mình publish vào topic, partition theo customer_id". Bạn phải hiểu đủ sâu để đặt câu hỏi đúng, viết yêu cầu đúng, và lường trước được rủi ro về thứ tự, trùng lặp và độ trễ. Đó là ranh giới giữa một BA "ghi chép lại" và một Technical BA "thiết kế cùng".
Khái niệm cốt lõi
Hãy hình dung Kafka như một cuốn sổ nhật ký (log) khổng lồ, chỉ ghi thêm, mà nhiều bên cùng ghi vào và nhiều bên cùng đọc ra. Khác với hàng đợi truyền thống (đọc xong là thông điệp biến mất), Kafka giữ lại thông điệp trong một khoảng thời gian cấu hình được (ví dụ 7 ngày), nên nhiều bên có thể đọc cùng một dòng sự kiện một cách độc lập.
Topic — kênh logic phân loại sự kiện
Topic là một kênh logic để gom các sự kiện cùng loại. Ví dụ orders.completed, payments.failed, users.registered. Quy ước đặt tên rõ ràng theo domain.event giúp cả tổ chức hiểu ngay topic chứa gì. Một BA giỏi sẽ là người chuẩn hóa quy ước đặt tên này, vì sau 2 năm hệ thống có thể có hàng trăm topic và việc đặt tên lộn xộn sẽ thành cơn ác mộng.
Partition — chìa khóa của song song hóa và thứ tự
Đây là khái niệm quan trọng nhất bạn cần nắm. Mỗi topic được chia thành nhiều partition. Hãy nghĩ partition như nhiều làn đường trên một xa lộ: cùng đi về một đích (topic) nhưng chạy song song để tăng thông lượng.
Hai nguyên tắc vàng về partition mà BA phải khắc cốt ghi tâm:
- Thứ tự chỉ được đảm bảo trong PHẠM VI một partition, không phải toàn topic. Nghĩa là nếu bạn ném sự kiện ngẫu nhiên vào các partition, hệ thống không hứa hẹn sự kiện A xảy ra trước sẽ được xử lý trước sự kiện B.
- Muốn các sự kiện liên quan giữ đúng thứ tự, chúng phải vào CÙNG một partition. Cách làm: chọn một partition key hợp lý. Ví dụ dùng
customer_idlàm key thì mọi sự kiện của cùng một khách hàng luôn rơi vào cùng partition, nên thứ tự "đặt hàng → thanh toán → hủy" của khách đó được bảo toàn. Còn thứ tự giữa hai khách hàng khác nhau thì không quan trọng.
Producer và Consumer
Producer là bên ghi sự kiện vào topic (ví dụ service đặt hàng). Consumer là bên đọc và xử lý sự kiện (ví dụ service tích điểm, service gửi email). Một sự kiện có thể có nhiều consumer độc lập cùng đọc — đây là sức mạnh "một lần phát, nhiều nơi nghe" của Kafka.
Consumer Group — chia tải xử lý
Khi một consumer xử lý không xuể, ta cho nhiều instance cùng nhóm lại thành một consumer group. Kafka tự chia partition cho các instance trong nhóm. Quy tắc cần nhớ: số consumer hoạt động đồng thời tối đa trong một group bằng số partition. Nếu topic có 6 partition, bạn chạy 10 instance thì 4 instance ngồi chơi. Vì vậy số partition là quyết định về trần năng lực mở rộng, và đây là thứ BA cần đưa vào phần Non-Functional Requirements khi ước lượng tải tương lai.
Offset — vị trí đọc
Mỗi sự kiện trong partition có một số thứ tự gọi là offset. Consumer ghi nhớ mình đã đọc tới offset nào (gọi là commit offset). Nhờ đó nếu consumer chết và khởi động lại, nó đọc tiếp từ chỗ dang dở chứ không mất việc. Khả năng "tua lại" (replay) từ một offset cũ cũng là lý do Kafka được dùng để xây lại dữ liệu khi cần.
Retention — giữ dữ liệu bao lâu
Kafka cho cấu hình giữ sự kiện theo thời gian (ví dụ 7 ngày) hoặc theo dung lượng. Đây là một điều khoản nghiệp vụ thật sự: giữ 7 ngày hay 30 ngày ảnh hưởng tới chi phí lưu trữ và khả năng audit/khôi phục. BA nên hỏi rõ và viết vào spec.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT giữ sai thứ tự tồn kho
Một sàn thương mại điện tử giả định tên ShopNhanh dùng topic inventory.changes để đồng bộ tồn kho từ kho hàng sang website. Đội kỹ thuật ban đầu để Kafka chia sự kiện ngẫu nhiên qua 12 partition cho "chạy nhanh". Kết quả: với một sản phẩm hot, sự kiện "nhập thêm 100 cái" và "bán đi 1 cái" rơi vào hai partition khác nhau, được xử lý lệch nhau. Có lúc website hiển thị tồn kho âm, có lúc hiện hết hàng dù kho còn.
Diễn giải: vấn đề không phải Kafka chậm hay lỗi, mà là thiếu đảm bảo thứ tự do partition key sai. Khi BA ngồi rà lại cùng kiến trúc sư, họ đổi partition key thành product_id. Từ đó mọi thay đổi tồn kho của cùng một sản phẩm luôn vào cùng partition, xử lý đúng tuần tự. Tồn kho hiển thị chính xác trở lại.
Bài học: BA phải đặt câu hỏi "sự kiện nào cần đúng thứ tự?" ngay từ giai đoạn phân tích, và ghi rõ partition key vào spec. Một dòng yêu cầu "đảm bảo thứ tự thay đổi tồn kho theo từng SKU" đáng giá hơn cả tuần debug.
Tình huống 2 — Ví điện tử và bài toán xử lý trùng
Một ví điện tử kiểu MoMo dùng topic wallet.cashback để xử lý hoàn tiền. Consumer nhận sự kiện "hoàn 10.000đ cho giao dịch X". Một hôm consumer xử lý xong nhưng chưa kịp commit offset thì bị restart do deploy. Khi khởi động lại, nó đọc lại đúng sự kiện đó và hoàn tiền lần hai. Một số khách hàng nhận cashback gấp đôi.
Diễn giải: Kafka mặc định đảm bảo at-least-once — mỗi sự kiện được giao ít nhất một lần, nên việc đọc lại là chuyện bình thường, không phải bug của Kafka. Giải pháp nghiệp vụ là làm cho việc xử lý trở nên idempotent (chạy nhiều lần kết quả vẫn như một lần) — chủ đề bạn sẽ học kỹ ở Bài 29. Ở đây đội thêm một bảng ghi nhận transaction_id đã hoàn, xử lý lần hai thì bỏ qua.
Bài học: Khi viết spec cho luồng Kafka liên quan tới tiền, BA phải mặc định rằng sự kiện có thể được giao trùng, và yêu cầu rõ cơ chế chống trùng. Đừng bao giờ cho rằng "mỗi sự kiện chỉ chạy đúng một lần".
Tình huống 3 — Ngân hàng số tách luồng phân tích
Một ngân hàng số tại Đông Nam Á muốn xây dashboard phân tích hành vi giao dịch theo thời gian thực, nhưng đội core banking lo việc query phân tích sẽ làm chậm hệ thống giao dịch chính. Giải pháp: core banking publish mọi giao dịch vào topic transactions.events. Đội phân tích lập một consumer group riêng đọc cùng topic đó để nạp vào kho dữ liệu, hoàn toàn độc lập với hệ thống lõi.
Diễn giải: nhờ tính chất "một lần phát, nhiều consumer group độc lập", đội phân tích đọc dữ liệu mà không hề chạm tới database giao dịch. Nếu consumer phân tích chậm hay sập, hệ thống giao dịch không bị ảnh hưởng — chỉ là dashboard cập nhật trễ hơn.
Bài học: Kafka không chỉ là "đường ống truyền tin". Nó là cách tách rời (decouple) các đội và các hệ thống, cho phép thêm consumer mới mà không sửa producer. BA nên nhận diện cơ hội này khi có nhu cầu "lấy dữ liệu để làm việc khác mà không đụng hệ thống gốc".
Hướng dẫn từng bước
Khi bạn được giao phân tích một luồng nghiệp vụ sẽ dùng Kafka, hãy đi theo trình tự sau:
- Xác định sự kiện nghiệp vụ. Liệt kê những "việc đã xảy ra" cần thông báo: đơn hàng hoàn tất, thanh toán thất bại, người dùng đăng ký. Đặt tên topic theo quy ước
domain.event.
- Mô tả nội dung sự kiện (event schema). Mỗi sự kiện mang theo dữ liệu gì? Ví dụ
order.completedgồmorder_id,customer_id,amount,timestamp. Đây là phần BA viết rõ ràng nhất, tương tự như đặc tả một payload API.
- Quyết định yêu cầu thứ tự. Tự hỏi: sự kiện nào phải xử lý đúng tuần tự với sự kiện nào? Từ câu trả lời, chọn partition key. Ghi rõ trong spec, ví dụ: "Partition theo
customer_idđể bảo toàn thứ tự sự kiện của từng khách hàng".
- Liệt kê các consumer. Ai sẽ nghe topic này và làm gì? Ghi rõ từng consumer group và hành động của nó (gửi email, tích điểm, nạp data warehouse).
- Xác định ngữ nghĩa giao nhận và chống trùng. Mặc định at-least-once. Với luồng nhạy cảm (tiền, kho), yêu cầu rõ cơ chế idempotent kèm khóa định danh chống trùng (ví dụ
transaction_id).
- Ước lượng tải và retention. Bao nhiêu sự kiện mỗi giây ở giờ cao điểm? Cần giữ dữ liệu bao lâu để audit hoặc replay? Đưa các con số này vào phần Non-Functional Requirements.
- Mô tả xử lý lỗi. Khi consumer xử lý thất bại thì sao? Thử lại bao nhiêu lần? Sự kiện hỏng đẩy vào đâu (thường là Dead Letter Topic — nơi chứa sự kiện không xử lý được để con người xem lại)?
Lỗi thường gặp & mẹo
Nhầm Kafka là hàng đợi đọc xong là mất. Kafka giữ lại sự kiện theo retention, nhiều consumer group đọc độc lập. Nếu bạn thiết kế theo tư duy "đọc xong xóa", bạn sẽ viết spec sai.
Cho rằng Kafka đảm bảo thứ tự toàn topic. Sai. Thứ tự chỉ trong một partition. Đây là lỗi tư duy phổ biến nhất và là nguồn gốc của vô số bug dữ liệu.
Quên rủi ro xử lý trùng. Vì là at-least-once, sự kiện có thể được giao lại. Luồng nào liên quan tiền hoặc thay đổi trạng thái quan trọng đều phải idempotent.
Đặt quá ít partition rồi tắc nghẽn. Số partition là trần mở rộng của consumer group. Tăng partition về sau lại có thể phá vỡ thứ tự theo key cũ. Vì vậy ước lượng tải tương lai ngay từ đầu rất quan trọng — đây là lúc BA tỏa sáng.
Mẹo đặt câu hỏi trong họp: Ba câu hỏi vàng của Technical BA về một luồng Kafka là — "Partition theo key gì?", "Có cần idempotent không?", và "Retention bao lâu?". Hỏi được ba câu này, bạn đã đứng ngang hàng với đội kỹ thuật.
Mẹo về schema: Khuyến khích đội dùng schema registry và versioning cho event (liên quan Bài 13 về backward compatibility). Một event đổi cấu trúc đột ngột có thể làm sập mọi consumer cũ.
Bài tập thực hành
Hãy lấy bối cảnh: một ứng dụng giao đồ ăn kiểu ShopeeFood muốn dùng Kafka cho luồng "tài xế cập nhật trạng thái đơn" với các trạng thái: nhận đơn → đang lấy món → đang giao → đã giao.
- Đặt tên topic theo quy ước
domain.eventvà viết schema cho một sự kiện cập nhật trạng thái (liệt kê các trường dữ liệu cần thiết).
- Chọn partition key sao cho thứ tự các trạng thái của cùng một đơn hàng luôn được bảo toàn. Giải thích vì sao bạn chọn key đó và chuyện gì xảy ra nếu chọn sai.
- Liệt kê ít nhất hai consumer group sẽ nghe topic này và mô tả hành động của mỗi group.
- Hệ thống có thể giao trùng một sự kiện "đã giao". Mô tả một cơ chế đơn giản để consumer cập nhật trạng thái không bị lỗi khi nhận trùng (gợi ý: idempotent theo
order_id+ trạng thái).
- Đề xuất con số retention và một câu giải thích nghiệp vụ cho lựa chọn đó (audit khiếu nại của khách trong bao lâu?).
Tóm tắt
Kafka đối với Technical BA không phải là chuyện cài đặt server, mà là chuyện dịch nhu cầu nghiệp vụ thành các quyết định về sự kiện, thứ tự và độ tin cậy. Hãy ghi nhớ những điểm cốt lõi:
- Topic là kênh logic phân loại sự kiện; đặt tên theo
domain.event. - Partition cho phép song song hóa, nhưng thứ tự chỉ được đảm bảo trong một partition. Muốn giữ thứ tự, chọn đúng partition key.
- Consumer group chia tải; số partition là trần mở rộng.
- Kafka mặc định at-least-once — luôn lường trước xử lý trùng và yêu cầu idempotent cho luồng quan trọng.
- Offset và retention cho phép replay và audit; đây là các điều khoản nghiệp vụ cần ghi rõ.