Kubernetes 底座、Watch 与 ZooKeeper 对比
基于 etcd 3.5 / K8s 1.30 · 核于 2026-08
速查
- etcd 是 K8s 唯一状态存储:K8s 把所有集群状态(Pod/Service/Deployment/ConfigMap/Secret)都存进 etcd,apiserver 是唯一读写入口,controller-manager/scheduler/kubelet 通过 Watch etcd 驱动控制循环。etcd 挂 = K8s 瘫(已有 Pod 继续跑但无法调度增删)。
- 资源 = KV:每个 K8s 资源在 etcd 是一条 KV,key 用层级化路径(
/registry/pods/<ns>/<name>、/registry/services/<ns>/<name>),value 是 JSON 序列化的资源对象。 - Watch 是控制循环根基:controller/scheduler/kubelet 调 apiserver 的 Watch 接口(底层是 etcd Watch),etcd 一变就推送,组件据此「看实际状态趋近期望状态」。Watch 基于 revision,断线重连从上次 revision 续看,不丢事件。
- Revision + MVCC:每次写 etcd 全局 revision 递增。K8s 的 resourceVersion 就是 etcd revision——并发更新时 apiserver 用它做乐观锁(CAS),revision 不匹配则冲突重试。
- 命名服务(业务层):用 etcd KV + Watch + Lease 拼注册中心——服务启动 put
/services/order/instance-1(带 lease),调用方 get/services/order/(prefix)拿实例列表,服务挂了 lease 过期 key 自动删。CoreDNS、Registrator、gRPC 的 naming 就是这套。 - 分布式锁:
etcdctl lock /my-lock(封装 Lease+txn)或 concurrency 包,跟 Consul 的 Session-Lock 同理。 - etcd vs ZooKeeper:Raft(易理解)vs ZAB(不透明);Go 轻量 vs JVM 重型;扁平 KV vs 树形 znode;流式 Watch vs 一次性 Watch(3.6 前);K8s 主力 vs Hadoop/Kafka 主力。etcd 渐替代 ZK。
- etcd vs Consul/Nacos:etcd 是纯 KV 底座(无业务服务发现语义,要自拼);Consul/Nacos 是业务服务发现开箱即用(注册/健康检查/DNS/配置中心)。业务微服务发现选 Consul/Nacos,做 K8s 底座或自研协调才用 etcd。
一、etcd 作为 K8s 状态存储
K8s 是声明式系统——用户声明「我要 3 个 nginx Pod」,K8s 持续工作直到实际状态匹配。所有状态都存 etcd:
用户:kubectl create -f deployment.yaml (replicas: 3)
│
▼
apiserver(唯一读写入口)
│
│ PUT /registry/deployments/default/nginx
│ {"replicas":3, "template":...}
▼
etcd: revision=N+1
/registry/deployments/default/nginx = {...}
── Watch 驱动 ──
controller-manager Watch /registry/deployments/ → 发现 replicas=3
→ 创建 3 个 Pod(写 /registry/pods/...)
scheduler Watch /registry/pods/ (未调度) → 分配 node → 更新 pod.spec.nodeName
kubelet Watch /registry/pods/?nodeName=self → 调 containerd 拉起容器- 一切皆 KV:Deployment、Pod、Service、ConfigMap、Secret、Node 在 etcd 都是
/registry/<kind>/<ns>/<name>的 KV,value 是资源 JSON。 - apiserver 网关:所有组件只连 apiserver,apiserver 内部连 etcd。apiserver 做认证/鉴权/准入/序列化,etcd 只管存。
- resourceVersion = revision:K8s 资源的
metadata.resourceVersion就是 etcd 的 revision。并发更新时 apiserver 用它做乐观锁——A、B 都读到 revision=100 的 Pod,A 先改成功(revision 变 101),B 带 revision=100 改会冲突(OptimisticLockError),B 重试。 - etcd 挂的后果:apiserver 无法读写 →
kubectl全部失败、controller 无法 reconcile、不能调度新 Pod。但已在运行的 Pod 不受影响(kubelet 本地缓存,短期不依赖 etcd)。所以 etcd 备份是 K8s 灾难恢复的核心。
二、Watch 机制:K8s 控制循环的根基
etcd 的 Watch 是 K8s 声明式控制循环的物理基础:
bash
# 监听单个 key
etcdctl watch /config/db/host
# 监听前缀(K8s 常用,监听一类资源)
etcdctl watch --prefix /registry/pods/default/
# 输出(key 变化时实时推送):
# PUT /registry/pods/default/nginx-xxx {...} revision=101
# DELETE /registry/pods/default/nginx-yyy revision=102- 基于 revision:每个 Watch 事件带 revision。客户端记录「已处理到 revision=X」,断线重连时
etcdctl watch --rev=X+1,从断点续看,不丢事件、不重复。 - 流式持续:etcd 3.x 的 Watch 是 gRPC 双向流(不像 ZK 3.6 前的一次性 Watch),持续推送直到客户端关闭。
- 历史回看:可指定起始 revision,看「从某 revision 到现在」的所有变化——用于组件重启后追上进度(如 controller 重启后,先 list 全量再从 list 的 revision 开始 watch)。
- K8s 控制循环:每个 controller 都 Watch 自己关心的资源前缀,etcd 一变就触发 reconcile(实际 vs 期望),形成「期望状态驱动」的自愈系统。
三、用 etcd 拼业务服务发现(命名服务)
etcd 本身没有「服务发现」语义,但用 KV + Watch + Lease 能拼出来(gRPC、CoreDNS、Registrator 都这么做):
bash
# 1. 服务启动:注册自己(带 lease,宕机自动注销)
LEASE_ID=$(etcdctl lease grant 30 | awk '{print $2}')
etcdctl put /services/order/instance-1 '{"ip":"10.0.0.5","port":8080}' --lease=$LEASE_ID
etcdctl lease keep-alive $LEASE_ID & # 后台续约
# 2. 调用方:查实例列表(prefix 查询)
etcdctl get /services/order/ --prefix
# → /services/order/instance-1 {"ip":"10.0.0.5","port":8080}
# → /services/order/instance-2 {"ip":"10.0.0.6","port":8080}
# 3. 调用方:Watch 实例变化(实时感知扩缩容)
etcdctl watch --prefix /services/order/
# 实例上线/下线时推送,调用方更新本地实例列表
# 4. 服务宕机:lease 过期 → key 自动删除 → Watch 推送 DELETE → 调用方摘除- 这是「自拼注册中心」:etcd 只给原语,要自己写注册/查询/心跳/负载均衡逻辑。Consul/Nacos 把这些封装好了开箱即用,etcd 要团队自研或用 gRPC naming 这类库。
- gRPC 默认服务发现:gRPC 的客户端负载均衡默认支持 etcd 做命名后端(
grpc.NewServiceResolver),就是上面这套 KV+Watch+Lease。 - CoreDNS + etcd:CoreDNS 有 etcd 插件,把 etcd KV 暴露成 DNS(
myservice.default.svc.cluster.local→ 实例 IP),是 K8s 服务发现的底层之一(不过 K8s 主要用 CoreDNS + apiserver,etcd 间接)。
四、分布式锁
bash
# 方式 1:etcdctl lock 命令(封装好了)
etcdctl lock /my-lock -- echo "I hold the lock"
# 拿到锁才执行 echo,释放后下一个等待者获取
# 方式 2:concurrency 包(编程式,Go 客户端)
# mutex := concurrency.NewMutex(session, "/my-lock")
# mutex.Lock(ctx) // 内部 Lease + txn 抢占
# defer mutex.Unlock(ctx)- 原理:跟领导选举一样,Lease + compare-and-swap。第一个抢到
/my-lock/lease-xxx的持锁,释放(删 key 或 lease 过期)后其他等待者抢。 - 公平性:etcd 的 concurrency 包支持公平锁(FIFO,按请求顺序排队),靠创建有序的 lease key 实现。
五、etcd vs ZooKeeper 详细对比
| 维度 | etcd | ZooKeeper |
|---|---|---|
| 起源 | CoreOS 2013,对标 ZK | Yahoo 2008,Apache 顶级 |
| 共识协议 | Raft(2014,论文清晰) | ZAB(2008,论文不透明) |
| 实现语言 | Go(单二进制) | Java(JVM,部署重) |
| 数据模型 | 扁平 KV + prefix | 树形 znode(路径树) |
| 临时节点 | Lease(key 带 TTL) | Ephemeral Node(连接断开删) |
| Watch | 流式持续(gRPC stream) | 3.6 前一次性(要重注册),3.6+ 持续 |
| 多版本 | MVCC(按 revision 读旧值) | 无(只能读当前) |
| 事务 | TXN(if-then-else on keys) | 多操作原子(create/set check) |
| 跨 DC | 需集群联邦 | 观察者节点(Observer,手动配) |
| 主要用户 | Kubernetes、CoreDNS、TiKV、Rook | Hadoop、Kafka(旧版)、HBase、Spark |
| 运维 | 轻量,snapshot 备份简单 | 重型,JVM 调优 + snapshot+log |
| 现状 | CNCF 毕业,活跃,新项目首选 | 成熟但渐被 etcd 替代,新项目少用 |
- 为什么 etcd 赢了:①Raft 比 ZAB 易理解易实现;②Go 单二进制比 JVM 部署轻;③K8s 选了 etcd 带飞生态;④流式 Watch 比 ZK 一次性 Watch 好用。
- ZK 的存量:Hadoop/YARN/HBase/Kafka(旧版,新版 Kafka 用 KRaft 去 ZK)仍用 ZK,这些是大数据生态的核心。新分布式项目(TiKV、CockroachDB、Rook)多选 etcd/Raft。
六、选型:etcd vs Consul vs Nacos
要做 K8s 底座 / 自研分布式协调服务? → etcd
业务微服务发现(要开箱即用)?
→ 中国 Spring Cloud → Nacos
→ 国际/多 DC/DNS → Consul
→ 已有 K8s 基础设施 → 用 K8s Service(底层 etcd)即可
要做 Hadoop/Kafka 协调(存量生态)? → ZooKeeper- 层级关键:etcd 是基础设施层底座(K8s 等系统构建在它之上);Consul/Nacos 是应用层注册中心(业务服务直接用它发现彼此)。两者不在同一层级,不是直接替代关系。
- 业务微服务:除非你自研框架或有特殊定制需求,否则业务服务发现选 Consul/Nacos 更省事(开箱即用,不必自己拼 KV+Watch+Lease)。
下一步
理解了 K8s 底座与用法后,下一步看参考——Raft 术语速查、etcd API 清单、一致性级别表、易错点与命令速查,作为速查手册。