Skip to content

协调服务:ZooKeeper 与 etcd

本文是分布式系统系统学习系列的 L2 核心篇。前置:[28. 共识算法:从 Paxos 到 Raft 的推导]。 学完可以配合面试题食用:07-分布式锁实现方案对比12-service-discovery-zk-etcd-nacos-consul-cap

什么是协调服务,为什么需要它

分布式系统里,多个节点需要就"谁做主"、"配置变了没"、"锁在哪"这些元信息达成一致。你不能把选主结果存 MySQL——MySQL 本身就是那个需要协调的分布式系统。所以有了专门的协调服务:存元数据,不存业务数据

典型场景:配置中心(推送配置变更)、选主(决定谁当 leader)、分布式锁(防止并发踩数据)、服务发现(问 etcd 要 provider 地址列表)。协调服务就是分布式系统的"控制面板"。

ZK 数据模型:树 + 临时 + 顺序 + Watch

ZooKeeper 把数据组织成树形 znode 结构,类似文件系统,但每个节点可以存数据(默认 1MB 上限,实测过大会拖垮性能)。

三种特殊 znode 组合起来就是 ZK 协调能力的核心:

  • 持久节点:create /path data,手动删除才消失
  • 临时节点:create -e /path data,session 断开自动消失——这是 ZK 做服务发现的基石
  • 顺序节点:create -s /path data,路径自动加 10 位递增序号——靠这个实现公平锁

Watch 机制:客户端在 znode 上设 watch,节点变化时触发一次通知。注意"一次性"——触发后必须重新设 watch,否则下次变化收不到。这是新手最容易踩的坑:设了 watch 忘重设,变更全丢。

etcd 数据模型:扁平 Key + MVCC + Lease + Watch

etcd 走的是另一条路:扁平 key-value 存储 + 多版本并发控制。每个 key 的每次修改都产生一个新修订号(revision),你可以按 revision 范围 watch 历史变更。

三个核心概念:

  • Revision:全局单调递增的版本号,相当于 etcd 的全局时钟。Get 带 revision 参数可以做"读历史快照"。
  • Lease 租约:类似 ZK 的 session,但更灵活——一个 lease 可以绑定多个 key,续约只需续租约本身。key 在租约到期时批量清理。
  • Watch持续流式推送,不是一次性触发。客户端通过 gRPC stream 连接,收到 event 后继续等下一个。不用重设。
go
// etcd 流式 watch 的核心逻辑(简化)
func watchKey(cli *clientv3.Client, key string) {
    rch := cli.Watch(context.Background(), key)
    for wresp := range rch {
        for _, ev := range wresp.Events {
            fmt.Printf("Type: %s Key:%s Value:%s\n",
                ev.Type, ev.Kv.Key, ev.Kv.Value)
        }
    }
}
// 收到事件后自动继续监听,不需要重设 watch

ZAB vs Raft:协议实现差异

ZK 用的 ZAB 协议和 Raft 同属 leader-based 共识,但关键区别:

维度ZK (ZAB)etcd (Raft)
leader 选举先选 leader 再同步数据,两个阶段一次性选举,一个任期只有一个 leader
写请求全部走 leader;follower 拒绝写请求全部走 leader;follower 转发给 leader
读请求任意节点可读(可能读到旧数据)默认 leader 读(线性一致);follower 读可能旧
会话机制Session(超时断开自动删临时节点)Lease(租约绑定 key,续约保活)
超时粒度tickTime 整数倍(通常 2s)electionTicks 倍数(通常 1s)

ZK 的一个设计权衡:读不保证线性一致。如果客户端 A 写成功,客户端 B 从 follower 读,可能看到旧值。要读最新必须调 sync 后再读,代价是性能。

etcd 默认走 ReadIndex 读:leader 确认自己仍是 leader(heartbeat 验证),然后返回当前 commited 日志的数据。比 ZK 在读一致性上更严格。

分布式锁实现对比:ZK vs etcd

这是实际工程里最常用的场景之一。

ZK 实现:临时顺序节点 + watch 前驱

