想象你是一栋 30 层写字楼的物业经理。楼里入驻了几十家公司(微服务),每家公司都需要向物业中心查水电工电话(服务发现)、预约会议室(分布式锁)、了解业委会主任是谁(领导者选举)、查看公告栏的最新通知(配置管理)。一旦物业中心自己先乱了套,全楼都会停摆——这就是分布式系统中“协调服务”存在的意义。 现实世界里,最出名的两套“物业中心系统”就是 ZooKeeper 和 Etcd。ZooKeeper 像一家老牌行政中心:档案按部门、项目、服务逐层锁进档案柜(树形数据模型),所有文件由主任按严格顺序签发(ZAB 协议),还有成熟的临时登记牌和排队号制度。Etcd 则像一家新式政务大厅:资料全部录入智能平板(扁平键值存储),采用“轮值班长”制度(Raft 共识协议),通知通过群聊实时推送,而不是靠保安跑腿喊话。两套系统都能协调全楼秩序,但办事风格、适用场景截然不同。 本篇将把 ZooKeeper 和 Etcd 放到同一张工作台上,从核心职责、数据模型、一致性协议、核心机制、API 设计、性能特点、典型场景与选型决策八个维度逐一拆解对比,帮你建立直觉,选出最适合自己系统的“物业中心”。

岗位关联

在云原生与大规模分布式系统建设中,协调服务就是系统的“地基”。掌握 ZooKeeper 与 Etcd 的区别和选型逻辑,能有效避免架构踩坑,是后端与基础架构工程师的核心竞争力。

目录
  1. 可视化演示
  2. 核心职责
  3. 数据模型
  4. 一致性协议
  5. 核心机制
  6. API设计
  7. 性能对比
  8. 典型场景
  9. 选型决策
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示ZK树形与Etcd KV数据模型、ZAB与Raft共识协议流程及性能指标对比,交互切换理解两大协调服务差异。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+modeData.model.caption+'

核心职责

写字楼的物业中心,首要职责是“让全楼只认一个公告栏”。无论哪家公司要发布新装修通知、更换安全负责人,都得先到物业中心登记,再由中心统一广播。分布式系统也一样:几十上百个服务节点运行在不同机器上,它们需要共享一份“全网公认的事实”——谁还活着、哪个节点是主、配置变成什么了。这两套协调服务的核心职责,就是提供这个唯一可信的中央协调点。

ZooKeeper 和 Etcd 都属于分布式协调服务,它们在架构上有一个共同点:集群由多个节点组成,通过“多数派投票”来决定事务是否成功。为什么需要多数派?因为任何一次重大决策(比如选主、提交配置变更),只要有超过一半的节点确认,即使部分节点宕机,系统依然知道自己没有“分叉”。这种“少数服从多数,宁缺毋滥”的机制,保证了协调结果在全网范围内强一致。

而“多数”到底是多少?设集群总节点数为被 $n$,法定人数为 $q$,那么必须满足 $q > n/2$,即严格超过一半。

回到比喻:老城区政务中心和新城区政务中心虽然办事风格不同,但都有一个硬规矩——办成任何大事,必须让过半数的窗口都签字确认。这就是协调服务的第一性原理:用多数派共识换取全网统一的秩序。

q=⌊n2⌋+1\displaystyle q = \left\lfloor \frac{n}{2} \right\rfloor + 1
法定人数公式:一个 5 节点集群需要 3 个节点确认才能提交事务;要容忍任意 2 台宕机。
  1. n(节点总数)集群中参与投票的服务器数量。假设你部署了 5 台协调服务器,那么 $n = 5$。
    n\displaystyle n
  2. ⌊n/2⌋(向下取整)计算机计算“一半”之后向下取整。5 台机器的一半是 2.5,向下取整得到 2。
    ⌊n2⌋\displaystyle \left\lfloor \frac{n}{2} \right\rfloor
  3. +1(严格多数)在取整结果上加一,确保是“超过一半”而不是“恰好一半”,避免投票打平。
    q=⌊n2⌋+1\displaystyle q = \left\lfloor \frac{n}{2} \right\rfloor + 1

数据模型

走进老城区政务中心的档案室,你会看到一排排档案柜:第一层是“部门”,第二层是“项目组”,第三层才是具体“服务实例”。要找一份材料,必须先知道它在哪个柜、哪一格、第几层,这就是 ZooKeeper 的树形数据模型。而新城区政务中心的办事员手里只有一台平板:所有材料都拍平了存在一张大表里,每份材料有一个唯一编号,直接搜编号就能拿到,这就是 Etcd 的扁平键值模型。

