Bạn đăng ký nhận một tờ báo, có thư xác nhận đàng hoàng, rồi ngồi đợi số đầu tiên mãi không tới — vì toà soạn chưa từng cho máy in chạy. Đăng ký của bạn hợp lệ, không lỗi, chỉ là chẳng có gì được gửi. listen() của Vert.x Config mà thiếu scanPeriod hành xử đúng như vậy: nó nhận đăng ký của bạn, biên dịch trơn, chạy không lỗi — và không bao giờ gọi lại. Bài này nói về cấu hình trong Vert.x, và cái bẫy im lặng đó là chỗ đáng dừng lại lâu nhất.

Vert.x không có application.properties tự động như Spring Boot. Cấu hình là một module riêng, bạn khai rõ lấy từ đâu — đổi lại là kiểm soát hoàn toàn, và một cái bẫy im lặng.

Nhiều nguồn, gộp lại theo thứ tự khai báo

var opts = new ConfigRetrieverOptions()
    .addStore(new ConfigStoreOptions().setType("file").setFormat("json")
              .setConfig(new JsonObject().put("path", "ung-dung.json")))
    .addStore(new ConfigStoreOptions().setType("sys"));       // thuoc tinh he thong

Tệp khai ten = "tu-tep", thuộc tính hệ thống khai ten = "tu-thuoc-tinh-he-thong":

kết quả: ten = tu-thuoc-tinh-he-thong        -> nguồn khai SAU thắng
chỉ-có-ở-tệp vẫn còn? true                   -> gộp chứ không thay thế

Đảo thứ tự hai addStore:

kết quả: ten = tu-tep

Hai luật, và cả hai đều đơn giản: nguồn khai sau ghi đè nguồn khai trước, và những khoá chỉ có ở một nguồn thì vẫn còn nguyên — đây là phép gộp, không phải phép thay thế.

Thứ tự thực dụng: tệp mặc định trước, rồi tệp theo môi trường, rồi biến môi trường, rồi thuộc tính hệ thống. Cái nào cụ thể hơn thì khai sau.

Nạp nóng chạy tốt — nếu bạn khai một dòng

var opts = new ConfigRetrieverOptions()
    .setScanPeriod(1000)                     // DONG NAY
    .addStore(...);

cr.listen(ch -> capNhat(ch.getNewConfiguration()));
scanPeriod = 1 000 ms -> phát hiện thay đổi sau 499 ms
giá trị mới: ten = da-doi

Sửa tệp, và trong vòng nửa giây listen chạy với cấu hình mới. Chính xác và nhanh hơn tôi tưởng — 499 ms với chu kỳ quét 1 giây, tức nó bắt được ngay ở lần quét kế tiếp.

Và đây là cái bẫy

Bỏ đúng một dòng setScanPeriod, giữ nguyên mọi thứ khác:

sau 3 giây, số lần listen được gọi: 0
giá trị trong bộ nhớ: (vẫn là giá trị cũ)
Không có scanPeriod, ConfigRetriever không quét lại bao giờ. listen() vẫn nhận đăng ký của bạn, không ném lỗi, không cảnh báo — nó chỉ không bao giờ chạy. Mã biên dịch được, chạy được, và tính năng "cấu hình nóng" của bạn đơn giản là không tồn tại.

Đây là kiểu lỗi tệ nhất vì nó âm thầm và trông giống như đã xong việc. Cách duy nhất phát hiện là thử sửa tệp và xem listener có chạy không — đúng phép thử ở cuối bài.

Chọn nguồn nào

Kiểu store Dùng khi
file (json, yaml, properties) Giá trị mặc định đi cùng ứng dụng
env Biến môi trường — chuẩn de facto cho container
sys Thuộc tính hệ thống, tiện để ghi đè lúc chạy thử
http Máy chủ cấu hình tập trung
kubernetes ConfigMap và Secret, đọc thẳng từ API

