Có một khác biệt tưởng nhỏ mà quyết định gần như mọi thứ: RabbitMQ giống tờ giấy nhớ việc — làm xong là xé đi, không còn dấu; Kafka giống cuốn nhật ký tàu — mỗi dòng ghi vào là ở lại mãi, ai muốn cũng lật lại đọc. Bốn mươi bài, bốn mươi phép đo, và bài cuối trả lời câu hỏi hay được đặt nhất — làm điều mà mọi bảng so sánh RabbitMQ với Kafka trên mạng đều không làm: chạy thật cả hai trên cùng một máy, cùng kích thước thông điệp, rồi nói rõ chỗ phép đo không công bằng.
Đo cạnh nhau
RabbitMQ 4 và Kafka 3.9, mỗi bên một container, một node, thông điệp 200 byte.
| RabbitMQ | Kafka | |
|---|---|---|
| Gửi (chờ broker xác nhận) | 137 085 msg/s | 362 598 msg/s |
| Độ trễ một thông điệp | p50 0,39 ms · p95 0,65 ms | p50 0,28 ms · p95 0,38 ms |
| Đọc | 120 500 msg/s | 1 976 393 msg/s |
| Consumer bắt đầu đọc sau | tức thì | 3,0 s (gia nhập nhóm) |
| RAM lúc rảnh | 144,6 MiB | 349,5 MiB |
| Ảnh Docker | 111 MB | 208 MB |
| Khởi động | 2,8 s | 2,0 s |
Kafka thắng ở mọi cột thông lượng, và thắng đậm nhất ở đọc: 16 lần. Điều đó không có gì lạ — nó được thiết kế để đọc tuần tự từ một tệp log, còn RabbitMQ phải theo dõi trạng thái từng thông điệp.
Nhưng nhìn dòng thứ tư. Consumer Kafka mất ba giây để gia nhập nhóm và nhận phân vùng trước khi đọc được bản ghi đầu tiên. Với một tiến trình chạy suốt ngày thì ba giây là không có gì; với một hàm chạy theo yêu cầu rồi tắt, hoặc một dịch vụ mở rộng co giãn liên tục, đó là ba giây mỗi lần.
Chỗ phép so sánh này không công bằng
Hai bên không hứa cùng một điều. RabbitMQ trong bảng trên gửi thông điệp persistent và chờ publisher confirm — phần 7 đo được rằng nó thật sự nằm trên đĩa. Kafka chạy acks=all nhưng trên một broker, nghĩa là "đã ghi vào bản sao dẫn đầu", mà theo mặc định Kafka không fsync từng bản ghi — nó dựa vào bộ nhớ đệm của hệ điều hành cộng với nhân bản.
Nói cách khác: một phần lợi thế thông lượng của Kafka ở đây đến từ việc nó hứa ít hơn. Trên cụm nhiều node với min.insync.replicas đủ lớn thì cả hai đều đắt hơn, và khoảng cách sẽ khác con số ở trên.
Một node không phải sân chơi của Kafka. Kafka mạnh khi phân vùng ra nhiều máy để đọc song song; bảng trên chỉ có một phân vùng. Ngược lại, phần 34 đo được RabbitMQ quorum queue mất một nửa thông lượng khi lên cụm ba node. Cả hai con số đều là sàn của hệ thống thật.
Đưa hai lưu ý này ra trước, vì một bảng số không kèm điều kiện là cách dễ nhất để nói dối bằng phép đo.
Bốn câu hỏi phân định
Sau bốn mươi bài, tôi nghĩ việc chọn giữa hai thứ này không phải chuyện thông lượng. Bốn câu hỏi sau quyết định gần hết:
1. Thông điệp đã xử lý thì nên biến mất hay nên ở lại?
Đây là câu hỏi gốc. RabbitMQ là hàng đợi việc: ack xong là đi. Kafka là log: đọc xong vẫn còn, ai cũng đọc lại được. Phần 35 cho thấy RabbitMQ có stream để làm việc thứ hai, nhưng đó là một kiểu hàng đợi phụ chứ không phải bản chất của nó.
2. Có cần định tuyến phức tạp không?
Đây là chỗ RabbitMQ hơn hẳn, và tám bài của chặng định tuyến là bằng chứng: topic exchange với * và #, alternate exchange hứng thông điệp lạc, dead letter exchange tách theo bốn nguyên nhân, thử lại có độ trễ. Kafka không có gì tương đương — bên nhận tự lọc, tự xử lý lỗi, tự dựng đường thử lại.
3. Mỗi thông điệp một người làm, hay nhiều bên cùng nghe?
Hàng đợi việc với một trăm consumer tranh nhau là RabbitMQ. Một luồng sự kiện mà mười đội cùng đọc theo nhịp riêng là Kafka. Phần 9 đo được cái giá khi ép RabbitMQ làm việc thứ hai: fanout tới 100 hàng đợi làm thông lượng tụt 68 lần, vì mỗi đích là một lần ghi thật.
4. Cần giữ lịch sử bao lâu?
Vài phút cho tới khi consumer xử lý xong → hàng đợi. Vài tuần để dựng lại trạng thái, chạy lại, kiểm toán → log. Phần 32 đo được RabbitMQ giữ một triệu thông điệp mà bộ nhớ không nhúc nhích, nên nó chịu được tồn đọng lớn — nhưng "chịu được tồn đọng" khác hẳn "thiết kế để lưu trữ".
Và câu trả lời hay gặp nhất trong hệ thống thật là cả hai: RabbitMQ cho công việc và định tuyến, Kafka cho luồng sự kiện và lịch sử. Chúng không thay thế nhau.
Nhìn lại bốn mươi bài
Điều tôi không lường trước khi bắt đầu: phép đo đi ngược lời khuyên phổ biến nhiều đến thế.
- Dòng
queueDeclare("hello", false, false, false, null)trong mọi hướng dẫn RabbitMQ đã bị cấm từ bản 4, và nó giết cả connection. - Thông điệp persistent nhanh hơn transient 31%, vì cả hai đều đã nằm trên đĩa —
messages_ram = 1ở cả hai chế độ. basicQosbị bỏ qua hoàn toàn khiautoAck=true: đặtprefetch=1cho an toàn mà vẫn mất 949 thông điệp.x-delivery-limitkhông đếmbasicNack— nó bảo vệ bạn khỏi consumer sập, không khỏi mã bắt ngoại lệ rồi requeue.- Vòng thử lại bằng TTL bị broker xoá âm thầm, vòng bằng reject thì quay mãi — cùng một topology, hai kết cục ngược nhau.
- Confirm từng thông điệp chậm hơn 133 lần confirm bất đồng bộ, cùng mức đảm bảo.
- Hàng đợi chỉ nghe
don.vn.#vẫn nhận được thông điệp gửi thẳng bằng tên nó — binding là bảng chỉ đường, không phải hàng rào. - Stream luôn báo
messages = 0dù đang giữ đủ dữ liệu.
Và kết luận lớn nhất, gói trong phép đo tổng kết ở bài trước: cùng một mức đảm bảo, cách viết tệ chậm hơn cách viết tốt 122 lần — mà không một tham số broker nào được đổi. Gần như mọi vấn đề hiệu năng RabbitMQ tôi gặp trong bốn mươi bài đều nằm ở cách client dùng nó.
Cũng nên nói phần không đẹp: vài lần tôi phải bỏ hẳn một mục đã viết vì đo ra không như dự đoán và tôi chưa giải thích được — cờ global của basicQos, TypePrecedence.INFERRED, consumer_utilisation. Và một lần suýt viết rằng rabbitmqctl báo sai với quorum queue, trước khi đối chiếu ba nguồn và thấy nó chỉ lắng chậm hơn. Không đo thì không biết mình sai ở đâu.
Đi tiếp từ đây
Vận hành sâu: cấu hình cluster_partition_handling tường minh (thứ phần 34 đo được là đang để mặc định), chính sách sao lưu, nâng cấp không dừng dịch vụ.
Kafka, nếu bốn câu hỏi ở trên đưa bạn tới đó — và giờ bạn có một bộ khái niệm để so sánh chứ không phải một bảng chép trên mạng.
Hiệu năng ở tầng dưới: đọc mã nguồn classic queue v2, hiểu vì sao messages_ram luôn bằng 1.
Và nếu bạn chỉ mang đi một thứ, hãy để nó là thói quen đo trước khi tin — kể cả khi thứ bạn định tin là tài liệu chính thức, hay là bài viết này. Cách kiểm nhanh nhất là mở broker ra mà đọc:
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues name type messages consumers
Đọc được cả bốn cột đó và biết chính xác mỗi con số nghĩa là gì, thì sê-ri này đã làm xong việc của nó.
Mẫu số chung
Cái bẫy lớn nhất của mọi bảng so sánh không phải con số sai, mà là con số đúng nhưng thiếu điều kiện. "Kafka đọc nhanh gấp 16 lần" là thật trên đúng cái máy đó — và vô nghĩa cho tới khi bạn hỏi nhanh hơn ở mức hứa hẹn nào: Kafka ở đây không fsync từng bản ghi, RabbitMQ thì đã nằm trên đĩa và chờ confirm. "Nhanh hơn" luôn phải đi kèm "để đổi lấy gì" — độ bền, độ trễ đuôi, bộ nhớ, thời gian khởi động — nếu không nó chỉ là một nửa câu. Cùng cái nửa-câu ấy ở mọi benchmark bạn đọc: một CSDL "nhanh gấp mười" mà giấu mức cô lập giao dịch, một framework "ít cấp phát hơn" đo trên tải chẳng giống thật, một thuật toán "O(1)" với hằng số khổng lồ. Quy tắc chốt cả sê-ri: so sánh chỉ có nghĩa khi hai bên hứa cùng một điều — bằng không bạn đang đo hai thứ khác nhau và gọi nó là cuộc đua.
Và điều thứ hai, cũng là sợi chỉ xuyên suốt bốn mươi bài: khoảng cách 122 lần giữa cách viết tốt và cách viết tệ không nằm ở công cụ mà nằm ở cách bạn dùng nó. Đổi broker, đổi ngôn ngữ, đổi hẳn sang Kafka — cái quyết định vẫn là bạn có hiểu thứ mình gọi hay không. Nên thứ đáng mang theo không phải một bảng "chọn cái nào", mà một thói quen: đo trước khi tin, kể cả tài liệu chính thức, kể cả chính mình.