具体来说,ZooKeeper 把数据组织成一棵“目录树”,树上的每个节点叫 znode,路径形如 /app/cart/instance1。它就像文件系统:一层套一层,天然表达“归属关系”。znode 还分为持久节点(文件一直存在)、临时节点(会话结束自动消失)、顺序节点(自动编号)等类型。Etcd 则放弃层级,把所有数据都当成扁平的 key-value:key = "app/cart/instance1",value 是任意字节串。这个 key 看起来像路径,但只是字符串,没有父子关系,也不存在“删了父节点连带删子节点”的级联效应。

数据模型决定了两个系统的“操作心智”:

老中心的档案系统层级分明、谁属于谁一目了然,适合表达复杂归属;新中心的平板则简单直接、没有“剪不断理还乱”的父子牵绊,适合海量扁平数据的快速读写。树与表,是这两套系统给人的第一印象。

Ptree=/a/b/c⟷Kkv="a/b/c"\displaystyle P_{tree} = /a/b/c \qquad \longleftrightarrow \qquad K_{kv} = \text{"a/b/c"}
同一个配置项,ZooKeeper 中是一条从根到叶子逐层展开的路径,Etcd 中是一个完全扁平的键;两者在语义上等价,但在存储、查询和生命周期管理上差异巨大。
  1. P_tree(树形路径)ZooKeeper 的访问方式是从根 / 出发,沿层级逐段定位,路径有父子关系。
    Ptree=/a/b/c\displaystyle P_{tree} = /a/b/c
  2. K_kv(扁平键)Etcd 的 key 是一条完整字符串,斜杠只是普通字符,不表达层级。
    Kkv="a/b/c"\displaystyle K_{kv} = \text{"a/b/c"}
  3. 双向箭头(语义对应)两者虽然长得像,但左边需要逐层遍历,右边可以直接寻址;删除左边 /a 会连带删除 /a/b,删除右边 "a/b/c" 不会影响任何其他键。
    ⟷\displaystyle \longleftrightarrow

一致性协议

老城区政务中心有一套“主任签发制”:所有的重要文件必须由主任先起草(proposal),然后按名单顺序逐一发给各位窗口负责人确认,收到超过一半人的回执后,主任才会正式盖章发布。这个过程就叫 ZAB(ZooKeeper Atomic Broadcast)协议。它的核心特征是“一个 Leader 主导 + 全局严格的顺序号”:所有写操作都要经过 Leader,并且每个写请求都带一个全局递增的事务编号 zxid,谁编号大谁就更新。

新城区政务中心则采用“轮值班长制”,这就是 Raft 协议。系统里所有节点都有资格当班长,但每个“任期”(term)只能有一个班长。想当班长要先拉票,获得超过半数支持后上任;上任后所有日志都通过“复制到多数派”来确认。Raft 的排序规则同样依赖一个全局递增的“任期号 + 日志序号”,这与 ZAB 异曲同工。两者都能保证线性一致性(Linearizability):系统对客户端呈现的效果,就像只有一个单一副本在按顺序执行所有操作。

ZAB 用于区分的核心令牌是 zxid,它由两段拼接而成:

回到比喻:老中心的“主任签发制”让文件永远只有一个来源、一个顺序,严谨但更换主任时的交接流程繁琐;新中心的“轮值班长制”选举更灵活,同样保证任何时刻只有一个人说了算。两种制度殊途同归,都保证了“楼里永远只有一份权威公告”。

zxid=(epoch, counter)\displaystyle zxid = (epoch,\ counter)
想象“第 3 届主任签发的第 42 号文件”:epoch 是届数,counter 是这届主任任期内签发的第几号文件。任何两个文件的 zxid 都能比较出先后。
  1. epoch(领导纪元)每换一届 Leader,epoch 就加一。它就像一个“任期编号”,用来区分不同领导任期内的文件,防止旧领导的残留文件干扰新秩序。
    epoch\displaystyle epoch
  2. counter(事务计数器)同一届 Leader 任期内,每处理一个写事务就递增一次。它记录“这一届已经发了多少号文件”。
    counter\displaystyle counter
  3. 组合排序比较两个 zxid 时先看 epoch,epoch 大者一定更新;epoch 相同则看 counter,counter 大者更新。这样整个集群就有了一个全局唯一的“先后顺序”。
    (epoch1,counter1)<(epoch2,counter2)\displaystyle (epoch_1, counter_1) < (epoch_2, counter_2)

