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 --versionetcdctl versionetcdutl version 为准。

1. 集群部署与运维管理

etcd 生产部署的核心目标不是“把进程跑起来”,而是:

  • 多数派可用;
  • 节点身份可信;
  • 客户端访问受控;
  • 数据可恢复;
  • 容量和延迟可观测;
  • 成员变更可控。

1.1 高可用集群的奇数原则

etcd 使用 Raft。Raft 写入需要多数派确认,所以集群容错能力取决于多数派。

多数派公式:

1
2
quorum = floor(n / 2) + 1
fault_tolerance = n - quorum

常见节点数:

节点数 多数派 可容忍故障数 适用建议
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
2
3
4
5
6
7
8
9
10
11
12
3 节点:
quorum = 2
容忍 1 个节点故障

4 节点:
quorum = 3
仍然只容忍 1 个节点故障
但多了一个节点的复制和故障面

5 节点:
quorum = 3
容忍 2 个节点故障

更重要的是,etcd 不是靠堆节点数提高吞吐。写请求需要 leader 提交日志并等待多数派,节点越多,网络和磁盘尾延迟越容易影响提交速度。

生产建议:

  • 普通业务控制面:优先 3 节点;
  • 跨机架、跨可用区且容错要求更高:评估 5 节点;
  • 不要轻易超过 7 节点;
  • 不建议跨很远地域部署同一个强一致 etcd 集群;
  • Kubernetes 控制面 etcd 和业务 etcd 不要混用。

1.2 静态引导部署集群

etcd 集群启动方式有多种,生产里最容易理解的是静态引导:提前明确每个成员的名字、peer 地址和集群成员列表。

假设有 3 台机器:

1
2
3
infra0  10.0.1.10
infra1 10.0.1.11
infra2 10.0.1.12

端口约定:

端口 作用
2379 client URL,业务客户端和 etcdctl 访问
2380 peer URL,etcd 成员之间 Raft 通信

三节点静态引导命令示例:

1
2
3
TOKEN=etcd-cluster-1
CLUSTER=infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380
CLUSTER_STATE=new

infra0

1
2
3
4
5
6
7
8
9
10
etcd \
--name infra0 \
--data-dir /var/lib/etcd \
--initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--listen-client-urls http://10.0.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--initial-cluster-token ${TOKEN} \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE}

infra1

1
2
3
4
5
6
7
8
9
10
etcd \
--name infra1 \
--data-dir /var/lib/etcd \
--initial-advertise-peer-urls http://10.0.1.11:2380 \
--listen-peer-urls http://10.0.1.11:2380 \
--listen-client-urls http://10.0.1.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.11:2379 \
--initial-cluster-token ${TOKEN} \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE}

infra2

1
2
3
4
5
6
7
8
9
10
etcd \
--name infra2 \
--data-dir /var/lib/etcd \
--initial-advertise-peer-urls http://10.0.1.12:2380 \
--listen-peer-urls http://10.0.1.12:2380 \
--listen-client-urls http://10.0.1.12:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.12:2379 \
--initial-cluster-token ${TOKEN} \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE}

几个参数必须理解:

参数 含义
--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
2
3
4
5
6
export ETCDCTL_API=3
export ENDPOINTS=10.0.1.10:2379,10.0.1.11:2379,10.0.1.12:2379

etcdctl --endpoints=${ENDPOINTS} endpoint health
etcdctl --endpoints=${ENDPOINTS} --write-out=table endpoint status
etcdctl --endpoints=${ENDPOINTS} member list --write-out=table

输出里重点看:

  • IS LEADER:谁是当前 leader;
  • RAFT TERM:当前任期;
  • RAFT INDEXRAFT APPLIED INDEX:日志提交和应用进度;
  • DB SIZE:后端数据库大小;
  • ERRORS:是否有错误。

1.3 静态引导的常见坑

1.3.1 listenadvertise 搞混

listen 是“我监听什么地址”,advertise 是“别人应该通过什么地址访问我”。

常见错误:

1
2
--listen-client-urls http://0.0.0.0:2379
--advertise-client-urls http://127.0.0.1:2379

如果这是远程服务器,其他机器拿到 127.0.0.1:2379 当然连不上,因为它们会访问自己的本地回环地址。

建议:

1
2
3
4
5
listen:
可以是 0.0.0.0 或具体网卡 IP

advertise:
必须是其他节点和客户端真实可达的地址

