i007.cc

i007.cc

优先队列-降维打击

05.价值资料

ETCD 原理

etcd 是一个基于 Raft 一致性算法实现的分布式、强一致性 key-value 存储系统,专门为存储分布式系统里最关键的元数据设计(比如配置信息、服务发现数据、分布式锁),最典型的应用就是 Kubernetes 用它存储整个集群的所有状态数据。它的设计目标和 Redis 完全不同:Redis 追求极致性能、可以容忍最终一致(异步复制),而 etcd 追求的是强一致性和高可靠,宁可牺牲一些性能也要保证数据不丢、不脏读。

核心:Raft 一致性算法

etcd 集群通常由奇数个节点组成(推荐 3 或 5 个,方便计算多数派),节点之间通过 Raft 协议保证所有节点的数据最终一致。Raft 把问题拆成三个子问题来解决。

Leader 选举方面,集群里任意时刻只有一个 Leader,其余是 Follower。节点有选举超时时间,如果 Follower 在超时时间内没收到 Leader 的心跳,就会变成 Candidate 发起选举,给自己投票并向其他节点拉票,拿到多数票(超过半数节点)的成为新 Leader。这个机制保证了任何时候集群只有一个”权威写入点”,不会出现脑裂时两边都能写的情况(因为拿不到多数票就选不出 Leader)。

日志复制方面,所有写请求都必须先发给 Leader,Leader 把这条变更作为一条日志(entry)追加到自己的日志里,然后并行发给所有 Follower,当多数节点(不是全部)都确认写入成功后,这条日志才算”已提交(committed)”,Leader 才会真正应用到状态机并返回客户端成功。这里的关键设计是”多数派确认”而不是”全部确认”,这样即使少数节点(比如 5 节点里的 2 个)挂掉或者网络分区,集群依然能正常写入,不会因为个别节点故障就整体不可用,这也是 etcd 集群通常配置奇数节点、且能容忍 (n-1)/2 个节点故障的原因。

安全性方面,Raft 还有一套规则保证已经提交的日志不会丢失、不会被覆盖,比如选举时候选人的日志必须”足够新”(term 更大或者日志更长)才有资格当选,防止一个日志落后的节点当上 Leader 后覆盖掉已提交的数据。

数据模型:MVCC

etcd 的数据模型不是简单的”key 覆盖 value”,而是 MVCC(多版本并发控制):每次写入(增、改、删)都会生成一个全局递增的版本号(revision),旧版本的数据不会被立即抹掉,而是被保留下来。这带来两个直接好处:一是可以支持”查询某个历史版本的数据是什么样”,二是天然支撑了后面要讲的 Watch 机制,可以从任意一个 revision 开始订阅之后发生的所有变更,不会漏事件。

etcd 内部用一棵内存中的 B-tree(treeIndex)维护 key 到各版本 revision 的映射关系,实际的版本化数据则持久化存储在磁盘上的 boltdb(一个基于 B+ 树的嵌入式单机 KV 存储引擎)里,每个 revision 作为 boltdb 里的 key,这样读取历史版本本质上就是查 boltdb 里对应 revision 的记录。

持久化:WAL + Snapshot

为了保证 Leader 崩溃重启后数据不丢,etcd 在数据真正落盘 boltdb 之前,会先写 WAL(Write-Ahead Log,预写日志),所有写操作先顺序追加写入 WAL 文件,这一步是同步刷盘的,只有 WAL 写成功才认为这条日志安全落地,即使进程崩溃,重启后也能通过重放 WAL 恢复到崩溃前的状态。

但 WAL 是一直追加增长的,不能无限膨胀,所以 etcd 会定期做 Snapshot(快照):把当前状态机的完整数据打一个快照落盘,并把这个快照点之前的 WAL 日志清理掉,类似 Redis RDB 的思路,兼顾恢复速度和存储空间。

Watch 机制

etcd 一个很重要的能力是让客户端可以”订阅某个 key 或某个前缀的变化”,一旦发生变更立刻收到通知,这是 Kubernetes 能实时感知集群状态变化(比如 Pod 被创建/删除)的基础。Watch 的实现正是依赖前面说的 MVCC:客户端可以指定”从某个 revision 开始 watch”,etcd 会把这之后发生的所有变更事件通过 gRPC 的流式接口(server streaming)持续推送给客户端,不会因为网络抖动断线重连就丢事件,只要按上次收到的 revision 继续 watch 即可续上。

Lease 机制:实现 TTL 和心跳

etcd 支持给 key 绑定一个 Lease(租约),租约有 TTL,到期如果没有续约(renew/keepalive),绑定的 key 会被自动删除。这个机制是实现服务发现和分布式锁的基础:服务实例注册自己的地址到 etcd 上并绑定一个租约,定期发心跳续约,如果服务挂掉停止续约,租约到期,对应的 key 自动被清理,其他服务通过 watch 就能感知到这个实例下线了,不需要额外的心跳检测组件。

读一致性:线性一致读 vs 串行读

etcd 默认提供的是线性一致读(linearizable read),保证读到的数据一定是最新已提交的,不会读到过期数据。早期实现是每次读都要走一遍 Raft 日志确认(成本较高),后来优化成 ReadIndex 机制:Leader 在处理读请求时记录当前的 commit index,只需要跟多数节点确认一次自己仍然是合法 Leader(不需要真正写日志),再等状态机应用到这个 index,就能保证返回的数据是最新的,省掉了不必要的磁盘 IO。如果对一致性要求没那么高、更看重性能,也可以用串行读(serializable read),直接读本地某个节点的状态机,可能读到略微落后的数据,但延迟更低。

一句话总结

etcd 的核心是用 Raft 协议在多个节点间对”写操作顺序”达成强一致共识,配合 WAL 保证单机故障后数据不丢、Snapshot 控制日志增长、MVCC 数据模型天然支撑历史版本查询和 Watch 事件订阅、Lease 机制提供 TTL 和心跳续约能力,共同构成了一个能被 Kubernetes 这类系统安心拿来存放”生死攸关”的元数据的强一致存储。

如果想继续深入,可以聊聊 etcd 是怎么处理网络分区下的脑裂问题的、boltdb 的 B+树存储细节、etcd 在 Kubernetes 里具体是怎么被 apiserver 使用的,或者对比一下 etcd(CP)和 ZooKeeper 在设计取舍上的异同。

发表回复