Event bus của Vert.x là một kênh nhắn tin nằm sẵn trong ứng dụng, và nó có ba cách gửi giống ba cách nhờ việc trong một nhóm: đưa cho một người đang rảnh (send, luân phiên), hô to cho cả nhóm cùng nghe (publish), hay nhờ rồi đứng chờ trả lời (request). Bài này đo cả ba — và đo trúng một cái bẫy: đếm số lời bạn nói ra được mỗi giây thì nhanh khủng khiếp, nhưng đó không phải số lời có người nghe và làm.

Ba kiểu, ba ngữ nghĩa

eb.send("viec", "x");                          // MOT nguoi nhan
eb.publish("suKien", "x");                     // TAT CA cung nhan
eb.request("hoi", "x").onSuccess(m -> ...);    // gui va cho tra loi

Đăng ký ba consumer trên cùng một địa chỉ rồi gửi 3 000 thông điệp:

send    3 000 thông điệp tới 3 consumer -> 1000 / 1000 / 1000
publish 1 000 thông điệp tới 3 consumer -> 1000 / 1000 / 1000  (tổng 3000)

send chia vòng tròn tuyệt đối đều. publish nhân bản cho tất cả. Hai dòng này là toàn bộ khác biệt, và nó đúng cả khi các consumer nằm trên những node khác nhau của cụm — bài 21 sẽ đo phần đó.

Gửi vào địa chỉ không ai nghe

send    -> không lỗi, không ngoại lệ (thông điệp biến mất)
request -> NO_HANDLERS (mã -1)

send im lặng nuốt mất — giống hệt điều mà sê-ri RabbitMQ đo được với thông điệp không khớp binding. request thì báo ngay, vì nó đang chờ một câu trả lời sẽ không bao giờ tới.

Hệ quả thực tế: một lỗi gõ sai tên địa chỉ trong send không để lại dấu vết nào. Nếu địa chỉ quan trọng, hãy dùng request kể cả khi bạn không cần dữ liệu trả về — chỉ để biết có ai nghe hay không.

Con số tôi suýt đăng

Phép đo đầu tiên của tôi cho send 40 238 413 thông điệp mỗi giây. Bốn mươi triệu.

Nó sai. Vòng lặp của tôi chỉ gọi send hai trăm nghìn lần rồi đo thời gian — mà send không chờ giao, nó chỉ xếp thông điệp vào hàng của event loop rồi trả về. Bốn mươi triệu là tốc độ xếp hàng, không phải tốc độ giao.

Đo lại, chờ tới khi consumer thật sự nhận đủ 200 000 thông điệp:

Kiểu Thông lượng thật
send (chờ giao đủ) 3 402 692 /giây
request (chờ trả lời) 776 937 /giây
Đây đúng là cái bẫy mà sê-ri RabbitMQ gặp với basicPublish, chỉ đổi thư viện. Mọi API "gửi rồi trả về ngay" đều đo ra một con số đẹp và vô nghĩa nếu bạn không chờ đầu kia. Quy tắc: đo cái gì hoàn tất được, đừng đo cái gì chỉ xếp hàng.

Ba triệu bốn trăm nghìn vẫn là con số rất lớn — cao hơn ba mươi lần thông lượng HTTP tối đa mà phần 16 đo được. Event bus gần như không bao giờ là nút thắt.

Hỏi đáp chậm hơn bốn lần, và đó là điều đương nhiên

776 937 so với 3 402 692 — request chậm hơn 4,4 lần, vì mỗi lượt là hai chặng cộng một Future phải hoàn tất, và phần 6 đã đo mỗi chuỗi Future tốn khoảng 25 ns.

Độ trễ một lượt hỏi đáp trong cùng JVM:

p50 9,3 µs | p95 17,7 µs | p99 25,9 µs

Chín micro giây. Để so sánh, bài sau đo một lời gọi HTTP loopback không làm gì cả ở p50 56,5 µs — tức event bus rẻ hơn khoảng năm lần khi hai bên ở cùng tiến trình. Con số 20 ms mà phần 11 dùng là độ trễ tôi cố ý thêm vào để mô phỏng một dịch vụ chậm, không phải chi phí của bản thân HTTP.

Dùng kiểu nào

Bạn cần Kiểu
Giao việc cho một trong nhiều bản sao send — cân bằng tải sẵn
Báo cho mọi bên quan tâm publish
Cần kết quả, hoặc cần biết có ai nghe không request
Việc quan trọng nhưng không cần kết quả request rồi bỏ qua kết quả — để bắt NO_HANDLERS

Và nhớ rằng cả ba đều chạy trên event loop. Một consumer chặn 50 ms làm đúng những gì phần 3 đo được, chỉ khác là nó không có kết nối HTTP nào để nhìn thấy triệu chứng.

Muốn thấy toàn bộ bản đồ giao tiếp nội bộ của mình, gắn một bộ chặn in ra mọi địa chỉ đang gửi:

vertx.eventBus().addOutboundInterceptor(ctx -> {
    System.out.println("gui -> " + ctx.message().address());
    ctx.next();
});

Chạy nó một lần trên ứng dụng thật là bạn có ngay bản đồ đó — kể cả những địa chỉ gõ sai mà send đang lặng lẽ nuốt.

Mẫu số chung

Một API trả về trước khi việc xong sẽ khiến bạn đo nhầm thứ: bạn bấm giờ tốc độ trao tay, không phải tốc độ đầu kia làm xong. send báo 40 triệu/giây chỉ vì nó xếp thông điệp vào hàng của event loop rồi trả về ngay; con số thật — chờ consumer nhận đủ 200 000 — là 3,4 triệu. Đúng cái bẫy của basicPublish bên RabbitMQ, của một write() được đệm, một INSERT chưa commit, một Promise bạn không await. Quy tắc gói một dòng: đo cái hoàn tất được, đừng đo cái chỉ nộp vào — và dấu hiệu nhận ra là một con số thông lượng cao đến vô lý, vì thao tác nộp bất đồng bộ gần như miễn phí, phần tốn kém nằm ở khâu giao tới nơi.

Điều thứ hai: ba kiểu gửi này là ba hình dạng nhắn tin kinh điển — một-trong-nhiều/hàng-đợi-công-việc (send, tự cân tải), quảng-bá/pub-sub (publish, ai cũng nhận), và hỏi-đáp/RPC (request, gửi rồi chờ). Đúng bộ ba ấy gặp lại ở khắp nơi: queue so với topic của JMS, một consumer group so với quảng bá của Kafka, một lời gọi unary của gRPC so với một cái bus. Chọn theo ý định, đừng chọn theo thói quen. Và có một góc an toàn cắt ngang cả ba: kiểu gửi-rồi-quên (send/publish) không cho tín hiệu nào khi không ai nghe — nên khi một đích thật sự quan trọng, hãy chọn kiểu biết báo hỏng (request → NO_HANDLERS) dù bạn vứt câu trả lời đi, vì một kênh không thể nói cho bạn biết nó gửi hụt còn tệ hơn một kênh biết nói.

Bài sau: gửi POJO qua event bus, và một loại codec không sao chép gì cả.