核心机制

老城区政务中心有几件招牌工具,新城区也各有对应。第一件是“临时登记牌”:来政务中心办事的临时人员先领一块牌子,人一走牌子立刻作废。ZooKeeper 的临时节点(Ephemeral Node)就是这块牌子——它绑定客户端会话,会话断开节点自动消失,用来记录“谁还活着”再好不过。第二件是“排队号”:大家先取号,按顺序叫号办理。ZooKeeper 的顺序节点(Sequential Node)会自动附加一个单调递增的序号,谁序号小谁先办,天然支持公平的分布式锁。

新城区政务中心的对应物是“电子租约”(Lease):所有事物都是“租”来的,租期到了必须续费,不续费就自动释放。Etcd 的租约机制比 ZooKeeper 的临时节点更灵活——一个租约可以绑定多个 key,续约一次全部延期。而通知方式差异更大:ZooKeeper 的 Watch 像“门卫大叔喊一嗓子”——数据一变,门卫通知你一次,之后你再想被通知就得重新跟门卫登记;Etcd 的 Watch 像“群里订阅推送”——只要不取消订阅,每次变更都会一直推给你,中途不会断流。

临时节点的生命期与客户端会话严格绑定,可以用下面这个等号表达:

从“一次性门卫通知”到“持续订阅推送”,从“临时登记牌”到“可批量续租的电子租约”,两种机制的设计哲学一目了然:老中心偏保守稳定,新中心更灵活高效。

life(znodeephemeral)=life(session)\displaystyle life(znode_{ephemeral}) = life(session)
通俗说:人走茶凉。一旦客户端与服务端之间维持的会话断开,ZooKeeper 会自动把该客户端创建的临时节点从目录树中删除。
  1. life(znode_ephemeral)临时节点在目录中存在的时间。它不是一个永久的文件,而是“活期账户”,随会话的生死而生死。
    life(znodeephemeral)\displaystyle life(znode_{ephemeral})
  2. life(session)客户端与集群之间保持连接的存活时间。只要客户端不断开、没有超过超时时间,会话就一直有效。
    life(session)\displaystyle life(session)
  3. 等号(同生共死)临时节点不会比它的会话活得更久。利用这个特性,服务实例宕机后,注册在 ZooKeeper 上的临时节点就会自动消失,其他节点立刻感知。
    life(znodeephemeral)=life(session)\displaystyle life(znode_{ephemeral}) = life(session)

API设计

如果你去老城区政务中心办事,会发现每个窗口只干一件事:领表窗口、盖章窗口、查询窗口……ZooKeeper 的 API 也是这样高度分化的:create 建节点、delete 删节点、exists 查存在、getData 读内容、setData 写内容、getChildren 列子节点。每个操作语义单一,开发者需要像指挥官一样把多个命令串起来完成复杂逻辑。

新城区政务中心则只有一个“综合终端”:Etcd v3 提供了统一的 gRPC 接口,核心操作就四个——Put(写入)、Get(读取)、Delete(删除)、Watch(监听),外加一个强大的 Txn(事务)。这个 Txn 接口特别像“一单式办理”:你可以一次提交多个条件判断和多个操作,要么全部成功,要么全部失败。其中最经典的就是 CAS(Compare-And-Swap):先检查版本的当前值,匹配了才写入。

CAS 的语义可以写成一条条件规则:

老中心的“专用窗口”命令虽多,但每个都很直观;新中心的“综合终端”命令虽少,但借助事务能力能表达更复杂的组合逻辑。如果你习惯用 SQL 里的 UPDATE ... WHERE version = ?,那么 Etcd 的 Txn 会让你倍感亲切。

if version(k)=v0 then put(k,v1)\displaystyle \text{if } version(k) = v_0 \text{ then } put(k, v_1)
好比政务中心规定:只有当材料编号还是 v0 时,你才能把它改成 v1。如果别人已经改过、版本号不再是 v0,你的修改就会被拒绝,从而避免“覆盖他人更新”。
  1. version(k)(版本号检查)读取键 k 当前的修订版本号。Etcd 中每个 key 维护一个版本号,每次修改都会递增。
    version(k)\displaystyle version(k)
  2. 条件判断将读到的版本号与期望值 v0 比较。相等说明这期间没人动过它,你有权修改;不相等说明有人抢先改了,你的操作应该失败。
    version(k)=v0\displaystyle version(k) = v_0
  3. then put(k, v1)条件满足时执行写入,把值更新为 v1。整个过程在事务内原子完成,不会出现“检查完又被别人插队”的竞态。
    put(k,v1)\displaystyle put(k, v_1)

