Skip to content
集群与高可用

集群与高可用

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