etcd 知识点总结
一、 核心概念:etcd 到底是什么?
1.1 通俗理解:分布式系统的”公共记事本”
想象一个大型公司的公共白板:
所有员工都能在上面读写信息(多客户端访问)信息实时同步:一个人写了东西,其他人立即能看到(数据一致性)不会丢失:重要信息会被拍照存档(持久化存储)自动清理:临时信息会定时擦除(租约机制)有人监督:确保大家按规则使用(共识算法)
1.2 官方定义解析
etcd = “etc”(配置目录)+ “d”(分布式)
本质:分布式的键值存储系统目标:解决分布式系统中的数据一致性问题特点:像单个系统一样工作,但实际上运行在多个机器上
1.3 为什么需要 etcd?
传统单机系统的痛点:
单机Redis存储配置
↓
应用服务器1 ← 读取配置
应用服务器2 ← 读取配置
应用服务器3 ← 读取配置
↓
问题:Redis宕机 → 所有服务无法获取配置
etcd 的解决方案:
etcd集群[节点A, 节点B, 节点C]
↓
应用服务器1 ← 从任意健康节点读取
应用服务器2 ← 从任意健康节点读取
应用服务器3 ← 从任意健康节点读取
↓
优势:单个节点宕机不影响服务,数据自动同步
二、 架构设计:etcd 如何工作?
2.1 整体架构:邮局系统比喻
把 etcd 集群想象成一个邮政系统:
总邮局 (Leader)
↓ 分发指令
分局A (Follower) 分局B (Follower) 分局C (Follower)
↓ ↓ ↓
保存邮件副本 保存邮件副本 保存邮件副本
各组件职责:
1. 客户端层(寄信人/收信人)
应用程序通过 gRPC 协议与 etcd 通信支持多种编程语言
2. API 层(邮局前台)
接收客户的寄信/查信请求提供多种服务:
KV服务:存信、取信Watch服务:监控信箱变化Lease服务:设置信件有效期Auth服务:身份验证
3. Raft共识层(邮局调度中心)
决定信件的处理顺序确保所有分局的信件一致选举总邮局负责人
4. 存储层(邮局档案室)
WAL:收发信登记簿(操作日志)Snapshot:定期整理的档案摘要BoltDB:永久档案库
2.2 数据模型:如何组织数据?
层次化键空间 – 类似于文件系统目录:
/ (根目录)
├── /registry/ (服务注册)
│ ├── /services/ (所有服务)
│ │ ├── /web/ (web服务)
│ │ │ ├── /node1 → "192.168.1.10:8080"
│ │ │ └── /node2 → "192.168.1.11:8080"
│ │ └── /database/ (数据库服务)
├── /configs/ (配置信息)
│ ├── /app/ (应用配置)
│ │ ├── /version → "v2.1.0"
│ │ └── /timeout → "30s"
└── /locks/ (分布式锁)
└── /job-scheduler → "当前持有者信息"
键值对特性:
键(Key):字节数组,支持任意二进制数据值(Value):字节数组,etcd 不解析内容版本(Revision):全局单调递增的版本号创建版本:记录键的创建版本修改版本:记录最后修改版本
2.3 MVCC:多版本并发控制
现实世界比喻:图书馆的书籍管理
传统方式(有问题):
书架上只有最新版的书
读者A正在读书 ← 管理员要更新书的内容
↓
冲突:读者读到一半书被换掉了
MVCC 方式(解决方案):
书架上保存所有版本的书
读者A读v1版本 ← 管理员发布v2版本
读者B读v2版本
↓
优势:互不干扰,还可以查阅历史版本
MVCC 核心机制:
版本号(Revision)
全局单调递增的整数每次修改都会生成新版本示例:PUT → rev=100, PUT → rev=101, DELETE → rev=102
多版本存储
Key: /config/database
├── 版本100: "mysql://localhost:3306"
├── 版本101: "mysql://10.0.1.10:3306"
└── 版本102: (已删除)
并发控制优势
读不阻塞写:读旧版本,写新版本写不阻塞读:写操作不影响正在进行的读历史查询:可以查询任意时间点的数据状态
三、 Raft 共识算法:如何保证一致性?
3.1 共识问题的本质
将军问题比喻:
多个将军包围城堡
需要同时进攻才能获胜
但传令兵可能被俘或传错消息
如何确保所有将军在同一时间发动进攻?
分布式系统面临同样问题:
多个节点需要就某个决定达成一致网络可能延迟、中断、重复消息节点可能故障、重启
3.2 Raft 核心概念
1. 节点状态:
Leader(领导者):负责所有写请求,像团队的”项目经理”Follower(跟随者):被动响应,像”团队成员”Candidate(候选者):选举过程中的临时状态,像”候选人”
2. 任期(Term):
单调递增的整数,标识领导者的执政期每个任期内最多只有一个有效的 Leader类似于总统任期,保证权力有序交接
3. 日志(Log):
所有操作都记录在日志中日志条目包含:任期号 + 操作命令保证所有节点的日志最终一致
3.3 领导者选举:民主投票过程
正常工作情况:
Leader 每150ms → 向所有 Follower 发送"心跳"
Follower 收到心跳 → 重置"选举计时器"
Leader 故障时的选举过程:
步骤1:发现异常
FollowerA: ⏰ (300ms没收到心跳) → 怀疑Leader故障
FollowerB: ⏰ (250ms没收到心跳) → 还在等待
FollowerC: ⏰ (280ms没收到心跳) → 准备行动
步骤2:成为候选者
FollowerA: 任期号+1 (term=2),为自己投票
状态: Follower → Candidate
向其他节点发送投票请求:"选我当Leader(term=2)"
步骤3:投票决策
FollowerB: 收到请求,检查任期号
- 如果当前任期 < 请求任期,投票赞成
- 如果已经投过票,拒绝请求
FollowerC: 同样逻辑判断
步骤4:赢得选举
FollowerA 获得超过半数投票 (2/3)
状态: Candidate → Leader
立即向所有节点发送心跳:"我是新Leader"
关键设计细节:
随机超时:每个节点的选举超时时间随机(150-300ms),避免同时发起投票多数派原则:需要 N/2 + 1 个节点同意任期递增:保证不会选出过期的 Leader
3.4 日志复制:数据同步机制
写请求处理流程:
步骤1:客户端请求
客户端 → Leader: "设置 /config/timeout = 30s"
步骤2:日志追加
Leader:
1. 生成日志条目: [term=3, index=101, cmd="SET timeout=30s"]
2. 将条目追加到本地日志(未提交)
步骤3:日志复制
Leader → Followers: "请复制日志条目[101]"
Followers:
1. 验证任期号和前一个日志条目
2. 追加到本地日志
3. 回复Leader:"复制成功"
步骤4:提交确认
Leader 等待多数节点确认 (2/3)
然后提交该日志条目 (应用到状态机)
通知Followers:"可以提交条目[101]"
步骤5:响应客户端
Leader → 客户端: "操作成功"
日志一致性保证:
日志匹配特性:
如果两个日志条目有相同的索引和任期号,它们存储相同的命令如果某个日志条目被提交,那么之前的所有条目也都已提交
领导者完全性:
只有包含所有已提交日志条目的节点才能成为 Leader
3.5 网络分区处理
场景描述:
5节点集群: [A(Leader), B, C, D, E]
网络故障形成两个分区:
分区1: [A, B] 分区2: [C, D, E]
处理过程:
分区1的情况:
A(Leader): 只能与B通信,无法联系CDE
写请求: 需要3个节点确认,但只有2个 → 写操作失败
状态: 继续作为Leader,但无法处理写请求
分区2的情况:
C, D, E: 无法收到Leader心跳
选举超时后发起新选举:
C成为Candidate,获得D和E的投票 (3票 > 5/2)
C成为新Leader(term+1),可以正常处理写请求
网络恢复后:
分区恢复,A和C发现彼此
A(term=1) 收到 C(term=2) 的心跳
A自动降级为Follower,同步C的数据
集群恢复一致状态
四、 存储引擎:数据如何持久化?
4.1 WAL:预写日志(操作记录本)
现实比喻:公司的财务记账
传统做法(有问题):
直接修改账本数字
突然断电 → 不知道修改到哪一步,账目混乱
WAL做法(解决方案):
1. 先在草稿本记录要做的修改
2. 确认草稿本写完后,再正式修改账本
3. 断电恢复时,查看草稿本重做未完成的操作
WAL 技术细节:
写入流程:
收到写请求 → 序列化操作 → 写入WAL文件 → 更新内存 → 响应客户端
文件结构:
wal/
├── 0000000000000000-0000000000000000.wal (初始文件)
├── 0000000000000001-0000000000000000.wal
└── 0000000000000002-0000000000000000.wal
故障恢复:
重启时读取 WAL 文件重放所有操作,重建内存状态保证数据不丢失
4.2 Snapshot:快照(数据快照)
问题:WAL 文件会无限增长
解决方案:定期生成数据快照
WAL文件增长到100MB → 生成快照 → 删除旧的WAL文件
快照生成过程:
选择生成快照的时机(文件大小或时间间隔)将当前内存状态序列化到快照文件更新元数据,记录快照对应的最后日志索引清理之前的所有 WAL 文件
快照价值:
快速恢复:从快照恢复比重放所有WAL快得多节省空间:删除历史WAL文件新节点同步:新节点先接收快照,再同步最新日志
4.3 BoltDB:存储引擎(最终存储)
BoltDB 特点:
单文件键值数据库基于 B+ 树索引支持 ACID 事务零管理开销
数据组织:
bolt.db
├── meta page (元数据)
├── freelist (空闲页面管理)
└── B+ tree (数据索引)
├── key: /config/database
├── key: /registry/services/web/node1
└── key: /locks/job-scheduler
五、 核心功能原理
5.1 租约机制(Lease):带过期时间的合约
现实比喻:租房合同
传统键值对:
设置键值对 → 永久有效(除非手动删除)
带租约的键值对:
签1年租房合同 → 1年内有效 → 到期自动退租
租约工作机制:
1. 创建租约:
客户端: "创建60秒的租约"
etcd: 返回租约ID "0x12345678"
2. 绑定键值:
客户端: "设置键 /services/web/node1,绑定租约0x12345678"
etcd: 键值与租约关联
3. 自动续约:
客户端定期: "续租0x12345678"
etcd: 重置租约过期时间
4. 到期清理:
租约60秒到期 → 自动删除所有关联的键值
应用场景:
服务注册发现:
服务启动:
1. 创建60秒租约
2. 注册服务实例: /services/web/node1
3. 每30秒续租一次
服务故障:
停止续租 → 60秒后自动注销 → 客户端发现服务下线
分布式锁:
获取锁:
1. 创建租约
2. 尝试创建 /lock/resource-xxx
3. 成功则获得锁,定期续租
释放锁:
停止续租 → 租约到期 → 锁自动释放
5.2 监听机制(Watch):实时消息订阅
现实比喻:微信消息提醒
传统轮询方式:
不断问服务器: "有我的新消息吗?"
服务器: "没有" ... "没有" ... "没有" ... "有一条"
↓
浪费资源,延迟高
Watch监听方式:
告诉服务器: "有我的消息请通知我"
服务器: (静默等待) ... "你有新消息了!"
↓
实时推送,资源高效
Watch 技术实现:
1. 建立监听:
客户端: "监听 /config/ 前缀的所有变化"
etcd: 创建监听器,记录客户端连接
2. 事件推送:
某个客户端修改 /config/timeout
etcd:
1. 生成事件: {type: PUT, key: /config/timeout, value: "30s"}
2. 查找所有匹配的监听器
3. 通过gRPC流推送给客户端
3. 监听选项:
从当前版本开始:只监听未来的变化从历史版本开始:重放历史变化带前缀监听:监听某个目录下的所有键
应用场景:
配置热更新:
应用启动:
1. 读取当前配置
2. 监听配置键的变化
3. 配置更新时自动重新加载
服务发现:
客户端:
1. 获取当前服务列表
2. 监听服务注册目录
3. 服务上下线时自动更新本地缓存
5.3 事务操作:原子性保证
现实比喻:银行转账
非原子操作(有问题):
账户A: 100元,账户B: 100元
步骤1: A账户扣款50元 → A:50, B:100
步骤2: B账户收款50元 ← 此时服务器宕机!
结果: A少了50元,B没收到钱
原子事务(解决方案):
IF A账户 >= 50元 THEN
A账户 -= 50元
B账户 += 50元
ELSE
拒绝操作
全部成功或全部失败
etcd 事务语法:
compare: # 条件判断
- key: "/account/A"
result: EQUAL
target: MOD
mod_revision: 100
success: # 条件成立执行
- request_put:
key: "/account/A"
value: "50"
- request_put:
key: "/account/B"
value: "150"
failure: # 条件不成立执行
- request_range:
key: "/account/A"
应用场景:
分布式锁:
IF /lock/resource 不存在 THEN
CREATE /lock/resource
return "获得锁"
ELSE
return "锁已被占用"
资源分配:
IF /resources/instance-5 未分配 THEN
SET /resources/instance-5 = "已分配"
return "分配成功"
ELSE
return "资源已分配"
六、 集群管理与运维
6.1 集群部署:搭建 etcd 集群
节点数量选择:
1节点:仅测试使用,无高可用3节点:容忍1个节点故障,推荐用于中小集群5节点:容忍2个节点故障,用于生产环境7节点:容忍3个节点故障,用于超大规模集群
为什么需要奇数个节点?
3节点集群: 需要2个节点达成一致 (2 > 3/2)
4节点集群: 需要3个节点达成一致 (3 > 4/2)
容错能力: 3节点(宕机1个) vs 4节点(宕机1个)
↓
奇数节点在同样容错能力下更节省资源
集群启动配置:
# 节点1配置
name: node1
data-dir: /var/lib/etcd
listen-client-urls: http://0.0.0.0:2379
listen-peer-urls: http://10.0.1.1:2380
initial-advertise-peer-urls: http://10.0.1.1:2380
initial-cluster: node1=http://10.0.1.1:2380,node2=http://10.0.1.2:2380,node3=http://10.0.1.3:2380
initial-cluster-token: etcd-cluster-1
initial-cluster-state: new
6.2 成员管理:动态调整集群
添加新节点:
步骤1:获取当前集群信息
etcdctl member list
# 输出: 节点ID, 状态, 名称, peer URLs
步骤2:添加新成员
etcdctl member add node4 --peer-urls=http://10.0.1.4:2380
步骤3:新节点启动
# 新节点使用 existing 状态启动
etcd --name node4
--initial-cluster-state existing
--initial-cluster "node1=http://10.0.1.1:2380,...,node4=http://10.0.1.4:2380"
移除故障节点:
# 查看节点状态
etcdctl endpoint status
# 移除故障节点
etcdctl member remove <节点ID>
# 验证集群健康
etcdctl endpoint health
6.3 备份与恢复:数据安全
备份策略:
1. 定期快照备份:
# 创建快照
etcdctl snapshot save backup.db
# 查看快照信息
etcdctl snapshot status backup.db
2. 自动化备份脚本:
#!/bin/bash
DATE=$(date +%Y%m%d-%H%M%S)
etcdctl snapshot save /backups/etcd-snapshot-${DATE}.db
# 保留最近7天备份
find /backups/ -name "etcd-snapshot-*.db" -mtime +7 -delete
恢复场景:
单节点故障恢复:
节点A故障 → 从剩余健康节点自动同步 → 重启后恢复数据
多数节点故障恢复:
3节点集群中2个故障 → 从快照恢复 → 重新初始化集群
恢复步骤:
# 停止所有etcd服务
systemctl stop etcd
# 从快照恢复
etcdctl snapshot restore backup.db
--name node1
--initial-cluster "node1=http://10.0.1.1:2380,..."
--initial-cluster-token etcd-cluster-1
--data-dir /var/lib/etcd
# 重启服务
systemctl start etcd
七、 性能优化与监控
7.1 性能影响因素分析
硬件因素:
磁盘 I/O:
WAL 日志是顺序写,但fsync操作影响性能推荐:SSD 磁盘,RAID 10配置避免:网络存储(NFS),机械硬盘
网络带宽:
节点间数据同步占用网络客户端请求响应需要网络推荐:万兆网络,低延迟交换机
内存大小:
存储索引和缓存数据推荐:足够内存避免swap
软件配置因素:
快照配置:
# 快照触发条件
snapshot-count: 100000 # 提交100000个条目后触发快照
snapshot-interval: 3600 # 1小时触发一次快照
心跳间隔:
heartbeat-interval: 100 # Leader心跳间隔(ms)
election-timeout: 1000 # 选举超时时间(ms)
7.2 关键监控指标
集群健康指标:
节点状态:
etcdctl endpoint status --write-out=table
输出:
+-------------------+------------------+---------+---------+-----------+-----------+------------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM |
+-------------------+------------------+---------+---------+-----------+-----------+------------+
| 10.0.1.1:2379 | 8e9e05c52164694d | 3.5.0 | 25 MB | true | false | 4 |
| 10.0.1.2:2379 | 1e9e05c52164694d | 3.5.0 | 25 MB | false | false | 4 |
+-------------------+------------------+---------+---------+-----------+-----------+------------+
领导者信息:
当前Leader是哪个节点任期号是否稳定是否有频繁的Leader切换
性能指标:
请求延迟:
写请求延迟(P95, P99)读请求延迟监视:延迟突增可能表示性能问题
吞吐量:
每秒处理请求数网络带宽使用率连接数监控
存储指标:
数据库大小:
当前数据大小增长趋势预测剩余存储空间
压缩状态:
最后压缩版本待压缩的数据量压缩操作频率
7.3 常见性能问题与优化
问题1:写性能下降
症状:
写请求延迟增加客户端超时增多CPU 使用率正常
可能原因:
磁盘 I/O 瓶颈:WAL 同步操作慢网络延迟:节点间同步慢客户端批量操作:大量小请求
解决方案:
# 检查磁盘性能
iostat -x 1
# 优化客户端:使用批量操作
# 而不是频繁的小请求
问题2:存储空间增长过快
症状:
磁盘使用率快速上升频繁触发存储告警
可能原因:
历史版本积累:未及时压缩大键值对:存储了不应在 etcd 中的数据租约泄漏:租约未正确清理
解决方案:
# 定期压缩历史版本
etcdctl compact $(etcdctl endpoint status --write-out=json | grep revision | cut -d: -f2)
# 整理碎片
etcdctl defrag
# 检查大键
etcdctl get / --prefix --keys-only | awk '{print length, $0}' | sort -nr | head
八、 故障处理与排查
8.1 常见故障场景
场景1:节点无法启动
症状:
etcd[1234]: failed to start: corrupt wal file
可能原因:
WAL 文件损坏数据目录权限问题存储空间不足
排查步骤:
# 检查磁盘空间
df -h /var/lib/etcd
# 检查文件权限
ls -la /var/lib/etcd/
# 尝试数据恢复
etcdctl snapshot restore backup.db --data-dir /var/lib/etcd-new
场景2:集群脑裂(Split Brain)
症状:
客户端收到不一致的数据日志中出现选举冲突监控显示多个Leader
可能原因:
网络分区时钟不同步配置错误
解决方案:
# 检查集群状态
etcdctl endpoint status --cluster
# 强制重新选举
# 重启所有节点(按顺序)
场景3:客户端连接超时
症状:
客户端请求超时监控显示高延迟连接数异常
排查步骤:
# 检查网络连通性
ping <etcd-node>
telnet <etcd-node> 2379
# 检查etcd负载
etcdctl endpoint status
# 检查客户端配置
# 确保使用正确的endpoints列表
8.2 系统化排查方法
排查框架:
步骤1:基础检查
# 节点状态
etcdctl member list
# 集群健康
etcdctl endpoint health
# 系统资源
top, free, df, iostat
步骤2:日志分析
# 查看etcd日志
journalctl -u etcd -f
# 关键日志模式:
# - 选举相关: "raft: ... became leader"
# - 网络问题: "failed to send out heartbeat"
# - 存储问题: "wal: sync duration"
步骤3:性能分析
# 使用metrics接口
curl http://localhost:2379/metrics | grep etcd_
# 关键metrics:
# - etcd_disk_wal_fsync_duration_seconds
# - etcd_network_peer_round_trip_time_seconds
# - etcd_server_leader_changes_seen_total
步骤4:网络诊断
# 节点间连通性
ping <peer-ip>
# 端口检查
nc -zv <etcd-ip> 2379
nc -zv <etcd-ip> 2380
# 带宽使用
iftop -i <interface>
8.3 预防性维护
日常维护任务:
定期健康检查:
# 每日检查脚本
#!/bin/bash
etcdctl endpoint health
etcdctl endpoint status
curl -s http://localhost:2379/metrics | grep "etcd_server_leader_changes"
存储维护:
# 每周执行
etcdctl compact $(etcdctl endpoint status --write-out=json | grep revision | cut -d: -f2)
etcdctl defrag --cluster
备份验证:
# 恢复测试
etcdctl snapshot restore backup.db --data-dir /tmp/etcd-restore-test
etcdctl snapshot status backup.db
容量规划:
存储容量:
预估公式: 数据量 × 副本数 × 安全系数(1.5)
示例: 1GB数据 × 3副本 × 1.5 = 4.5GB
性能容量:
单节点写性能: 1000-10000 TPS (取决于硬件)
集群写性能: 受限于Leader节点
读性能: 可水平扩展
九、 实际应用场景
9.1 Kubernetes 中的 etcd
角色定位:Kubernetes 的”大脑”
存储的数据类型:
etcd
├── /registry/pods/ (所有Pod信息)
├── /registry/services/ (服务定义)
├── /registry/deployments/ (部署信息)
├── /registry/nodes/ (节点信息)
└── /registry/configmaps/ (配置信息)
高可用架构:
Kubernetes控制面
↓
etcd集群[节点A(Leader), 节点B, 节点C]
↓
API Server 1 → 读写请求
API Server 2 → 读写请求
API Server 3 → 读写请求
关键配置:
# kube-apiserver配置
--etcd-servers=https://10.0.1.1:2379,https://10.0.1.2:2379,https://10.0.1.3:2379
--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
9.2 微服务架构中的应用
服务注册发现:
服务注册:
// 服务启动时注册
func registerService(serviceName, endpoint string) {
// 创建租约
lease := clientv3.NewLease(client)
grantResp, _ := lease.Grant(context.TODO(), 30)
// 注册服务
key := fmt.Sprintf("/services/%s/%s", serviceName, generateID())
client.Put(context.TODO(), key, endpoint, clientv3.WithLease(grantResp.ID))
// 自动续约
keepAlive, _ := lease.KeepAlive(context.TODO(), grantResp.ID)
go func() {
for range keepAlive {
// 保持租约活跃
}
}()
}
服务发现:
func discoverServices(serviceName string) []string {
// 获取当前服务列表
resp, _ := client.Get(context.TODO(),
fmt.Sprintf("/services/%s/", serviceName),
clientv3.WithPrefix())
var endpoints []string
for _, kv := range resp.Kvs {
endpoints = append(endpoints, string(kv.Value))
}
return endpoints
}
配置中心:
配置存储:
# 存储配置
etcdctl put /config/app/database.url "mysql://localhost:3306"
etcdctl put /config/app/cache.enabled "true"
etcdctl put /config/app/log.level "info"
配置监听:
func watchConfig() {
watchChan := client.Watch(context.TODO(), "/config/app/", clientv3.WithPrefix())
for watchResp := range watchChan {
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
fmt.Printf("配置更新: %s = %s
", event.Kv.Key, event.Kv.Value)
reloadConfig(string(event.Kv.Key), string(event.Kv.Value))
case clientv3.EventTypeDelete:
fmt.Printf("配置删除: %s
", event.Kv.Key)
}
}
}
}
9.3 分布式锁实现
基于租约的分布式锁:
type DistributedLock struct {
client *clientv3.Client
lease clientv3.Lease
leaseID clientv3.LeaseID
key string
isLocked bool
}
func (dl *DistributedLock) Lock() error {
// 创建租约
grantResp, err := dl.lease.Grant(context.TODO(), 30)
if err != nil {
return err
}
dl.leaseID = grantResp.ID
// 尝试获取锁
txn := dl.client.Txn(context.TODO())
txn.If(clientv3.Compare(clientv3.CreateRevision(dl.key), "=", 0)).
Then(clientv3.OpPut(dl.key, "locked", clientv3.WithLease(dl.leaseID))).
Else(clientv3.OpGet(dl.key))
txnResp, err := txn.Commit()
if err != nil {
return err
}
if !txnResp.Succeeded {
return fmt.Errorf("锁已被占用")
}
dl.isLocked = true
// 保持租约活跃
go dl.keepAlive()
return nil
}
func (dl *DistributedLock) Unlock() error {
if !dl.isLocked {
return nil
}
_, err := dl.client.Delete(context.TODO(), dl.key)
if err != nil {
return err
}
dl.isLocked = false
return nil
}
十、 最佳实践总结
10.1 集群部署最佳实践
硬件选择:
磁盘:SSD,足够的IOPS内存:根据数据量配置,建议16GB+网络:万兆网络,低延迟CPU:多核,支持并发操作
集群配置:
# 生产环境推荐配置
name: "etcd-node-1"
data-dir: "/var/lib/etcd"
wal-dir: "/var/lib/etcd/wal"
listen-client-urls: "https://0.0.0.0:2379"
listen-peer-urls: "https://0.0.0.0:2380"
advertise-client-urls: "https://node1.example.com:2379"
initial-advertise-peer-urls: "https://node1.example.com:2380"
initial-cluster: "etcd-node-1=https://node1.example.com:2380,..."
initial-cluster-token: "etcd-cluster-prod"
initial-cluster-state: "new"
10.2 安全最佳实践
传输加密:
# TLS配置
client-transport-security:
cert-file: "/etc/etcd/ssl/etcd.pem"
key-file: "/etc/etcd/ssl/etcd-key.pem"
trusted-ca-file: "/etc/etcd/ssl/ca.pem"
client-cert-auth: true
peer-transport-security:
cert-file: "/etc/etcd/ssl/etcd.pem"
key-file: "/etc/etcd/ssl/etcd-key.pem"
trusted-ca-file: "/etc/etcd/ssl/ca.pem"
peer-client-cert-auth: true
访问控制:
# 创建用户和角色
etcdctl user add webapp
etcdctl role add app-role
etcdctl role grant-permission app-role readwrite --prefix=true /config/app/
etcdctl user grant-role webapp app-role
# 启用认证
etcdctl auth enable
10.3 运维最佳实践
监控告警:
关键告警项:
集群健康:节点不可用、Leader频繁切换性能指标:请求延迟突增、高错误率存储告警:磁盘空间不足、存储配额超限资源告警:CPU、内存、网络使用率过高
备份策略:
#!/bin/bash
# 自动化备份脚本
BACKUP_DIR="/backups/etcd"
DATE=$(date +%Y%m%d_%H%M%S)
ENDPOINTS="https://node1:2379,https://node2:2379,https://node3:2379"
CERT_DIR="/etc/etcd/ssl"
# 创建快照
etcdctl --endpoints=$ENDPOINTS
--cacert=$CERT_DIR/ca.pem
--cert=$CERT_DIR/etcd.pem
--key=$CERT_DIR/etcd-key.pem
snapshot save $BACKUP_DIR/snapshot_$DATE.db
# 验证快照
etcdctl snapshot status $BACKUP_DIR/snapshot_$DATE.db
# 清理旧备份 (保留7天)
find $BACKUP_DIR -name "snapshot_*.db" -mtime +7 -delete
容量规划:
存储容量估算:
总存储需求 = (数据大小 + 索引开销) × 副本数 × 安全系数(1.5)
示例: (10GB + 2GB) × 3 × 1.5 = 54GB
性能容量估算:
所需TPS = 峰值请求数 × 冗余系数(1.2)
单节点能力: 1000-10000 TPS
所需节点数 = ceil(所需TPS / 单节点能力)
10.4 客户端使用最佳实践
连接管理:
// 正确的客户端使用
func createEtcdClient() (*clientv3.Client, error) {
config := clientv3.Config{
Endpoints: []string{"https://node1:2379", "https://node2:2379", "https://node3:2379"},
DialTimeout: 5 * time.Second,
// TLS配置
TLS: &tls.Config{
// TLS配置
},
}
return clientv3.New(config)
}
// 使用连接池,避免频繁创建销毁
var etcdClient *clientv3.Client
func init() {
var err error
etcdClient, err = createEtcdClient()
if err != nil {
log.Fatal(err)
}
}
错误处理:
// 重试机制
func withRetry(operation func() error, maxRetries int) error {
var err error
for i := 0; i < maxRetries; i++ {
err = operation()
if err == nil {
return nil
}
// 可重试的错误
if isRetryableError(err) {
time.Sleep(time.Duration(i) * time.Second)
continue
}
// 不可重试的错误
return err
}
return err
}
func isRetryableError(err error) bool {
// 网络错误、超时错误等可以重试
// 业务逻辑错误不应该重试
return strings.Contains(err.Error(), "deadline exceeded") ||
strings.Contains(err.Error(), "connection refused")
}