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
Hai mươi lần. Mười tám request mỗi giây, và 18 ≈ 1 ÷ 0,05 — đúng một tác vụ chạy tại một thời điểm, trong khi pool có sẵn hai mươi luồng ngồi không. Không lỗi, không cảnh báo, không có gì trong log; chỉ là mọi thứ chậm kinh khủng.

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.