性能对比

老城区政务中心办一次盖章要过三关:提交申请、主任审批、窗口确认,每个环节都要存档留痕,所以“写”很慢;但查询窗口散布各处,大家同时查也不会打架,所以“读”相对轻松。ZooKeeper 正是如此:所有写请求必须由 Leader 广播给多数派并落盘,一次写至少要等一次磁盘 fsync 和一轮网络往返;而读请求可以由任意 Follower 直接响应,天然做了负载均衡。

新城区政务中心则上了电子化系统:Etcd 将多个写请求打包成一沓,批量写入磁盘,提交效率更高。它的写入性能通常优于 ZooKeeper,这也解释了为什么 Kubernetes 这种超大规模集群敢于把 Etcd 当成唯一的数据存储。但 Etcd 的读路径需要经过 Raft 层的校验,大规模读压力下往往要配合缓存或 Watch 缓存来缓解。

一次写事务从发起到提交,延迟大致由两部分构成:多数派中落盘最慢的节点耗时,加上一轮网络往返:

在实际运维中,Etcd 依靠批量提交获得了更好的写吞吐,但频繁的磁盘抖动和网络延迟同样会拖垮它;ZooKeeper 写性能天花板较低,但它的读路径分散和成熟的 Watch 机制让它在传统业务中依然能打。性能没有绝对的胜负,只有是否匹配你的压力模型。

Tcommit=max⁡i∈Q(tfsync(i))+tRTT\displaystyle T_{commit} = \max_{i \in Q} \left( t_{fsync}^{(i)} \right) + t_{RTT}
想象政务中心要等签字最慢的窗口负责人签完字,再把文件来回快递一次。所以集群里最慢的节点,决定了你办一件事要等多久。
  1. t_fsync(落盘耗时)每个节点把日志写入磁盘所需的时间。磁盘越快,这步越短。
    tfsync(i)\displaystyle t_{fsync}^{(i)}
  2. max over Q(最慢的多数派)Q 是法定多数,取集合中最大的落盘耗时。Leader 必须等多数派都落盘,所以最慢的那台机器决定了整体延迟。
    max⁡i∈Q(tfsync(i))\displaystyle \max_{i \in Q} \left( t_{fsync}^{(i)} \right)
  3. t_RTT(网络往返)Leader 与 Follower 之间一次请求-确认的往返时间。消息在网络上的时间无法省略。
    tRTT\displaystyle t_{RTT}

典型场景

两个政务中心都能办四件大事,只是手法不同。第一件:配置管理。ZooKeeper 把配置放在树形目录里,适合“按服务分组”的配置;Etcd 则凭借流式 Watch 让配置一改、全网秒级生效,所以 Kubernetes 的 ConfigMap 用 Etcd 存储。第二件:服务发现。ZooKeeper 的临时节点天然适合做注册中心:服务启动时注册一个临时节点,宕机时节点自动消失,Dubbo、Kafka 等老牌框架都这么干;Etcd 在云原生场景同样可以,但需要自己借助 Lease 模拟“临时节点”。

第三件:分布式锁。ZooKeeper 的经典做法是“排队号锁”:多个客户端同时在同一个锁目录下创建临时顺序节点,谁序号最小谁就拿到锁;Etcd 则用事务 + 租约:通过 CAS 尝试写入锁 key,设置租约防止持锁者崩溃后锁不被释放。第四件:领导者选举。两者的思路完全一致——竞选者去某个路径下创建节点,谁成功谁就当 Leader。

排队号锁的判定规则,可以浓缩成一个条件式:

回到比喻:老中心靠“排队号”维护公平,新中心靠“抢单 + 租约”保证容错。无论哪种方式,四件差事——配置、发现、锁、选举——都能妥妥办妥,只是你需要知道在哪扇窗口办。