shell
# ZK 分布式锁实验(zkCli.sh)
create -e -s /lock/lock-
# 返回 /lock/lock-0000000001
# 取所有子节点,排序,如果自己是第一个——获得锁
# 如果不是——watch 前一个节点,等它删除
ls /lock
# [lock-0000000001, lock-0000000002]
# 如果我的序号是 0000000002,我就 watch /lock/lock-0000000001
# 前一个节点删除时触发 watch,我检查自己是否变成最小序号了

优点是公平锁(按顺序排队),适合选主场景。缺点是"惊群":释放锁时所有等待者同时被唤醒,在高并发下压力大。ZK 官方建议锁的竞争节点数控制在 100 以内。

etcd 实现:事务 + Lease 续期

go
// etcd 分布式锁核心逻辑(伪代码)
func tryAcquire(cli *clientv3.Client, lockKey, myId string, ttl int64) bool {
    lease := cli.Grant(ctx, ttl)                // 1. 创建租约
    txn := cli.Txn(ctx).
        If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)).
        Then(clientv3.OpPut(lockKey, myId, clientv3.WithLease(lease.ID))).
        Else()
    resp, _ := txn.Commit()
    if resp.Succeeded {
        go keepAliveLoop(cli, lease.ID)          // 2. 后台续租
        return true
    }
    return false
}

func release(cli *clientv3.Client, lockKey, myId string) {
    // 比较+删除,防止 A 释放 B 的锁
    txn := cli.Txn(ctx).
        If(clientv3.Compare(clientv3.Value(lockKey), "=", myId)).
        Then(clientv3.OpDelete(lockKey)).
        Commit()
}

etcd 的锁是非公平的:谁的事务先 commit 成功谁拿锁,后面的尝试失败就重试。需要公平锁就自己包一层排队逻辑。

关键细节:CreateRevision 检查 key 不存在则创建,保证了原子性取锁。释放时用事务检查 value 是否还是自己的 ID,防止持有者超时后锁被续给新客户端,旧客户端释放时误删别人的锁。

选型建议

  • 需要公平锁 + 强一致性 -> ZK
  • 需要高频续期 + 流式 watch + gRPC -> etcd(云原生生态默认)
  • 你的团队已经在用哪套了 -> 就用那套(协调服务迁移成本高,别为了换而换)

典型用法对照

服务发现:ZK 用临时节点注册服务,客户端 watch 子节点列表变化。etcd 用 lease 绑定 key,同样 watch 前缀。两者的客户端都维护本地缓存,避免每次调用都查服务端。

配置中心:ZK 把配置做成持久 znode,客户端 watch 内容变化。etcd 也是类似,但优势是历史版本可查(按 revision Get),方便回滚。

选主:ZK 竞争创建临时节点,谁成功谁是 leader,leader 挂了自动释放。etcd 基于 lease + 事务,谁续约成功谁是 leader,续约失败自动触发重新选主。

常见误区与小结

  • Watch 不是事件队列:ZK 的 watch 是一次性的,丢了就丢了。etcd 的流式 watch 更可靠,但积压太多事件时 gRPC 流可能被限流。
  • 协调服务不是 KV 数据库:etcd 有 2GB 存储上限,ZK 默认 1MB 每节点。拿来做业务缓存迟早出事。
  • Session/Lease 超时设置:设太短网络抖动就断连,设太长故障发现慢。生产环境 ZK 用 10-40s,etcd 用 5-15s,看网络质量。
  • 读性能 vs 一致性的取舍:ZK 让 follower 读但可能旧,etcd 默认 leader 读性能有上限。都不适合做读密集型缓存。
  • etcd 的 watch 流是顺序的:同一个 key 的多个事件按 revision 顺序推送,但 watch 不同 key 的事件之间没有全局顺序。

小结:ZK 和 etcd 解决了同一个问题——分布式系统里"元数据怎么达成一致"——但走了不同的技术路线。ZK 以树形模型+临时节点闻名,API 经典但 watch 机制有易用性缺陷。etcd 以扁平模型+流式 watch+gRPC 协议后来居上,成为 Kubernetes 的默认选择。理解两者差异,本质上是理解了"用共识算法做一个协调服务"这一设计空间的主要决策点。下一篇讲 30. 分布式事务全景,从协调扩展到跨库原子性。

参考

参考:ZooKeeper 官方文档(Zab 协议)、etcd 官方文档(Raft 实现)、《Designing Data-Intensive Applications》第 8 章(分布式一致性)

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1