最佳实践与陷阱
etcd 全手册陷阱汇总、最佳实践清单,以及生产环境上线前检查清单。
陷阱汇总表
集群与高可用
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 2 节点集群 | 🔴 高 | 无法容忍任何节点故障, 一个节点挂掉即无法形成 quorum |
| 🚨 节点数为偶数 | 🔴 高 | 偶数节点无法提高容错(如 4 节点仍只能容忍 1 个故障),浪费资源 |
| 🚨 NOSPACE 被忽略 | 🔴 高 | 触发 NOSPACE 告警后写入被拒绝,服务不可用 |
| 🚨 新节点直接加为 Voter | 🟡 中 | 日志严重落后的节点加入集群会阻塞 quorum 提交 |
| 🚨 跨地域 RTT 过大 | 🟡 中 | Raft 日志复制受限于网络延迟,Leader 必须在多数节点确认后才能返回写入成功 |
| 🚨 时钟不同步 | 🟡 中 | etcd 内部使用逻辑时钟(revision),但租赁(lease)TTL 依赖于本地时钟;时钟偏差可能导致提前或延迟过期 |
存储与性能
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 不配置自动压缩 | 🔴 高 | 默认不会自动压缩,revision 持续增长直到磁盘耗尽 |
| 🚨 大 Value 写入 | 🟡 中 | 单个 Value 超过 1MB 时 Raft 提案和复制明显变慢 |
| 🚨 Key 数量极大 | 🟡 中 | 百万级 Key 时需要适当提高 --quota-backend-bytes |
| 🚨 频繁碎片整理 | 🟡 中 | defrag 会锁住数据库,操作期间服务中断 |
| 🚨 磁盘 IO 性能差 | 🟡 中 | HDD 或共享磁盘严重影响 etcd 写延迟,推荐专用 SSD |
客户端
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 Endpoints 仅配一个节点 | 🔴 高 | 该节点挂掉整个客户端不可用 |
| 🚨 忘记 Close Client | 🟡 中 | gRPC 连接不回收,goroutine 随连接数增长 |
| 🚨 AutoSyncInterval = 0 | 🟡 中 | 集群成员变化后客户端不感知,持续的请求可能失败 |
| 🚨 Context 无超时 | 🟡 中 | 网络故障时请求可能永久阻塞 |
| 🚨 每个 goroutine 创建 Client | 🟡 中 | Client 并发安全,一个全局实例即可 |
Watch
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 Compaction 后 Watch 断连 | 🔴 高 | 从已压缩 revision 恢复 watch 会收到 ErrCompacted |
| 🚨 忘记 WithPrefix | 🟡 中 | Watch(ctx, "/foo") 只监听精确匹配的 /foo |
| 🚨 Watch channel 不消费 | 🟡 中 | 服务端为每个 watcher 维护推送队列,消费慢会占用 etcd 内存 |
| 🚨 不检查 resp.Err() | 🟡 中 | Watch 可能因为网络问题返回错误,不及时处理导致事件丢失 |
Lease
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 KeepAlive 用过期 ctx | 🔴 高 | ctx 超时或取消 → channel 关闭 → Lease 到期 → Key 被删 |
| 🚨 不监控 KeepAlive channel | 🟡 中 | channel 无声关闭后应用以为注册有效,实际 Key 已过期 |
| 🚨 TTL 设置不当 | 🟡 中 | 太短:网络抖动导致不必要的 Key 删除;太长:故障检测慢 |
| 🚨 忘记 Close Session | 🟡 中 | Session 持有 Lease,不 Close 持续续约直到连接断开 |
并发
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 Mutex Lock 无 ctx 超时 | 🟡 中 | 前一持锁者若永不释放,Lock 永久阻塞 |
| 🚨 多 Compare 全 AND 逻辑 | 🟡 中 | 所有 Compare 必须满足才执行 Then,不能实现"任一满足" |
| 🚨 STM 不支持 Delete | 🟡 中 | STM 只有 Get/Put,无法事务性删除 Key |
| 🚨 Election Session 断开 | 🟡 中 | Session 断开自动 Resign,但没有心跳监控时 Leader 可能不自知 |
安全
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| 🚨 etcd 暴露在公网 | 🔴 高 | 攻击者可读写任意数据,读取所有敏感配置 |
| 🚨 root 密码忘记 | 🔴 高 | root 密码无法找回,需重建集群 |
| 🚨 auto-tls 上生产 | 🟡 中 | 自签名证书不提供身份验证,仅用于测试 |
| 🚨 Auth 启用后已有连接失败 | 🟡 中 | 启用 Auth 前建立的连接后续请求全部被拒 |
| 🚨 权限粒度理解错误 | 🟡 中 | 精确匹配 /foo 不等于前缀匹配 /foo/ |
最佳实践清单
部署与运维
- 💡 生产环境至少 3 个节点,推荐 5 个节点
- 💡 使用 SSD 专用磁盘 存储 data-dir
- 💡 必配自动压缩:
--auto-compaction-mode=periodic --auto-compaction-retention=1 - 💡 监控 5 个核心指标:DB 大小、Leader 是否存在、fsync 延迟、Raft 提案延迟、快照频率
- 💡 定期创建快照并异地备份(建议 cron 每天 1 次)
- 💡 新节点加入使用 Learner 模式,追上日志后再 Promote
- 💡 跨可用区部署时适当调大
election-timeout
客户端
- 💡 一个应用一个全局 Client,并发安全共享
- 💡
Endpoints列出所有集群节点 - 💡 生产环境设置
AutoSyncInterval(建议 30s ~ 5min) - 💡 所有请求必须带 context 超时
- 💡 程序退出前
defer cli.Close()
数据设计
- 💡 Key 使用层级命名:
/<domain>/<subsystem>/<entity>/<id> - 💡 单个 Value 建议 ≤ 1MB,大对象存外部存储,etcd 只存引用 URL
- 💡 临时数据绑定 Lease,避免手动清理
- 💡 一个服务实例一个 Lease,而非一个应用一个 Lease
Watch
- 💡 标准模式:先 Get → 记住 revision → 再 Watch(revision+1)
- 💡 必须处理
ErrCompacted(全量重新同步) - 💡 必须检查
resp.Err()和resp.Canceled - 💡 建议开启
WithProgressNotify()检测连接活性 - 💡 单个客户端 watcher 数建议 < 1000,超过时按 range 合并
并发
- 💡 简单 CAS 用手动 Txn,复杂逻辑用 STM
- 💡 分布式锁必须绑定 Lease,防止死锁
- 💡 Lock 操作必须带 context 超时
- 💡 监控 Mutex/Election 的 Session 状态
安全
- 💡 生产环境开启 mTLS + RBAC
- 💡 遵循最小权限原则分配角色
- 💡 证书设置合理的过期时间并建立轮换机制
- 💡 etcd 端口仅绑定内网 IP
生产环境检查清单
使用以下清单在上线前逐项确认:
部署检查
| 检查项 | 说明 |
|---|---|
| ☐ 集群节点数 | ≥ 3 的奇数个节点 |
| ☐ 存储介质 | data-dir 在专用 SSD 上 |
| ☐ 自动压缩 | --auto-compaction-mode=periodic --auto-compaction-retention=1 |
| ☐ 存储上限 | --quota-backend-bytes 合理设置(如 8GB) |
| ☐ Raft 参数 | election-timeout ≥ 5 × heartbeat-interval |
| ☐ 文件描述符 | LimitNOFILE ≥ 65536(systemd) |
| ☐ 网络端口 | 2379/2380 仅绑定内网 IP,防火墙限制访问来源 |
客户端检查
| 检查项 | 说明 |
|---|---|
| ☐ Endpoints | 列出所有集群节点 |
| ☐ DialTimeout | 有合理的超时(如 5s) |
| ☐ AutoSyncInterval | 已设置(如 30s) |
| ☐ 全局 Client 单例 | 未创建多余 Client |
| ☐ 带超时 ctx | 所有操作都有 context 超时 |
| ☐ 优雅关闭 | 程序退出时调用 cli.Close() |
数据设计检查
| 检查项 | 说明 |
|---|---|
| ☐ Key 命名规范 | 统一使用层级命名 |
| ☐ Value 大小 | 没有超大 Value(> 1MB) |
| ☐ Lease 使用 | 临时数据绑定 Lease |
| ☐ Key 数量预估 | 评估是否在百万级以内 |
Watch 检查
| 检查项 | 说明 |
|---|---|
| ☐ compact 处理 | 有 ErrCompacted 的全量同步逻辑 |
| ☐ Err 处理 | 有 resp.Err() 检查 |
| ☐ revision 恢复 | 使用 Get + revision 的增量监听模式 |
| ☐ watcher 数量 | 未超过建议上限 |
安全检查
| 检查项 | 说明 |
|---|---|
| ☐ TLS 加密 | 已启用 |
| ☐ mTLS 认证 | 已启用(非 auto-tls) |
| ☐ RBAC 认证 | 已启用,root 密码已设置 |
| ☐ 最小权限 | 各用户仅有所需 Key 范围的必要权限 |
| ☐ 证书过期 | 证书有轮换计划 |
监控与运维检查
| 检查项 | 说明 |
|---|---|
| ☐ DB 大小告警 | 达 quota-backend-bytes 80% 时告警 |
| ☐ Leader 监控 | 集群始终有 Leader |
| ☐ 快照备份 | 定期快照 + 异地存储 |
| ☐ 恢复演练 | 至少执行过一次快照恢复演练 |
| ☐ etcd 版本 | 所有节点版本一致 |
| ☐ 日志级别 | 生产用 info 或 warn |
快速排障
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| 写入被拒绝 | NOSPACE 告警 | etcdctl alarm list |
| 读不到最新数据 | 使用了 WithSerializable() |
去掉该 option 或改用默认的 Linearizable |
| Watch 连接断 | revision 被 compact | 全量 Get 后重新 Watch |
| 客户端连不上 | 端点不可达 / TLS 配置错误 | etcdctl endpoint health |
| 频繁 Leader 切换 | election-timeout 太短 / 网络不稳定 | 检查日志中的 election 相关消息 |
| 写操作延迟高 | 磁盘 IO 瓶颈 / 跨地域复制 / 网络延迟 | etcdctl endpoint status 查看 DB size 和 Raft index |