Phần 1 so Vert.x trần với Spring MVC và cho một bảng rất chênh lệch. Bài này làm lại đầy đủ hơn: dùng Vert.x Web cho công bằng, thêm endpoint không có I/O để đo chi phí khung, và thêm một đối thủ thứ ba mà Java 21 vừa mang tới — Spring MVC bật luồng ảo.

Ba khung, hai endpoint

/nhanh trả JSON ngay lập tức. /io chờ 20 ms rồi trả cùng JSON.

Endpoint /nhanh — chi phí thuần của khung:

Kết nối Vert.x Web Spring MVC Spring MVC + luồng ảo
200 85 415 65 008 81 233
1 000 97 182 90 645 77 383
5 000 100 063 79 714 67 543

Ba khung nằm cùng một khoảng, 65–100 nghìn req/s. Chi phí của khung không phải chỗ tạo ra khác biệt — nếu ứng dụng của bạn không chờ gì cả, chọn khung nào cũng được.

Endpoint /io — có chờ 20 ms:

Kết nối Vert.x Web Spring MVC Spring MVC + luồng ảo
200 9 211 8 628 7 961
1 000 44 700 8 814 42 589
5 000 99 410 8 986 58 397

Điều đáng nói nhất

Nhìn dòng 1 000 kết nối: 42 589 so với 44 700 — chênh 5%. Spring MVC bật luồng ảo bám sát Vert.x, trong khi Spring MVC thường đứng yên ở 8 814.

Đây là thay đổi lớn nhất với hệ sinh thái Java trong nhiều năm. Cột giữa — mô hình một luồng mỗi request — là thứ Vert.x sinh ra để thay thế, và nó dừng ở khoảng 8 900 req/s vì pool 200 luồng chia cho 22 ms. Bật một dòng cấu hình luồng ảo là trần đó biến mất.

Nhưng đọc tiếp hai dòng còn lại. Ở 5 000 kết nối, Vert.x cho 99 410 còn luồng ảo cho 58 397 — 1,7 lần. Và bảng tài nguyên giải thích một phần:

Số luồng RSS
Vert.x Web 49 709 MiB
Spring MVC 237 613 MiB
Spring MVC + luồng ảo 54 1 210 MiB

Luồng ảo giữ số luồng nền thấp như Vert.x (54 so với 49), nhưng tốn gần gấp đôi bộ nhớ — mỗi luồng ảo vẫn là một ngăn xếp trên heap, và ở 5 000 request đồng thời thì đó là 5 000 ngăn xếp.

Chỗ phép so sánh này không công bằng

Thread.sleep không phải I/O thật. Với luồng ảo, Thread.sleep park luồng đúng như một lời gọi mạng, nên phép đo hợp lệ. Nhưng một lời gọi JDBC thật sẽ pin luồng ảo vào luồng nền trong nhiều trường hợp, và khi đó cột phải sẽ tụt về gần cột giữa. Muốn biết chắc, phải đo bằng chính driver bạn dùng.

Vert.x chạy 16 bản sao verticle, Spring chạy cấu hình mặc định. Tôi không chỉnh pool của Tomcat. Tăng server.tomcat.threads.max lên 2 000 sẽ đẩy cột giữa lên đáng kể — cùng với bộ nhớ.

Cùng một JVM, cùng một máy, không có mạng thật. Độ trễ mạng thật sẽ làm mọi cột gần nhau hơn.

Endpoint quá đơn giản. Không có CSDL, không có tuần tự hoá phức tạp, không có bảo mật. Trong ứng dụng thật, phần lớn thời gian nằm ở những thứ đó chứ không ở khung.

Vậy chọn gì

Tình huống Chọn
Đội đã quen Spring, tải vừa phải Spring MVC + luồng ảo — một dòng cấu hình, không viết lại gì
Hàng chục nghìn kết nối giữ mở Vert.xphần 13 đo 4 KB mỗi kết nối
Cần kiểm soát bộ nhớ chặt chẽ Vert.x — bảng trên cho thấy nó dùng ít nhất
Dịch vụ chủ yếu gọi ra ngoài, độ trễ đuôi quan trọng Vert.x — cột /io ở 5 000 kết nối
Ứng dụng CRUD bình thường Khung nào cũng được; bảng /nhanh nói vậy

Kết luận thật của bài này không phải "Vert.x thắng". Nó là: luồng ảo đã xoá phần lớn lý do hiệu năng để chọn mô hình bất đồng bộ, và những lý do còn lại — bộ nhớ, kết nối giữ mở, độ trễ đuôi ở quy mô rất lớn — vẫn thật nhưng hẹp hơn nhiều so với hai năm trước.

Và cái giá của Vert.x thì không đổi: mọi thứ phần 3phần 8 đo được — một Thread.sleep lạc chỗ làm sập thông lượng 59 lần, ThreadLocal trả về dữ liệu của người khác. Với luồng ảo, mã chặn vẫn cứ là mã chặn bình thường.

Bài sau: event bus — thứ Spring không có gì tương đương, và là lý do kiến trúc để chọn Vert.x.

Thử ba mươi giây

# trong application.properties cua ban
spring.threads.virtual.enabled=true

Bật lên, chạy lại phép đo tải của chính bạn ở mức đồng thời cao. Nếu thông lượng nhảy vọt như cột phải trong bảng /io, bạn vừa tìm ra rằng nút thắt của mình là pool luồng chứ không phải khung.