etcd原理与高可用
etcd原理与高可用
上一篇 01-etcd基础.md 解决的是“etcd 怎么用”:CRUD、Lease、Watch、MVCC、Txn,以及 Go 微服务里的注册发现、锁、选主、动态配置。
这一篇继续往下走,重点解决两个问题:
- 生产怎么跑稳:集群部署、TLS、RBAC、备份恢复、监控维护、扩缩容。
- 底层为什么这样工作:Raft、WAL、bbolt、MVCC 存储、线性一致性读、写请求生命周期。
etcd 是典型控制面组件。它不一定是系统里 QPS 最高的服务,但一旦出问题,影响往往很大:服务注册发现停摆、配置无法更新、Kubernetes 控制面异常、选主失败、分布式锁不可用。所以学习 etcd 不能只停在 put/get,必须理解它的高可用边界和故障语义。
版本说明:本文以 etcd v3 API 和 v3.7 官方文档口径为主。不同部署版本在命令细节上可能有差异,生产操作前应以实际版本的官方文档和
etcd --version、etcdctl version、etcdutl version为准。
1. 集群部署与运维管理
etcd 生产部署的核心目标不是“把进程跑起来”,而是:
- 多数派可用;
- 节点身份可信;
- 客户端访问受控;
- 数据可恢复;
- 容量和延迟可观测;
- 成员变更可控。
1.1 高可用集群的奇数原则
etcd 使用 Raft。Raft 写入需要多数派确认,所以集群容错能力取决于多数派。
多数派公式:
1 | |
常见节点数:
| 节点数 | 多数派 | 可容忍故障数 | 适用建议 |
|---|---|---|---|
| 1 | 1 | 0 | 本地开发、临时测试 |
| 2 | 2 | 0 | 不推荐,坏 1 个就无法写 |
| 3 | 2 | 1 | 最常见生产起点 |
| 4 | 3 | 1 | 不如 3 节点划算,容错没提升 |
| 5 | 3 | 2 | 更高容错,写入成本更高 |
| 6 | 4 | 2 | 不如 5 节点划算 |
| 7 | 4 | 3 | 大规模关键集群可考虑,但运维成本高 |
为什么推荐 3、5、7 个节点?
因为偶数节点通常不会带来同等幅度的容错提升,却会增加 Raft 复制、选举和运维成本。
1 | |
更重要的是,etcd 不是靠堆节点数提高吞吐。写请求需要 leader 提交日志并等待多数派,节点越多,网络和磁盘尾延迟越容易影响提交速度。
生产建议:
- 普通业务控制面:优先 3 节点;
- 跨机架、跨可用区且容错要求更高:评估 5 节点;
- 不要轻易超过 7 节点;
- 不建议跨很远地域部署同一个强一致 etcd 集群;
- Kubernetes 控制面 etcd 和业务 etcd 不要混用。
1.2 静态引导部署集群
etcd 集群启动方式有多种,生产里最容易理解的是静态引导:提前明确每个成员的名字、peer 地址和集群成员列表。
假设有 3 台机器:
1 | |
端口约定:
| 端口 | 作用 |
|---|---|
2379 |
client URL,业务客户端和 etcdctl 访问 |
2380 |
peer URL,etcd 成员之间 Raft 通信 |
三节点静态引导命令示例:
1 | |
infra0:
1 | |
infra1:
1 | |
infra2:
1 | |
几个参数必须理解:
| 参数 | 含义 |
|---|---|
--name |
成员名,必须和 --initial-cluster 里的名字一致 |
--data-dir |
数据目录,保存 WAL、snapshot、backend db |
--listen-client-urls |
本机监听哪些客户端地址 |
--advertise-client-urls |
对外声明的客户端访问地址 |
--listen-peer-urls |
本机监听哪些 peer 通信地址 |
--initial-advertise-peer-urls |
对外声明的 peer 通信地址 |
--initial-cluster |
初始集群成员表 |
--initial-cluster-token |
集群 token,避免误加入旧集群 |
--initial-cluster-state |
新建集群用 new,加入已有集群用 existing |
启动后检查:
1 | |
输出里重点看:
IS LEADER:谁是当前 leader;RAFT TERM:当前任期;RAFT INDEX和RAFT APPLIED INDEX:日志提交和应用进度;DB SIZE:后端数据库大小;ERRORS:是否有错误。
1.3 静态引导的常见坑
1.3.1 listen 和 advertise 搞混
listen 是“我监听什么地址”,advertise 是“别人应该通过什么地址访问我”。
常见错误:
1 | |
如果这是远程服务器,其他机器拿到 127.0.0.1:2379 当然连不上,因为它们会访问自己的本地回环地址。
建议:
1 | |
1.3.2 数据目录复用导致集群身份混乱
data-dir 里保存了成员身份、cluster ID、WAL、snapshot 等信息。不要拿旧集群的数据目录直接启动新集群。
如果是重新初始化测试集群,应明确清理旧数据目录。生产环境不能随便删目录,必须先确认是否有快照和恢复方案。
1.3.3 只开放了 client 端口,没开放 peer 端口
etcd 成员之间需要通过 peer URL 通信。只开放 2379 不够,2380 也必须在成员之间互通。
1 | |
1.3.4 跨地域部署强一致集群
Raft 写入要等多数派。跨地域 RTT 高,会直接拉高写入延迟和选举不稳定风险。
如果确实有多地域需求,通常要重新设计:
- 每个地域独立 etcd;
- 上层做异步同步;
- 或使用专门支持多地域模型的系统;
- 不要直接把一个 etcd 集群横跨很远地域。
1.4 TLS/SSL:节点间通信与客户端认证
etcd 生产安全至少分两层:
1 | |
不要只给客户端入口加 TLS,却让 peer 通信裸奔。
1.4.1 证书角色
常见证书文件:
| 文件 | 用途 |
|---|---|
ca-client.crt |
签发客户端访问 etcd 的 CA |
server.crt / server.key |
etcd client 端口使用的服务端证书 |
client.crt / client.key |
etcdctl 或业务客户端证书 |
ca-peer.crt |
签发 peer 通信证书的 CA |
peer.crt / peer.key |
etcd 成员之间通信使用 |
实际生产里可以用同一个 CA,也可以按 client/peer 分开。分开更清晰,权限边界更好。
1.4.2 启用 client TLS 与客户端证书认证
服务端关键参数:
1 | |
客户端访问:
1 | |
--client-cert-auth 表示服务端要求客户端提供由可信 CA 签发的证书。
1.4.3 启用 peer TLS
成员间通信关键参数:
1 | |
如果启用 peer TLS,--initial-cluster 里也要使用 https:
1 | |
1.4.4 TLS 排障重点
TLS 问题往往不是 etcd 逻辑错,而是证书元信息错。
重点检查:
- 证书 SAN 是否包含访问用的 IP 或 DNS;
- 客户端访问地址是否和证书匹配;
- peer 证书是否被
--peer-trusted-ca-file信任; - client 证书是否被
--trusted-ca-file信任; - 系统时间是否正确;
- 证书是否过期;
- client URL 和 peer URL 是否混用了证书。
常见症状:
1 | |
1.5 RBAC:用户、角色与权限控制
TLS 解决“连接是否可信”,RBAC 解决“可信连接能操作什么 key”。
etcd 的 RBAC 模型很简单:
1 | |
flowchart LR
A["user: order-service"] --> B["role: order-service-role"]
B --> C["readwrite /services/prod/order/ --prefix"]
B --> D["read /config/prod/order-rpc/ --prefix"]
1.5.1 初始化 root 用户
开启鉴权前,先创建 root 用户和 root 角色。
1 | |
执行 user add root 时会交互输入密码。不要把密码写进脚本、笔记或命令历史。
开启鉴权:
1 | |
开启后,普通请求必须带用户:
1 | |
1.5.2 创建业务角色
例如订单服务需要:
- 写自己的服务注册前缀;
- 读自己的配置前缀;
- 不能访问支付、用户等其他服务 key。
创建角色:
1 | |
授予服务注册读写权限:
1 | |
授予配置只读权限:
1 | |
创建用户并绑定角色:
1 | |
检查:
1 | |
1.5.3 Prefix 权限的本质
--prefix=true readwrite /services/prod/order/ 本质是给一个 key range 授权。
可以理解为:
1 | |
工程建议:
- key 设计时先规划好 prefix;
- 每个服务只给最小必要权限;
- 服务注册写权限和配置写权限分开;
- 管理后台、发布系统、业务服务使用不同用户;
- root 用户只用于运维,不给业务进程使用。
1.5.4 RBAC 的风险点
开启鉴权前要确认:
- root 用户和 root 角色已经创建;
- root 用户已经绑定 root 角色;
- 运维人员知道如何带认证参数访问;
- 业务客户端已经配置认证;
- 证书和用户名密码管理方式已经确定。
否则容易出现“开启 auth 后自己也访问不了”的事故。
1.6 备份与灾难恢复
etcd 的备份不是可选项。它保存的是控制面事实,一旦损坏,服务发现、配置、Kubernetes 状态都可能受影响。
1.6.1 快照保存
保存快照:
1 | |
如果启用了 TLS 和 RBAC:
1 | |
快照建议:
- 从健康 endpoint 保存;
- 保存在 etcd 节点之外;
- 定期传到对象存储或备份服务器;
- 记录 etcd 版本、集群成员、启动参数、证书信息;
- 做恢复演练,不要只做保存。
1.6.2 快照状态查看
新版本推荐使用 etcdutl 查看快照:
1 | |
输出关注:
| 字段 | 含义 |
|---|---|
HASH |
快照 hash |
REVISION |
快照对应的全局 revision |
TOTAL KEYS |
key 数量 |
TOTAL SIZE |
快照大小 |
如果你的版本仍支持 etcdctl snapshot status,也可以按实际版本使用,但要以当前二进制帮助信息为准:
1 | |
1.6.3 从快照恢复
恢复不是把 snapshot.db 拷贝回原目录这么简单。正确做法是用快照重新生成数据目录。
三节点恢复示意:
1 | |
在 infra0:
1 | |
在 infra1:
1 | |
在 infra2:
1 | |
然后用新的 data-dir 启动 etcd。恢复后的集群是基于快照的新集群,应该用新的 initial-cluster-token,避免和旧集群身份混淆。
1.6.4 灾备演练清单
一次合格的恢复演练至少要验证:
- 快照文件可读取;
snapshot status正常;- 能在隔离环境恢复出新集群;
- 恢复后
endpoint health正常; - 关键 key 存在;
- RBAC 用户和权限仍然正确;
- TLS 证书配置能正常访问;
- 应用能连接恢复后的 endpoint;
- 恢复耗时符合 RTO;
- 快照 revision 对应的数据新旧程度符合 RPO。
不要把主从、集群高可用当成备份。高可用解决“节点故障继续服务”,备份解决“误删、逻辑损坏、全局故障后恢复”。
1.7 监控:Prometheus 与 Grafana
etcd 原生暴露 Prometheus 指标,通常在 client URL 的 /metrics 路径。
Prometheus scrape 示例:
1 | |
如果没有启用 TLS,scheme 可以是 http。生产不要为了监控方便把 etcd 裸露到公网或大网段。
1.7.1 核心监控维度
| 维度 | 关注点 |
|---|---|
| 集群健康 | leader 是否存在、leader 变更次数、成员是否在线 |
| Raft | proposal 提交、失败、pending 数量、term 变化 |
| 磁盘 | WAL fsync 延迟、backend commit 延迟 |
| 网络 | peer round trip 延迟、连接失败 |
| 存储 | DB size、quota 使用率、碎片情况 |
| MVCC | key 数量、watcher 数量、lease 数量 |
| 请求 | gRPC 请求延迟、失败码、慢请求 |
| 进程 | CPU、内存、goroutine、fd、磁盘 IO |
一些常见指标名称会随版本有变化,但排查方向稳定:
1 | |
1.7.2 告警建议
| 告警 | 可能含义 |
|---|---|
| 没有 leader | 集群不可写,控制面严重故障 |
| leader 频繁变化 | 网络抖动、磁盘慢、节点压力大 |
| pending proposals 持续升高 | 写入堆积,leader 或 follower 跟不上 |
| WAL fsync p99 高 | 磁盘延迟影响 Raft 提交 |
| backend commit p99 高 | bbolt 提交慢,可能有 IO 或 DB 膨胀问题 |
| DB size 接近 quota | 可能触发 NOSPACE alarm |
| peer RTT 高 | 网络延迟影响复制和选举 |
| gRPC 错误率升高 | 客户端请求失败、鉴权/TLS/负载问题 |
etcd 的很多故障最终都会落到两个基础面:
1 | |
1.8 日常维护:Compaction、Defrag、Quota
上一篇已经讲过 MVCC 和 compaction。这里从运维角度再串起来:
1 | |
1.8.1 Compaction
手动压缩到某个 revision:
1 | |
自动压缩示例:
1 | |
或者:
1 | |
取舍:
- 保留太短:慢 Watch 客户端更容易遇到 compacted revision;
- 保留太长:DB 增长更快,碎片和 IO 压力更大。
1.8.2 Defrag
Compaction 是逻辑清理,defrag 是物理整理。
对整个集群执行:
1 | |
也可以逐个 endpoint 执行,降低对集群的瞬时影响:
1 | |
注意:
- defrag 可能带来短暂延迟抖动;
- 不要在业务高峰执行;
- 大 DB defrag 前先确认磁盘空间;
- defrag 不是备份替代品。
1.8.3 Quota 与 NOSPACE alarm
etcd 有 backend quota。超过空间配额后,集群会进入 alarm 状态并拒绝写入,常见错误类似:
1 | |
处理流程一般是:
1 | |
命令示例:
1 | |
配置 quota 示例:
1 | |
这个值不是越大越好。过大的 backend 会拉长恢复、备份、defrag 和 cache 压力。根因上,要避免把 etcd 当高频业务库。
1.9 集群成员动态扩缩容
etcd 支持动态成员变更,但这不是普通无状态服务扩容。成员变更会改变 Raft quorum,必须谨慎。
1.9.1 查看成员
1 | |
1.9.2 添加成员
例如已有两个健康成员,要添加 infra3:
1 | |
命令会返回新成员启动所需的环境变量,关键是:
1 | |
然后在新机器启动:
1 | |
注意:添加成员后,新成员追数据期间可能给集群带来复制压力。
1.9.3 删除成员
先查 member ID:
1 | |
删除:
1 | |
删除成员通常用于:
- 节点永久损坏;
- 机器下线;
- 缩容;
- 替换节点。
1.9.4 更新成员 peer URL
如果成员 IP 或 peer URL 变化,可以更新:
1 | |
这种操作要配合实际节点启动参数一起改,否则成员表和进程配置不一致。
1.9.5 成员变更原则
- 一次只变更一个成员;
- 变更前确认当前集群健康;
- 不要在没有多数派时强行扩缩容;
- 添加新成员后确认同步完成;
- 删除成员前确认删除后仍有多数派;
- 替换节点通常是先 remove 旧成员,再 add 新成员;
- 每次变更后执行
endpoint health、endpoint status、member list。
2. 底层架构剖析
理解 etcd 底层架构,不是为了背名词,而是为了在故障时能判断:
- 是网络导致 leader 抖动?
- 是磁盘慢导致写入卡住?
- 是 Watch 客户端落后太多?
- 是 quota 满了?
- 是线性读被 quorum 延迟拖慢?
- 是 follower 读到了旧数据?
2.1 Raft 一致性协议精解
Raft 解决的问题是:多台机器如何对同一串日志达成一致。
etcd 可以简单理解为:
1 | |
sequenceDiagram
participant C as Client
participant L as Leader
participant F1 as Follower 1
participant F2 as Follower 2
participant S as MVCC Store
C->>L: Put /config/order v2
L->>L: append Raft log
L->>F1: AppendEntries
L->>F2: AppendEntries
F1-->>L: ack
F2-->>L: ack
L->>L: quorum reached, commit
L->>S: apply committed entry
L-->>C: success
2.2 Leader、Follower、Candidate
Raft 节点有三种主要角色:
| 角色 | 作用 |
|---|---|
| Leader | 接收写请求,复制日志,推进 commit |
| Follower | 接收 leader 日志,参与投票 |
| Candidate | 发起选举,争取成为 leader |
正常情况下只有一个 leader。所有写请求最终都要由 leader 处理。如果客户端连到 follower,follower 会把请求转发给 leader 或让客户端重试到 leader。
2.3 Term:任期
Term 是 Raft 里的逻辑任期,是单调递增的整数。每次发生新的 leader 选举,term 都会增加。
可以理解成:
1 | |
Term 的作用:
- 区分新旧 leader;
- 防止旧 leader 恢复后继续以 leader 身份写入;
- 投票和日志复制时判断消息新旧;
- 帮助集群在网络分区恢复后收敛。
一个任期内最多只有一个 leader 被选出。旧 term 的 leader 收到更高 term 的消息后,会退回 follower。
2.4 Leader 选举过程
简化过程:
1 | |
stateDiagram-v2
[*] --> Follower
Follower --> Candidate: election timeout
Candidate --> Leader: wins quorum votes
Candidate --> Follower: discovers higher term
Leader --> Follower: discovers higher term
Leader --> Leader: send heartbeat
选举稳定性依赖两个基础条件:
- 网络延迟和丢包不能太夸张;
- 节点磁盘和 CPU 不能卡到错过 heartbeat。
所以 leader 频繁变化时,不要只看 etcd 日志,也要看:
- peer RTT;
- 磁盘 fsync;
- CPU throttling;
- GC 暂停;
- 宿主机负载;
- 防火墙或安全组;
- 证书过期。
2.5 日志复制
Leader 处理写入时,会把操作封装成日志条目,然后复制给 followers。
日志条目里重要的是:
1 | |
复制过程:
1 | |
Raft 的关键约束:
- 已提交日志不会被覆盖;
- 新 leader 必须拥有足够新的日志;
- follower 日志冲突时,以 leader 日志为准回退修正;
- 只有多数派确认的日志才能成为集群事实。
这就是 etcd 在网络分区里能避免脑裂写入的原因。
2.6 网络分区时的行为
以 3 节点为例:
1 | |
如果旧 leader infra0 落到少数派:
1 | |
这就是 CP 的取舍:少数派不可写,保证不会出现两个分区都成功提交写入。
如果分区是:
1 | |
多数派一侧可以继续服务,少数派不能提交写入。
恢复后:
1 | |
注意:旧 leader 在少数派期间可能接收到了客户端请求,但这些请求没有多数派提交,就不能算成功事实。客户端必须以 etcd 返回成功为准,不能以“请求发出去了”为准。
2.7 etcd 存储架构总览
etcd 的存储可以分成两层:
1 | |
flowchart TD
A["Client Request"] --> B["etcdserver"]
B --> C["Raft"]
C --> D["WAL"]
C --> E["Raft Snapshot"]
C --> F["Apply committed entry"]
F --> G["MVCC"]
G --> H["Memory Index"]
G --> I["bbolt Backend"]
I --> J["member/snap/db"]
可以这样理解:
- Raft 负责“多节点对同一批操作达成一致”;
- WAL 负责“操作在崩溃后可恢复”;
- MVCC 负责“key 的多版本视图”;
- bbolt 负责“把最终应用后的 KV 数据持久化”;
- 内存索引负责“快速从用户 key 找到 revision 位置”。
2.8 内存索引:B-Tree 机制
etcd 对外暴露的是用户 key:
1 | |
但底层 MVCC 需要按 revision 保存历史版本。
官方数据模型可以概括为:
1 | |
etcd 会维护内存索引,把用户 key 映射到它对应的版本位置:
1 | |
简化理解:
1 | |
为什么需要内存索引?
- 快速定位某个 key 当前版本;
- 支持 prefix range 查询;
- 支持历史 revision 查询;
- 支持 Watch 根据 revision 推进;
- 避免每次查询都全量扫描 bbolt。
注意:这里说的 B-Tree/B-tree 是索引结构层面的理解。etcd 的持久化 backend 是 bbolt 的 B+tree,内存层维护 tree index 来加速用户 key 到 revision 的查找。
2.9 WAL:预写式日志
WAL 是 Write-Ahead Log,预写式日志。
它的核心思想很朴素:
状态真正变更前,先把操作日志可靠写下来。崩溃后可以用日志恢复状态。
etcd 的 WAL 保存 Raft 相关内容,包括:
- Raft log entries;
- hard state,例如当前 term、vote、commit index;
- snapshot 相关元数据;
- cluster/member 元数据;
- checksum。
写请求进入 Raft 后,日志持久化非常关键。否则 leader 如果在复制或应用前崩溃,集群无法可靠恢复日志状态。
简化流程:
1 | |
WAL 对排障很重要,因为 etcd 写入延迟经常和磁盘 fsync 有关。
如果看到:
1 | |
通常要查:
- 磁盘是否是低延迟 SSD;
- 是否和业务高 IO 混部;
- 云盘是否有突发额度耗尽;
- 宿主机是否有 IO wait;
- 文件系统或挂载参数是否异常;
- 是否有备份、扫描任务影响磁盘。
2.10 bbolt:后端存储引擎
etcd v3 的后端数据存储在 member/snap/db,这是 bbolt 数据库文件。
bbolt 可以理解为嵌入式、单文件、事务型 KV 存储,底层使用 B+tree。etcd 把已经 apply 的 MVCC 数据、成员信息、鉴权信息、元数据等存进去。
目录结构大致类似:
1 | |
不同文件大致职责:
| 文件/目录 | 作用 |
|---|---|
member/wal/ |
Raft WAL,保存日志、hard state 等 |
member/snap/ |
Raft snapshot 和 backend db |
member/snap/db |
bbolt backend,保存 v3 MVCC 数据等 |
bbolt 的特点:
- 单机嵌入式,不需要独立服务;
- 事务提交依赖磁盘;
- 文件可能因为历史写入和删除出现碎片;
- compaction 后仍需要 defrag 才能回收物理空间;
- 大 DB 会影响启动、备份、defrag、恢复。
所以 etcd 的存储治理不是“磁盘没满就行”,还要关注:
1 | |
2.11 写请求生命周期
以 put /config/order/rate-limit 100 为例。
完整路径:
1 | |
flowchart TD
A["client Put"] --> B{"hit leader?"}
B -->|no| C["forward / retry to leader"]
B -->|yes| D["Raft proposal"]
C --> D
D --> E["append log"]
E --> F["write WAL"]
F --> G["replicate to followers"]
G --> H{"quorum ack?"}
H -->|no| I["wait / fail on timeout"]
H -->|yes| J["commit index advance"]
J --> K["apply to MVCC"]
K --> L["commit to bbolt backend"]
L --> M["notify watchers"]
M --> N["return success"]
这个流程解释了很多现象:
| 现象 | 背后原因 |
|---|---|
| 写入延迟受磁盘影响 | WAL fsync 和 backend commit 都依赖磁盘 |
| 写入延迟受网络影响 | leader 要等多数派 follower ack |
| leader 抖动会影响写入 | 新 leader 选出前无法提交写 |
| 少数派不可写 | 无法获得 quorum ack |
| Watch 能看到顺序事件 | 事件来自已 apply 的 revision |
2.12 线性一致性读与串行读
etcd 读请求有两种重要一致性模式:
| 读模式 | 英文 | 特点 | 风险 |
|---|---|---|---|
| 线性一致性读 | Linearizable Read | 默认强一致,确认读到的是最新已提交数据 | 延迟更高,需要和 quorum/leader 协调 |
| 串行读 | Serializable Read | 可以由本地成员直接返回,性能更好 | 可能读到旧数据 |
2.12.1 Linearizable Read
线性一致性读适合:
- 读取分布式锁状态;
- 读取 leader 选举结果;
- 读取强一致配置;
- 判断某个关键 key 是否存在;
- 对新旧值敏感的控制面逻辑。
为什么线性读慢一点?
因为它不能随便从一个落后的 follower 返回。它需要确认当前读不会落后于已经提交的写。etcd 通常会通过 ReadIndex 等机制和 Raft leader/quorum 协调,避免返回陈旧数据。
命令默认就是线性一致性读:
1 | |
2.12.2 Serializable Read
串行读可以从当前连接的成员本地状态返回,不必每次都和 leader/quorum 协调。
命令示例:
1 | |
适合:
- 对短暂旧值不敏感;
- 大量只读查询;
- 监控、巡检、低风险展示;
- 已有本地缓存兜底的场景。
不适合:
- 锁;
- 选主;
- CAS 前置读取;
- 强一致配置发布确认;
- 任何“读旧会造成错误决策”的链路。
2.12.3 怎么选
一句话:
控制决策用线性一致性读;低风险观察可以用串行读。
表格化:
| 场景 | 建议 |
|---|---|
| 服务发现客户端启动全量拉取 | 通常用默认线性读更稳 |
| Watch 后的本地缓存调用 | 调用时读本地缓存,不直接读 etcd |
| 配置中心发布后确认 | 线性读 |
| 监控页面展示 key 列表 | 可评估串行读 |
| 分布式锁判断 | 线性读或直接使用官方 concurrency 原语 |
| 选主观察 | 线性读更稳 |
3. 故障排查思路
etcd 故障排查要先分层,不要一上来就猜“是不是 Raft 坏了”。Raft 通常没坏,坏的是网络、磁盘、证书、容量、客户端使用方式。
3.1 快速体检命令
1 | |
带 TLS:
1 | |
3.2 没有 leader
表现:
1 | |
排查:
- 成员之间
2380是否互通; - peer TLS 是否正常;
- 是否多数节点故障;
- 节点时间是否异常;
- 磁盘 fsync 是否极慢;
- CPU 是否被打满或 throttling;
- 是否发生网络分区。
3.3 写入变慢
重点看:
1 | |
判断:
- WAL fsync 慢:优先查磁盘;
- peer RTT 高:优先查网络;
- proposals pending 高:leader 提交或 follower 应答跟不上;
- backend commit 慢:bbolt 提交、DB 膨胀、磁盘延迟。
3.4 Watch 客户端频繁全量恢复
可能原因:
- compaction 保留窗口太短;
- 客户端处理事件太慢;
- Watch 连接经常断;
- 客户端没有记录 revision;
- 客户端遇到 compacted revision 后没有正确全量恢复。
修复方向:
- 增大 auto compaction 保留窗口;
- 降低 Watch 前缀范围;
- 减少单个 key 大 value;
- 客户端本地处理异步化;
- Watch 断线后从
lastRevision + 1恢复; - compacted 后重新 Get prefix。
3.5 database space exceeded
表现:
1 | |
处理:
1 | |
根因治理:
- 不要高频写 etcd;
- 不要写大 value;
- 开启 auto compaction;
- 监控 DB size 和 in-use size;
- 合理设置 quota;
- 对业务错误写入做限流和告警。
4. 面试和工程回答要点
4.1 为什么 etcd 推荐奇数节点
可以这样答:
etcd 基于 Raft,需要多数派确认。3 节点 quorum 是 2,可容忍 1 个故障;4 节点 quorum 是 3,也只能容忍 1 个故障,但复制和运维成本更高。所以通常推荐 3、5、7 这样的奇数节点。节点不是越多越好,写入要等多数派,节点越多越容易受到网络和磁盘尾延迟影响。
4.2 etcd 如何避免脑裂
可以这样答:
etcd 的写入必须经过 Raft 多数派提交。网络分区时,只有拥有多数派的一侧能选出 leader 并提交日志,少数派无法获得 quorum,所以不能成功写入。旧 leader 如果落入少数派,即使接到客户端请求,也无法提交;恢复后看到更高 term 会退回 follower。这就是 CP 系统牺牲部分可用性来保证一致性的体现。
4.3 etcd 写请求为什么会受磁盘影响
可以这样答:
etcd 的写请求会先进入 Raft proposal,并写入 WAL,复制到多数派后 commit,再 apply 到 MVCC store 和 bbolt backend。WAL fsync 和 backend commit 都依赖磁盘延迟,所以磁盘慢会直接拉高写入延迟,严重时还会导致 leader heartbeat 不稳定和选举抖动。
4.4 Linearizable Read 和 Serializable Read 怎么区分
可以这样答:
Linearizable Read 是默认强一致读,需要确认读不会落后于已提交写,通常要和 leader/quorum 协调,延迟更高但结果最新。Serializable Read 可以由本地成员返回,性能更好,但可能读到旧数据。锁、选主、配置确认这类控制决策用线性读;低风险展示、监控巡检可以评估串行读。
4.5 为什么 compaction 后还要 defrag
可以这样答:
compaction 删除的是 MVCC 历史版本的逻辑可见性,旧 revision 不能再查询或 Watch 回放。但底层 bbolt 文件不一定马上把物理空间还给操作系统,所以 DB 文件大小可能没有下降。defrag 才是物理整理和空间回收。生产上通常是 compaction 控制历史,defrag 定期低峰执行。
5. 运维清单
5.1 部署前
- 确定 3 或 5 节点规模;
- 确定节点 IP、主机名、机架或可用区分布;
- 确定 client URL 和 peer URL;
- 准备 TLS 证书,SAN 覆盖访问地址;
- 准备数据目录和低延迟磁盘;
- 配置 systemd 或容器编排;
- 规划备份目录和异地备份;
- 规划 RBAC 用户、角色和 key prefix。
5.2 上线后
endpoint health正常;endpoint status中 leader、term、db size 正常;member list成员符合预期;- Prometheus 成功采集
/metrics; - Grafana 看板有 leader、Raft、磁盘、DB、gRPC 指标;
- 快照任务成功;
- 恢复演练完成;
- RBAC 最小权限验证通过;
- 业务客户端具备超时、重试、本地缓存和 Watch 恢复能力。
5.3 日常维护
- 定期检查 leader changes;
- 定期检查 WAL fsync 和 backend commit p99;
- 定期检查 DB size、in-use size、quota;
- 开启 auto compaction;
- 低峰执行 defrag;
- 定期做 snapshot;
- 定期做恢复演练;
- 证书过期前轮换;
- 成员变更一次只操作一个节点;
- 不把 etcd 当 Redis 或业务数据库使用。