11. ZooKeeper 与分布式协调

ZooKeeper 核心原理:ZAB 协议、Watcher 机制、分布式锁、Leader 选举、配置管理与命名服务

ZooKeeper 是 Apache 的分布式协调服务,为分布式应用提供一致性数据存储、配置管理、命名服务、分布式锁和集群管理。

1. 核心特性

特性说明
顺序一致性客户端的更新按发送顺序执行
原子性更新操作要么全部成功,要么全部失败
单一视图所有客户端看到相同的数据视图
可靠性写入一旦完成,数据将持续有效
实时性在一定时间内,客户端能读到最新数据

2. ZAB 协议 (ZooKeeper Atomic Broadcast)

ZAB 是 ZooKeeper 的一致性协议,类似 Raft 但专为协调服务设计。

2.1 协议阶段

阶段 1: 崩溃恢复(Leader 选举)
  - 选举拥有最新数据的节点为 Leader
  - Leader 同步数据给 Follower

阶段 2: 消息广播(正常运行)
  - 所有写请求转发给 Leader
  - Leader 生成 ZXID,广播给 Follower
  - 过半 Follower ACK 后提交

2.2 ZXID 结构

ZXID: 64 位
  高 32 位: epoch(Leader 轮次)
  低 32 位: 事务计数(单调递增)

3. 数据模型

ZooKeeper 提供类文件系统的树形命名空间:

/
├── zookeeper          # 内置节点
│   └── config
├── services           # 服务注册
│   ├── user-service
│   │   ├── node-001  [10.0.1.1:8080]
│   │   └── node-002  [10.0.1.2:8080]
│   └── order-service
├── config             # 配置管理
│   └── database
│       └── url       [jdbc:mysql://...]
└── locks              # 分布式锁
    └── order-123

节点类型:

类型说明
Persistent持久节点,客户端断开后仍存在
Ephemeral临时节点,会话结束自动删除(用于服务注册)
Sequential顺序节点,自动附加递增序号

ZK 应用场景全景

场景 1:分布式配置中心

// 配置集中管理,所有节点监听变更
// 优点:配置变更实时生效,无需重启应用
public class DistributedConfig {
    private static final String CONFIG_PATH = "/config/app";
    
    public void init() {
        // 1. 加载配置
        loadConfig(CONFIG_PATH);
        
        // 2. 注册监听器
        zk.watch(CONFIG_PATH, event -> {
            if (event.getType() == NodeDataChanged) {
                loadConfig(CONFIG_PATH);  // 热更新
                log.info("配置已热更新");
            }
        });
    }
}

场景 2:分布式唯一 ID 生成

// 利用 Sequential 节点特性生成全局唯一 ID
String path = zk.create("/seq/id-", data, EPHEMERAL_SEQUENTIAL);
// 返回 /seq/id-0000000001,序号即唯一 ID
long id = Long.parseLong(path.substring(path.lastIndexOf('-') + 1));

场景 3:Master 选举

所有节点竞争创建同一个临时节点,创建成功者成为 Master,其他节点监听该节点的删除事件。

节点 A 创建 /election/master (成功) → A 成为 Master
节点 B 创建 /election/master (失败) → B 监听 /election/master
节点 A 崩溃 → /election/master 自动删除
节点 B 收到事件 → B 创建 /election/master → B 成为 Master

适用边界:存储状态数据 < 1MB。ZK 不是通用数据库,其设计目标是协调而非存储。

ZooKeeper 提供类文件系统的树形命名空间:

/
├── zookeeper          # 内置节点
│   └── config
├── services           # 服务注册
│   ├── user-service
│   │   ├── node-001  [10.0.1.1:8080]
│   │   └── node-002  [10.0.1.2:8080]
│   └── order-service
├── config             # 配置管理
│   └── database
│       └── url       [jdbc:mysql://...]
└── locks              # 分布式锁
    └── order-123

节点类型:

类型说明
Persistent持久节点,客户端断开后仍存在
Ephemeral临时节点,会话结束自动删除(用于服务注册)
Sequential顺序节点,自动附加递增序号

4. Watcher 机制

// 一次性 Watch
zk.getData("/config/database", new Watcher() {
    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == EventType.NodeDataChanged) {
            // 重新读取并重新注册 Watch
            byte[] data = zk.getData("/config/database", this, null);
            System.out.println("配置更新: " + new String(data));
        }
    }
}, null);

// Curator 框架的持久 Watch
curator.watchers().add().withMode(AddWatchMode.PERSISTENT)
    .usingWatcher((CuratorWatcher) event -> {
        System.out.println("事件: " + event);
    }).forPath("/services");

注意:Watch 是一次性的,触发后需重新注册。

5. 分布式锁

5.1 排他锁

// Curator 互斥锁
InterProcessMutex lock = new InterProcessMutex(curator, "/locks/resource-1");
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        // 执行业务逻辑
    }
} finally {
    lock.release();
}

5.2 共享锁(读写锁)

InterProcessReadWriteLock rwlock = new InterProcessReadWriteLock(
    curator, "/locks/rw-resource");