Với http và kubernetes thì scanPeriod mới thật sự đáng giá: cấu hình đổi ở nơi khác và ứng dụng tự nhận, không cần khởi động lại.

Nhưng cân nhắc kỹ cái gì nên nạp nóng được. Đổi ngưỡng cảnh báo hay mức log lúc chạy thì tốt; đổi chuỗi kết nối CSDL lúc chạy thì bạn phải tự lo chuyện đóng pool cũ, và không có gì trong Vert.x làm hộ. Nạp nóng cho bạn giá trị mới, không cho bạn cách áp dụng nó an toàn.

Ba điều nên làm

Khai scanPeriod nếu bạn dùng listen. Không có nó thì listen là mã chết.

Đặt nguồn cụ thể nhất ở cuối. Bảng đầu bài là luật, và nó ngược với trực giác của một số người quen với thứ tự ưu tiên "cái đầu tiên thắng".

Kiểm tra cấu hình lúc khởi động. Vert.x Config trả về JsonObject trần — không có kiểu, không có validation, không báo lỗi khi thiếu khoá. config.getString("db.url") trả null và ứng dụng cứ thế chạy tiếp.

Cách duy nhất chắc chắn để biết nạp nóng có thật sự chạy là thử: đăng ký một listener in ra mốc thời gian, rồi sửa tệp cấu hình.

cr.listen(ch -> System.out.println("cau hinh doi luc " + System.currentTimeMillis()));
// roi sua tep cau hinh

Sửa tệp mà không thấy dòng nào in ra nghĩa là bạn đang ở đúng cái bẫy trên — tính năng nạp nóng bạn tưởng đã có thật ra chưa bao giờ chạy.

Mẫu số chung

Một điểm dễ tưởng đã xong mà chưa: được báo có thay đổi không giống với áp dụng thay đổi đó một cách an toàn. Nạp nóng đưa cho bạn giá trị mới — nhưng việc chuyển sang dùng nó an toàn là của bạn. Đổi ngưỡng log thì chỉ cần gán biến; đổi chuỗi kết nối CSDL thì phải đóng pool cũ, mở pool mới, chờ truy vấn đang chạy xong, và Vert.x không làm hộ một bước nào. Cùng khoảng cách "tín hiệu đến rồi, nhưng chuyển trạng thái mới là việc khó" ở một SIGHUP reload cấu hình nginx, một feature-flag đổi giữa chừng một request đang xử lý, một chứng chỉ TLS xoay vòng mà kết nối cũ vẫn ôm bản cũ. Nguyên tắc: một cơ chế "nghe thay đổi" mới chỉ giải nửa bài — nửa còn lại là vòng đời của cái bị thay (đóng cái cũ, dựng cái mới, không làm rơi việc đang dở), và nửa đó gần như luôn là nửa khó.

Điều thứ hai: cấu hình không được kiểm là một quả bom hẹn giờ đặt xa chỗ nổ. JsonObject trần trả null cho khoá gõ sai, ứng dụng khởi động ngon lành, rồi ngã vào 3 giờ sáng ở một NullPointerException cách nguyên nhân thật mười tầng gọi hàm và vài giờ đồng hồ. Đây là lý do phải kiểm cấu hình ngay lúc khởi động và fail-fast: đọc mọi khoá bắt buộc, ép kiểu, kiểm ràng buộc, và nếu thiếu thì chết ngay tại boot với một thông báo nói rõ khoá nào — chứ đừng để một getString lặng lẽ trả null trôi vào hệ thống. Cùng nguyên tắc "parse, đừng validate rải rác" ở một biến môi trường bắt buộc, một secret chưa mount, một schema request: biến một lỗi cấu hình từ sự cố runtime mù mờ thành một lỗi khởi động rõ ràng — vì chỗ rẻ nhất để một cấu hình sai chết là ngay giây đầu tiên, trước khi nó nhận lưu lượng thật.

Bài sau: health check và readiness — hai thứ hay bị khai nhầm cho nhau, và cái giá lúc triển khai.