lockacq  ⟺  seqi=min⁡{seqj∣j∈Waiters}\displaystyle lock_{acq} \iff seq_i = \min \{ seq_j \mid j \in Waiters \}
排队办事:只有当你手里的号是所有排队号中最小的那个时,才轮到你办理。ZooKeeper 的分布式公平锁正是这样工作的。
  1. seq_i(自己的序号)每个竞争者创建顺序节点后获得一个递增序号,队伍里的人都有号。
    seqi\displaystyle seq_i
  2. min{seq_j}(当前最小号)等待集合中所有序号的最小值,也就是队伍最前面的那个人。
    min⁡{seqj}\displaystyle \min \{ seq_j \}
  3. iff(当且仅当)自己的序号恰好等于最小号时,锁才算获取成功;否则继续等待并监听前一个节点释放。
    seqi=min⁡{seqj∣j∈Waiters}\displaystyle seq_i = \min \{ seq_j \mid j \in Waiters \}

选型决策

最后的问题:这栋大楼到底该选哪家物业中心?答案取决于你楼里已经住了哪些公司。如果你的技术栈是 Hadoop、HBase、Kafka、Dubbo,这些组件很多已经把 ZooKeeper 写死在配置里,强行换 Etcd 等于连地基一起拆;反过来,如果你的主线是 Kubernetes、云原生、微服务网格,Etcd 就是绕不开的选择,它早已是 k8s 生态的标准组件。

选型时还要考虑一致性体验和运维成本。ZooKeeper 和 Etcd 都能提供强一致性,但 ZAB 的顺序广播对传统“通知—确认”模式更友好,Raft 的日志复制和批量提交则更适合高吞吐的现代化系统。运维上,ZooKeeper 历史悠久、资料多但配置项也多;Etcd 依靠 bolt 存储和内置的 etcdctl 工具,上手门槛普遍更低。规模化之后,Etcd 要特别注意磁盘性能,ZooKeeper 则要留意连接数上限。

如果把选型看作一个多目标决策,可以给每个候选方案打分再求和:

老牌物业资历深、兼容老系统;新锐物业现代化、拥抱云原生。两者没有绝对的优劣,只有“适不适合你当前的楼”。把生态、性能、运维、团队熟悉度放到决策表里过一遍,答案自然浮现。

decision∗=arg⁡max⁡i∑jwj×scorei,j\displaystyle decision^* = \arg\max_{i} \sum_{j} w_j \times score_{i,j}
选型就像挑物业:先列指标(生态、性能、运维、上手难度),再给每个指标定权重,最后算总分。哪个行政中心总分高,就签哪个。
  1. score_{i,j}(方案评分)第 i 个候选方案(ZooKeeper 或 Etcd)在第 j 个维度(生态、性能、运维等)的打分。
    scorei,j\displaystyle score_{i,j}
  2. w_j(指标权重)第 j 个维度的重要程度。比如你特别在意云原生,就把“云原生适配”的权重调高。
    wj\displaystyle w_j
  3. argmax(取总分最大者)对所有候选方案计算加权总分,选出分数最高的那个。
    arg⁡max⁡i∑jwj×scorei,j\displaystyle \arg\max_{i} \sum_{j} w_j \times score_{i,j}

AI 应用场景

模型服务注册与版本管理

阶段:模型服务化部署与版本发布

解决的问题:在 AI 推理集群中,模型服务实例经常上下线、版本频繁迭代。传统做法靠人工维护 IP 清单,扩容缩容时很容易漏改、错改。利用 Etcd 的扁平 KV 和流式 Watch,每个模型服务启动时向注册中心写入自己的地址与版本号,下线时通过租约自动清理;调用方通过 Watch 持续感知实例变化,实现无感知的服务发现和灰度切换。

具体案例:某推荐系统团队把数百个模型推理实例全部注册到 Etcd,特征平台通过 Watch 实时更新下游路由表,模型版本从 v12 升级到 v13 时无需重启任何网关。

分布式训练主节点选举

阶段:分布式训练任务的协调与容错

解决的问题:在大规模分布式训练中,需要从多个工作节点中选出一个“首席节点”负责全局梯度聚合和任务调度。如果主节点宕机,系统要快速感知并选出新主。利用 ZooKeeper 的临时节点和顺序节点,所有训练节点在协调目录下竞选,序号最小者当选;主节点会话断开时临时节点自动消失,其余节点收到 Watch 通知后立刻发起新一轮竞选。

具体案例:某深度学习平台用 ZooKeeper 管理海量训练任务的领导节点,单任务主节点故障恢复时间从分钟级压缩到秒级,训练中断损失大幅降低。

A/B 实验配置与特征开关管理

阶段:在线推理与实验调优

