etcd 是一个分布式的、可靠的 key-value 存储系统,它用于存储分布式系统中的关键数据,这个定义非常重要。
etcd与raft
一个 etcd 集群,通常会由 3 个或者 5 个节点组成,多个节点之间通过 Raft 一致性算法的完成分布式一致性协同,算法会选举出一个主节点作为 leader,由 leader 负责数据的同步与数据的分发。当 leader 出现故障后系统会自动地选取另一个节点成为 leader,并重新完成数据的同步。
客户端在多个节点中,仅需要选择其中的任意一个就可以完成数据的读写,内部的状态及数据协同由 etcd 自身完成。
在 etcd 整个架构中,有一个非常关键的概念叫做 quorum,quorum 的定义是 (n+1)/2,也就是说超过集群中半数节点组成的一个团体,在 3 个节点的集群中,etcd 可以容许 1 个节点故障,也就是只要有任何 2 个节点可用,etcd 就可以继续提供服务。同理,在 5 个节点的集群中,只要有任何 3 个节点可用,etcd 就可以继续提供服务,这也是 etcd 集群高可用的关键。
Raft 一致性算法能够工作的一个关键点是:任意两个 quorum 的成员之间一定会有一个交集(公共成员),也就是说只要有任意一个 quorum 存活,其中一定存在某一个节点(公共成员),它包含着集群中所有的被确认提交的数据。正是基于这一原理,Raft 一致性算法设计了一套数据同步机制,在 Leader 任期切换后能够重新同步上一个 quorum 被提交的所有数据,从而保证整个集群状态向前推进的过程中保持数据的一致。
API介绍
第一组是 Put 与 Delete。上图可以看到 put 与 delete 的操作都非常简单,只需要提供一个 key 和一个 value,就可以向集群中写入数据了,删除数据的时候只需要指定 key 即可;
第二组是查询操作。etcd 支持两种类型的查询:第一种是指定单个 key 的查询,第二种是指定的一个 key 的范围;
第三组是数据订阅。etcd 提供了 Watch 机制,我们可以利用 watch 实时的订阅到 etcd 中增量的数据更新,watch 支持指定单个 key,也可以指定一个 key 的前缀,在实际应用场景中的通常会采用第二种形势;
第四组是事务操作。etcd 提供了一个简单的事务支持,用户可以通过指定一组条件满足时执行某些动作,当条件不成立的时候执行另一组操作,类似于代码中的 if else 语句,etcd 确保整个操作的原子性;
第五组是 Leases 接口。Leases 接口是分布式系统中常用的一种设计模式,其用法后面会具体展开。
数据版本机制
- 首先 etcd 中有个 term 的概念,代表的是整个集群 Leader 的任期。当集群发生 Leader 切换,term 的值就会 +1。在节点故障,或者 Leader 节点网络出现问题,再或者是将整个集群停止后再次拉起,都会发生 Leader 的切换。
- 第二个版本号叫做 revision,revision 代表的是全局数据的版本。当数据发生变更,包括创建、修改、删除,其 revision 对应的都会 +1。特别的,在集群中跨 Leader 任期之间,revision 都会保持全局单调递增。
正是 revision 的这一特性,使得集群中任意一次的修改都对应着一个唯一的 revision,因此我们可以通过 revision 来支持数据的 MVCC,也可以支持数据的 Watch。
mvcc & streaming watch
在 etcd 中支持对同一个 Key 发起多次数据修改,每次数据修改都对应一个版本号。etcd 在实现上记录了每一次修改对应的数据,也就意味着一个 key 在 etcd 中存在多个历史版本。在查询数据的时候如果不指定版本号,etcd 会返回 Key 对应的最新版本,当然 etcd 也支持指定一个版本号来查询历史数据。
因为 etcd 将每一次修改都记录了下来,使用 watch 订阅数据时,可以支持从任意历史时刻(指定 revision)开始创建一个 watcher,在客户端与 etcd 之间建立一个数据管道,etcd 会推送从指定 revision 开始的所有数据变更。etcd 提供的 watch 机制保证,该 Key 的数据后续的被修改之后,通过这个数据管道即时的推送给客户端。
如下图所示,etcd 中所有的数据都存储在一个 b+tree 中(灰色),该 b+tree 保存在磁盘中,并通过 mmap 的方式映射到内存用来支持快速的访问。
灰色的 b+tree 中维护着 revision 到 value 的映射关系,支持通过 revision 查询对应的数据。因为 revision 是单调递增的,当我们通过 watch 来订阅指定 revision 之后的数据时,仅需要订阅该 b+ tree 的数据变化即可。
在 etcd 内部还维护着另外一个 btree(蓝色),它管理着 key 到 revision 的映射关系。当客户端使用 key 查询数据时,首先需要经过蓝色的 btree 将 key 转化为对应的 revision,再通过灰色的 btree 查询到对应的数据。
etcd 将每一次修改都记录下来会导致数据持续增长,这会带来内存及磁盘的空间消耗,同时也会影响 b+tree 的查询效率。在 etcd 中会运行一个周期性的 Compaction 的机制来清理历史数据,将一段时间之前的同一个 Key 的多个历史版本数据清理掉。最终的结果是灰色的 b+tree 依旧保持单调递增,但可能会出现一些空洞。
mini-transations
etcd 的 transaction 机制比较简单,基本可以理解为一段 if-else 程序,在 if 中可以提供多个操作
在 etcd 内部会保证整个事务操作的原子性。也就是说 If 操作所有的比较条件,其看到的视图一定是一致的。同时它能够确保多个操作的原子性不会出现 Then 中的操作仅执行了一半的情况。
**
通过 etcd 提供的事务操作,我们可以在多个竞争中去保证数据读写的一致性,比如说前面已经提到过的 Kubernetes 项目,它正是利用了 etcd 的事务机制,来实现多个 KubernetesAPI server 对同样一个数据修改的一致性。
lease
lease 是分布式系统中一个常见的概念,用于代表一个分布式租约。典型情况下,在分布式系统中需要去检测一个节点是否存活的时,就需要租约机制。
上图示例中的代码示例首先创建了一个 10s 的租约,如果创建租约后不做任何的操作,那么 10s 之后,这个租约就会自动过期。接着将 key1 和 key2 两个 key value 绑定到这个租约之上,这样当租约过期时 etcd 就会自动清理掉 key1 和 key2,使得节点 key1 和 key2 具备了超时自动删除的能力。
如果希望这个租约永不过期,需要周期性的调用 KeeyAlive 方法刷新租约。
比如说需要检测分布式系统中一个进程是否存活,可以在进程中去创建一个租约,并在该进程中周期性的调用 KeepAlive 的方法。如果一切正常,该节点的租约会一致保持,如果这个进程挂掉了,最终这个租约就会自动过期。
在 etcd 中,允许将多个 key 关联在同一个 lease 之上,这个设计是非常巧妙的,可以大幅减少 lease 对象刷新带来的开销。