Kết nối giữ mở là chỗ mô hình event loop tỏa sáng nhất: mười nghìn client đang chờ thì với mô hình một luồng mỗi kết nối là mười nghìn luồng, còn ở đây là con số bạn sắp thấy.

Máy chủ

vertx.createHttpServer()
     .webSocketHandler(ws -> {
         if (!ws.path().equals("/ws")) { ws.reject(); return; }
         WS.add(ws);
         ws.closeHandler(v -> WS.remove(ws));
         ws.textMessageHandler(m -> ws.writeTextMessage("echo:" + m));
     })
     .requestHandler(router)      // HTTP thường vẫn dùng chung cổng
     .listen(8080);

Hai dòng đáng chú ý: reject() cho đường dẫn lạ — không có nó thì mọi đường dẫn đều nâng cấp được thành WebSocket; và closeHandler gỡ khỏi tập — thiếu nó là rò rỉ bộ nhớ chắc chắn, vì tập giữ tham chiếu tới kết nối đã chết.

Đo

Số kết nối Heap tăng thêm Mỗi kết nối RSS Số luồng Phát tin cho tất cả
1 000 4 MiB 4 263 B 120 MiB 37 7,5 ms
5 000 20 MiB 4 199 B 150 MiB 39 17,9 ms
9 767 39 MiB 4 037 B 177 MiB 39 21,3 ms

Ba điều đọc ra:

Số luồng không đổi. Ba mươi bảy luồng phục vụ một nghìn kết nối, ba mươi chín luồng phục vụ gần mười nghìn. Đây là toàn bộ lý do dùng mô hình này cho kết nối giữ mở — phần 1 đo được Spring MVC dùng 237 luồng chỉ để phục vụ request ngắn.

Khoảng 4 KB heap mỗi kết nối, và con số này ổn định khi số kết nối tăng — không có chi phí ẩn nào bùng lên ở quy mô lớn. Mười nghìn kết nối là 39 MiB; một trăm nghìn sẽ là khoảng 400 MiB, vẫn nằm trong tầm một máy chủ bình thường.

RSS tăng chậm hơn nhiều so với heap — 120 lên 177 MiB, phần lớn là bộ đệm của Netty được tái dùng.

Cái bẫy nằm ở phía client

Lần đo đầu tiên của tôi cho ra đúng 50 kết nối dù yêu cầu 10 000, và không có lỗi nào:

maxWebSockets = 50     ← mặc định của HttpClientOptions
maxPoolSize   = 5

HttpClient của Vert.x giới hạn 50 WebSocket cho mỗi endpoint. Yêu cầu thứ 51 không bị từ chối — nó xếp hàng đợi cho tới khi có chỗ, nên chương trình chỉ đơn giản là treo ở đó.

Nếu bạn viết một dịch vụ mở nhiều WebSocket ra ngoài — cầu nối, bot, bộ thu thập dữ liệu — thì đây là trần bạn sẽ đâm vào, và triệu chứng là "chậm không rõ lý do" chứ không phải một lỗi.

new HttpClientOptions().setMaxWebSockets(5000);

Phát tin cho tất cả tốn tuyến tính

Bảy phẩy năm mili giây cho một nghìn, 21,3 ms cho gần mười nghìn — khoảng 2,2 micro giây mỗi kết nối. Tuyến tính, không có bất ngờ.

Nhưng nhớ rằng đó là thời gian ngồi trên event loop. Phần 3 đo được chặn 50 ms làm thông lượng sập 59 lần; một vòng lặp phát tin cho 100 000 kết nối mất khoảng 220 ms và sẽ làm đúng như vậy. Ở quy mô đó, phải chia nhỏ vòng lặp qua nhiều lượt event loop, hoặc phân tán kết nối qua nhiều bản sao verticle và để mỗi bản tự phát cho phần của mình.

Ba việc phải nhớ

Luôn closeHandler để gỡ khỏi tập. Không có nó, tập kết nối chỉ có lớn lên.

Luôn reject() cho đường dẫn không mong đợi. Mặc định là chấp nhận mọi đường dẫn.

Kiểm tra writeQueueFull() khi gửi nhiều. Đúng bài học của phần 12: writeTextMessage không chặn, nên một client đọc chậm sẽ làm hàng chờ ghi phình lên y hệt.

Bài sau: Server-Sent Events — làm được phần lớn những gì WebSocket làm, và đo xem nó rẻ hơn hay không.

Thử ba mươi giây

# dem ket noi websocket dang mo va bo nho tuong ung
lsof -nP -p <pid> -iTCP -sTCP:ESTABLISHED | wc -l
jcmd <pid> GC.heap_info | head -2

Chia số MiB cho số kết nối. Ra hơn 10 KB mỗi kết nối nghĩa là bạn đang giữ thêm thứ gì đó cho mỗi client — thường là một buffer hoặc một danh sách chưa bao giờ được dọn.