解决的问题:推荐系统每天要调整成百上千个特征开关和 A/B 实验参数,传统做法是改配置重启服务,代价高昂。借助 Etcd 的 Txn 事务与流式 Watch,配置中心把每条实验配置作为独立 key,支持带版本地原子更新;各推理节点订阅前缀,配置一变立即热加载,秒级灰度生效且不会读到中间状态。

具体案例:某电商推荐场景中,运营团队通过 Etcd 配置中心同时调控 200 多个实验开关,上线新策略不再需要发版,实验流量随时可调。

Python 演示

python87 行
# -*- coding: utf-8 -*-"""模拟 ZooKeeper 风格与 Etcd 风格的领导者选举流程"""  class ZKStyleElection:    """模拟 ZooKeeper 顺序节点小号优先 + 临时节点自动消失"""     def __init__(self):        self.sequence = 0        self.candidates = {}     def join(self, node_id):        """节点加入竞选, 模拟创建临时顺序节点并拿到序号"""        self.sequence += 1        self.candidates[node_id] = self.sequence        return self.sequence     def get_leader(self):        """选举结果: 序号最小的节点当选"""        if not self.candidates:            return None        return min(self.candidates, key=lambda n: self.candidates[n])     def leave(self, node_id):        """节点退出或会话断开, 临时节点自动消失"""        self.candidates.pop(node_id, None)  class EtcdStyleElection:    """模拟 Etcd 抢锁式选主 + 租约续期"""     def __init__(self):        self.revision = 0        self.term = 0        self.cur_leader = None     def try_campaign(self, node_id):        """模拟事务 CAS: 没有 Leader 时第一个写入者成功"""        self.revision += 1        if self.cur_leader is None:            self.cur_leader = node_id            self.term += 1            return True        return False     def renew(self, node_id):        """模拟租约续期: 只有当前 Leader 能续约"""        self.revision += 1        return node_id == self.cur_leader     def expire(self):        """模拟租约过期, Leader 身份失效"""        self.revision += 1        self.cur_leader = None  def main():    nodes = ["node-a", "node-b", "node-c", "node-d"]     print("======== ZooKeeper 风格: 顺序节点小号当 Leader ========")    zk = ZKStyleElection()    orders = {}    for n in nodes:        orders[n] = zk.join(n)        print("join %s seq=%s" % (n, orders[n]))    print("当前 Leader ->", zk.get_leader())     zk.leave(zk.get_leader())    print("Leader 掉线后新 Leader ->", zk.get_leader())     print()    print("======== Etcd 风格: 抢锁式选主 + 租约续期 ========")    etcd = EtcdStyleElection()    for n in nodes:        ok = etcd.try_campaign(n)        print("candidate %s granted=%s term=%s" % (n, ok, etcd.term))        if ok:            break    print("当前 Leader 续约 ->", etcd.renew(etcd.cur_leader))    print("非 Leader 续约 ->", etcd.renew("node-b"))     etcd.expire()    print("租约过期后 Leader ->", etcd.cur_leader)  if __name__ == "__main__":    main()
目的:用纯 Python 模拟两种协调服务的领导选举核心逻辑,直观对比 ZooKeeper 的“序号排队制”和 Etcd 的“抢锁 + 租约制”。
  1. self.candidates[node_id] = self.sequence模拟 ZooKeeper 创建顺序节点,每来一个竞选者自动获得递增序号。
  2. return min(self.candidates, key=lambda n: self.candidates[n])选举规则:序号最小的节点当选 Leader,这就是“小号优先”。
  3. self.candidates.pop(node_id, None)模拟临时节点生命周期:节点掉线后,其在目录树上的“登记牌”自动消失。
  4. self.term += 1模拟 Raft 的任期(term),每产生一届 Leader 任期号就递增一次。
  5. if self.cur_leader is None:模拟 Etcd 的事务 CAS:只有锁为空时第一个写入者才能胜出。
  6. return node_id == self.cur_leader租约续期:只允许当前 Leader 在租约过期前续约,防止其他节点越权。
  7. etcd.expire()租约过期后 Leader 失效,所有节点重新回到竞选状态,对应“持有者崩溃自动让位”。

常见误区

误区听说 ZooKeeper 是 CP 系统,那它是不是一遇到网络分区就彻底罢工、一个请求都处理不了?

