Mọi bài giới thiệu Vert.x đều mở đầu bằng cụm "không chặn" và "hướng sự kiện". Bài này bỏ qua phần đó và đi thẳng vào chỗ khác biệt hiện ra thành số: chuyện gì xảy ra khi số kết nối đồng thời tăng lên.

Bàn thí nghiệm

Hai máy chủ, cùng một máy 16 nhân, cùng một endpoint làm đúng một việc: chờ 20 mili giây rồi trả lời — tương đương một lời gọi CSDL hoặc một API hạ nguồn.

// Vert.x — chờ bằng timer, không giữ luồng nào
vertx.setTimer(20, id -> req.response().end("ok"));

// Spring MVC — chờ bằng cách chặn luồng đang phục vụ request
Thread.sleep(20);
return "ok";

Bộ tạo tải viết bằng luồng ảo Java 21, cả hai đo trên cùng HTTP/1.1, mỗi mức chạy 6 giây.

Bảng đo

Kết nối đồng thời Thông lượng p50 p95 p99
200 Vert.x 9 143 req/s 21,5 ms 23,1 ms 29,0 ms
Spring MVC 8 622 req/s 22,8 ms 25,3 ms 27,2 ms
1 000 Vert.x 44 740 req/s 21,7 ms 24,5 ms 41,0 ms
Spring MVC 8 749 req/s 114,1 ms 118,6 ms 121,2 ms
5 000 Vert.x 86 804 req/s 47,1 ms 117,6 ms 203,5 ms
Spring MVC 8 937 req/s 567,0 ms 574,3 ms 577,2 ms

Không có lỗi nào ở cả sáu lần chạy.

Hai điều bảng đó nói

Ở 200 kết nối, hai bên như nhau. Vert.x nhanh hơn 6% — nằm trong nhiễu. Nếu ứng dụng của bạn chưa bao giờ vượt vài trăm kết nối đồng thời, bảng này không phải lý do để đổi khung.

Spring MVC dừng lại ở khoảng 8 900 req/s và không nhúc nhích nữa. Con số đó không ngẫu nhiên:

200 luồng Tomcat ÷ 0,0224 giây mỗi request ≈ 8 900 req/s

Đúng bằng kích thước pool chia cho thời gian xử lý. Tăng số kết nối lên gấp 25 lần không làm thông lượng tăng thêm một chút nào — nó chỉ làm hàng đợi chờ dài ra, và độ trễ tăng đúng theo tỉ lệ đó: 22 ms → 114 ms → 567 ms.

Vert.x thì không có pool nào để cạn. Cùng 16 event loop phục vụ 200 hay 5 000 kết nối, nên thông lượng đi theo số việc thật sự có, và độ trễ trung vị tăng chậm hơn nhiều.

Cái giá: luồng và bộ nhớ

Đo ngay trong lúc cả hai chịu tải 5 000 kết nối:

Số luồng Heap đang dùng
Vert.x 49 97 MiB
Spring MVC 237 242 MiB

Bốn mươi chín luồng phục vụ năm nghìn kết nối. Đó là toàn bộ ý tưởng của mô hình này.

Một chỗ tôi không giải thích được: RSS của tiến trình Vert.x lại cao hơn (613 MiB so với 502 MiB), dù heap thấp hơn nhiều — nhiều khả năng là bộ đệm ngoài heap của Netty, nhưng tôi chưa đào sâu nên không kết luận gì thêm.

Hai lần tôi đo sai

Đáng kể hơn cả bảng số, vì bạn sẽ vấp đúng hai chỗ này nếu tự đo.

Lần một: Vert.x chậm hơn Spring 17 lần. Kết quả đầu tiên cho Vert.x 505 req/s. Hoá ra cổng 8081 trên máy tôi đang bị một container Docker khác chiếm, nên máy chủ Vert.x không bao giờ bind được và bộ đo bắn vào một dịch vụ hoàn toàn khác. lsof -nP -iTCP:8081 -sTCP:LISTEN mất năm giây và trả lời mọi thứ.

Lần hai: 1,4 triệu lỗi. Sau khi đổi cổng, Vert.x chạy nhưng lỗi nhiều hơn thành công. Thông báo là too many concurrent streamsHttpClient của Java mặc định thương lượng HTTP/2, Vert.x chấp nhận, còn Tomcat thì không. Hai máy chủ đang bị đo trên hai giao thức khác nhau, và HTTP/2 giới hạn 100 luồng trên mỗi kết nối. Ép HttpClient.Version.HTTP_1_1 cho cả hai thì lỗi về 0 và bảng ở trên mới xuất hiện.

Bài học chung: một phép đo cho kết quả gây sốc thì gần như luôn là phép đo hỏng. Đi tìm lỗi trước khi đi viết kết luận.

Cái giá thật không nằm ở bảng nào

Bốn mươi chín luồng phục vụ năm nghìn kết nối chỉ đúng khi không handler nào chặn. Một lời gọi JDBC, một Thread.sleep, một thư viện đọc tệp đồng bộ — bất cứ thứ gì giữ event loop lại — sẽ làm mọi kết nối mà luồng đó đang phục vụ đứng chờ theo.

Với Spring MVC, một request chậm làm chậm đúng một luồng trong hai trăm luồng. Với Vert.x, nó làm chậm một phần mười sáu toàn bộ máy chủ.

Đó là đánh đổi thật của mô hình này, và cũng là lý do bài 3 và bài 4 của sê-ri dành trọn cho đúng một luật: đừng chặn event loop.

Thử ba mươi giây

# may chu cua ban dang co bao nhieu luong, va bao nhieu trong so do dang cho
jcmd <pid> Thread.print | grep -c '^"'
jcmd <pid> Thread.print | grep -c "java.lang.Thread.State: WAITING"

Nếu số luồng xấp xỉ kích thước pool và phần lớn đang WAITING, bạn đang trả tiền cho hai trăm luồng để chúng ngồi chờ mạng — đúng tình huống mà mô hình event loop sinh ra để giải quyết.