1.3.2 数据目录复用导致集群身份混乱

data-dir 里保存了成员身份、cluster ID、WAL、snapshot 等信息。不要拿旧集群的数据目录直接启动新集群。

如果是重新初始化测试集群,应明确清理旧数据目录。生产环境不能随便删目录,必须先确认是否有快照和恢复方案。

1.3.3 只开放了 client 端口,没开放 peer 端口

etcd 成员之间需要通过 peer URL 通信。只开放 2379 不够,2380 也必须在成员之间互通。

1
2
3
4
5
client traffic:
app / etcdctl -> 2379

peer traffic:
etcd member -> 2380 -> etcd member

1.3.4 跨地域部署强一致集群

Raft 写入要等多数派。跨地域 RTT 高,会直接拉高写入延迟和选举不稳定风险。

如果确实有多地域需求,通常要重新设计:

  • 每个地域独立 etcd;
  • 上层做异步同步;
  • 或使用专门支持多地域模型的系统;
  • 不要直接把一个 etcd 集群横跨很远地域。

1.4 TLS/SSL:节点间通信与客户端认证

etcd 生产安全至少分两层:

1
2
3
4
5
6
7
client TLS:
保护客户端 -> etcd 的访问
对应 2379

peer TLS:
保护 etcd 成员 -> etcd 成员的 Raft 通信
对应 2380

不要只给客户端入口加 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
2
3
4
5
6
7
etcd \
--listen-client-urls https://10.0.1.10:2379 \
--advertise-client-urls https://10.0.1.10:2379 \
--cert-file=/etc/etcd/pki/server.crt \
--key-file=/etc/etcd/pki/server.key \
--client-cert-auth \
--trusted-ca-file=/etc/etcd/pki/ca-client.crt

客户端访问:

1
2
3
4
5
6
etcdctl \
--endpoints=https://10.0.1.10:2379 \
--cacert=/etc/etcd/pki/ca-client.crt \
--cert=/etc/etcd/pki/client.crt \
--key=/etc/etcd/pki/client.key \
endpoint health

--client-cert-auth 表示服务端要求客户端提供由可信 CA 签发的证书。

1.4.3 启用 peer TLS

成员间通信关键参数:

1
2
3
4
5
6
7
etcd \
--listen-peer-urls https://10.0.1.10:2380 \
--initial-advertise-peer-urls https://10.0.1.10:2380 \
--peer-cert-file=/etc/etcd/pki/peer.crt \
--peer-key-file=/etc/etcd/pki/peer.key \
--peer-client-cert-auth \
--peer-trusted-ca-file=/etc/etcd/pki/ca-peer.crt

如果启用 peer TLS,--initial-cluster 里也要使用 https

1
infra0=https://10.0.1.10:2380,infra1=https://10.0.1.11:2380,infra2=https://10.0.1.12:2380

1.4.4 TLS 排障重点

TLS 问题往往不是 etcd 逻辑错,而是证书元信息错。

重点检查:

  • 证书 SAN 是否包含访问用的 IP 或 DNS;
  • 客户端访问地址是否和证书匹配;
  • peer 证书是否被 --peer-trusted-ca-file 信任;
  • client 证书是否被 --trusted-ca-file 信任;
  • 系统时间是否正确;
  • 证书是否过期;
  • client URL 和 peer URL 是否混用了证书。

常见症状:

1
2
3
x509: certificate signed by unknown authority
x509: certificate is valid for ..., not ...
remote error: tls: bad certificate

1.5 RBAC:用户、角色与权限控制

TLS 解决“连接是否可信”,RBAC 解决“可信连接能操作什么 key”。

etcd 的 RBAC 模型很简单:

1
2
3
User  -> 绑定一个或多个 Role
Role -> 拥有 key/range/prefix 的 read/write/readwrite 权限
Auth -> 启用后,所有请求都要鉴权
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
2
3
4
5
6
export ETCDCTL_API=3
export ENDPOINTS=10.0.1.10:2379,10.0.1.11:2379,10.0.1.12:2379

etcdctl --endpoints=${ENDPOINTS} user add root
etcdctl --endpoints=${ENDPOINTS} role add root
etcdctl --endpoints=${ENDPOINTS} user grant-role root root

执行 user add root 时会交互输入密码。不要把密码写进脚本、笔记或命令历史。

开启鉴权:

1
etcdctl --endpoints=${ENDPOINTS} auth enable

开启后,普通请求必须带用户:

