分布式系统的"大脑" —— 一个高可用、强一致的分布式键值存储系统


什么是 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

ZooKeeper

Consul

Redis

一致性

强一致(Raft)

强一致(ZAB)

强一致(Raft)

最终一致

协议

Raft

ZAB

Raft

主从复制

语言

Go

Java

Go

C

Watch 机制

✅ 原生高效

✅ Watcher

❌(需轮询)

TTL/租约

✅ 原生

✅ 临时节点

✅ EXPIRE

API

gRPC/HTTP

自有协议

HTTP/gRPC

RESP 协议

K8s 使用

✅ 官方默认

可选

运维复杂度

高(JVM)

适合场景

配置/协调

配置/协调

服务发现

缓存/队列


应用场景汇总

┌────────────────────────────────────────────────────┐
│               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 就是最佳选择。