正解ZooKeeper 在分区时依然可以服务,只是“只服务多数派一侧”。如果集群 5 台中有 3 台处于同一分区,这 3 台仍能正常选主、读写;少数派一侧的节点会拒绝写请求,但读请求依然可以返回旧数据。Etcd 同样如此。两者都是“多数派可用”,而非“全部不可用”。

为何常见:很多人把 CAP 理论简化为“CP=不可用”、“AP=可用”,忽略了“多数派”这个关键前提,导致误判系统的真实容错能力。

误区ZooKeeper 有目录层级,Etcd 只是一张扁平的 key-value 表,那 Etcd 的表达能力是不是更差?

正解扁平 KV 配合 key 前缀扫描完全可以覆盖绝大多数场景,而且更简单。Etcd 的 lease 可以模拟临时节点,Txn 可以模拟顺序节点逻辑;而树形结构在 ZooKeeper 中还会带来“删除父节点必须递归删除子节点”的额外心智负担。模型的“强弱”取决于你要表达什么,而不是形态是否复杂。

为何常见:人们容易把“看起来更复杂”等同于“更强大”,忽视了扁平结构在工程上的简洁性和可维护性。

误区两者都叫 Watch,那我用 ZooKeeper 的注册一次、触发一次的习惯去写 Etcd 的监听逻辑应该没问题吧?

正解两者机制完全不同。ZooKeeper 的 Watch 是一次性触发,事件发生后必须重新注册,否则后续变更不再通知;Etcd v3 的 Watch 是长连接流式推送,注册一个前缀就能持续收到所有变更,直到客户端主动取消。把 ZK 习惯带到 Etcd,会漏掉大量更新事件。

为何常见:名字相同让人放松警惕,再加上很多教程只讲“能监听变更”,不讲触发后的生命周期差异,埋下了隐蔽的 bug。

误区Etcd 性能好、API 友好、又是云原生标配,那是不是所有 ZooKeeper 场景都能无脑迁移到 Etcd?

正解替代成本的关键在“生态绑定”而非“能力差距”。Kafka、HBase、Dubbo 等大量框架在代码层深度依赖 ZooKeeper 的临时节点、顺序节点和 watch 语义,迁移等于重写这些框架的协调模块。如果现有系统已经稳定运行,贸然迁移带来的风险远超收益。

为何常见:技术选型时人们容易把“新技术更好”当成充分条件,忽略了存量系统的兼容性成本和迁移风险。

误区只要用 ZooKeeper 顺序节点或 Etcd 事务锁实现了分布式锁,就能保证所有并发安全,万事大吉。

正解分布式锁只是“互斥”手段,真正的安全还要考虑持锁者被 GC 暂停导致锁提前过期、网络分区导致旧主“复活”等问题。业界标准做法是引入 fencing token(令牌)机制:每次加锁拿到一个递增令牌,写操作必须携带令牌,存储端拒绝过期令牌,才能彻底避免“两个主同时写”。协调服务能给你好锁,但不能替你挡住所有分布式陷阱。

为何常见:单机锁的心智模型让人误以为“抢到锁 = 安全”,忽略了分布式环境下网络延迟、GC 停顿、时钟漂移等额外威胁。

面试题

Q1. ZooKeeper 采用的数据模型是下面哪一种?

单选题 · easy

  • A.树形分层目录结构← 正确答案
  • B.扁平键值对存储结构
  • C.文档型集合结构
  • D.图结构递归查询结构

答案:A。ZooKeeper 以 znode 组成树形目录,类似文件系统,路径从根 / 开始逐层展开。扁平键值对是 Etcd 的模型,文档集合是 MongoDB 的风格,图结构则对应图数据库,均不符合 ZooKeeper 的特点。

Q2. Etcd 使用的一致性协议是?

单选题 · easy

  • A.Raft 共识算法← 正确答案
  • B.Paxos 投票协议
  • C.ZAB 广播协议
  • D.PBFT 拜占庭容错

答案:A。Etcd 是 Raft 协议最著名的工业级实现,靠领导者选举和日志复制达成共识。ZAB 是 ZooKeeper 自研协议,Paxos 是理论模型,PBFT 面向拜占庭故障,均不是 Etcd 的选择。

Q3. ZooKeeper 临时节点的生命周期与什么绑定?

单选题 · easy

  • A.客户端会话存活时间← 正确答案
  • B.服务端进程启动时间
  • C.网络请求超时时间
  • D.磁盘剩余空间大小

