001. Message-Driven Programming với Message Broker
Bài mở đầu của series Kafka In Depth. Trước khi chạm vào Kafka, mình muốn dừng lại ở câu hỏi gốc rễ: tại sao chúng ta cần một thứ như message broker? Vì nếu không hiểu vấn đề nó giải quyết, mọi kiến thức về Kafka sau này chỉ là học vẹt.
Bắt đầu từ một request đăng ký tài khoản
Hãy tưởng tượng bạn viết một API đăng ký tài khoản. Nghe thì đơn giản, nhưng khi user bấm nút "Đăng ký", hệ thống phải làm kha khá việc, ví dụ:
- Lưu user vào database.
- Gửi email chào mừng.
- Tạo hồ sơ mặc định.
- Ghi log analytics.
- Đẩy một event sang hệ thống chống gian lận.
Cách trực tiếp và tự nhiên nhất là gọi lần lượt từng service:
Client ──► Auth Service ──► Email Service
──► Profile Service
──► Analytics Service
──► Fraud Service
Đây là mô hình đồng bộ (synchronous): Auth Service phải gọi từng service,
đứng chờ response của mỗi service xong mới đi tiếp, rồi mới trả kết quả về cho user.
Cách này chạy được, nhưng có nhiều rủi ro nếu như có một service bị down.
Vấn đề của giao tiếp đồng bộ - synchronous
Mô hình gọi trực tiếp, đồng bộ có ba điểm yếu chí mạng — và bạn sẽ chỉ thật sự "thấm" chúng khi hệ thống golive production.
1. Coupling
Auth Service phải biết địa chỉ, biết cách gọi của cả 4 service kia. Thêm
một service mới (ví dụ gửi SMS)? Bạn phải mở code Auth Service ra, sửa, deploy
lại. Một service đổi API? Auth Service lãnh đủ.
Một service, đáng lẽ chỉ nên lo việc xác thực, giờ phải "gánh" hiểu biết về cả nửa hệ thống.
2. Blocking
User bấm "Đăng ký" và... đợi. Vì Auth Service đang đứng chờ Email Service, mà Email Service thì đang chờ SMTP server ở đâu đó.
Việc gửi email lẽ ra không cần user phải đứng nhìn spinner quay. Nhưng vì mọi thứ đồng bộ, tốc độ phản hồi cho user bị kéo xuống bằng mắt xích chậm nhất.
3. Một mắt xích gãy, cả dây chuyền sập
Worst case: nếu Email Service down, thì sao?
Trong mô hình đồng bộ, lời gọi email lỗi → thường kéo theo cả request đăng ký thất bại. User không đăng ký được chỉ vì cái server gửi email đang bảo trì. Một việc phụ làm hỏng cả việc chính.
Đây chính là nút thắt mà mọi hệ thống lớn đều phải gỡ: làm sao để các service không phải đứng chờ nhau, và không kéo nhau cùng down?
Tư duy bất đồng bộ (Asynchronous)
Ok, vậy thì sollution là: thôi thì đừng chờ nữa.
Hãy nghĩ tới quầy gọi món ở KFC. Bạn gọi món, nhận một số thứ tự, rồi đi tìm chỗ ngồi — bạn không đứng chôn chân ở quầy nhìn người ta rán gà. Nhân viên thu ngân cũng rảnh tay phục vụ khách tiếp theo. Khi món xong, số của bạn được gọi.
Đó là bất đồng bộ: tách rời luồng request và luồng response. Người gửi yêu cầu không cần đứng chờ kết quả ngay lập tức.
Nhưng bất đồng bộ mới chỉ giải quyết được chuyện "chờ". Auth Service vẫn phải biết Email Service là ai, vẫn phải gọi thẳng tới nó. Cái coupling vẫn còn nguyên đó. Chúng ta cần đi thêm một bước nữa.
Message-Driven Programming
Ý tưởng đột phá nằm ở chỗ: nếu các service đừng call trực tiếp với nhau nữa thì sao?
Thay vì gọi thẳng, mỗi service chỉ cần gửi một tin nhắn (message) mô tả chuyện vừa xảy ra — ví dụ: "Có một user vừa đăng ký, đây là thông tin" — và ném nó cho một bên thứ ba đứng giữa. Bên thứ ba đó có nhiệm vụ chuyển tin nhắn tới đúng nơi cần nhận.
Đây là message-driven programming: hệ thống vận hành bằng cách trao đổi message, thay vì gọi hàm/gọi API trực tiếp lẫn nhau.
Hãy so sánh với việc bạn nhắn tin SMS. Bạn không cần biết người nhận đang ở đâu, điện thoại họ đang bật hay tắt, đang dùng mạng nào. Bạn chỉ cần soạn tin, bấm gửi, và tin cậy rằng nhà mạng sẽ lo phần còn lại. Nếu máy người nhận đang tắt, tin nhắn được giữ lại và giao khi họ mở máy.
Cái "nhà mạng" trong thế giới lập trình đó chính là message broker.
Message Broker là gì?
Message broker là một thành phần trung gian, đứng giữa các service, nhận message từ nơi gửi (producer) và chuyển tới nơi nhận (consumer). Nó đảm bảo message được giao thành công và chính xác.
Producer ──► [ Message Broker ] ──► Consumer
(gửi) (nhận · lưu · giao) (nhận)
Nói đơn giản, broker gánh 4 trách nhiệm chính:
- Giảm giao tiếp trực tiếp giữa các service — chúng chỉ cần "nói chuyện" với broker, không cần biết nhau.
- Lưu trữ message khi có sự cố — consumer đang chết? Broker giữ message lại, chờ consumer sống dậy rồi giao tiếp.
- Phân phối message tới nhiều server/nhiều consumer.
- Đơn giản hóa việc giao tiếp giữa nhiều service với nhau.
Hai kiểu phân phối message
Khi làm việc với broker, bạn sẽ gặp hai mô hình gửi tin cơ bản:
Point-to-Point (Queue) — một–một
Message được đưa vào một hàng đợi (queue), và chỉ một consumer lấy ra xử lý. Giống như bốc số xếp hàng ở ngân hàng: một số chỉ được gọi tới một quầy.
Hợp với: xử lý tác vụ, mỗi việc chỉ nên được làm đúng một lần (gửi 1 email, trừ tiền 1 lần...).
Publish-Subscribe (Topic) — một–nhiều
Message được phát (broadcast) tới một topic, và tất cả consumer đang lắng nghe topic đó đều nhận được một bản.
Hợp với: một sự kiện cần nhiều bên cùng xử lý. "User vừa đăng ký" → Email Service gửi mail, Analytics ghi nhận, Fraud Service kiểm tra — mỗi bên nhận cùng một message và tự làm phần việc của mình.

