etcd 深度剖析
概述
etcd 是一个高可用的分布式键值存储系统,是 Kubernetes 集群的核心数据存储层。所有集群状态(Pod、Service、ConfigMap、Deployment 等资源)都存储在 etcd 中。etcd 使用 Raft 共识算法保证数据一致性和高可用性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ┌─────────────────────────────────────────────────────────────────────────┐ │ etcd 架构图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Kubernetes 集群 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │kube- │ │kube- │ │kube- │ │ kubelet │ │ │ │ │ │apiserver│ │scheduler│ │controller│ │ │ │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ └────────────┼────────────┼────────────┘ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ kube-apiserver │ │ │ │ │ └───────────┬─────────────┘ │ │ │ └───────────────────────┼───────────────────────────────────────────┘ │ │ │ │ │ │ gRPC │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ etcd 集群 │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ etcd-0 │◄──│ etcd-1 │──►│ etcd-2 │ │ │ │ │ │ (Leader) │ │ (Follower) │ │ (Follower) │ │ │ │ │ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ Raft 共识层 │ │ │ │ │ │ (日志复制/选举) │ │ │ │ │ └────────┬────────┘ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ MVCC 存储引擎 │ │ │ │ │ │ (BoltDB) │ │ │ │ │ └─────────────────┘ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
1. etcd 核心概念
1.1 Raft 一致性算法
etcd 使用 Raft 算法实现分布式一致性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ┌─────────────────────────────────────────────────────────────────────────┐ │ Raft 角色转换图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ │ │ │ Leader │ │ │ │ (处理所有请求) │ │ │ └────────┬─────────┘ │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ │ 心跳超时 │ │ 心跳超时 │ │ │ 转为 Candidate │ │ 转为 Candidate │ │ ▼ │ ▼ │ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │ Candidate │ │ │ Candidate │ │ │ │ (发起选举) │ │ │ (发起选举) │ │ │ └────────┬─────────┘ │ └────────┬─────────┘ │ │ │ │ │ │ │ │ 获得多数票 │ │ 获得多数票 │ │ │ 成为 Leader │ │ 成为 Leader │ │ ▼ │ ▼ │ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │ Leader │ │ │ Leader │ │ │ │ │ │ │ │ │ │ └──────────────────┘ │ └──────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Follower │ │ │ │ (同步日志) │ │ │ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
1.2 etcd 术语
| 术语 |
说明 |
| Node |
etcd 集群中的一个实例 |
| Member |
etcd 集群中的一个成员 |
| Peer |
集群中的其他节点 |
| Leader |
负责处理所有写请求的节点 |
| Follower |
跟随 Leader 的节点 |
| Candidate |
参与选举的节点 |
| Term |
选举任期,每个任期最多一个 Leader |
| Log Entry |
日志条目,包含命令和任期号 |
| WAL |
Write-Ahead Log,预写日志 |
| Snapshot |
快照,用于压缩日志 |
2. 数据存储结构
2.1 Kubernetes 数据目录
1 2 3 4 5 6 7 8
| /var/lib/etcd/ ├── member/ │ ├── wal/ │ │ └── 0000000000000000-0000000000000000.wal │ ├── snap/ │ │ └── 0000000000000000-0000000000000000.snap │ └── kv/ │ └── db
|
2.2 Kubernetes 资源存储路径
etcd 使用分层键空间存储 Kubernetes 资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| / ├── registry/ │ ├── pods/ │ │ └── default/ │ │ └── nginx-pod/ │ │ └── <uid>/ │ │ └── ... │ ├── services/ │ │ └── default/ │ │ └── myapp/ │ │ └── ... │ ├── deployments/ │ │ └── apps/ │ │ └── default/ │ │ └── nginx/ │ │ └── ... │ ├── configmaps/ │ │ └── default/ │ │ └── app-config/ │ ├── secrets/ │ ├── namespaces/ │ │ ├── default/ │ │ ├── kube-system/ │ │ └── ... │ └── persistentvolumeclaims/ │ └── ...
#Lease 锁机制(用于 leader 选举) ├── lease/ │ └── <lease-uid>/
# Kubernetes 事件 ├── events/
|
2.3 存储格式
1 2 3 4 5 6 7 8 9 10
|
Key: /registry/pods/default/nginx-7fb96c846b-abc12 Value: { "typeUrl": "type.googleapis.com/kubernetes.Pod", "value": <protobuf encoded Pod object> }
|
3. 写请求流程
3.1 完整写入流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| ┌─────────────────────────────────────────────────────────────────────────┐ │ etcd 写入流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Client Leader Follower 1 Follower 2 │ │ │ │ │ │ │ │ │ 1. 写请求 │ │ │ │ │ │ ─────────────► │ │ │ │ │ │ │ │ │ │ │ │ │ 2. 写入 WAL │ │ │ │ │ │ ─────────────────────│ │ │ │ │ │ │ │ │ │ │ │ 3. 复制日志条目 │ │ │ │ │ │ ─────────────────────│────────────────│ │ │ │ │ │ │ │ │ │ │ 4. 等待多数节点确认 │ │ │ │ │ │ ◄───────────────────│────────────────│ │ │ │ │ │ │ │ │ │ │ 5. 提交到状态机 │ │ │ │ │ │ ◄───────────────────┤ │ │ │ │ │ │ │ │ │ │ 6. 返回结果 │ │ │ │ │ │ ◄──────────── │ │ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
3.2 核心代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| func (s *EtcdServer) Process(ctx context.Context, r etcdserverpb.Request) (rr etcdserverpb.Response, err error) { if err := s.validateRequest(r); err != nil { return etcdserverpb.Response{}, err }
raftReq := &etcdsnap.Message{ Type: raft.MsgApp, Entries: []raftpb.Entry{ { Term: s.reqIDGen.Tick(), Index: s.be.NextIndex(), Data: mustMarshal(&r), }, }, }
s.node.Step(ctx, raftReq)
ch := s.w.Register(raftReq)
select { case <-ch: return s.applyRequest(r) case <-ctx.Done(): return etcdserverpb.Response{}, ctx.Err() } }
func (s *EtcdServer) applyRequest(r etcdserverpb.Request) etcdserverpb.Response { switch r.Type { case etcdserverpb.Range: return s.applyRange(r) case etcdserverpb.Put: return s.applyPut(r) case etcdserverpb.Delete: return s.applyDelete(r) case etcdserverpb.Txn: return s.applyTxn(r) } }
|
4. 读请求流程
4.1 线性读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| ┌─────────────────────────────────────────────────────────────────────────┐ │ etcd 线性读流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 方法一:Leader 读(默认) │ │ │ │ Client Leader Follower 1 │ │ │ │ │ │ │ │ 1. 读请求 │ │ │ │ │ ─────────────► │ │ │ │ │ │ │ │ │ │ │ 2. 检查 lease │ │ │ │ │ │ │ │ │ │ 3. 直接读取状态机 │ │ │ │ │ │ │ │ │ 4. 返回结果 │ │ │ │ │ ◄──────────── │ │ │ │ │ │ 方法二:Follower 读(Serializable) │ │ │ │ Client Leader Follower 1 │ │ │ │ │ │ │ │ │ │ 1. 读请求 │ │ │ │ │ ◄──────────── │ │ │ │ │ │ │ │ │ ◄───── 2. 获取 commit index │ │ │ │ │ │ │ │ │ ────── 3. 返回 read index │ │ │ │ │ │ │ │ │ │ 4. 等待应用到 read index │ │ │ │ │ │ │ │ │ │ 5. 读取本地数据 │ │ │ │ │ ─────────────► │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
4.2 读模式对比
| 模式 |
说明 |
一致性 |
性能 |
| Linearizable |
线性读,保证全局顺序 |
强一致 |
较低 |
| Serializable |
串行化读,本地数据读取 |
中等一致 |
较高 |
| Snapshot |
快照读 |
读取创建时的状态 |
高 |
5. Watch 机制
5.1 Watch 流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ┌─────────────────────────────────────────────────────────────────────────┐ │ etcd Watch 流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Client Leader WatchHub Follower │ │ │ │ │ │ │ │ │ 1. 创建 Watch │ │ │ │ │ │ ─────────────► │ │ │ │ │ │ │ 2. 注册 Watch │ │ │ │ │ │ ──────────────────► │ │ │ │ │ │ │ │ │ │ │ │ 3. 返回 Watch ID │ │ │ │ │ ◄──────────── │ │ │ │ │ │ │ │ │ │ │ │ 4. 等待事件 │ │ │ │ │ │ ◄──────────── │ │ │ │ │ │ │ │ │ │ │ │ │ 5. 数据变更 │ │ │ │ │ │ ────────────────────│──────────────►│ │ │ │ │ │ │ │ │ │ │ │ 6. 推送事件 │ │ │ │ │ │ ─────────────►│ │ │ │ │ │ │ │ │ │ 7. 收到变更 │ │ │ │ │ │ ◄──────────── │ │ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
5.2 Watch 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| type Watcher struct { streams map[int64]*watchStream }
func (w *watcher) Watch(ctx context.Context, r *pb.WatchCreateRequest) (WatchID, error) { watch := &Watch{ Key: r.Key, RangeEnd: r.RangeEnd, StartRev: r.StartRevision, Filters: r.Filters, }
if isLocal(r.Key) { return w.watchLocal(ctx, watch) }
return w.watchRemote(ctx, watch) }
func (ws *watchStream) run() { for { select { case rev := <-ws.revChan: events, err := ws.getEvents(rev) if err != nil { continue } ws.send(WatchResponse{Events: events}) } } }
|
5.3 Kubernetes 中的 Watch 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { watchable, ok := c.watchCache[*opts.ResourceVersion] if !ok { watchable = c.watchCache.WaitUntilFreshResourceVersion(opts.ResourceVersion) }
return watchable.Watch(ctx, key, opts) }
|
6. 事务与租约
6.1 事务 (Txn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
func AtomicUpdate(cli *clientv3.Client, key string, updateFn func(old []byte) ([]byte, error)) error { for { resp, err := cli.Get(ctx, key) if err != nil { return err }
newValue, err := updateFn(resp.Kvs[0].Value) if err != nil { return err }
txn := cli.Txn(ctx) txn.If(clientv3.Compare(clientv3.Value(key), "=", string(resp.Kvs[0].Value))) txn.Then(clientv3.Put(key, string(newValue))) _, err = txn.Commit()
if err == nil { return nil } } }
|
6.2 租约 (Lease)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
func putWithLease(cli *clientv3.Client, key, value string) error { lease, err := cli.Grant(ctx, 10) if err != nil { return err }
_, err = cli.Put(ctx, key, value, clientv3.WithLease(lease.ID)) return err }
type LeaderElectionRecord struct { Holder LeaderElectionOwner `json:"holder"` LeaseDurationSeconds int `json:"leaseDurationSeconds"` AcquireTime metav1.Time `json:"acquireTime"` RenewTime metav1.Time `json:"renewTime"` LeaderTransitions int `json:"leaderTransitions"` }
|
7. 快照与压缩
7.1 快照机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ┌─────────────────────────────────────────────────────────────────────────┐ │ etcd 快照与压缩 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ WAL 日志文件序列: │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ 0001 │ │ 0002 │ │ 0003 │ │ 0004 │ │ 0005 │ │ 0006 │ ... │ │ │ WAL │ │ WAL │ │ WAL │ │ WAL │ │ WAL │ │ WAL │ │ │ └──┬───┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ │ │ │ 定期创建快照 │ │ ▼ │ │ ┌──────┐ │ │ │Snap- │ ◄── 0001-0004 的状态快照 │ │ │ 01 │ │ │ └──┬───┘ │ │ │ │ │ │ 删除旧 WAL │ │ ▼ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ 0005 │ │ 0006 │ │ ... │ <- 保留 0005 之后的日志 │ │ │ WAL │ │ WAL │ │ │ │ │ └──────┘ └──────┘ └──────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
7.2 压缩配置
1 2 3 4 5 6 7 8 9
|
etcd --auto-compaction-mode=revision --auto-compaction-retention=1000
etcdctl compaction 1000
etcdctl del --prefix "" --from-key 1000
|
8. 高可用与故障恢复
8.1 集群部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
etcd --name=etcd-0 \ --initial-advertise-peer-urls=http://192.168.1.1:2380 \ --listen-peer-urls=http://192.168.1.1:2380 \ --listen-client-urls=http://192.168.1.1:2379 \ --advertise-client-urls=http://192.168.1.1:2379 \ --initial-cluster=etcd-0=http://192.168.1.1:2380,etcd-1=http://192.168.1.2:2380,etcd-2=http://192.168.1.3:2380
etcd --name=etcd-1 \ --initial-advertise-peer-urls=http://192.168.1.2:2380 \ ...
etcd --name=etcd-2 \ --initial-advertise-peer-urls=http://192.168.1.3:2380 \ ...
|
8.2 故障恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| etcdctl --endpoints=https://192.168.1.1:2379 endpoint health
etcdctl --endpoints=https://192.168.1.1:2379 member list
etcdctl --endpoints=https://192.168.1.1:2379 member remove <member-id>
etcdctl --endpoints=https://192.168.1.1:2379 member add etcd-new --peer-urls=http://192.168.1.4:2380
etcdctl snapshot save backup.db etcdctl snapshot restore backup.db --data-dir=/var/lib/etcd
|
9. 关键源码路径
| 功能 |
源码路径 |
| Raft 实现 |
etcd/raft/ |
| Server 实现 |
etcd/server/etcdserver/ |
| 存储引擎 |
etcd/server/etcdserver/api/v2/ |
| MVCC |
etcd/server/etcdserver/api/mvcc/ |
| Watch |
etcd/server/etcdserver/api/v3rpc/ |
| gRPC |
etcd/server/etcdserver/api/v3client/ |
| WAL |
etcd/wal/ |
| Snap |
etcd/snap/ |
面试题
基础题
1. etcd 在 Kubernetes 中的作用是什么?
etcd 是 Kubernetes 的后端存储,保存所有集群状态数据:
- 资源对象(Pod、Service、Deployment、ConfigMap 等)
- 集群元数据(节点信息、命名空间)
- 认证信息(Token、证书)
- Leader 选举状态
当 kube-apiserver 需要读写资源时,实际上是与 etcd 交互。
2. etcd 如何保证数据一致性?
etcd 使用 Raft 共识算法保证一致性:
- Leader 选举:集群通过选举产生 Leader,所有写请求必须经过 Leader
- 日志复制:写请求先写入 WAL,然后复制到多数节点才算提交
- Term 机制:每个选举周期有唯一 Term 号,防止脑裂
- 多数派原则:只有获得多数节点确认的操作才是有效的
3. 什么是 MVCC?
MVCC(Multi-Version Concurrency Control)多版本并发控制:
- etcd 每次修改都会创建新版本,不覆盖旧数据
- 每个 key-value 都有 revision(版本号)
- 支持读取历史版本
- 通过压缩机制清理旧版本
4. etcd 的 Watch 机制有什么用?
Watch 用于监听 key 的变化:
- kube-apiserver 用 Watch 监听资源变化
- kubelet 监听 Pod 分配变化
- 控制器监听相关资源变化并触发 reconcile
- 实现近实时的状态同步
5. etcd 的 Lease 是什么?
Lease 是带 TTL 的租约:
- 创建时指定过期时间
- 定期续约可延长 TTL
- 过期后关联的 key 自动删除
- Kubernetes 用它实现 leader election、endpoint 刷新
中级题
6. etcd 读请求是如何处理的?
etcd 支持多种读模式:
- **线性读 (Linearizable)**:从 Leader 读取,保证全局一致
- **串行化读 (Serializable)**:可从 Follower 读取,本地数据
- 快照读:读取创建时的数据版本
kube-apiserver 默认使用线性读确保数据一致。
7. etcd 如何进行数据备份和恢复?
1 2 3 4 5 6 7 8 9 10
| etcdctl snapshot save backup.db
etcdctl snapshot status backup.db
etcdctl snapshot restore backup.db --data-dir=/var/lib/etcd/new
|
生产环境建议定期备份并验证备份可恢复性。
8. etcd 的 WAL 和快照是什么?
- **WAL (Write-Ahead Log)**:预写日志,记录所有操作,用于故障恢复
- Snapshot:状态快照,保存某一时刻的完整数据
日志先写入 WAL,然后定期创建快照压缩日志文件。
9. 如何优化 etcd 性能?
- 使用 SSD:etcd 需要快速磁盘 IO
- 合理部署:控制平面节点单独部署
- 网络优化:低延迟网络
- 参数调优:增加
--snapshot-count
- 压缩配置:合理设置压缩周期
- 监控告警:监控磁盘 IO、延迟、可用空间
10. etcd 集群如何处理节点故障?
- Follower 故障:Leader 无法与其通信,移除该节点后重新添加
- Leader 故障:触发新的选举,选举成功后继续服务
- 多数节点故障:集群不可用,无法接受写请求
etcd 需要 (n+1)/2 个节点正常才能工作。
高级题
11. 分析 etcd 的 Raft 日志复制流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| func (r *raft) Step(ctx context.Context, msg pb.Message) { switch msg.Type { case pb.MsgApp: r.handleAppendEntries(ctx, msg) } }
func (r *raft) handleAppendEntries(msg pb.Message) { if msg.Index < r.raftLog.lastIndex() { return }
r.raftLog.append(msg.Entries...)
if msg.Commit > r.raftLog.commitIndex { r.raftLog.commitIndex = min(msg.Commit, msg.Index) } }
func (r *raft) sendHeartbeat(to uint64) { r.send(pb.Message{ Type: pb.MsgHeartbeat, Commit: r.raftLog.commitIndex, From: r.id, To: to, }) }
|
日志复制遵循:写入 WAL → 复制到多数节点 → 提交到状态机。
12. Kubernetes 中 etcd 的数据隔离是如何实现的?
1 2 3 4 5 6 7 8 9 10 11
|
/registry/pods/default/nginx-7fb96c846b-abc12 /registry/services/default/myapp /registry/deployments.apps/default/myapp
|
这种分层设计支持命名空间级别的隔离和访问控制。
13. etcd 的线性一致性是如何实现的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
type lease struct { ID leaseID TTL int64 }
func (r *raft) ReadIndex(ctx context.Context) error { }
|
场景题
14. etcd 集群中出现”etcd cluster is unavailable”错误如何排查?
排查步骤:
- 检查 etcd 进程状态:
systemctl status etcd
- 检查网络连通性:
etcdctl endpoint health
- 检查磁盘空间:
df -h
- 检查日志:
journalctl -u etcd
- 常见原因:
- 多数节点不可用
- 磁盘空间不足
- 网络分区
- Leader 选举失败
- 证书过期
15. 如何调整 etcd 的压缩策略?
1 2 3 4 5 6 7 8 9 10 11 12 13
| etcd --auto-compaction-mode=revision \ --auto-compaction-retention=1000
etcd --auto-compaction-mode=periodic \ --auto-compaction-retention=1h
etcdctl compaction 1000
etcdctl del --prefix "" --from-key 1000
|
建议生产环境使用自动压缩,避免手动操作。
16. 如何实现 etcd 的安全加固?
- TLS 加密:启用 mTLS 加密所有通信
- 认证:启用客户端认证
- 授权:使用 RBAC 限制访问
- 防火墙:限制访问端口
- 定期备份:快照备份
- 监控:监控关键指标
- 限流:防止过大请求影响集群
1 2 3 4 5 6
| etcd --cert-file=/path/to/cert \ --key-file=/path/to/key \ --peer-cert-file=/path/to/peer-cert \ --peer-key-file=/path/to/peer-key \ --trusted-ca-file=/path/to/ca
|