1
etcdctl --endpoints=${ENDPOINTS} --user=root get / --prefix --keys-only

1.5.2 创建业务角色

例如订单服务需要:

  • 写自己的服务注册前缀;
  • 读自己的配置前缀;
  • 不能访问支付、用户等其他服务 key。

创建角色:

1
etcdctl --endpoints=${ENDPOINTS} --user=root role add order-service-role

授予服务注册读写权限:

1
2
3
etcdctl --endpoints=${ENDPOINTS} --user=root \
role grant-permission order-service-role \
--prefix=true readwrite /services/prod/trade/order-rpc/

授予配置只读权限:

1
2
3
etcdctl --endpoints=${ENDPOINTS} --user=root \
role grant-permission order-service-role \
--prefix=true read /config/prod/trade/order-rpc/

创建用户并绑定角色:

1
2
3
4
etcdctl --endpoints=${ENDPOINTS} --user=root user add order-service

etcdctl --endpoints=${ENDPOINTS} --user=root \
user grant-role order-service order-service-role

检查:

1
2
etcdctl --endpoints=${ENDPOINTS} --user=root role get order-service-role
etcdctl --endpoints=${ENDPOINTS} --user=root user get order-service

1.5.3 Prefix 权限的本质

--prefix=true readwrite /services/prod/order/ 本质是给一个 key range 授权。

可以理解为:

1
2
3
4
5
6
7
8
允许访问:
/services/prod/order/
/services/prod/order/inst-1
/services/prod/order/inst-2

不允许访问:
/services/prod/pay/inst-1
/config/prod/order-rpc/rate-limit

工程建议:

  • key 设计时先规划好 prefix;
  • 每个服务只给最小必要权限;
  • 服务注册写权限和配置写权限分开;
  • 管理后台、发布系统、业务服务使用不同用户;
  • root 用户只用于运维,不给业务进程使用。

1.5.4 RBAC 的风险点

开启鉴权前要确认:

  • root 用户和 root 角色已经创建;
  • root 用户已经绑定 root 角色;
  • 运维人员知道如何带认证参数访问;
  • 业务客户端已经配置认证;
  • 证书和用户名密码管理方式已经确定。

否则容易出现“开启 auth 后自己也访问不了”的事故。

1.6 备份与灾难恢复

etcd 的备份不是可选项。它保存的是控制面事实,一旦损坏,服务发现、配置、Kubernetes 状态都可能受影响。

1.6.1 快照保存

保存快照:

1
2
3
4
export ETCDCTL_API=3
export ENDPOINTS=10.0.1.10:2379

etcdctl --endpoints=${ENDPOINTS} snapshot save /backup/etcd-snapshot.db

如果启用了 TLS 和 RBAC:

1
2
3
4
5
6
7
etcdctl \
--endpoints=https://10.0.1.10:2379 \
--cacert=/etc/etcd/pki/ca-client.crt \
--cert=/etc/etcd/pki/client.crt \
--key=/etc/etcd/pki/client.key \
--user=root \
snapshot save /backup/etcd-snapshot.db

快照建议:

  • 从健康 endpoint 保存;
  • 保存在 etcd 节点之外;
  • 定期传到对象存储或备份服务器;
  • 记录 etcd 版本、集群成员、启动参数、证书信息;
  • 做恢复演练,不要只做保存。

1.6.2 快照状态查看

新版本推荐使用 etcdutl 查看快照:

1
etcdutl --write-out=table snapshot status /backup/etcd-snapshot.db

输出关注:

字段 含义
HASH 快照 hash
REVISION 快照对应的全局 revision
TOTAL KEYS key 数量
TOTAL SIZE 快照大小

如果你的版本仍支持 etcdctl snapshot status,也可以按实际版本使用,但要以当前二进制帮助信息为准:

1
etcdctl snapshot status /backup/etcd-snapshot.db --write-out=table

1.6.3 从快照恢复

恢复不是把 snapshot.db 拷贝回原目录这么简单。正确做法是用快照重新生成数据目录。

三节点恢复示意:

1
2
3
SNAPSHOT=/backup/etcd-snapshot.db
TOKEN=etcd-cluster-restore-1
CLUSTER=infra0=https://10.0.1.10:2380,infra1=https://10.0.1.11:2380,infra2=https://10.0.1.12:2380

infra0

