Bốn mươi bài, bốn mươi phép đo. Bài cuối trả lời câu hỏi hay được đặt nhất — và 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.
Còn nếu bạn chỉ mang đi một thứ từ bốn mươi bài này, 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.
Thử ba mươi giây
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues name type messages consumers
Nếu bạn đọc được cả bốn cột đó và biết chính xác mỗi con số nghĩa là gì, sê-ri này đã làm xong việc của nó.