答案:A。临时节点随会话创建,会话断开节点自动删除。这正是“人走牌消”的服务发现机制。Etcd 用租约 Lease 实现类似能力,而服务端进程、网络超时、磁盘空间都与临时节点生命周期无关。

Q4. 一个 5 节点的 ZooKeeper 集群,最多可以容忍几个节点宕机?

单选题 · mid

  • A.1 个节点宕机
  • B.2 个节点宕机← 正确答案
  • C.3 个节点宕机
  • D.4 个节点宕机

答案:B。法定人数 q = ⌊5/2⌋ + 1 = 3,集群只要有 3 台存活就能选主和提交。宕机 2 台还剩 3 台,仍然满足多数派;宕机 3 台只剩 2 台,无法选出 Leader,系统拒绝服务。所以最多容忍 2 台宕机。

Q5. Etcd v3 提供的 Txn 事务接口,最突出的能力是什么?

单选题 · mid

  • A.多个条件判断与更新原子执行← 正确答案
  • B.批量写入大幅提升吞吐
  • C.完全替代分布式锁全部功能
  • D.自动完成数据分库分表

答案:A。Txn 允许在 if-then-else 结构中对多个键做版本比较并原子执行更新,类似数据库中的 CAS。批量写入(B)是性能优化而非事务本质;分布式锁只是 Txn 的应用之一(C 说法绝对);分库分表与 Txn 无关。

Q6. 关于 ZooKeeper 的 Watch 通知机制,下列描述正确的是?

单选题 · mid

  • A.事件触发一次后需要重新注册← 正确答案
  • B.通知会持续流式推送给客户端
  • C.一次注册之后永久自动生效
  • D.只有写事件能触发通知机制

答案:A。ZooKeeper 的 Watch 是一次性触发器:节点数据变化时回调一次,之后失效,必须重新注册才能继续监听。Etcd 的 Watch 才是持续流式推送。永久生效、只限写事件的说法均不准确。

Q7. ZooKeeper 集群中,读请求主要由以下哪个角色直接响应?

单选题 · mid

  • A.任意 Follower 节点← 正确答案
  • B.必须由 Leader 节点
  • C.只能由客户端本地缓存
  • D.只能由 Observer 节点

答案:A。ZooKeeper 的读请求可以被任意节点(Follower 或 Observer)直接处理,以此分散读压力;写请求才必须交给 Leader 广播到多数派。Observer 只是只读节点中的一种,并非唯一读通道,本地缓存更不是官方读路径。

Q8. 相比 ZooKeeper,Etcd 的以下哪些特征属于其优势?

多选题 · hard

  • A.扁平 KV 接口更为简洁← 正确答案
  • B.Watch 支持流式持续推送← 正确答案
  • C.树形目录结构更直观
  • D.云原生生态围绕它构建← 正确答案

答案:ABD。扁平 KV 让数据读写更简单,流式 Watch 省去反复注册,Kubernetes 等云原生体系将 Etcd 作为标准存储,都是 Etcd 的突出优势。树形目录是 ZooKeeper 的特征,并非 Etcd 优势,故 C 不选。

Q9. 设计一个可靠的分布式锁,需要正确处理哪些问题?

多选题 · hard

  • A.持有者崩溃后自动释放锁← 正确答案
  • B.持锁时间过长允许续约← 正确答案
  • C.网络分区时避免出现双主← 正确答案
  • D.加锁操作具备原子性和顺序性← 正确答案

答案:ABCD。临时节点或租约解决崩溃释放(A);租约续期防止持锁者业务未完成锁却被回收(B);多数派共识和任期编号防止分区后双主(C);ZooKeeper 顺序节点或 Etcd 事务保证加锁的原子与有序(D)。四个方向都是分布式锁的必备考虑。

Q10. 关于 ZAB 与 Raft 两种协议的差异,下列说法正确的有?

多选题 · extreme

  • A.ZAB 用 zxid 标识全局事务顺序← 正确答案
  • B.Raft 用 term 与日志索引保证有序← 正确答案
  • C.两者都要求 Leader 追加提交通知消息
  • D.分区时两者都只服务多数派一侧← 正确答案

答案:ABD。ZAB 的 zxid 和 Raft 的 term+index 都是全局有序标识,说法 A、B 正确。分区时两类协议都只让多数派提供服务,D 正确。C 错误:ZAB 在提交阶段有明确的 commit 消息广播,而 Raft 的提交通过下一条 AppendEntries 中的 commitIndex 捎带通知,并非追加独立通知消息。