Vậy rốt cuộc, được gì khi dùng Message Broker?
Quay lại bài toán đăng ký ban đầu, giờ nó trông như thế này:
Client ──► Auth Service ──► [ Broker ] ──► Email Service
(lưu user, ──► Profile Service
bắn 1 message, ──► Analytics Service
trả về cho user ngay) ──► Fraud Service
Auth Service chỉ làm đúng việc của nó, bắn ra một message "user_registered", rồi trả kết quả cho user ngay lập tức. Phần còn lại để broker và các consumer tự lo. Bốn lợi ích lớn xuất hiện:
-
Loose coupling (giảm phụ thuộc). Auth Service không cần biết ai đang lắng nghe. Thêm SMS Service mới? Chỉ cần cho nó subscribe vào topic — không đụng một dòng nào của Auth Service.
-
Resilience (chịu lỗi). Email Service chết? Không sao. Message vẫn nằm an toàn trong broker, chờ nó sống lại rồi xử lý tiếp. Việc đăng ký của user không còn bị kéo sập bởi một service phụ.
-
Scalability (co giãn). Message dồn nhiều quá xử lý không kịp? Chỉ cần thêm consumer vào để chia tải. Broker lo việc phân phối.
-
Asynchronous (bất đồng bộ). User được phản hồi tức thì. Những việc nặng và chậm được đẩy ra background, xử lý theo nhịp riêng của chúng.
Phân chia Message broker theo cách thức hoạt động
Dựa trên cách thức hoạt động, message broker thường được chia thành 2 nhóm: Message base và Data pipeline.
| Tiêu chí | Message base | Data pipeline |
|---|---|---|
| Đại diện | ActiveMQ, RabbitMQ, ZeroMQ | RocketMQ, Kafka |
| Trạng thái Consumer | Lưu trạng thái của Consumer để đảm bảo tất cả đều nhận được message từ topic đang subscribe | Không lưu trạng thái của Consumer |
| Vòng đời message | Message bị xóa sau khi Consumer nhận được | Message chưa bị xóa ngay sau khi Consumer nhận |
| Cách đọc message | Khi có message mới, Consumer chỉ lấy được đúng message đó | Consumer có thể tùy ý lấy về một danh sách message, bao gồm cả message cũ |
Từ bảng trên, có thể chọn loại broker phù hợp với bài toán:
- Chọn Message base khi yêu cầu quan trọng nhất là mỗi Consumer đều nhận được message, và nhận đúng một lần.
- Chọn Data pipeline khi bài toán đòi hỏi độ chính xác cao và tuyệt đối không được mất message.
Tạm kết
Message broker không phải để hệ thống chạy nhanh hơn, mà để các thành phần trong hệ thống tách rời khỏi nhau — độc lập, bền bỉ, và dễ mở rộng.
Chúng ta đã đi từ nỗi đau của giao tiếp đồng bộ, qua tư duy bất đồng bộ, tới message-driven programming, và hiểu vì sao message broker lại là mảnh ghép then chốt. Nhưng "message broker" mới chỉ là một khái niệm. Câu hỏi tiếp theo là: ai sẽ hiện thực hóa nó, và hiện thực như thế nào để vừa nhanh, vừa không mất message, lại vừa chịu được quy mô khổng lồ?
Đó là lúc Apache Kafka toả sáng — và cũng là nội dung của bài tiếp theo trong series. 👋
All Rights Reserved