Năm mươi bài, mỗi bài một phép đo chạy thật. Bài này gom lại thành thứ dùng được: một danh sách kiểm, một bảng những kết quả đi ngược trực giác, và một danh sách những lần tôi đo sai.
Luật lặp lại nhiều nhất
Đọc lại toàn bộ số liệu, có một khuôn hình xuất hiện đi xuất hiện lại:
| Chuyện gì xảy ra | Hậu quả | Có lỗi không |
|---|---|---|
| Health check chặn event loop | 102 118 → 156 req/s | không |
| Hai phụ thuộc chung một pool | 57 225 → 5 req/s | loi 0 |
| Consumer Kafka xử lý nặng | 105 425 → 281 req/s | không |
| Consumer RabbitMQ, autoAck | HTTP chết hẳn 50 giây | không |
| Log ERROR qua AsyncAppender mặc định | 108 315 → 720 req/s | không |
| Bộ giới hạn tần suất trong Verticle | hạn mức 1 000 thành 8 000/giây | không |
Sáu sự cố nghiêm trọng, không cái nào sinh ra một dòng ngoại lệ. Cảnh báo dựa trên tỉ lệ lỗi sẽ im lặng trong cả sáu.
Đó là luật của sê-ri này: trong một ứng dụng bất đồng bộ, hỏng hóc biểu hiện thành độ trễ và hàng đợi, không phải thành lỗi. Nếu bảng theo dõi của bạn chỉ có tỉ lệ lỗi và số request, bạn đang mù trước đúng những thứ nguy hiểm nhất.
Tệ hơn, phần 44 đo được rằng ngay cả biểu đồ độ trễ cũng có thể nói dối: khi event loop kẹt, client đo p95 599,9 ms trong khi vertx_http_server_response_time_seconds_max đứng yên ở 13,9 ms, không nhúc nhích một chữ số.
Danh sách kiểm trước khi lên sản xuất
Mười một mục, mỗi mục dẫn về bài đã đo nó.
1. Không có gì chặn event loop. Đây là mục quan trọng nhất. Rắc một đoạn đo vào mọi handler:
long t0 = System.nanoTime();
xuLy(rec);
long ms = (System.nanoTime() - t0) / 1_000_000;
if (ms > 10) log.warn("handler giu event loop {} ms", ms);
2. Có chỉ số độ trễ event loop. Vert.x không xuất chỉ số này; bảy dòng mã tự làm ở phần 44 cho 0 ms lúc rảnh và 205,6 ms khi kẹt.
3. Mỗi phụ thuộc một pool riêng, hoặc có trần đếm. Xem phần 31. Nhớ WebClient.maxPoolSize mặc định chỉ là 5.
4. Chỉ một tầng thử lại. Ba tầng cùng thử ba lần biến một request thành 27 lượt đập vào hạ nguồn. Truyền hạn chót tuyệt đối xuống dưới.
5. Áp lực ngược liên tục từ CSDL tới client. Đọc theo luồng mà quên áp lực ngược còn tệ hơn nạp hết vào bộ nhớ — 3 421 MB so với 2 243 MB. Dùng pipeTo khi không cần biến đổi gì.
6. Liveness không hỏi bất cứ thứ gì ngoài tiến trình. Khai nhầm readiness thành liveness làm một sự cố CSDL biến thành mất sạch dịch vụ (phần 27).
7. Cấu hình log không phải quả bom. queueSize mặc định 256, neverBlock mặc định false — phần 45 đo được 720 req/s khi log ERROR gặp đĩa chậm.
8. Tài nguyên khai trong Verticle đã nhân với setInstances(). Đúng ba lần trong sê-ri này tôi gặp nó: bộ đếm tần suất, pool PostgreSQL, và pool client. Kiểm chứng bằng pg_stat_activity, đừng tin cấu hình.
9. Đã chạy thử một lần trong cụm thật. getLock và getCounter chạy tốt khi không cụm và hỏng khi có cụm — test xanh, sản xuất chết.
10. Kích thước pool chọn từ phép quét, không từ công thức. Đỉnh của tôi nằm ở 4 bản sao trên máy 16 nhân, và pool worker nên bằng số việc đồng thời chứ không bằng số nhân.
11. Biết mình mất bao lâu để nhận ra một node chết. Mặc định Hazelcast là 60 giây.
Mười kết quả đi ngược trực giác
Những chỗ phép đo cãi lại lời khuyên phổ biến, kể cả lời khuyên của chính tôi trước khi đo:
- Client CSDL reactive không nhanh hơn JDBC — 40 392 so với 40 388 req/s. Nó mua luồng, không mua thông lượng (phần 34).
- Luồng ảo của Java 21 chậm nhất trong ba cách chạy JDBC, đều đặn 14% (phần 34).
- Đọc theo luồng mà quên áp lực ngược tệ hơn nạp hết vào bộ nhớ (phần 40).
synchronizedtrên event loop miễn phí khi vùng tranh chấp là 24 ns (phần 29).- Ghi 4,4 triệu dòng log trong 8 giây không tốn gì đo được — nhưng vứt mất 99,4% (phần 45).
- Ảnh native chậm hơn JVM 26% ở thông lượng đỉnh, đổi lấy khởi động nhanh 13 lần (phần 47).
- Nhiều event loop hơn thì chậm hơn — đỉnh ở 2 trên máy 16 nhân (phần 48).
- Rung ngẫu nhiên không giảm tổng tải, chỉ đổi hình dạng, và không giúp gì cho đợt đầu (phần 30).
- Service proxy tốn đúng bằng viết tay — lớp sinh mã không thêm gì đo được (phần 33).
- Vert.x và Spring WebFlux hoà nhau ở bốn trên năm route (phần 49).
Những lần tôi đo sai
Phần này quan trọng hơn cả bảng số, vì nó là thứ bạn sẽ gặp khi tự đo.
Đo tốc độ của thất bại. Redis báo 72 triệu lệnh/giây trên một tiến trình đơn luồng. 959 trên 1 000 lô đã hỏng với ConnectionPoolTooBusyException, và thất bại thì trả về tức thì. Từ đó mọi hàm đo của tôi đều đếm lỗi và vứt bỏ kết quả nếu có bất kỳ lỗi nào.
Hâm nóng JIT không đủ. Service proxy trông nhanh hơn viết tay 2,8 lần. Chạy lại đúng đoạn mã đó ở vị trí muộn hơn thì hết chênh lệch — phép đo đầu tiên chạm vào một hạ tầng luôn trả giá cho tất cả phép đo sau.
Đo nhầm thứ mình nghĩ. Tiến trình WebFlux của tôi có 200 luồng Tomcat chạy lẫn, làm Vert.x trông thắng 35%. Classpath sạch thì hai bên hoà.
Vượt trần vật lý. Pool 4 luồng, mỗi việc 5 ms, trần là 800 req/s — tôi đo được 3 345 (phần 48). Nguyên nhân là zsh không tách từ nên hai cờ JVM thành một tham số. Lần thứ ba trong sê-ri dính đúng lỗi shell đó.
Đo dữ liệu cũ. Sau khi giết một node, AsyncMap trả null và tôi suýt viết rằng cụm mất dữ liệu. Khoá đó được ghi ở lần dựng cụm trước. Ghi lại rồi đo lại thì đủ cả 5.
Dự đoán sai và bị số liệu cãi lại. Tôi viết một test để làm ví dụ về "xanh giả" và nó đỏ — ctx.succeeding che nhiều hơn tôi tưởng.
Bài học chung, và là thứ tôi muốn để lại nhất từ sê-ri này: luôn có một phép kiểm tra độc lập cho mỗi con số. Trần lý thuyết, một bộ đếm ở phía bên kia, pg_stat_activity, đếm luồng, đếm dòng log. Bất cứ thứ gì không đến từ cùng một đường ống với con số đang nghi ngờ.
Và khi một kết quả vừa bất ngờ vừa hợp với định kiến sẵn có của bạn, đó là lúc phải nghi ngờ nhất.
Đi tiếp từ đây
Sê-ri này cố ý không chạm tới ba thứ:
- Vert.x 5 — đã ra và bỏ hẳn API callback,
Futurethành mặc định. Mọi phép đo ở đây chạy trên 4.5.11; hình dạng kết quả sẽ giữ nguyên, con số thì nên đo lại. - Kotlin coroutine với Vert.x —
vertx-lang-kotlin-coroutineslàm mã bất đồng bộ đọc như mã tuần tự, và nó xoá gần hết phần khó đọc mà phần 41 đã bàn. - Vận hành thật ở quy mô lớn — mọi thứ ở đây chạy trên một máy. Cụm ba node là cụm ba tiến trình cùng máy, không phải ba máy qua mạng thật.
Nếu bạn chỉ nhớ được một câu từ năm mươi bài: đo trước khi tin, và đo lại khi kết quả làm bạn hài lòng.
Thử ba mươi giây
Bài cuối, nên phép thử cuối là phép thử chẩn đoán rẻ nhất trong cả sê-ri:
jcmd <pid> Thread.print | grep -oE '^"[^"]+' | sed 's/[-#]\?[0-9]*$//' | sort | uniq -c | sort -rn
Một dòng, không cài gì. Nó cho biết ứng dụng của bạn thật sự có bao nhiêu event loop, bao nhiêu luồng worker, và có nhóm luồng nào bạn không biết là mình đang chạy. Trong sê-ri này nó tìm ra Tomcat lẩn trong tiến trình WebFlux, tìm ra pool worker nở ra 200 luồng, và tìm ra rằng setInstances(16) không có nghĩa là 16 event loop.
Cảm ơn bạn đã đọc tới đây.