Phần trước liệt kê những thứ tính là "chặn" — JDBC, đọc tệp, băm mật khẩu — và chúng không biến mất chỉ vì bạn dùng Vert.x. Bài này đo bốn cách chạy chúng cho đúng.
Bốn cách
// 1. executeBlocking, mặc định — CÓ MỘT CÁI BẪY Ở ĐÂY
vertx.executeBlocking(() -> { Thread.sleep(50); return "ok"; })
// 2. executeBlocking, không xếp thứ tự
vertx.executeBlocking(() -> { Thread.sleep(50); return "ok"; }, false)
// 3. Worker verticle — nhận việc qua event bus
new DeploymentOptions().setThreadingModel(ThreadingModel.WORKER)
// 4. Virtual thread verticle (Vert.x 4.5 trở lên)
new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD)
Cả bốn đều cứu được event loop
Đây là điều quan trọng nhất, nên đo trước. Cho mỗi cách chịu tải nặng, rồi đo /nhanh — endpoint không liên quan gì, chỉ trả "ok":
| Trong lúc tải endpoint | /nhanh đạt |
p50 |
|---|---|---|
| chạy thẳng trên event loop | 573 req/s | 528,7 ms |
executeBlocking ordered |
95 007 req/s | 2,8 ms |
executeBlocking unordered |
96 177 req/s | 2,9 ms |
| worker verticle | 97 805 req/s | 2,9 ms |
| virtual thread verticle | 99 482 req/s | 2,9 ms |
Bốn cơ chế đúng đều giữ /nhanh ở khoảng 96–99 nghìn req/s; chạy thẳng trên event loop làm nó sập 166 lần. Chọn cách nào cũng được — miễn là chọn một cách.
Nhưng thông lượng của chính việc chặn thì khác hẳn
Đo thông lượng của chính endpoint chậm, 200 kết nối, mỗi request 50 ms:
| Cách | Thông lượng | Lý thuyết |
|---|---|---|
| chạy thẳng trên event loop | 297 req/s | 16 loop ÷ 0,05 s = 320 |
executeBlocking ordered (16 context) |
298 req/s | 16 context ÷ 0,05 s = 320 |
executeBlocking unordered |
375 req/s | 20 luồng ÷ 0,05 s = 400 |
| worker verticle (20 bản sao) | 375 req/s | 20 ÷ 0,05 = 400 |
| virtual thread verticle | 372 req/s | 20 ÷ 0,05 = 400 |
Mọi con số đo được đều khớp phép chia đơn giản ở cột phải. Và dòng thứ hai đang che giấu một cái bẫy.
Bẫy ordered=true
executeBlocking mặc định xếp thứ tự: mọi tác vụ gửi từ cùng một context chạy lần lượt, không bao giờ song song. Ở bảng trên nó vô hại vì máy chủ có 16 bản sao verticle, tức 16 context độc lập.
Chạy lại với một bản sao:
| Cách | Thông lượng | p50 |
|---|---|---|
executeBlocking ordered |
18 req/s | 8 390 ms |
executeBlocking unordered |
372 req/s | 532,8 ms |
Mặc định này có lý do chính đáng — nhiều mã dựa vào việc các tác vụ của cùng một request chạy theo thứ tự. Nhưng nếu bạn chỉ đang gọi CSDL thì thứ tự đó không có nghĩa gì, và bạn đang trả giá bằng toàn bộ tính song song.
Luật thực dụng: cứ truyền false trừ khi bạn biết chính xác vì sao cần thứ tự.
Pool hai mươi luồng là trần cứng
Ba cách còn lại đều dừng ở khoảng 375 req/s, vì workerPoolSize mặc định là 20. Với công việc 50 ms thì trần là 400 req/s và không có cách nào vượt qua ngoài việc tăng pool:
new VertxOptions().setWorkerPoolSize(100);
Nhưng tăng pool là quay lại đúng mô hình một luồng mỗi request mà phần 1 đo được là dừng ở 8 900 req/s. Pool càng lớn, Vert.x càng giống Tomcat.
Virtual thread verticle
Cách thứ tư đáng chú ý nhất về lâu dài: bên trong verticle kiểu này, mã chặn không giữ luồng nền vì luồng ảo bị park chứ không chiếm carrier. Trong phép đo trên nó cho 372 req/s — ngang worker verticle, vì tôi giới hạn ở 20 bản sao. Với số bản sao lớn hơn, trần đó biến mất theo cách mà worker pool không làm được.
Đây là tính năng còn mới trong 4.5; tôi chưa đo nó ở quy mô lớn nên không kết luận gì thêm ngoài con số ở trên.
Chọn cái nào
| Tình huống | Cách |
|---|---|
| Một đoạn chặn ngắn, lẻ tẻ | executeBlocking(..., **false**) |
| Cả một verticle toàn việc chặn (JDBC, xử lý tệp) | worker verticle |
| Cần giữ thứ tự trong một luồng nghiệp vụ | executeBlocking ordered — cố ý, không phải vì quên |
| Mã chặn nhiều và bạn dùng Java 21 | virtual thread verticle, và đo lại |
Và cách tốt nhất vẫn là không cần cách nào — dùng client không chặn cho CSDL, HTTP, Redis. Chặng dữ liệu của sê-ri sẽ đo chúng.
Thử ba mươi giây
# pool worker cua ban co dang can khong
jcmd <pid> Thread.print | grep -c "vert.x-worker-thread"
jcmd <pid> Thread.print | grep -A1 "vert.x-worker-thread" | grep -c "TIMED_WAITING\|WAITING"
Nếu số luồng worker bằng 20 và gần như tất cả đang chờ, bạn đã chạm trần pool. Nếu chỉ một luồng bận trong khi mười chín cái rảnh, bạn vừa tìm thấy ordered=true.