集群与高可用
etcd 集群搭建、成员管理、Raft 参数调优、Learner 模式、快照与灾难恢复,以及生产级运维要点。
集群基础
为什么需要集群
| 节点数 | 容忍故障 | 说明 |
|---|---|---|
| 1 | 0 | 无容错,仅适用于开发测试 |
| 2 | 0 | 无法形成多数派(quorum),不推荐 |
| 3 | 1 | 最小生产部署 |
| 5 | 2 | 推荐的高可用配置 |
| 7 | 3 | 大规模部署(性能可能因日志复制变慢) |
💡 最佳实践:生产环境推荐 3 或 5 个节点。更多节点增加容错但降低写入性能(日志需要复制到多数节点)。
集群启动
方式一:静态配置(推荐)
# 节点 1
etcd --name etcd1 \
--initial-advertise-peer-urls http://10.0.0.1:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://10.0.0.1:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster etcd1=http://10.0.0.1:2380,etcd2=http://10.0.0.2:2380,etcd3=http://10.0.0.3:2380 \
--initial-cluster-state new \
--data-dir /var/lib/etcd
# 节点 2
etcd --name etcd2 \
--initial-advertise-peer-urls http://10.0.0.2:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://10.0.0.2:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster etcd1=http://10.0.0.1:2380,etcd2=http://10.0.0.2:2380,etcd3=http://10.0.0.3:2380 \
--initial-cluster-state new \
--data-dir /var/lib/etcd
# 节点 3
etcd --name etcd3 \
--initial-advertise-peer-urls http://10.0.0.3:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://10.0.0.3:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster etcd1=http://10.0.0.1:2380,etcd2=http://10.0.0.2:2380,etcd3=http://10.0.0.3:2380 \
--initial-cluster-state new \
--data-dir /var/lib/etcd🚨 陷阱:
--initial-cluster中的名字(如etcd1)必须与--name完全一致。
方式二:动态发现
# 使用已有的 etcd 集群作为 discovery
etcd --discovery-srv example.com \
--initial-cluster-state new \
...
# 使用 public discovery 服务(仅用于开发测试)
etcd --discovery https://discovery.etcd.io/new?size=3 \
--initial-cluster-state new \
...成员管理
Go 客户端操作
cluster := clientv3.NewCluster(cli)
// 列出所有成员
resp, err := cluster.MemberList(ctx)
for _, m := range resp.Members {
fmt.Printf("ID=%x Name=%s PeerURLs=%v ClientURLs=%v IsLearner=%v\n",
m.ID, m.Name, m.PeerURLs, m.ClientURLs, m.IsLearner)
}
// 添加成员
resp2, err := cluster.MemberAdd(ctx, []string{"http://10.0.0.4:2380"})
// 添加 Learner(不参与投票,用于热备份或跨地域复制)
resp3, err := cluster.MemberAddAsLearner(ctx, []string{"http://10.0.0.5:2380"})
// Learner 提升为 Voter
_, err = cluster.MemberPromote(ctx, learnerID)
// 移除成员
_, err = cluster.MemberRemove(ctx, memberID)
// 更新成员的 PeerURLs
_, err = cluster.MemberUpdate(ctx, memberID, []string{"http://new-host:2380"})命令行操作
etcdctl member list # 列出成员
etcdctl member add etcd4 --peer-urls=http://10.0.0.4:2380
etcdctl member remove <member-id>
etcdctl member update <id> --peer-urls=http://new:2380
etcdctl member promote <learner-id>Learner 模式
Learner(学习者):接收 Raft 日志但不参与投票(不参与 quorum 计算)
用途:
- 新节点加入时,先用 Learner 追上日志,再提升为 Voter
- 跨地域只读副本
- 热备份节点
流程:
① member add --learner → ② 日志追上 → ③ member promote → ④ 成为 Voter💡 最佳实践:添加新节点时优先使用 Learner,避免新节点日志落后时影响集群的 quorum 提交。
Raft 参数调优
核心参数
| 参数 | 默认值 | 建议范围 | 说明 |
|---|---|---|---|
--heartbeat-interval |
100ms | 100-250ms | Leader 发心跳的频率 |
--election-timeout |
1000ms | 500-5000ms | Follower 无心跳则发起选举的超时时间 |
--snapshot-count |
100000 | 50000-200000 | 多少次 commit 后触发一次快照 |
推荐配比:election-timeout ≥ 5 × heartbeat-interval
网络延迟与参数调整
| 场景 | heartbeat-interval | election-timeout |
|---|---|---|
| 同机房(低延迟) | 100ms | 1000ms(默认) |
| 跨可用区 | 150-200ms | 1500-2500ms |
| 跨地域(WAN) | 250-500ms | 3000-5000ms |
🚨 陷阱:
election-timeout太短会导致网络抖动时频繁 Leader 切换;太长会导致故障检测慢。
快照与灾难恢复
创建快照
maintenance := clientv3.NewMaintenance(cli)
// 创建快照并保存到文件
reader, err := maintenance.Snapshot(ctx)
if err != nil {
log.Fatal(err)
}
defer reader.Close()
f, _ := os.Create("backup.db")
defer f.Close()
io.Copy(f, reader)# 命令行
etcdctl snapshot save backup.db恢复快照
# 1. 从快照恢复数据目录
etcdctl snapshot restore backup.db \
--name etcd-restored \
--data-dir /var/lib/etcd-restored \
--initial-cluster etcd-restored=http://localhost:2380 \
--initial-advertise-peer-urls http://localhost:2380
# 2. 以恢复后的数据目录启动
etcd --data-dir /var/lib/etcd-restored ...灾难恢复流程
第 1 步:保存所有剩余节点的快照
etcdctl --endpoints=NODE1:2379 snapshot save node1.db
etcdctl --endpoints=NODE2:2379 snapshot save node2.db
第 2 步:选择最新的快照
etcdctl snapshot status node1.db
etcdctl snapshot status node2.db
第 3 步:从最新快照恢复新集群
etcdctl snapshot restore latest.db \
--name new-1 \
--initial-cluster new-1=http://10.0.0.1:2380,new-2=http://10.0.0.2:2380,new-3=http://10.0.0.3:2380 \
...
第 4 步:启动新集群
在各节点上以恢复后的 data-dir 启动 etcd
第 5 步:更新客户端 endpoints
指向新集群地址存储与性能
存储容量管理
etcd 使用 BoltDB 作为后端存储(v3 默认 2GB 上限)。
# 查看 DB 大小和剩余空间
etcdctl endpoint status --write-out=table
# 压缩历史版本(清理旧 revision)
etcdctl compact <latest-revision>
# 碎片整理(释放空间回操作系统)
etcdctl defrag
# 集群级别碎片整理
etcdctl defrag --cluster告警处理
# etcd 存储空间超过 --quota-backend-bytes 的 95% 发出 NOSPACE 告警
etcdctl alarm list
# memberID:xxx alarm:NOSPACE
# 此时写入会被拒绝 → 必须先解除告警
etcdctl compact <rev>
etcdctl defrag
etcdctl alarm disarm性能优化清单
| 优化项 | 说明 |
|---|---|
| ⚡ 使用 SSD | BoltDB 对 IO 延迟敏感,SSD 显著优于 HDD |
| ⚡ 独立磁盘 | data-dir 建议独占磁盘,避免与其他服务争 IO |
| ⚡ 开启自动压缩 | --auto-compaction-mode=periodic --auto-compaction-retention=1 |
| ⚡ 合理设置 snapshot-count | 写入密集型场景适当增大(如 200000),减少快照频率 |
| ⚡ 控制 Key 数量 | 超过 100 万 Key 时注意调大 --quota-backend-bytes |
| ⚡ 单 Value 不宜过大 | 建议 ≤ 1MB,大对象存外部存储 |
| ⚡ 限制 Lease 数量 | 单节点建议 ≤ 10 万 Lease |
| ⚡ 客户端连接数控制 | 配置合理的 keepalive 参数,避免连接数膨胀 |
维护端点 API
maintenance := clientv3.NewMaintenance(cli)
// 获取节点状态
status, _ := maintenance.Status(ctx, "localhost:2379")
fmt.Printf("Version=%s, DB Size=%d MB, Leader=%x, IsLearner=%v\n",
status.Version, status.DbSize/1024/1024, status.Leader, status.IsLearner)
// 哈希一致性检查
hashResp, _ := maintenance.HashKV(ctx, "localhost:2379", 0)
fmt.Printf("Hash=%d, CompactRev=%d\n", hashResp.Hash, hashResp.CompactRevision)
// 碎片整理
_, err := maintenance.Defragment(ctx, "localhost:2379")
// 强制切换 Leader
_, err := maintenance.MoveLeader(ctx, targetMemberID)
// 告警列表
alarms, _ := maintenance.AlarmList(ctx)
for _, a := range alarms.Alarms {
if a.Alarm == pb.AlarmType_NOSPACE {
log.Println("警告:存储空间不足!")
}
}常见陷阱
| 陷阱 | 说明 |
|---|---|
| 🚨 2 节点集群 | 无法容忍任何节点故障,一个节点挂掉即无法形成 quorum |
| 🚨 NOSPACE 告警忽略 | 可用空间不足 5% 触发告警后写入被拒绝,服务不可用 |
| 🚨 Leader 选举期间不可写入 | 选举通常在 1s 内完成,但网络问题可能导致长时间的写入中断 |
| 🚨 新节点直接加为 Voter | 新节点日志落后时影响 quorum。始终先用 Learner 追日志再 Promote |
| 🚨 快照恢复后未更新 endpoints | 恢复后的新集群地址可能与旧集群不同,客户端需更新 |
| 🚨 跨地域 RTT 过大 | Raft 日志复制受限于网络延迟,跨地域写入延迟 ≥ RTT/2 |