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.
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.x — phầ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 3 và phầ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.