1
2
3
4
5
6
etcdutl snapshot restore ${SNAPSHOT} \
--name infra0 \
--data-dir /var/lib/etcd-restore \
--initial-cluster ${CLUSTER} \
--initial-cluster-token ${TOKEN} \
--initial-advertise-peer-urls https://10.0.1.10:2380

infra1

1
2
3
4
5
6
etcdutl snapshot restore ${SNAPSHOT} \
--name infra1 \
--data-dir /var/lib/etcd-restore \
--initial-cluster ${CLUSTER} \
--initial-cluster-token ${TOKEN} \
--initial-advertise-peer-urls https://10.0.1.11:2380

infra2

1
2
3
4
5
6
etcdutl snapshot restore ${SNAPSHOT} \
--name infra2 \
--data-dir /var/lib/etcd-restore \
--initial-cluster ${CLUSTER} \
--initial-cluster-token ${TOKEN} \
--initial-advertise-peer-urls https://10.0.1.12:2380

然后用新的 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
2
3
4
5
6
7
8
9
10
11
12
13
scrape_configs:
- job_name: "etcd"
scheme: "https"
metrics_path: "/metrics"
static_configs:
- targets:
- "10.0.1.10:2379"
- "10.0.1.11:2379"
- "10.0.1.12:2379"
tls_config:
ca_file: /etc/prometheus/pki/ca-client.crt
cert_file: /etc/prometheus/pki/client.crt
key_file: /etc/prometheus/pki/client.key

如果没有启用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
leader:
etcd_server_has_leader
etcd_server_leader_changes_seen_total

proposal:
etcd_server_proposals_committed_total
etcd_server_proposals_applied_total
etcd_server_proposals_pending
etcd_server_proposals_failed_total

disk:
etcd_disk_wal_fsync_duration_seconds
etcd_disk_backend_commit_duration_seconds

backend:
etcd_mvcc_db_total_size_in_bytes
etcd_mvcc_db_total_size_in_use_in_bytes

network:
etcd_network_peer_round_trip_time_seconds

grpc:
grpc_server_handled_total
grpc_server_handling_seconds

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
2
3
4
5
网络:
peer RTT 高、丢包、分区、DNS/证书问题

磁盘:
fsync 慢、backend commit 慢、空间不足、碎片过多

1.8 日常维护:Compaction、Defrag、Quota

上一篇已经讲过 MVCC 和 compaction。这里从运维角度再串起来:

1
2
3
4
5
写入越来越多
-> MVCC 保留历史 revision
-> backend db 变大
-> compaction 删除旧 revision 的逻辑历史
-> defrag 回收底层文件空间

1.8.1 Compaction

手动压缩到某个 revision:

1
etcdctl --endpoints=${ENDPOINTS} compact 100000

自动压缩示例:

1
2
3
etcd \
--auto-compaction-mode=periodic \
--auto-compaction-retention=1h

或者:

1
2
3
etcd \
--auto-compaction-mode=revision \
--auto-compaction-retention=10000

取舍:

  • 保留太短:慢 Watch 客户端更容易遇到 compacted revision;
  • 保留太长:DB 增长更快,碎片和 IO 压力更大。

1.8.2 Defrag

Compaction 是逻辑清理,defrag 是物理整理。

对整个集群执行:

1
etcdctl --endpoints=${ENDPOINTS} defrag --cluster

也可以逐个 endpoint 执行,降低对集群的瞬时影响:

1
2
3
etcdctl --endpoints=10.0.1.10:2379 defrag
etcdctl --endpoints=10.0.1.11:2379 defrag
etcdctl --endpoints=10.0.1.12:2379 defrag

注意:

  • defrag 可能带来短暂延迟抖动;
  • 不要在业务高峰执行;
  • 大 DB defrag 前先确认磁盘空间;
  • defrag 不是备份替代品。

1.8.3 Quota 与 NOSPACE alarm

etcd 有 backend quota。超过空间配额后,集群会进入 alarm 状态并拒绝写入,常见错误类似:

1
etcdserver: mvcc: database space exceeded

处理流程一般是:

1
2
3
4
5
1. 查看 alarm
2. compact 旧 revision
3. defrag 回收空间
4. disarm alarm
5. 检查是否需要调整 quota 或治理写入模式

命令示例:

1
2
3
4
etcdctl --endpoints=${ENDPOINTS} alarm list
etcdctl --endpoints=${ENDPOINTS} compact 100000
etcdctl --endpoints=${ENDPOINTS} defrag --cluster
etcdctl --endpoints=${ENDPOINTS} alarm disarm

