☰
Etcd分布式协调实战:从Raft原理到集群故障排查
2026/10/14 6:18:08 网站建设 项目流程

Etcd,一个在分布式系统圈子里熟悉到不能再熟悉的名字,经常被叫做“分布式系统的协调神器”。我第一次在项目里真正把它用起来的时候,并没有觉得它有多高大上——不就是个键值存储吗?后来频繁遇到选主失败、集群空间告警、数据被 compaction 影响这类问题,才开始明白,它能把“配置分发、服务发现、选主、分布式锁”这些其实特别容易翻车的动作,用一套强一致的协议模型统一起来。这篇文章我想按自己的理解,从原理到实操,再到故障排查,把 Etcd 的里里外外讲清楚,适合刚接触分布式协调的开发者,也适合已经在用但还没系统性踩过坑的运维和 SRE 同学。

1. 先搞清楚:分布式协调到底需要什么

1.1 为什么是 etcd 而不是数据库

很多人第一次看 Etcd,会觉得“这不就是个 KV 数据库吗,跟 Redis 有什么区别”。这个想法我也有过,直到我仔细看过它的设计逻辑才扭转过来。传统数据库或者缓存解决的是“数据存哪里、怎么查得快”,但分布式协调要解决的是“多个节点对同一个事实达成一致”。

举个例子:你的服务有 10 个副本,这些副本都需要知道自己当前是不是主节点。如果只是往数据库里写一个“我是主”,问题马上就来了——A 节点写“我是主”,B 节点也写“我是主”,两个节点都觉得自己是主,双主就出现了。要避免这个问题,不在于存得快,而在于写入必须经过“多数派确认”,并且一旦确认,所有节点都必须看到同一个结果。这正是 Raft 协议做的事情,也正是 Etcd 的核心。

所以 Etcd 不适合当成普通 KV 缓存去存大对象,它是给系统性的“状态决策”用的。这种定位决定了它有几个别人很难替代的优势:严格线性一致的读写、版本化数据模型、事件订阅机制、租约和键值过期能力,再加上一个靠谱的 Go 客户端库。把这些东西组合在一起,常见的协调场景就能用几十行代码落地。

1.2 一台 etcd 和三台 etcd 的区别

单机部署 Etcd 很容易,etcd命令直接起来就能读写。但单机没有协调的意义,因为单机宕机整个集群也就停了。生产环境通常至少三台,N 台节点组成的集群,Raft 要求大多数节点存活才能正常工作,也就是N/2 + 1。三台集群允许挂一台,五台集群允许挂两台。

这个“多数派”概念是整个 Etcd 的命门。很多新手在压测时习惯性只拉两台机器做集群,结果一台机器挂掉,集群直接不可用,因为两台节点里多数派至少需要两台都活着,任意一台故障就失去法定人数。两台甚至比一台更脆弱,这是我见过最多的一个初始化误区。

另一个容易忽略的点是集群角色不对等。Etcd 的节点有 Leader、Follower 之分,写请求最终由 Leader 处理,Follower 只做日志同步和转发。所以不能指望客户端的负载均衡算法能像后端服务一样把请求均匀打在每一台节点上,真正的均衡需要靠 SDK 内部控制,或者至少做好客户端侧的故障转移逻辑。

2. 原理逐层拆:Raft、MVCC、Watch、Lease

2.1 一致性的底座:Raft 选主与日志复制

Raft 是 Etcd 实现一致性的核心。它把共识问题拆成了三个子问题:领导选举、日志复制、安全性。简单说,每个节点都会有选举超时时间,如果一段时间没收到 Leader 的心跳,就会转成 Candidate 并发起投票。得票过半的节点成为新 Leader,然后开始通过心跳维持自己的地位,同时接收写请求并把日志条目复制给所有 Follower。

这个机制里最关键的参数是heartbeat-interval和election-timeout。默认值分别是 100ms 和 1000ms。心跳太频繁,网络压力大,CPU 也会升高;心跳太慢,节点误判 Leader 失联的概率增加;选举超时设太短,网络抖动时容易频繁选主。这几个参数在极端情况下需要调,但常规生产环境我建议先原样跑,别在没压测的情况下乱改。

实际操作里,日志复制和提交是严格有序的。客户端告诉 Leader 一个写操作,Leader 把它追加到自己的日志,然后并行发到各个 Follower,等到多数派都确认写入了这份日志,这个操作才算真正提交。所以三节点集群里,一次写至少要完成两次网络通信,延迟自然比单机数据库高。理解了这一点,就不会再拿 Etcd 和 Redis 比 QPS 了,它们定位完全不一样。

2.2 多版本与压缩:MVCC 帮你读取历史

Etcd 不覆盖旧值,而是维护一个全局自增的版本号 revision。每次写入,不管是 put 还是 delete,revision 都会加一。客户端读取时如果指定 revision,就能拿到那个时间点的数据快照。这个设计对 Watch 和事务非常有用,因为“在哪个版本上做的操作”可以被准确追踪。

