Hình dung việc khởi động một Verticle như một khâu trên dây chuyền phải bấm đèn xanh cho khâu sau bắt đầu. Vert.x không coi verticle là xong khi hàm start chạy hết, mà khi bạn bấm cái đèn đó — gọi Promise.complete(). Cơ chế này cho bạn xếp thứ tự khởi động rất gọn; nhưng nó cũng giấu một cái bẫy: làm xong hết việc mà quên bấm đèn thì cả dây chuyền đứng im, không còi, không lỗi. 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.
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. Thói quen rẻ nhất là nối luôn hai nhánh vào chính lời deployVerticle:
vertx.deployVerticle(new MayChu())
.onSuccess(id -> System.out.println("deploy xong: " + id))
.onFailure(Throwable::printStackTrace);
Nếu chạy mà không dòng nào in ra, bạn vừa gặp đúng cái bẫy này — và nếu không nối thì bạn còn chẳng biết là mình đang gặp.
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.
Mẫu số chung
Trong một vòng đời bất đồng bộ, "xong" là một tín hiệu bạn phải chủ động phát ra — không phải chuyện hàm của bạn chạy tới dòng cuối. Vert.x chỉ coi verticle đã lên khi bạn gọi complete(); làm hết việc mà quên bấm cái tín hiệu ấy thì cả chuỗi đứng im trong câm lặng, vì với kẻ đang chờ thì "chưa xong" và "treo vĩnh viễn" trông y hệt nhau. Mọi thế giới promise/future đều có đúng cái lỗ này: một Promise của JS không bao giờ resolve, một WaitGroup.Done() của Go bị quên hay một channel done không bao giờ đóng, một callback không bao giờ được gọi. Quy tắc gói gọn: trên mỗi nhánh bất đồng bộ, đúng một tín hiệu kết thúc phải nổ ra — và điều đó bao gồm cả nhánh lỗi, vì một fail() bị quên treo hệt như một complete() bị quên.
Điều thứ hai: một framework tối giản đổi tiện lợi lấy quyền kiểm soát, tốc độ khởi động và kích thước — và đó là một đánh đổi thật, không phải món hời một chiều. Vert.x khởi động nhanh 4,2 lần và nhẹ 3,3 lần chính vì không có DI, không ORM, không auto-config: cần gì bạn tự nối nấy. Bên nào thắng là câu hỏi về vòng đời và về đội ngũ (một dịch vụ sống lâu gần như không thấy nửa giây kia, còn một hàm serverless hay một bộ test dựng lại ứng dụng hàng chục lần thì thấy liên tục), và nó là cùng cái trục tự-dựng-hay-mua-sẵn đằng sau mọi lựa chọn "nhẹ so với đầy đủ pin" — Flask so với Django, Express so với Nest. Tiện lợi bạn không phải trả tiền là tiện lợi bạn không gỡ ra được khi nó vướng chân; còn quyền kiểm soát bạn nhận về là phần việc từ nay bạn tự gánh.
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.