Cấu trúc và đặc điểm cốt lõi của ZooKeeper
ZooKeeper là một dịch vụ điều phối phân tán, hỗ trợ các tính năng nền tảng như: xuất/nhập dữ liệu, cân bằng tải, dịch vụ đặt tên, đồng bộ hóa phân tán, thông báo sự kiện và khóa phân tán.
Mô hình dữ liệu dạng cây
Dữ liệu trong ZooKeeper được tổ chức dưới dạng cây phân cấp gồm các nút gọi là znode. Mỗi znode có thể chứa dữ liệu (dưới dạng mảng byte) hoặc không. Các thao tác cơ bản bao gồm:
create(path, data): tạo znode tại đường dẫn chỉ định với dữ liệu kèm theo.delete(path): xóa znode.exists(path): kiểm tra sự tồn tại của znode.setData(path, data, version): cập nhật toàn bộ dữ liệu của znode.getData(path): đọc toàn bộ dữ liệu của znode.getChildren(path): lấy danh sách các znode con.
Lưu ý: ZooKeeper không hỗ trợ ghi/đọc từng phần dữ liệu — mọi thao tác đều phải xử lý toàn bộ nội dung.
Các loại znode
ZooKeeper hỗ trợ bốn loại znode:
- Persistent: tồn tại vĩnh viễn cho đến khi bị xóa thủ công.
- Ephemeral: tự động xóa khi phiên kết nối của client chấm dứt.
- Persistent_sequential: persistent + tự động gắn số thứ tự tăng dần vào tên.
- Ephemeral_sequential: ephemeral + số thứ tự.
Znode ephemeral không được phép có con — chúng chỉ có thể là lá trong cây.
Cơ chế watch và thông báo
Thay vì polling liên tục, client đăng ký watch trên một znode. Khi znode thay đổi (dữ liệu, danh sách con, hoặc bị xóa), ZooKeeper gửi thông báo một lần duy nhất. Sau khi nhận thông báo, client phải đăng ký lại nếu muốn tiếp tục theo dõi.
Do khoảng trống giữa thời điểm nhận thông báo và đăng ký lại, client nên đọc lại trạng thái mới nhất trước khi thiết lập watch mới để tránh mất dữ liệu.
Phiên bản (version)
Mỗi znode có trường version tự tăng mỗi khi dữ liệu thay đổi. Các thao tác setData và getData có thể yêu cầu kiểm tra version để đảm bảo tính nhất quán trong môi trường đa luồng — thao tác chỉ thành công nếu version khớp.
Chế độ vận hành
ZooKeeper hoạt động ở hai chế độ:
- Standalone: một máy chủ đơn lẻ xử lý tất cả yêu cầu.
- Quorum (cluster): dữ liệu được sao chép trên nhiều node; một ghi được coi là thành công khi hơn một nửa node xác nhận.
Phiên làm việc (session)
Kết nối giữa client và server được gọi là session. Session có thời gian sống (timeout). Nếu server không nhận được bất kỳ tin nhắn nào từ client trong khoảng thời gian này, session bị coi là hết hạn và các znode ephemeral do client tạo sẽ bị xóa.
Client gửi heartbeat sau mỗi timeout/3, và bắt đầu tìm server khác sau 2*timeout/3.
Trạng thái session: NOT_CONNECTED → CONNECTING → CONNECTED. Khi mất kết nối, client quay về CONNECTING và thử kết nối lại.
Trong cụm (quorum mode), client phải kết nối tới node có trạng thái cập nhật mới nhất — được đảm bảo nhờ thứ tự toàn cục của các giao dịch (transaction ID).
Thực hành với ZooKeeper
Khởi động và sử dụng CLI
Sau khi giải nén, chạy:
./bin/zkServer.sh start
./bin/zkCli.sh -server localhost:2181
Một số lệnh phổ biến trong CLI:
create [-s] [-e] /path dataget /pathls /pathdelete /path
Tích hợp với Java qua ZkClient
Thêm phụ thuộc Maven:
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
Ví dụ tạo znode ephemeral:
ZkClient client = new ZkClient("localhost:2181");
client.create("/demo", "", CreateMode.EPHEMERAL);
// Node sẽ biến mất sau khi client disconnect
Ví dụ tạo node con ephemeral dưới node persistent:
client.create("/services", "", CreateMode.PERSISTENT);
client.create("/services/app1", "", CreateMode.EPHEMERAL);
client.create("/services/app2", "", CreateMode.EPHEMERAL);
List<String> children = client.getChildren("/services");
// Kết quả: ["app1", "app2"] (thứ tự không đảm bảo)
Ví dụ sử dụng sequential node:
client.create("/queue/item", "", CreateMode.EPHEMERAL_SEQUENTIAL);
client.create("/queue/item", "", CreateMode.EPHEMERAL_SEQUENTIAL);
// Tạo ra: /queue/item0000000001, /queue/item0000000002, ...
Ví dụ đăng ký lắng nghe thay đổi dữ liệu:
client.subscribeDataChanges("/config", new IZkDataListener() {
public void handleDataChange(String path, Object data) {
System.out.println("Dữ liệu mới: " + data);
}
public void handleDataDeleted(String path) {}
});
client.writeData("/config", "v2.0"); // Kích hoạt handleDataChange
Ứng dụng thực tế
Triển khai khóa phân tán
Phương án 1 (đơn giản nhưng gây "herd effect"):
- Client cố gắng tạo znode ephemeral tại
/lock. - Nếu thành công → có lock; thất bại → đăng ký watch trên
/lock. - Khi lock được giải phóng, tất cả client đang chờ cùng thử lại → lãng phí tài nguyên.
Phương án 2 (tối ưu, dùng sequential node):
- Mỗi client tạo ephemeral_sequential node dưới
/locks/, ví dụ:/locks/lock-0000000001. - Client kiểm tra xem node của mình có phải nhỏ nhất trong danh sách con không.
- Nếu có → chiếm lock; nếu không → đăng ký watch trên node liền trước.
- Khi node trước bị xóa, client kiểm tra lại — chỉ một client được đánh thức mỗi lần.
Phương án này loại bỏ herd effect, đảm bảo fairness và tự động giải phóng lock khi client crash.
Xây dựng registry service
ZooKeeper có thể đóng vai trò trung tâm đăng ký dịch vụ (service registry) như trong Dubbo:
- Thư mục gốc:
/dubbo - Mỗi service có thư mục riêng:
/dubbo/com.example.UserService - Bên trong gồm:
/providers: chứa các ephemeral node biểu diễn địa chỉ instance cung cấp service./consumers: chứa ephemeral node của các consumer.
Workflow:
- Provider khởi động → tạo ephemeral node trong
/providers. - Consumer khởi động → tạo ephemeral node trong
/consumersvà lấy danh sách provider. - Consumer đăng ký watch trên
/providersđể nhận thông báo khi có provider mới/thoát. - Khi provider crash → node ephemeral biến mất → consumer tự động cập nhật danh sách.
Cơ chế ephemeral node kết hợp watch giúp hệ thống tự động phát hiện sự cố và cân bằng tải động.