[System Design Version 1 - Bài 10] Message Brokers (RabbitMQ, Kafka): Tại sao các hệ thống lớn không thể sống thiếu hàng đợi?
Chào anh em. Khi mới bắt đầu chuyển từ nguyên khối (Monolith) sang Vi dịch vụ (Microservices), tôi đã từng phạm phải một sai lầm chết người: Thiết kế hệ thống theo kiểu "gọi điện thoại trói buộc". Chuyện là thế này. Khi người dùng bấm nút "Đặt hàng" trong một đợt Flash Sale mỹ phẩm, Order-Service của tôi sẽ gọi HTTP sang Payment-Service để trừ tiền, xong lại gọi sang Inventory-Service để trừ tồn kho, rồi gọi tiếp sang Email-Service để gửi mail xác nhận.
Mọi thứ chạy ngon ơ ở Local. Nhưng trên Production, mạng chập chờn, server của đối tác gửi mail phản hồi chậm mất 5 giây. Thế là request của người dùng bị treo cứng 5 giây. Vài ngàn người cùng bị treo như thế, CPU và RAM của Order-Service kịch trần vì phải giữ (hold) quá nhiều connection đang chờ (blocking). Cuối cùng, toàn bộ hệ thống sập cái rầm chỉ vì... cái dịch vụ gửi mail bị chậm.
Đó là lúc tôi nhận ra: Trong hệ thống phân tán, các Service không được phép chờ đợi nhau. Và vị cứu tinh xuất hiện mang tên: Message Broker (Hàng đợi tin nhắn).
1. Message Broker là gì và Giải pháp Giao tiếp Bất đồng bộ (Asynchronous)
Hãy tưởng tượng Message Broker giống như một Bưu Điện trung tâm.
Thay vì Order-Service phải gọi trực tiếp và mòn mỏi chờ Email-Service trả lời, nó chỉ việc viết một bức thư (Message) với nội dung: "Có một đơn hàng mới của user A", ném bức thư đó vào Bưu Điện (Broker), rồi lập tức báo "Thành công" cho người dùng trên màn hình. Mất chưa tới 10 mili-giây.
Sau đó, Email-Service cứ túc tắc ra Bưu Điện lấy thư về, xử lý, và gửi mail. Dù Email-Service có bị chết lâm sàng nửa ngày, thì thư vẫn nằm an toàn trong Bưu Điện. Khi nó sống lại, nó sẽ tiếp tục xử lý chỗ thư tồn đọng.
Hệ thống đã được Decoupling (Tách rời) hoàn toàn về mặt thời gian. Nút thắt cổ chai bị đập bỏ.
Hiện nay, hai "Bưu Điện" quyền lực nhất thế giới Backend là RabbitMQ và Apache Kafka. Mặc dù hay được đặt lên bàn cân, nhưng thực chất triết lý thiết kế của chúng hoàn toàn khác nhau.
2. RabbitMQ: Người Bưu Tá thông minh và tận tụy
RabbitMQ là một Message Broker truyền thống đích thực (theo chuẩn AMQP). Triết lý của nó là: Smart Broker, Dumb Consumer (Broker thông minh, Consumer ngốc nghếch).
- Cách hoạt động: RabbitMQ cực kỳ giỏi trong việc định tuyến (Routing). Bạn gửi một message vào Exchange (trung tâm phân loại), RabbitMQ sẽ dựa vào các Rules (Routing Key) phức tạp để ném message đó vào đúng các Queue (hàng đợi) tương ứng. Khi Consumer (Service nhận) đọc xong và báo ACK (xác nhận đã xử lý), RabbitMQ sẽ lập tức xóa message đó đi.
- Thế mạnh: Hoàn hảo cho các task queue (hàng đợi công việc). Ví dụ: Gửi email, xử lý hình ảnh, xuất báo cáo PDF cuối tháng. Nếu Consumer xử lý lỗi, RabbitMQ có cơ chế nhét lại vào hàng đợi (Retry) hoặc ném sang một khu vực riêng (Dead Letter Queue) để dev vào soi log.
3. Apache Kafka: Quái vật nuốt dữ liệu (Streaming Data)
Nếu RabbitMQ là Bưu Điện, thì Kafka giống như một cuốn Nhật Ký (Commit Log) hay một bảng thông báo công cộng khổng lồ không bao giờ bị xóa ngay. Triết lý của nó là: Dumb Broker, Smart Consumer (Broker ngốc nghếch, Consumer thông minh).
- Cách hoạt động: Kafka không quan tâm việc định tuyến phức tạp. Data ném vào Kafka sẽ được ghi tuần tự vào đĩa cứng (Disk) theo từng Topic (Chủ đề). Nó có thể ghi hàng triệu message mỗi giây mà không bị chậm đi. Đặc biệt, Kafka không xóa message khi Consumer đã đọc. Dữ liệu sẽ được giữ lại trong vài ngày (Retention Policy). Các Consumer tự nhớ xem mình đã đọc đến dòng thứ mấy (dựa vào Offset).
- Thế mạnh: Sinh ra để đối phó với Big Data, xử lý luồng dữ liệu cực lớn (High Throughput).
- Bài toán thực tế: Hãy nhìn vào hệ thống thu phí tự động (AFC) của tuyến Metro. Hàng triệu lượt quẹt thẻ tại cổng Gate gửi log đồng loạt về trung tâm. RabbitMQ sẽ chết ngộp. Nhưng Kafka thì nuốt trọn. Nó ghi nhận mọi log sự kiện quẹt thẻ vào Topic. Nhờ việc không xóa message, Service Đối soát (Clearing) có thể đọc để tính tiền, Service Báo cáo (Reporting) cũng có thể đọc luồng data đó để tổng hợp Origin-Destination (OD), hoặc nếu hệ thống báo cáo lỗi data, bạn có thể tua lại (rewind) offset để chạy lại từ đầu log của ngày hôm qua. Rất bá đạo!
4. Những cái bẫy "đau đớn" khi dùng Message Queue
Không có gì là hoàn hảo, xài Queue cũng tiềm ẩn những rủi ro vỡ mặt:
- Message Ordering (Thứ tự thông điệp): User cập nhật tên thành "A", sau đó lập tức cập nhật thành "B". Hai event ném vào queue. Nếu có nhiều Consumer cùng đọc, rất có thể event "B" được xử lý xong TRƯỚC event "A". Cuối cùng trong DB, tên user là "A". Dữ liệu sai bét!
- Exactly-once Delivery (Xử lý đúng 1 lần): Mạng chập chờn khiến Consumer xử lý xong nhưng không gửi được xác nhận (ACK) về Broker. Broker tưởng chưa xử lý, bèn gửi lại message đó lần nữa. Nếu code của bạn không có cơ chế Idempotent (Dù xử lý 100 lần thì kết quả vẫn như 1 lần), thì user có nguy cơ bị trừ tiền vé Metro đến 2 lần cho 1 lần quẹt thẻ.
Lời kết
Việc đưa Message Broker vào hệ thống đánh dấu sự trưởng thành của một kiến trúc. Đừng mang Kafka ra để làm những tác vụ gửi mail đơn giản, cũng đừng bắt RabbitMQ phải gồng gánh hàng triệu log sự kiện mỗi giây. Phân tích đúng bản chất nghiệp vụ để chọn đúng công cụ là tố chất của một System Designer.
Nhưng anh em có nhận ra không, khi chúng ta dùng Message Broker, cách các Service nói chuyện với nhau đã thay đổi hoàn toàn từ "Ra lệnh" (Command - Bắt buộc làm cái này) sang "Thông báo" (Event - Tôi báo cáo có việc này vừa xảy ra).
Sự thay đổi nhỏ này mở ra một trường phái thiết kế kiến trúc hoàn toàn mới, cực kỳ linh hoạt nhưng cũng vô cùng "hack não".
👉 Ở bài tiếp theo, chúng ta sẽ bước vào thế giới của: "Event-Driven Architecture: Thiết kế hệ thống phản ứng theo sự kiện."
Anh em trong project đang dùng RabbitMQ hay Kafka? Đã từng ai phải vã mồ hôi hột đi gỡ đống message bị kẹt trong Dead Letter Queue chưa? Chia sẻ dưới phần bình luận nhé!
All rights reserved