Phần 1 kết bằng một câu: 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. Bài này đo xem "chặn" tốn bao nhiêu — và con số lớn hơn tôi tưởng.

Luật

// ĐƯỢC — trả quyền điều khiển lại cho event loop ngay
vertx.setTimer(50, id -> req.response().end("ok"));

// KHÔNG ĐƯỢC — giữ luôn event loop 50 mili giây
Thread.sleep(50);
req.response().end("ok");

Hai đoạn trên cho người dùng cùng một trải nghiệm: trả lời sau 50 ms. Khác biệt là đoạn thứ hai giữ một trong mười sáu event loop của máy chủ suốt thời gian đó, còn đoạn đầu không giữ gì cả.

Đo

Máy chủ có hai endpoint: /nhanh trả lời ngay, /chan gọi Thread.sleep(50). Tôi đo thông lượng của /nhanh ở 500 kết nối, trong khi một dòng nhỏ khách khác gõ vào /chan.

Với 16 bản sao (16 event loop):

Số kết nối gọi /chan Thông lượng /nhanh p50
0 95 424 req/s 4,6 ms
16 9 979 req/s 54,0 ms
64 2 388 req/s 162,3 ms

Với 1 bản sao (1 event loop):

Số kết nối gọi /chan Thông lượng /nhanh p50
0 113 485 req/s 4,1 ms
4 1 937 req/s 217,7 ms
16 339 req/s 1 660,2 ms

Bốn kết nối. Không phải bốn nghìn — bốn. Chúng làm thông lượng của mọi người khác sập 59 lần.

Phép tính giải thích chính xác con số đó

Không cần đoán, chỉ cần nhân:

16 kết nối × (1 request ÷ 0,05 giây) = 320 request/giây vào /chan
320 request/giây × 50 ms = 16 giây thời-gian-event-loop mỗi giây
16 giây ÷ 16 event loop = 100% bão hoà

Đúng bằng lúc /nhanh tụt từ 95 nghìn xuống 10 nghìn req/s. Với một event loop và bốn kết nối:

4 × 20 = 80 request/giây × 50 ms = 4 giây mỗi giây trên MỘT loop = quá tải 400%
Đây là điều khác hẳn mô hình một luồng mỗi request. Với Spring MVC, một request chặn 50 ms chiếm một trong hai trăm luồng — 0,5% công suất. Với Vert.x một bản sao, nó chiếm 100% công suất trong 50 ms đó. Bán kính sát thương không phải một request, mà là toàn bộ kết nối đang gắn vào event loop ấy.

Bán kính sát thương chia theo số bản sao

So hai bảng: cùng 16 kết nối chặn, một bản sao cho 339 req/s còn mười sáu bản sao cho 9 979 req/s — gấp 29 lần. Tăng số bản sao chia nhỏ thiệt hại, nhưng không xoá nó.

phần 2 đã đo rằng với handler không chặn, tăng bản sao gần như không mua thêm được gì (1 bản sao đạt 86% thông lượng của 16). Nên setInstances không phải cách chữa — nó chỉ làm vết thương nông hơn.

Cái gì tính là "chặn"

Danh sách này dài hơn người ta tưởng, và phần lớn không có chữ sleep nào:

Việc Vì sao chặn
JDBC, JPA, Hibernate Mọi driver JDBC đều đồng bộ
File.readAllBytes, InputStream.read I/O tệp của thư viện chuẩn
HttpURLConnection, RestTemplate, OkHttp đồng bộ Chờ mạng trên luồng gọi
future.get(), latch.await() Chờ trên chính event loop
Băm mật khẩu (BCrypt), nén, phân tích ảnh Tốn CPU hàng chục mili giây
Log đồng bộ ra tệp chậm Ghi đĩa nằm trong lời gọi

Dòng cuối là dòng hay bị bỏ sót nhất: một appender ghi thẳng ra tệp trên đĩa chậm biến mọi lời gọi log.info thành một lần chặn nhỏ, nhân với mọi request.

Ba bài tới nói về cách chạy đúng những việc này: worker verticle và executeBlocking ở bài 5, và các client không chặn ở chặng sau.

Thử ba mươi giây

long t0 = System.nanoTime();
// ... doan ma ban nghi ngo ...
long ms = (System.nanoTime() - t0) / 1_000_000;
if (ms > 5 && Context.isOnEventLoopThread())
    log.warn("Chặn event loop {} ms tại {}", ms, "tên-chỗ-này");

Năm mili giây là ngưỡng đáng nghi trong một handler. Bài sau đo xem công cụ có sẵn của Vert.x phát hiện được chuyện này từ mốc nào — câu trả lời là muộn hơn bạn cần rất nhiều.