配置 quota 示例:

1
2
etcd \
--quota-backend-bytes=8589934592

这个值不是越大越好。过大的 backend 会拉长恢复、备份、defrag 和 cache 压力。根因上,要避免把 etcd 当高频业务库。

1.9 集群成员动态扩缩容

etcd 支持动态成员变更,但这不是普通无状态服务扩容。成员变更会改变 Raft quorum,必须谨慎。

1.9.1 查看成员

1
etcdctl --endpoints=${ENDPOINTS} member list --write-out=table

1.9.2 添加成员

例如已有两个健康成员,要添加 infra3

1
2
3
etcdctl --endpoints=10.0.1.10:2379,10.0.1.11:2379 \
member add infra3 \
--peer-urls=https://10.0.1.13:2380

命令会返回新成员启动所需的环境变量,关键是:

1
2
3
ETCD_NAME="infra3"
ETCD_INITIAL_CLUSTER="infra0=...,infra1=...,infra2=...,infra3=https://10.0.1.13:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"

然后在新机器启动:

1
2
3
4
5
6
7
8
9
etcd \
--name infra3 \
--data-dir /var/lib/etcd \
--initial-advertise-peer-urls https://10.0.1.13:2380 \
--listen-peer-urls https://10.0.1.13:2380 \
--listen-client-urls https://10.0.1.13:2379 \
--advertise-client-urls https://10.0.1.13:2379 \
--initial-cluster "infra0=https://10.0.1.10:2380,infra1=https://10.0.1.11:2380,infra2=https://10.0.1.12:2380,infra3=https://10.0.1.13:2380" \
--initial-cluster-state existing

注意:添加成员后,新成员追数据期间可能给集群带来复制压力。

1.9.3 删除成员

先查 member ID:

1
etcdctl --endpoints=${ENDPOINTS} member list --write-out=table

删除:

1
etcdctl --endpoints=${ENDPOINTS} member remove <member-id>

删除成员通常用于:

  • 节点永久损坏;
  • 机器下线;
  • 缩容;
  • 替换节点。

1.9.4 更新成员 peer URL

如果成员 IP 或 peer URL 变化,可以更新:

1
2
3
etcdctl --endpoints=${ENDPOINTS} \
member update <member-id> \
--peer-urls=https://10.0.1.20:2380

这种操作要配合实际节点启动参数一起改,否则成员表和进程配置不一致。

1.9.5 成员变更原则

  • 一次只变更一个成员;
  • 变更前确认当前集群健康;
  • 不要在没有多数派时强行扩缩容;
  • 添加新成员后确认同步完成;
  • 删除成员前确认删除后仍有多数派;
  • 替换节点通常是先 remove 旧成员,再 add 新成员;
  • 每次变更后执行 endpoint healthendpoint statusmember list

2. 底层架构剖析

理解 etcd 底层架构,不是为了背名词,而是为了在故障时能判断:

  • 是网络导致 leader 抖动?
  • 是磁盘慢导致写入卡住?
  • 是 Watch 客户端落后太多?
  • 是 quota 满了?
  • 是线性读被 quorum 延迟拖慢?
  • 是 follower 读到了旧数据?

2.1 Raft 一致性协议精解

Raft 解决的问题是:多台机器如何对同一串日志达成一致。

etcd 可以简单理解为:

1
2
3
4
5
6
7
8
客户端写请求
-> 转成 Raft proposal
-> 写入 leader 日志
-> 复制给 followers
-> 多数派确认
-> commit
-> apply 到 MVCC store
-> 返回客户端
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
2
3
4
5
6
7
term 1:
leader = infra0

infra0 故障,触发选举

term 2:
leader = infra1

Term 的作用:

  • 区分新旧 leader;
  • 防止旧 leader 恢复后继续以 leader 身份写入;
  • 投票和日志复制时判断消息新旧;
  • 帮助集群在网络分区恢复后收敛。

一个任期内最多只有一个 leader 被选出。旧 term 的 leader 收到更高 term 的消息后,会退回 follower。

2.4 Leader 选举过程

简化过程:

1
2
3
4
5
6
7
8
1. Follower 持续接收 leader heartbeat
2. 一段时间没有收到 heartbeat
3. Follower 变成 Candidate
4. Candidate 增加 term
5. Candidate 给自己投票
6. Candidate 向其他节点发起 RequestVote
7. 获得多数票后成为 Leader
8. 新 Leader 开始发送 heartbeat
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
2
3
index: 日志位置
term: 产生该日志的任期
data: 具体操作内容