// 读锁
InterProcessMutex readLock = rwlock.readLock();
readLock.acquire();

// 写锁
InterProcessMutex writeLock = rwlock.writeLock();
writeLock.acquire();

5.3 羊群效应优化

原始方案:所有客户端 Watch 父节点,导致大量通知。
优化方案:每个客户端只 Watch 前一个顺序节点。

public class ImprovedDistributedLock {
    public void lock() throws Exception {
        // 1. 创建临时顺序节点
        String node = curator.create()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath("/locks/mylock/lock-");
        
        // 2. 获取所有子节点
        List<String> nodes = curator.getChildren().forPath("/locks/mylock");
        Collections.sort(nodes);
        
        // 3. 如果是第一个,获取锁
        if (node.endsWith(nodes.get(0))) {
            return;
        }
        
        // 4. 找到前一个节点,只 Watch 它
        int index = nodes.indexOf(node.substring(node.lastIndexOf('/') + 1));
        String prevNode = nodes.get(index - 1);
        
        CountDownLatch latch = new CountDownLatch(1);
        curator.watchers().add()
            .usingWatcher((org.apache.zookeeper.Watcher) event -> {
                if (event.getType() == Event.EventType.NodeDeleted) {
                    latch.countDown();
                }
            }).forPath("/locks/mylock/" + prevNode);
        
        // 5. 等待前一个节点删除
        latch.await();
    }
}

6. Leader 选举

// Curator Leader 选举
LeaderSelector selector = new LeaderSelector(curator, "/election/leader", 
    new LeaderSelectorListenerAdapter() {
        @Override
        public void takeLeadership(CuratorFramework client) throws Exception {
            System.out.println("成为 Leader!");
            // 保持领导地位(此方法不返回则保持领导权)
            Thread.sleep(Long.MAX_VALUE);
        }
    });
selector.autoRequeue();
selector.start();

7. 配置管理

// 配置中心
public class ZkConfigCenter {
    private static final String CONFIG_PATH = "/config/app";
    private final Map<String, String> configCache = new ConcurrentHashMap<>();
    
    public void init() throws Exception {
        // 加载配置
        List<String> keys = curator.getChildren().forPath(CONFIG_PATH);
        for (String key : keys) {
            byte[] data = curator.getData().forPath(CONFIG_PATH + "/" + key);
            configCache.put(key, new String(data));
        }
        
        // 监听变更
        curator.watchers().add().withMode(AddWatchMode.PERSISTENT_RECURSIVE)
            .usingWatcher(event -> reloadConfig())
            .forPath(CONFIG_PATH);
    }
}

8. 与 etcd 对比

特性ZooKeeperetcd
协议ZABRaft
API专用客户端gRPC + HTTP
Watch一次性长期流式
事务多版本并发简单事务
K8s旧版用原生集成
性能万级 TPS万级 TPS
生态Hadoop/HBase/KafkaK8s/CoreDNS/TiKV

ZooKeeper 性能调优与运维

连接数与 Watch 数限制

单个 ZooKeeper 节点的连接数上限约为 6 万(受文件描述符限制),Watch 数量过多会导致内存压力。监控指标:

# ZK 四字命令查看状态
echo mntr | nc localhost 2181
# 关键指标:
# zk_num_alive_connections    当前连接数
# zk_watch_count              Watch 总数
# zk_approximate_data_size    数据总量
# zk_open_file_descriptor_count 文件描述符使用
# zk_max_file_descriptor_count  文件描述符上限

ZK 集群部署方案

模式节点数容错能力适用场景
单机1无开发测试
集群31 节点故障生产标准
集群52 节点故障高可用需求
集群7+3+ 节点故障不推荐(选举延迟增加)

奇数法则:ZK 集群节点数应为奇数。5 节点集群和 6 节点集群都最多容忍 2 个节点故障,但 6 节点写入时需要 4 个 ACK,延迟更高。

延伸阅读

  • https://plumephp.com/distributed-id-generation/ — 分布式 ID 生成策略对比
  • https://plumephp.com/observability-kubernetes/ — K8s 集群监控与 etcd 运维
  • https://plumephp.com/event-driven-architecture-domain-events/ — 分布式协调与事件驱动

总结

ZooKeeper 是分布式协调领域的经典实现,ZAB 协议、Watcher 机制和临时节点设计至今仍被广泛借鉴。在 Hadoop、Kafka、HBase 等大数据生态中,ZK 的地位短期内难以撼动。

然而,对于新项目,建议优先考虑 etcd:

  1. API 现代化:gRPC + HTTP/JSON,多语言 SDK 更丰富
  2. K8s 原生集成:作为 K8s 的默认数据存储,云原生项目的自然选择
  3. Watch 更优雅:长期流式 Watch,无需反复注册
  4. Raft 协议:更简洁的共识算法,社区理解度更高

选型决策应根据技术栈生态匹配:大数据项目选 ZK,云原生项目选 etcd,通用协调需求两者皆可。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「distributed-systems」更多文章

  1. 边缘计算架构与就近接入
  2. 分布式系统成本优化与容量治理
  3. 单体到分布式:遗留系统迁移与绞杀者模式