不过版本无限增多会撑爆存储。所以 Etcd 引入了压缩 compaction,把某个 revision 之前的历史版本丢弃掉。压缩之后,老版本的读取会报一个很经典的错误:etcdserver: mvcc: required revision has been compacted。这个错误我在排查线上服务时遇到过不止一次,通常是因为客户端维护了一个很老的 Watch,Compaction 之后追不上进度了。

这里有个容易忽视的点:compaction 只是逻辑上清理历史版本,磁盘空间并不会立刻释放,必须配合defrag做物理整理。很多团队的告警是“数据库空间超过配额”,查了半天才发现 Etcd 的>etcd \ --name node-a \ --data-dir /data/etcd \ --listen-client-urls https://10.0.0.11:2379 \ --listen-peer-urls https://10.0.0.11:2380 \ --advertise-client-urls https://10.0.0.11:2379 \ --initial-advertise-peer-urls https://10.0.0.11:2380 \ --initial-cluster node-a=https://10.0.0.11:2380,node-b=https://10.0.0.12:2380,node-c=https://10.0.0.13:2380 \ --initial-cluster-token etcd-cluster-demo \ --initial-cluster-state new \ --enable-v2=false

第二台第三台把--name和对应 IP 换掉即可。三个节点都启动后,用 etcdctl 检查集群状态:

export ETCDCTL_API=3 etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \ --cacert=/etc/etcd/ca.crt \ --cert=/etc/etcd/server.crt \ --key=/etc/etcd/server.key \ endpoint health --cluster

如果全部返回 healthy,集群就绪。这里有个很容易犯的错误:--initial-cluster-state在多节点全新启动时必须写成new,一旦某天某个节点数据目录损坏,你想用备份恢复时,就必须改写成existing,否则 Etcd 会认为这是另一个新的集群 token,拒绝加入。这个参数是典型的“改错一次,折腾半天”的坑。

3.3 高频操作的 etcdctl 命令集

日常和 Etcd 打交道,记住下面这组命令就够了。首先是基础读写:

# 写入 etcdctl put /app/config/db_host "10.0.0.200" # 读取单个 key etcdctl get /app/config/db_host # 读取前缀下所有 key etcdctl get /app/config --prefix # 删除前缀下所有 key etcdctl del /app/config --prefix

租约相关的操作:

# 创建一个 10 秒的租约 etcdctl lease grant 10 # 给指定 key 绑定租约,假设租约 ID 为 123456 etcdctl put /app/registry/instance-1 "10.0.0.31:8080" --lease=123456 # 手动续约 etcdctl lease keep-alive 123456

Watch 相关:

# 监听某个前缀下的所有变化 etcdctl watch /app/config --prefix

运维相关命令:

# 查看成员 etcdctl member list # 查看端点状态 etcdctl endpoint status --cluster -w table # 手动压缩到指定 revision etcdctl compact 123456 # 查看告警 etcdctl alarm list # 解除告警 etcdctl alarm disarm # 碎片整理 etcdctl defrag # 保存快照 etcdctl snapshot save /backup/etcd-snapshot.db

实际操作中我给几个提醒。第一,defrag在较新版本里支持在线执行,但建议还是放在业务低峰期,因为整理过程中对读写性能有影响。第二,snapshot save需要在所有节点能够正常运行的情况下做,才能得到全局一致的数据;如果一个节点已经坏了,直接从坏节点上救出来的快照未必完整。第三,删除操作del是带一个删除标记的,它本身也会推进 revision,大量删除会让历史版本增长迅速,所以大范围清理后务必配合 compaction 和 defrag。

4. 常见问题与故障排查速查手册

4.1 “集群没有主”了怎么定位

Etcd 最常见的问题就是选主异常,现象是客户端大量etcdserver: request timed out,member list正常但服务不可写。排查时先看所有节点的 etcd 日志,重点找两个关键词:leader changed和role changed。日志级别默认可能不够细,我会先确认日志是否全量落盘,而不是被系统日志收集器截断。

接下来看磁盘延迟。选主超时的根因里,磁盘 IO 是头号嫌疑。可以用工具看每个数据盘的平均 fsync 延迟,只要偶发超过 100ms,就必须重视。Etcd 的稳定性非常依赖“写 WAL 文件能及时刷盘”,任何可能抢占磁盘 IO 的定时任务,比如日志清理、备份任务、系统审计,都尽量和 Etcd 错峰。

再有一种情况是网络分区,节点之间互相不通,但每个节点都还活着。此时不会有任何节点能凑齐多数派,于是大家一起进入选举不可用状态。这种情况下,etcdctl 的 endpoint 依然是通着的,但写请求全部超时。处理思路是先恢复网络,不要急着对某个节点做--force-new-cluster。这个参数是最后手段,它会让单节点忽略其他成员直接形成新集群,一旦用了,原来的集群数据视图会被破坏。

4.2 磁盘和空间告警如何应对