复制过程:

1
2
3
4
5
6
7
8
1. leader append 本地日志
2. leader 向 followers 发送 AppendEntries
3. follower 检查 prevLogIndex 和 prevLogTerm
4. 如果匹配,追加日志
5. follower 返回 ack
6. leader 收到多数派 ack
7. leader 推进 commit index
8. 各节点 apply 到状态机

Raft 的关键约束:

  • 已提交日志不会被覆盖;
  • 新 leader 必须拥有足够新的日志;
  • follower 日志冲突时,以 leader 日志为准回退修正;
  • 只有多数派确认的日志才能成为集群事实。

这就是 etcd 在网络分区里能避免脑裂写入的原因。

2.6 网络分区时的行为

以 3 节点为例:

1
2
3
4
5
6
7
8
正常:
infra0 leader
infra1 follower
infra2 follower

发生网络分区:
分区 A: infra0
分区 B: infra1 + infra2

如果旧 leader infra0 落到少数派:

1
2
3
4
5
6
7
8
9
infra0:
收不到多数派 ack
无法提交新写入
最终发现更高 term 后退位

infra1 + infra2:
能形成多数派
选出新 leader
继续处理写入

这就是 CP 的取舍:少数派不可写,保证不会出现两个分区都成功提交写入。

如果分区是:

1
2
分区 A: infra0 + infra1
分区 B: infra2

多数派一侧可以继续服务,少数派不能提交写入。

恢复后:

1
2
3
4
5
1. 少数派节点重新连接集群
2. 发现更高 term 或新的 leader
3. 回退为 follower
4. 对齐 leader 日志
5. 继续参与复制

注意:旧 leader 在少数派期间可能接收到了客户端请求,但这些请求没有多数派提交,就不能算成功事实。客户端必须以 etcd 返回成功为准,不能以“请求发出去了”为准。

2.7 etcd 存储架构总览

etcd 的存储可以分成两层:

1
2
3
4
5
6
7
8
9
10
11
Raft 层:
WAL
Raft snapshot
hard state
log entries

MVCC KV 层:
memory index
bbolt backend
revision history
key-value data
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
2
/config/order/rate-limit
/services/order/10.1.1.8:8080

但底层 MVCC 需要按 revision 保存历史版本。

官方数据模型可以概括为:

1
2
3
4
5
6
7
8
logical key:
/config/order/rate-limit

revision:
main revision + sub revision

physical key:
(revision, sub_revision, type)

etcd 会维护内存索引,把用户 key 映射到它对应的版本位置:

1
2
3
4
user key
-> generation
-> revisions
-> backend key location

简化理解:

1
2
3
4
5
6
7
8
9
10
memory index:
/config/order/rate-limit
-> rev 100
-> rev 130
-> rev 188

bbolt backend:
(100, 0) -> old value
(130, 0) -> middle value
(188, 0) -> current value

为什么需要内存索引?

  • 快速定位某个 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
2
3
4
5
6
7
8
9
10
client put
-> leader receives proposal
-> append raft entry
-> persist to WAL
-> replicate to followers
-> quorum ack
-> commit
-> apply to MVCC
-> backend commit
-> response

WAL 对排障很重要,因为 etcd 写入延迟经常和磁盘 fsync 有关。

如果看到:

1
etcd_disk_wal_fsync_duration_seconds p99 high

通常要查:

  • 磁盘是否是低延迟 SSD;
  • 是否和业务高 IO 混部;
  • 云盘是否有突发额度耗尽;
  • 宿主机是否有 IO wait;
  • 文件系统或挂载参数是否异常;
  • 是否有备份、扫描任务影响磁盘。

2.10 bbolt:后端存储引擎

etcd v3 的后端数据存储在 member/snap/db,这是 bbolt 数据库文件。

bbolt 可以理解为嵌入式、单文件、事务型 KV 存储,底层使用 B+tree。etcd 把已经 apply 的 MVCC 数据、成员信息、鉴权信息、元数据等存进去。

目录结构大致类似:

1
2
3
4
5
6
7
8
9
/var/lib/etcd/
member/
wal/
0000000000000000-0000000000000000.wal
...
snap/
db
0000000000000003-0000000000010000.snap
...

不同文件大致职责:

