Phần trước đo xem mô hình event loop đổi được gì. Bài này dựng dự án và mổ đơn vị triển khai của Vert.x — Verticle — vì vòng đời của nó có một cái bẫy không báo lỗi.
Dự án tối thiểu
Vert.x không có "starter" hay auto-configuration. Một phụ thuộc là đủ:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.11</version>
</dependency>
public class App {
public static class MayChu extends AbstractVerticle {
@Override public void start(Promise<Void> p) {
vertx.createHttpServer()
.requestHandler(r -> r.response().end("ok"))
.listen(18080)
.onSuccess(s -> p.complete())
.onFailure(p::fail);
}
}
public static void main(String[] a) {
Vertx.vertx().deployVerticle(new MayChu());
}
}
Không annotation, không quét classpath, không context. Vertx.vertx() tạo ra event loop và mọi thứ khác là mã bạn viết.
Vòng đời: start và stop đều bất đồng bộ
Đây là điểm khác biệt lớn nhất so với các khung quen thuộc: start() nhận một Promise, và Vert.x coi verticle là đã triển khai xong chỉ khi promise đó hoàn tất.
Cho start() ngủ 300 ms rồi mới complete():
start() trên luồng vert.x-eventloop-thread-0
deploy trả về sau 308 ms
deployVerticle chờ đủ 308 ms. Nghĩa là bạn khai báo được thứ tự khởi động thật: verticle CSDL sẵn sàng rồi mới tới verticle HTTP, không cần cờ hay vòng lặp chờ.
stop() cũng vậy, và nó chạy trên đúng luồng event loop đã chạy start():
stop() trên luồng vert.x-eventloop-thread-0
undeploy xong
Nhiều bản sao, mỗi bản một event loop
vertx.deployVerticle(MayChu.class.getName(),
new DeploymentOptions().setInstances(4));
start() trên luồng vert.x-eventloop-thread-2
start() trên luồng vert.x-eventloop-thread-3
start() trên luồng vert.x-eventloop-thread-4
start() trên luồng vert.x-eventloop-thread-5
Bốn bản sao, bốn luồng khác nhau. Đây là cách Vert.x dùng hết số nhân: không phải một verticle chạy nhiều luồng, mà nhiều bản sao verticle, mỗi bản một luồng riêng. Mỗi bản sao vì thế không cần khoá gì cả — trạng thái bên trong nó chỉ có một luồng chạm vào.
Câu hỏi tự nhiên: bao nhiêu bản sao thì đủ? Tôi đo lại endpoint của phần trước ở 5 000 kết nối, chỉ đổi số bản sao:
| Số bản sao | Thông lượng | p95 |
|---|---|---|
| 1 | 87 029 req/s | 126,3 ms |
| 4 | 98 584 req/s | 101,1 ms |
| 16 | 101 336 req/s | 87,1 ms |
Một bản sao đã đạt 86% thông lượng của mười sáu. Tôi vào phép đo này với giả định ngược lại và số liệu bác bỏ nó — lý do là handler ở đây chỉ đặt một timer chứ không tính toán gì, nên một event loop không hề bị bão hoà. Số bản sao mua được nhiều nhất là ở cột p95, và nó chỉ thật sự quan trọng khi handler có việc tốn CPU.
Nói cách khác: setInstances không phải nút tăng tốc mặc định. Đo trước khi đặt nó bằng số nhân.
Cái bẫy: start() không bao giờ complete
public static class Quen extends AbstractVerticle {
@Override public void start(Promise<Void> p) {
vertx.createHttpServer().requestHandler(...).listen(18080);
// quên p.complete()
}
}
sau 3 giây, deploy đã hoàn tất chưa? CHƯA — treo vĩnh viễn, không lỗi, không timeout
deployVerticle đơn giản là không bao giờ hoàn tất, và nếu bạn xâu chuỗi khởi động bằng compose thì toàn bộ chuỗi đứng im. Ứng dụng không chết, không log gì, chỉ là mọi thứ phía sau không bao giờ chạy.
Hai cách tránh: dùng bản start() không tham số khi phần khởi tạo là đồng bộ (Vert.x tự coi là xong khi hàm trả về), và khi dùng bản có Promise thì luôn nối onSuccess/onFailure vào mọi nhánh — kể cả nhánh lỗi, vì p.fail() bị quên cũng treo y hệt.
Khởi động và kích thước gói
Cùng một endpoint trả "ok", đóng thành fat jar, đo thời gian từ lúc chạy tới lúc phục vụ được:
| Thời gian khởi động | Kích thước fat jar | |
|---|---|---|
| Vert.x | 0,18 s | 6,1 MB |
| Spring Boot MVC | 0,76 s | 20,0 MB |
Nhanh hơn 4,2 lần và nhẹ hơn 3,3 lần. Với dịch vụ chạy suốt ngày thì nửa giây khởi động không quan trọng; với hàm chạy theo yêu cầu, container co giãn liên tục, hoặc một bộ test dựng ứng dụng hàng chục lần thì nó cộng dồn rất nhanh.
Cái giá đi kèm cũng nằm ngay trong bảng: 6,1 MB đó không có dependency injection, không có ORM, không có auto-configuration. Mọi thứ bạn cần, bạn tự nối.
Thử ba mươi giây
vertx.deployVerticle(new MayChu())
.onSuccess(id -> System.out.println("deploy xong: " + id))
.onFailure(Throwable::printStackTrace);
Luôn nối hai nhánh đó vào mọi lần deploy. Nếu chạy mà không dòng nào in ra, bạn vừa gặp đúng cái bẫy ở trên — và nếu không nối thì bạn còn chẳng biết là mình đang gặp.
Bài sau: luật duy nhất của event loop, và đo xem một handler chặn 50 mili giây làm hỏng bao nhiêu request của người khác.