etcd 深度解析
分布式系统的"大脑" —— 一个高可用、强一致的分布式键值存储系统
什么是 etcd?
etcd 是一个开源的分布式键值存储系统,由 CoreOS 团队于 2013 年开发,现已成为 CNCF 毕业项目。名字来源于 Linux /etc 目录(存储配置)+ d(distributed,分布式)。
etcd 的核心定位:
不是普通数据库(不适合存大量业务数据)
不是缓存(不追求极致读写性能)
而是:
✅ 分布式系统的配置中心
✅ 服务注册与发现
✅ 分布式锁
✅ 选主(Leader Election)
✅ 集群状态协调
一句话:分布式系统里需要"达成共识"的数据,都适合放 etcd
谁在用 etcd?
Kubernetes ← 最大用户,所有集群状态存在 etcd
APISIX ← 路由/插件/上游配置
CoreDNS ← DNS 记录存储
Prometheus ← 部分元数据
TiKV/TiDB ← PD 组件用 etcd 选主
etcd 自己 ← 内部用 Raft 协调
全球运行着数百万个 etcd 集群实例
核心架构
整体结构
┌────────────────────────────────────────────────────────┐
│ etcd 集群 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 节点 1 │ │ 节点 2 │ │ 节点 3 │ │
│ │ (Leader) │ │ (Follower) │ │ (Follower) │ │
│ │ │ │ │ │ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │Raft 状态机│ │ │ │Raft 状态机│ │ │ │Raft 状态机│ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ WAL 日志 │ │ │ │ WAL 日志 │ │ │ │ WAL 日志 │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ BoltDB │ │ │ │ BoltDB │ │ │ │ BoltDB │ │ │
│ │ │ (存储层) │ │ │ │ (存储层) │ │ │ │ (存储层) │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↑ ↑ ↑ │
│ └──────────────────┴──────────────────┘ │
│ Raft 协议同步 │
└────────────────────────────────────────────────────────┘
↑ ↑ ↑
客户端 客户端 客户端
(gRPC) (gRPC) (gRPC)
存储层次
etcd 内部存储结构:
┌─────────────────────────────────┐
│ 客户端 API │ ← gRPC / HTTP
├─────────────────────────────────┤
│ KV 接口层 │ ← Put/Get/Delete/Watch
├─────────────────────────────────┤
│ MVCC 多版本控制 │ ← 每次写入生成新版本
├─────────────────────────────────┤
│ Raft 日志 │ ← 强一致性保证
├─────────────────────────────────┤
│ WAL 预写日志 │ ← 持久化,防数据丢失
├─────────────────────────────────┤
│ BoltDB(底层存储) │ ← 嵌入式 KV 数据库
└─────────────────────────────────┘
核心原理:Raft 共识算法
etcd 强一致性的根基,所有节点数据保持一致的秘密
角色模型
etcd 集群中每个节点处于以下三种状态之一:
Leader(领导者):
├── 全集群唯一
├── 负责处理所有写请求
├── 将日志复制给 Follower
└── 定期发送心跳维持地位
Follower(跟随者):
├── 被动接收 Leader 的日志
├── 响应 Leader 的心跳
├── 可以处理读请求
└── 超时未收到心跳 → 发起选举
Candidate(候选者):
├── Follower 超时后转变
├── 向其他节点拉票
└── 获得多数票 → 成为新 Leader
写入流程(强一致性保证)
客户端写入:PUT /config/route = "..."
步骤一:请求到达 Leader
┌─────────┐
│ Client │──→ 写请求
└─────────┘ ↓
┌────────┐
│ Leader │
└────────┘
步骤二:Leader 写入本地 WAL 日志
┌────────┐
│ Leader │ → 写 WAL ✅
└────────┘
步骤三:并行同步给所有 Follower
┌─────────────────────────────┐
│ Leader 并行发送日志条目 │
└──┬──────────────────────┬──┘
↓ ↓
Follower1 Follower2
写 WAL ✅ 写 WAL ✅
步骤四:多数派确认(Quorum)
集群3节点,需要 2/3 确认
Leader + Follower1 = 多数派 ✅
(不需要等 Follower2,提高可用性)
步骤五:Leader 提交,返回客户端成功
┌─────────┐
│ Client │← 写入成功 ✅
└─────────┘
步骤六:异步通知 Follower 提交
Follower1、Follower2 收到提交通知
应用到本地状态机(BoltDB)
关键保证:
✅ 只要多数派节点存活,写入就能成功
✅ 已提交的数据永远不会丢失
✅ 所有节点最终数据完全一致
Leader 选举
正常状态:
Leader 每隔 heartbeat_interval(100ms) 发心跳
Leader 故障:
Follower 在 election_timeout(1000ms) 内没收到心跳
↓
转变为 Candidate,任期号(term) +1
↓
向所有节点发送 RequestVote
↓
┌─────────────────────────────────────────┐
│ 投票规则: │
│ 1. 每个节点每个任期只投一票 │
│ 2. 候选者日志必须比投票者日志更新 │
│ 3. 先到先得 │
└─────────────────────────────────────────┘
↓
获得超过半数票(如3节点需2票)
↓
成为新 Leader,立即发送心跳
选举时间:通常 150ms ~ 300ms 完成
多数派原则(为什么推荐奇数节点)
集群节点数 vs 容忍故障数:
节点数 多数派 可容忍故障
1 1 0 ← 无容错
2 2 0 ← 无容错(2个都要同意)
3 2 1 ← 生产最低要求
4 3 1 ← 和3节点一样,浪费
5 3 2 ← 高可用推荐
7 4 3 ← 超大规模
结论:
✅ 生产环境最少 3 节点
✅ 推荐 3 或 5 节点
❌ 不要用偶数节点(容错能力和少一个节点一样)
核心特性详解
1. MVCC 多版本并发控制
etcd 每次写入不覆盖旧值,而是创建新版本:
时间线:
revision=1: PUT key="config" value="v1"
revision=2: PUT key="config" value="v2"
revision=3: PUT key="config" value="v3"
可以读取历史版本:
GET key="config" → "v3"(最新)
GET key="config" rev=1 → "v1"(历史)
GET key="config" rev=2 → "v2"(历史)
Watch 也可以从历史版本开始监听:
WATCH key="config" startRev=2 → 收到 v2→v3 的变更
MVCC 的价值:
✅ 无锁读写,高并发
✅ 支持 Watch 历史变更
✅ 支持事务(CAS 操作)
✅ 快照备份(某一时刻的完整状态)
2. Watch 机制(APISIX 动态配置的核心)
Watch 是 etcd 最强大的特性之一
传统轮询方式(低效):
客户端每秒查询 → "配置变了吗?"
etcd 回答 → "没变"
重复 1000 次/秒 → 大量无效请求
etcd Watch 方式(高效):
客户端建立 Watch → "有变化告诉我"
etcd 保持连接,有变化立即推送
无变化 = 零开销
Watch 工作原理:
┌─────────────────────────────────────────────┐
│ │
│ APISIX ──WATCH /apisix/routes──→ etcd │
│ │
│ 管理员修改路由 │
│ ↓ │
│ etcd 检测到 /apisix/routes 变化 │
│ ↓ │
│ 立即推送事件给所有 Watch 该前缀的客户端 │
│ ↓ │
│ APISIX 收到推送 → 更新内存配置(<100ms) │
│ │
└─────────────────────────────────────────────┘
Watch 支持:
✅ 单个 Key 监听:WATCH /config/db/host
✅ 前缀监听: WATCH /config/(所有子key)
✅ 范围监听: WATCH /config/a ~ /config/z
✅ 历史追溯: 从指定 revision 开始监听
3. 租约机制(TTL / Lease)
租约 = 带有过期时间的"令牌"
用途:
1. 服务注册(服务存活心跳)
2. 分布式锁(锁自动释放)
3. 临时配置(过期自动清除)
工作流程(服务注册示例):
服务A 启动
↓
向 etcd 申请租约(TTL=10s)
→ 获得 leaseID=1234
↓
注册服务:PUT /services/A = "192.168.1.1:8080" WITH leaseID=1234
↓
服务A 每 3s 续租一次(KeepAlive)
→ etcd 重置过期时间为 10s
↓
服务A 宕机 → 无法续租
↓
10s 后租约到期
↓
与该租约绑定的所有 Key 自动删除!
→ Watch 收到删除事件
→ 服务发现感知到服务下线
这就是微服务"心跳检测"的底层原理
4. 事务(Transaction)
etcd 支持原子性的 Compare-And-Swap 事务:
// 伪代码
Txn(
IF: key="lock" 的 value == "" // 条件:锁未被占用
THEN: PUT key="lock" value="nodeA" // 成功:加锁
ELSE: GET key="lock" // 失败:返回当前持锁者
)
这是实现分布式锁的基础:
┌─────────────────────────────────────────────┐
│ 节点A 和 节点B 同时申请锁 │
│ │
│ 节点A 发送 Txn → etcd 先收到 │
│ 节点B 发送 Txn → etcd 后收到 │
│ │
│ etcd 串行处理(Raft 保证顺序) │
│ 节点A → 条件成立 → 加锁成功 ✅ │
│ 节点B → 条件不成立 → 加锁失败,等待重试 │
└─────────────────────────────────────────────┘
全局唯一性由 Raft 强一致性保证
不会出现两个节点同时持锁的情况
实战操作
安装与启动
# Docker 单节点(开发测试)
docker run -d \
--name etcd \
-p 2379:2379 \
-p 2380:2380 \
quay.io/coreos/etcd:v3.5.7 \
etcd \
--name=etcd0 \
--listen-client-urls=http://0.0.0.0:2379 \
--advertise-client-urls=http://localhost:2379 \
--listen-peer-urls=http://0.0.0.0:2380
# 验证启动
etcdctl --endpoints=localhost:2379 endpoint health
# 输出: localhost:2379 is healthy
基础 CRUD 操作
# 写入
etcdctl put /config/database/host "192.168.1.100"
etcdctl put /config/database/port "5432"
etcdctl put /config/database/name "myapp"
# 读取
etcdctl get /config/database/host
# /config/database/host
# 192.168.1.100
# 前缀读取(读取所有 /config/database/ 下的key)
etcdctl get /config/database/ --prefix
# /config/database/host → 192.168.1.100
# /config/database/name → myapp
# /config/database/port → 5432
# 删除
etcdctl del /config/database/host
# 范围删除
etcdctl del /config/database/ --prefix
Watch 监听
# 终端1:监听变化
etcdctl watch /config/app --prefix
# 终端2:修改配置
etcdctl put /config/app/version "2.0.0"
# 终端1 立即收到:
# PUT
# /config/app/version
# 2.0.0
# 从历史版本开始 Watch
etcdctl watch /config/app --rev=5
租约操作
# 创建 30秒 租约
etcdctl lease grant 30
# lease 694d71ddacfda227 granted with TTL(30s)
# 绑定 Key 到租约
etcdctl put /services/web "192.168.1.10:8080" \
--lease=694d71ddacfda227
# 续租(保活)
etcdctl lease keep-alive 694d71ddacfda227
# 查看租约剩余时间
etcdctl lease timetolive 694d71ddacfda227
# lease 694d71ddacfda227 granted with TTL(30s), remaining(18s)
# 主动撤销租约(绑定的Key立即删除)
etcdctl lease revoke 694d71ddacfda227
集群管理
# 查看集群成员
etcdctl member list
# 节点ID 状态 名称 Peer地址 Client地址
# 8e9e05c52164694d started node1 http://node1:2380 http://node1:2379
# 91bc3c398fb3c146 started node2 http://node2:2380 http://node2:2379
# fd422379fda50e48 started node3 http://node3:2380 http://node3:2379
# 查看集群健康状态
etcdctl endpoint health \
--endpoints=node1:2379,node2:2379,node3:2379
# node1:2379 is healthy
# node2:2379 is healthy
# node3:2379 is healthy
# 查看集群状态(谁是Leader)
etcdctl endpoint status \
--endpoints=node1:2379,node2:2379,node3:2379 \
--write-out=table
# ┌───────────────┬──────────────────┬─────────┬─────────┬──────────┐
# │ ENDPOINT │ ID │ VERSION │ DB SIZE │ IS LEADER│
# ├───────────────┼──────────────────┼─────────┼─────────┼──────────┤
# │ node1:2379 │ 8e9e05c52164694d │ 3.5.7 │ 20 MB │ true ✅ │
# │ node2:2379 │ 91bc3c398fb3c146 │ 3.5.7 │ 20 MB │ false │
# │ node3:2379 │ fd422379fda50e48 │ 3.5.7 │ 20 MB │ false │
# └───────────────┴──────────────────┴─────────┴─────────┴──────────┘
生产部署要点
1. 节点配置建议
# etcd.conf.yaml 生产配置
# 节点基础配置
name: etcd-node1
data-dir: /var/lib/etcd/data
wal-dir: /var/lib/etcd/wal # WAL 建议单独磁盘
# 网络
listen-peer-urls: http://0.0.0.0:2380
listen-client-urls: http://0.0.0.0:2379
advertise-client-urls: https://etcd-node1.internal:2379
initial-advertise-peer-urls: https://etcd-node1.internal:2380
# 集群
initial-cluster: >
etcd-node1=https://etcd-node1.internal:2380,
etcd-node2=https://etcd-node2.internal:2380,
etcd-node3=https://etcd-node3.internal:2380
initial-cluster-state: new
initial-cluster-token: my-cluster-token-2024
# TLS 安全(生产必须开启)
client-transport-security:
cert-file: /etc/etcd/tls/server.crt
key-file: /etc/etcd/tls/server.key
trusted-ca-file: /etc/etcd/tls/ca.crt
client-cert-auth: true # 开启双向 TLS
peer-transport-security:
cert-file: /etc/etcd/tls/peer.crt
key-file: /etc/etcd/tls/peer.key
trusted-ca-file: /etc/etcd/tls/ca.crt
peer-client-cert-auth: true
# 性能调优
heartbeat-interval: 100 # 心跳间隔(ms),默认100
election-timeout: 1000 # 选举超时(ms),默认1000
snapshot-count: 10000 # 多少条日志后做快照
max-request-bytes: 1572864 # 最大请求体 1.5MB
quota-backend-bytes: 8589934592 # DB最大8GB
2. 硬件与磁盘要求
etcd 对磁盘延迟极为敏感!
推荐配置:
磁盘:
✅ SSD(强烈推荐)
❌ HDD(延迟太高,会导致选举超时)
最佳:NVMe SSD,顺序写入 > 50 MB/s
网络:
节点间延迟 < 1ms(同机房)
带宽 > 1Gbps
内存:
推荐 8GB+(数据量大时 BoltDB 需要足够内存映射)
CPU:
etcd 不是 CPU 密集型
2核以上即可
磁盘 I/O 对 etcd 的影响:
SSD: WAL 写入 < 1ms → 选举稳定,性能好
HDD: WAL 写入 10~50ms → 频繁触发选举,性能差
3. 数据备份与恢复
# 快照备份(全量)
etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db \
--endpoints=https://etcd-node1:2379 \
--cacert=/etc/etcd/tls/ca.crt \
--cert=/etc/etcd/tls/client.crt \
--key=/etc/etcd/tls/client.key
# 验证快照
etcdctl snapshot status /backup/etcd-20240101.db \
--write-out=table
# ┌──────────┬──────────┬────────────┬──────────┐
# │ HASH │ REVISION │ TOTAL KEYS │ TOTAL SIZE│
# ├──────────┼──────────┼────────────┼──────────┤
# │ a2dc6b45 │ 12345 │ 1024 │ 5 MB │
# └──────────┴──────────┴────────────┴──────────┘
# 从快照恢复(灾难恢复)
etcdctl snapshot restore /backup/etcd-20240101.db \
--name=etcd-node1 \
--data-dir=/var/lib/etcd/restore \
--initial-cluster=etcd-node1=http://node1:2380 \
--initial-advertise-peer-urls=http://node1:2380
# 定时备份脚本
cat > /etc/cron.d/etcd-backup << 'EOF'
0 2 * * * root etcdctl snapshot save /backup/etcd-$(date +\%Y\%m\%d).db \
&& find /backup -name "etcd-*.db" -mtime +7 -delete
EOF
性能基准与优化
etcd 官方性能基准(3节点,SSD):
写入性能:
单节点写: ~10,000 ops/s
并发写(线性):~2,000 ops/s(需走 Raft 共识)
读取性能:
串行读: ~100,000 ops/s(读 Leader)
线性化读: ~10,000 ops/s(需 Raft 确认)
延迟:
写入 P99: < 25ms(SSD)
读取 P99: < 5ms
注意:etcd 不适合高写入场景
✅ 适合:读多写少(配置数据)
❌ 不适合:高频写入(业务数据、日志)
常见性能问题排查
# 1. 查看慢查询
etcdctl check perf --endpoints=node1:2379
# 2. 查看数据库大小
etcdctl endpoint status --write-out=table
# DB SIZE 如果接近 quota-backend-bytes 就需要压缩
# 3. 碎片整理(定期执行)
etcdctl defrag --endpoints=node1:2379,node2:2379,node3:2379
# 4. 压缩历史版本(释放空间)
# 获取当前 revision
REVISION=$(etcdctl endpoint status --write-out=json \
| python3 -c "import json,sys; print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
# 压缩旧版本(保留最近1000个revision)
etcdctl compact $((REVISION - 1000))
# 之后执行碎片整理
etcdctl defrag
etcd 与其他组件对比
应用场景汇总
┌────────────────────────────────────────────────────┐
│ etcd 典型使用场景 │
├─────────────────┬──────────────────────────────────┤
│ 分布式配置中心 │ 配置动态下发,Watch 实时感知变更 │
│ │ → APISIX 路由配置 │
│ │ → Kubernetes 集群配置 │
├─────────────────┼──────────────────────────────────┤
│ 服务注册发现 │ 服务启动注册,宕机租约自动删除 │
│ │ → 微服务健康状态维护 │
├─────────────────┼──────────────────────────────────┤
│ 分布式锁 │ 事务+租约实现,防死锁 │
│ │ → 定时任务防重复执行 │
│ │ → 秒杀等互斥操作 │
├─────────────────┼──────────────────────────────────┤
│ Leader 选举 │ 分布式主节点选举 │
│ │ → K8s 控制器选主 │
│ │ → TiDB PD 组件选主 │
├─────────────────┼──────────────────────────────────┤
│ 集群状态存储 │ 小量关键状态的强一致存储 │
│ │ → K8s 所有对象状态 │
└─────────────────┴──────────────────────────────────┘
总结
┌──────────────────────────────────────────────────┐
│ etcd 核心要点 │
│ │
│ 本质:分布式强一致 KV 存储 │
│ 协议:Raft(Leader选举 + 日志复制) │
│ 特性:Watch推送 / 租约TTL / MVCC / 事务 │
│ │
│ ✅ 强项:配置协调、服务发现、分布式锁 │
│ ❌ 弱项:大数据量、高频写入 │
│ │
│ 生产规范: │
│ 节点数 → 3 或 5(奇数) │
│ 磁盘 → SSD 必须 │
│ 安全 → TLS 双向认证 │
│ 备份 → 定时快照 + 异地存储 │
│ 监控 → 磁盘/延迟/Leader变更 │
└──────────────────────────────────────────────────┘
一句话:etcd 是分布式系统的"神经中枢",用 Raft 算法保证强一致性,用 Watch 机制实现实时推送,用 租约机制实现故障自动感知——凡是需要在分布式节点间"达成共识"的数据,etcd 就是最佳选择。