文件/目录 作用
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
3
4
5
6
7
8
逻辑大小:
etcd_mvcc_db_total_size_in_use_in_bytes

物理大小:
etcd_mvcc_db_total_size_in_bytes

差距过大:
说明碎片较多,可以计划 defrag

2.11 写请求生命周期

put /config/order/rate-limit 100 为例。

完整路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. client 发送 Put 请求到某个 etcd endpoint
2. 如果 endpoint 不是 leader,请求被转发或重定向到 leader
3. leader 把 Put 转成 Raft proposal
4. leader 追加本地 Raft log
5. leader 写 WAL
6. leader 发送 AppendEntries 给 followers
7. followers 写自己的 WAL 并返回 ack
8. leader 收到多数派 ack
9. leader 推进 commit index
10. 已提交日志 apply 到 MVCC store
11. bbolt backend 提交数据
12. watch 事件被分发
13. client 收到成功响应
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
etcdctl --endpoints=${ENDPOINTS} get /config/order/rate-limit

2.12.2 Serializable Read

串行读可以从当前连接的成员本地状态返回,不必每次都和 leader/quorum 协调。

命令示例:

1
etcdctl --endpoints=${ENDPOINTS} get /config/order/rate-limit --consistency=s

适合:

  • 对短暂旧值不敏感;
  • 大量只读查询;
  • 监控、巡检、低风险展示;
  • 已有本地缓存兜底的场景。

不适合:

  • 锁;
  • 选主;
  • CAS 前置读取;
  • 强一致配置发布确认;
  • 任何“读旧会造成错误决策”的链路。

2.12.3 怎么选

一句话:

控制决策用线性一致性读;低风险观察可以用串行读。

表格化:

场景 建议
服务发现客户端启动全量拉取 通常用默认线性读更稳
Watch 后的本地缓存调用 调用时读本地缓存,不直接读 etcd
配置中心发布后确认 线性读
监控页面展示 key 列表 可评估串行读
分布式锁判断 线性读或直接使用官方 concurrency 原语
选主观察 线性读更稳

3. 故障排查思路

etcd 故障排查要先分层,不要一上来就猜“是不是 Raft 坏了”。Raft 通常没坏,坏的是网络、磁盘、证书、容量、客户端使用方式。

3.1 快速体检命令

1
2
3
4
5
6
7
export ETCDCTL_API=3
export ENDPOINTS=10.0.1.10:2379,10.0.1.11:2379,10.0.1.12:2379

etcdctl --endpoints=${ENDPOINTS} endpoint health
etcdctl --endpoints=${ENDPOINTS} --write-out=table endpoint status
etcdctl --endpoints=${ENDPOINTS} member list --write-out=table
etcdctl --endpoints=${ENDPOINTS} alarm list

带 TLS:

1
2
3
4
5
6
etcdctl \
--endpoints=https://10.0.1.10:2379,https://10.0.1.11:2379,https://10.0.1.12:2379 \
--cacert=/etc/etcd/pki/ca-client.crt \
--cert=/etc/etcd/pki/client.crt \
--key=/etc/etcd/pki/client.key \
endpoint health

3.2 没有 leader

表现:

1
2
3
4
etcd_server_has_leader = 0
写请求失败
endpoint health 不稳定
leader changes 增加

排查:

  • 成员之间 2380 是否互通;
  • peer TLS 是否正常;
  • 是否多数节点故障;
  • 节点时间是否异常;
  • 磁盘 fsync 是否极慢;
  • CPU 是否被打满或 throttling;
  • 是否发生网络分区。

3.3 写入变慢

重点看:

1
2
3
4
etcd_disk_wal_fsync_duration_seconds
etcd_disk_backend_commit_duration_seconds
etcd_server_proposals_pending
etcd_network_peer_round_trip_time_seconds

判断:

  • 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
etcdserver: mvcc: database space exceeded

处理:

1
2
3
4
etcdctl --endpoints=${ENDPOINTS} alarm list
etcdctl --endpoints=${ENDPOINTS} compact <revision>
etcdctl --endpoints=${ENDPOINTS} defrag --cluster
etcdctl --endpoints=${ENDPOINTS} alarm disarm

根因治理:

  • 不要高频写 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 或业务数据库使用。

6. 参考资料


etcd原理与高可用
https://www.albeltbo.com/posts/6300945f.html
作者
AlbeltBo
发布于
2026年7月14日
许可协议