Etcd 有一个硬性配额,默认后端数据超过 2GB 之后会触发NOSPACE告警,此时写入会被拒绝,读取还能继续。初学者经常在这里慌乱,想直接把数据删掉。注意,删除操作也是写操作,配额满了之后同样执行不了。正确的处理链路是:先看空间去哪了,确认 compaction 是否生效,然后执行 defrag,最后解除告警。

具体步骤参考:

  1. 使用etcdctl endpoint status -w table查看每个节点的 db 大小
  2. 使用etcdctl compaction压缩到当前最新 revision,或者确认自动压缩已开启
  3. 低峰期执行etcdctl defrag
  4. 确认空间降下来后执行etcdctl alarm disarm

这里还要提一个排查方向:如果你的数据量本身只有几百 MB,但 db 文件暴涨到好几 GB,八成不是真实数据多,而是短时间内发生了大量更新或删除,堆积了大量历史版本来不及压缩。方案是把--auto-compaction-retention缩短,比如只保留 5 分钟的 revision,压得更勤快一点。

4.3 快照恢复的正确姿势

快照恢复也是一门需要小心的手艺。假设三节点集群全部损坏,只剩一份快照文件。恢复流程不是把三台机器用同一份快照各自启动就完事了,那样会导致三个节点的集群 ID 冲突。正确方法是用etcdctl snapshot restore生成一个全新集群的数据目录,然后以initial-cluster-state=existing的方式启动成员。

具体来说,对每个恢复目标节点执行一次 restore,注意--name、--data-dir、--initial-cluster要和最终集群配置一致:

etcdctl snapshot restore /backup/etcd-snapshot.db \ --name node-a \ --data-dir /data/etcd-restore \ --initial-cluster node-a=https://10.0.0.11:2380,node-b=https://10.0.0.12:2380,node-c=https://10.0.0.13:2380 \ --initial-cluster-token etcd-cluster-restore

每台节点生成的>lease := clientv3.NewLease(c) leaseResp, _ := c.Grant(ctx, 10) txn := c.Txn(ctx). If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)). Then(clientv3.OpPut(lockKey, myId, clientv3.WithLease(leaseResp.ID))) txnResp, _ := txn.Commit() if txnResp.Succeeded { // 拿到锁 }

这里的 Compare 判断锁 key 的 CreateRevision 是否为 0,意思是这个 key 从创建至今一直没有被创建过。事务成功就拿到锁,失败说明锁已被别人持有。锁释放时通过 Delete 或者靠租约过期自动兜底。

这个方案背后是“一次性创建一个唯一版本的 key”的强一致保证。注意在实际生产代码里还需要考虑“锁续约”和“锁持有者信息校验”,防止误删别人持有的锁。比如释放前应该再读一次 key 的值,确认是自己的标识后再删除。

5.2 用 Etcd 做选主和服务发现

选主跟分布式锁很像,无非是“多个候选进程抢一个固定 key,谁建出来谁就是主”。但生产上更推荐直接用客户端库的 concurrency 模块,它里面封装好了 Campaign、Resign、Observe 等方法,能避免自己处理租约续约和失败重试。

服务发现则是 Etcd 最顺手的场景。服务启动时把自己的地址注册到/services/服务名/实例ID,设置一个租约,后台续约。其他依赖方对这个前缀发起 Watch,就能实时拿到在线实例清单。相比传统 DNS 缓存,这种方案的更新延迟可以控制在秒级甚至毫秒级,而且不会有“下线实例仍然被路由到”的长时间脏数据窗口。

做服务发现还要注意“只把注册信息写进 Etcd,不要把大配置也塞进去”。比如每个实例的元信息控制在 KB 级别,真的有问题吗?Etcd 本身有 request size 限制,默认最大请求体 1.5MB,实际生产我建议单 key 不要超过几十 KB。容器编排平台会把 Etcd 当集群状态数据库,但那是经过精心约束后的用法,普通人直接当数据库写大对象,很快就会被压缩和碎片整理折磨到怀疑人生。

5.3 什么数据适合放 Etcd

根据我自己的使用经验,适合放 Etcd 的数据有几类:两两数量都不大的配置项、服务实例的注册地址与状态、分布式锁和选主相关信息、集群自身元数据。不适合放的是:大数据量的业务数据、日志、消息积压、可恢复的缓存。

同时,容量规划上一定要留余地。Etcd 的性能瓶颈往往不是 CPU 而是磁盘和网络,尤其是每增加一个节点,写请求要过的复制环节就越多。能通过分片或者多集群隔离场景的,就不要把高并发业务和核心协调逻辑混在同一个 Etcd 集群里。很多团队会在压力和容量评估不足的时候,把 Etcd 当 Redis 用,结果上了生产不到三个月就开始频繁空间告警。

最后我想说一个很多人忽略的小技巧:Etcd 的监控指标非常丰富,除了基础 CPU 内存,一定要把disk fsync duration、raft term、backend commit duration这几个指标画到告警面板上。它们往往比业务接口的错误率更早暴露风险。在我维护过的集群里,很多重大故障的苗头都是先从这些指标的不正常波动开始的。可以这么说,学会看 Etcd 自己的指标,比会敲几十条 etcdctl 命令更值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询