Zookeeper và ứng dụng trong hệ thống phân tán
Giới thiệu về Zookeeper
Zookeeper là một dịch vụ điều phối phân tán mã nguồn mở, được duy trì bởi Apache Software Foundation. Nó cung cấp các tính năng mạnh mẽ để quản lý thông tin cấu hình, dịch vụ đặt tên, đồng bộ hóa phân tán và nhóm dịch vụ trong môi trường phân tán.
Các đặc điểm chính của Zookeeper:
- Tính nhất quán tuần tự: Các yêu cầu cập nhật sẽ được áp dụng theo thứ tự gửi.
- Tính nguyên tử: Một thao tác hoặc thành công hoặc thất bại hoàn toàn.
- Giao diện hệ thống đơn nhất: Tất cả các client kết nối đều thấy cùng một giao diện.
- Tính tin cậy: Khi một bản cập nhật được áp dụng, nó sẽ tồn tại cho đến khi bị ghi đè.
- Tính thời gian thực: Giao diện mà client nhìn thấy đảm bảo cập nhật trong khoảng thời gian xác định.
Ứng dụng điển hình của Zookeeper:
- Quản lý cấu hình tập trung.
- Khóa phân tán cho cơ chế loại trừ lẫn nhau giữa JVM.
- Theo dõi trạng thái các nút trong cụm.
- Dịch vụ đặt tên tương tự DNS.
- Đội ngũ phân phối đơn giản.
- Lựa chọn leader trong hệ thống phân tán.
Cài đặt và cấu hình Zookeeper
Cài đặt chế độ đơn lẻ
wget https://example.com/zookeeper-3.7.0.tar.gz
tar -zxvf zookeeper-3.7.0.tar.gz
cd zookeeper-3.7.0
echo "tickTime=2000" > conf/zoo.cfg
echo "dataDir=/tmp/zk_data" >> conf/zoo.cfg
echo "clientPort=2181" >> conf/zoo.cfg
echo "initLimit=10" >> conf/zoo.cfg
echo "syncLimit=5" >> conf/zoo.cfg
bin/zkServer.sh start
bin/zkCli.sh -server 127.0.0.1:2181
Cài đặt chế độ cụm
# Tạo file zoo.cfg trên mỗi nút:
echo "tickTime=2000" > conf/zoo.cfg
echo "dataDir=/var/lib/zk" >> conf/zoo.cfg
echo "clientPort=2181" >> conf/zoo.cfg
echo "initLimit=10" >> conf/zoo.cfg
echo "syncLimit=5" >> conf/zoo.cfg
echo "server.1=node1:2888:3888" >> conf/zoo.cfg
echo "server.2=node2:2888:3888" >> conf/zoo.cfg
echo "server.3=node3:2888:3888" >> conf/zoo.cfg
# Tạo file myid trên mỗi nút:
echo 1 > /var/lib/zk/myid # Trên node1
echo 2 > /var/lib/zk/myid # Trên node2
echo 3 > /var/lib/zk/myid # Trên node3
Các thao tác cơ bản với Zookeeper
Thao tác lệnh cơ bản
ls /
create /app "Dữ liệu app"
create -e /app/tempnode "Dữ liệu tạm"
create -s /app/seqnode "Dữ liệu tuần tự"
get /app
set /app "Dữ liệu mới"
delete /app/seqnode0000000001
rmr /app
stat /app
Các loại nút trong Zookeeper
- Nút vĩnh viễn (PERSISTENT).
- Nút tạm thời (EPHEMERAL).
- Nút tuần tự vĩnh viễn (PERSISTENT_SEQUENTIAL).
- Nút tuần tự tạm thời (EPHEMERAL_SEQUENTIAL).
Mechanism Watch trong Zookeeper
get /app watch
# Trong một session khác, sửa đổi dữ liệu tại /app
set /app "Dữ liệu đã thay đổi"
# Session trước đó nhận được sự kiện NodeDataChanged.
API Java Client cho Zookeeper
Java Client gốc
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
</dependency>
import org.apache.zookeeper.*;
import java.util.concurrent.CountDownLatch;
public class ZKClient {
private static final String CONNECT_STRING = "localhost:2181";
private static final int SESSION_TIMEOUT = 5000;
private static ZooKeeper zk;
private static final CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, event -> {
if (Event.KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
});
connectedSemaphore.await();
// Thêm nút
zk.create("/test", "dữ liệu thử nghiệm".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// Lấy dữ liệu nút
byte[] data = zk.getData("/test", false, null);
System.out.println("Dữ liệu nút: " + new String(data));
// Sửa đổi dữ liệu nút
zk.setData("/test", "dữ liệu mới".getBytes(), -1);
// Xóa nút
zk.delete("/test", -1);
zk.close();
}
}
Curator Client
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
</dependency>
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class CuratorExample {
private static final String CONNECT_STRING = "localhost:2181";
private static final int SESSION_TIMEOUT = 5000;
public static void main(String[] args) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(CONNECT_STRING)
.sessionTimeoutMs(SESSION_TIMEOUT)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
// Tạo nút
client.create().forPath("/curator_node", "dữ liệu khởi tạo".getBytes());
// Lấy dữ liệu nút
byte[] data = client.getData().forPath("/curator_node");
System.out.println("Dữ liệu nút: " + new String(data));
// Sửa đổi dữ liệu nút
client.setData().forPath("/curator_node", "dữ liệu thay đổi".getBytes());
// Xóa nút
client.delete().forPath("/curator_node");
client.close();
}
}
Tích hợp SpringBoot với Zookeeper
Xây dựng dự án SpringBoot
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Lớp cấu hình
@Configuration
public class ZKConfig {
@Value("${zookeeper.connect-string}")
private String connectString;
@Bean(initMethod = "start", destroyMethod = "close")
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.builder()
.connectString(connectString)
.sessionTimeoutMs(5000)
.connectionTimeoutMs(3000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.namespace("demo-springboot")
.build();
}
}
Lớp dịch vụ
@Service
public class ZKService {
@Autowired
private CuratorFramework client;
public String createNode(String path, String data, CreateMode mode) throws Exception {
return client.create()
.creatingParentsIfNeeded()
.withMode(mode)
.forPath(path, data.getBytes());
}
public String getNodeData(String path) throws Exception {
byte[] data = client.getData().forPath(path);
return new String(data);
}
public Stat updateNodeData(String path, String data) throws Exception {
return client.setData().forPath(path, data.getBytes());
}
public void deleteNode(String path) throws Exception {
client.delete().guaranteed().forPath(path);
}
}
Lớp Controller REST
@RestController
@RequestMapping("/zk")
public class ZKController {
@Autowired
private ZKService zkService;
@PostMapping("/node")
public String createNode(@RequestParam String path, @RequestParam String data,
@RequestParam(defaultValue = "PERSISTENT") String mode) throws Exception {
return zkService.createNode(path, data, CreateMode.valueOf(mode));
}
@GetMapping("/node")
public String getNodeData(@RequestParam String path) throws Exception {
return zkService.getNodeData(path);
}
@PutMapping("/node")
public String updateNodeData(@RequestParam String path, @RequestParam String data) throws Exception {
zkService.updateNodeData(path, data);
return "Cập nhật thành công";
}
@DeleteMapping("/node")
public String deleteNode(@RequestParam String path) throws Exception {
zkService.deleteNode(path);
return "Xóa thành công";
}
}
Ứng dụng nâng cao của Zookeeper
Thực hiện khóa phân tán
@Service
public class DistributedLockService {
private static final String LOCK_PATH = "/locks";
@Autowired
private CuratorFramework client;
public boolean acquireLock(String lockName, long waitTime, TimeUnit timeUnit) {
InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH + "/" + lockName);
try {
return lock.acquire(waitTime, timeUnit);
} catch (Exception e) {
return false;
}
}
public void releaseLock(String lockName) {
InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH + "/" + lockName);
try {
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
} catch (Exception ignored) {}
}
}
Thực hiện trung tâm cấu hình
@Service
public class ConfigCenterService {
private static final String CONFIG_PATH = "/configs";
private final Map configCache = new ConcurrentHashMap<>();
@Autowired
private CuratorFramework client;
@PostConstruct
public void init() throws Exception {
loadAllConfigs();
watchConfigChanges();
}
private void loadAllConfigs() throws Exception {
List<String> children = client.getChildren().forPath(CONFIG_PATH);
for (String child : children) {
String path = CONFIG_PATH + "/" + child;
byte[] data = client.getData().forPath(path);
configCache.put(child, new String(data));
}
}
private void watchConfigChanges() {
CuratorCache cache = CuratorCache.build(client, CONFIG_PATH);
CuratorCacheListener listener = CuratorCacheListener.builder()
.forCreates(node -> configCache.put(node.getPath().replace(CONFIG_PATH + "/", ""), new String(node.getData())))
.forChanges((oldNode, node) -> configCache.put(node.getPath().replace(CONFIG_PATH + "/", ""), new String(node.getData())))
.forDeletes(node -> configCache.remove(node.getPath().replace(CONFIG_PATH + "/", "")))
.build();
cache.listenable().addListener(listener);
cache.start();
}
public String getConfig(String key) {
return configCache.get(key);
}
}
Đăng ký và phát hiện dịch vụ
@Service
public class ServiceRegistry {
private static final String REGISTRY_PATH = "/services";
@Autowired
private CuratorFramework client;
@Value("${server.port}")
private int port;
@PostConstruct
public void register() throws Exception {
if (client.checkExists().forPath(REGISTRY_PATH) == null) {
client.create().creatingParentsIfNeeded().forPath(REGISTRY_PATH, "Service Registry".getBytes());
}
String ip = InetAddress.getLocalHost().getHostAddress();
String serviceInstance = ip + ":" + port;
String servicePath = client.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(REGISTRY_PATH + "/instance-", serviceInstance.getBytes());
}
}
@Service
public class ServiceDiscovery {
private static final String REGISTRY_PATH = "/services";
private final List<String> serviceInstances = new ArrayList<>();
@Autowired
private CuratorFramework client;
@PostConstruct
public void init() throws Exception {
discoverServices();
watchServices();
}
private void discoverServices() throws Exception {
serviceInstances.clear();
List<String> instances = client.getChildren().forPath(REGISTRY_PATH);
for (String instance : instances) {
String instancePath = REGISTRY_PATH + "/" + instance;
byte[] data = client.getData().forPath(instancePath);
serviceInstances.add(new String(data));
}
}
private void watchServices() {
CuratorCache cache = CuratorCache.build(client, REGISTRY_PATH);
CuratorCacheListener listener = CuratorCacheListener.builder()
.forCreates(node -> discoverServices())
.forChanges((oldNode, node) -> discoverServices())
.forDeletes(node -> discoverServices())
.build();
cache.listenable().addListener(listener);
cache.start();
}
public List<String> getAllServiceInstances() {
return new ArrayList<>(serviceInstances);
}
}
Lưu ý trong môi trường sản xuất
Tối ưu hóa hiệu suất Zookeeper
- Tách biệt thư mục dữ liệu và nhật ký giao dịch.
- Tùy chỉnh JVM heap size và GC parameters.
- Cấu hình tự động dọn dẹp snapshot cũ.
- Hạn chế số lượng kết nối client.
Theo dõi và vận hành
- Sử dụng các lệnh bốn chữ để kiểm tra trạng thái server.
- Kích hoạt JMX monitoring.
- Quản lý log level và policy cuộn log.
Giải quyết các vấn đề thường gặp
- Vấn đề kết nối: Kiểm tra firewall, trạng thái service, phiên bản tương thích.
- Lỗi nút đã tồn tại: Sử dụng API có version number, kiểm tra tồn tại trước khi thao tác.
- Hết hạn phiên: Tăng timeout, triển khai logic reconnect sau khi hết hạn.