etcd

解决集群管理系统中 OS 升级的分布式并发控制以及配置文件的存储与分发等问题。基于此,etcd 被设计为提供高可用、强一致的小型 keyvalue 数据存储服务。

一个 etcd 集群,通常会由 3 个或者 5 个节点组成,多个节点之间,通过一个叫做 Raft 一致性算法的方式完成分布式一致性协同,算法会选举出一个主节点作为 leader,由 leader 负责数据的同步与数据的分发,当 leader 出现故障后,系统会自动地选取另一个节点成为 leader,并重新完成数据的同步与分发。客户端在众多的 leader 中,仅需要选择其中的一个就可以完成数据的读写。

在 etcd 整个的架构中,有一个非常关键的概念叫做 quorum,quorum 的定义是 =(n+1)/2,也就是说超过集群中半数节点组成的一个团体,在 3 个节点的集群中,etcd 可以容许 1 个节点故障,也就是只要有任何 2 个节点重合,etcd 就可以继续提供服务。同理,在 5 个节点的集群中,只要有任何 3 个节点重合,etcd 就可以继续提供服务。这也是 etcd 集群高可用的关键。

当我们在允许部分节点故障之后,继续提供服务,这里就需要解决一个非常复杂的问题,即分布式一致性。在 etcd 中,该分布式一致性算法由 Raft 一致性算法完成,这个算法本身是比较复杂的,我们这里就不展开详细介绍了。

但是这里面有一个关键点,它基于一个前提:任意两个 quorum 的成员之间一定会有一个交集,也就是说只要有任意一个 quorum 存活,其中一定存在某一个节点,它包含着集群中最新的数据。正是基于这个假设,这个一致性算法就可以在一个 quorum 之间采用这份最新的数据去完成数据的同步,从而保证整个集群向前衍进的过程中其数据保持一致。

内部机制

虽然 etcd 内部的机制比较复杂,但是 etcd 给客户提供的接口是比较简单的。如上图所示,我们可以通过 etcd 提供的客户端去访问集群的数据,也可以直接通过 http 的方式,类似像 curl 命令直接访问 etcd。在 etcd 内部,其数据表示也是比较简单的,我们可以直接把 etcd 的数据存储理解为一个有序的 map,它存储着 key-value 数据。同时 etcd 为了方便客户端去订阅资料的数据,也支持了一个 watch 机制,我们可以通过 watch 实时地拿到 etcd 中数据的增量更新,从而保持与 etcd 中的数据同步。

接口

接下来我们看一下 etcd 提供的接口,这里将 etcd 的接口分为了 5 组:

img

  • 第一组是 Put 与 Delete。上图可以看到 put 与 delete 的操作都非常简单,只需要提供一个 key 和一个 value,就可以向集群中写入数据了,那么在删除数据的时候,只需要提供 key 就可以了;
  • 第二组操作是查询操作。查询操作 etcd 支持两种类型的查询:第一种是指定单个 key 的查询,第二种是指定的一个 key 的范围;
  • 第三组操作:etcd 启动了 Watch 的机制,也就是我们前面提到的用于实现增量的数据更新,watch 也是有两种使用方法,第一种是指定单个 key 的 Watch,另一种是指定一个 key 的前缀。在实际应用场景的使用过程中,经常采用第二种;
  • 第四组:API 是 etcd 提供的一个事务操作,可以通过指定某些条件,当条件成立的时候执行一组操作。当条件不成立的时候执行另外一组操作;
  • 第五组是 Leases 接口。Leases 接口是分布式系统中常用的一种设计模式,后面会具体介绍。

image-20220102204007917

image-20220102204305598

image-20220102204602227

etcd 的数据版本号机制

要正确使用 etcd 的 API,必须要知道内部对应数据版本号的基本原理。

  • 首先 etcd 中有个 term 的概念,代表的是整个集群 Leader 的标志。当集群发生 Leader 切换,比如说 Leader 节点故障,或者说 Leader 节点网络出现问题,再或者是将整个集群停止后再次拉起,这个时候都会发生 Leader 的切换。当 Leader 切换的时候,term 的值就会 +1。

  • 第二个版本号叫做 revision,revision 代表的是全局数据的版本。当数据发生变更,包括创建、修改、删除,revision 对应的都会 +1。在任期内,revision 都可以保持全局单调递增的更改。正是 revision 的存在才使得 etcd 既可以支持数据的 MVCC,也可以支持数据的 Watch。

  • 对于每一个 KeyValue 数据,etcd 中都记录了三个版本:

    • 第一个版本叫做 create_revision,是 KeyValue 在创建的时候生成的版本号;
    • 第二个叫做 mod_revision,是其数据被操作的时候对应的版本号; 其对应的 mod_revision = 旧revision + 1,并不是=旧mod_revision+1
    • 第三个 version 就是一个计数器,代表了 KeyValue 被修改了多少次。

这里可以用图的方式给大家展示一下:

img

在同一个 Leader 任期之内,我们发现所有的修改操作,其对应的 term 值都等于 2,始终保持不变,而 revision 则保持单调递增。

当重启集群之后,我们会发现所有的修改操作对应的 term 值都变成了 3。在新的任期内,所有的 term 值都等于3,且不会发生变化。而对应的 revision 值同样保持单调递增。

从一个更大的维度去看,可以发现:在多个任期内,其数据对应的 revision 值会保持全局的单调递增。

etcd mvcc streaming watch

etcd 多版本号的并发控制以及 watch 的使用方法。

首先,在 etcd 中,支持对同一个 Key 发起多次数据修改。因为已经知道每次数据修改都对应一个版本号,多次修改就意味着一个 key 中存在多个版本,在查询数据的时候可以通过不指定版本号查询,这时 etcd 会返回该数据的最新版本。当我们指定一个版本号查询数据后,可以获取到一个 Key 的历史版本。

img

因为每次操作修改数据的时候都会对应一个版本号。 在 watch 的时候指定数据的版本,创建一个 watcher,并通过这个 watcher 提供的一个数据管道,能够获取到指定的 revision 之后所有的数据变更。如果指定的 revision 是一个旧版本,可以立即拿到从旧版本到当前版本所有的数据更新。并且,watch 的机制会保证 etcd 中,该 Key 的数据发生后续的修改后,依然可以从这个数据管道中拿到数据增量的更新。

为了更好地理解 etcd 的多版本控制以及 watch 的机制,可以简单的介绍一下 etcd 的内部实现。

img

在 etcd 中所有的数据都存储在一个 b+tree 中。b+tree 是保存在磁盘中,并通过 mmap 的方式映射到内存用来查询操作。

在 b+tree 中(如图所示灰色部分),维护着 revision 到 value 的映射关系。也就是说当指定 revision 查询数据的时候,就可以通过该 b+tree 直接返回数据。当我们通过 watch 来订阅数据的时候,也可以通过这个 b+tree 维护的 revision 到 value 映射关系,从而通过指定的 revision 开始遍历这个 b+tree,拿到所有的数据更新。

同时在 etcd 内部还维护着另外一个 b+tree。它管理着 key 到 revision 的映射关系。当需要查询 Key 对应数据的时候,会通过蓝色方框的 b+tree,将 key 翻译成 revision。再通过灰色框 b+tree 中的 revision 获取到对应的 value。至此就能满足客户端不同的查询场景了。

这里需要提两点:

  • 一个数据是有多个版本的;
  • 在 etcd 持续运行过程中会不断的发生修改,意味着 etcd 中内存及磁盘的数据都会持续增长。这对资源有限的场景来说是无法接受的。因此在 etcd 中会周期性的运行一个 Compaction 的机制来清理历史数据。对于一个 Key 的历史版本数据,可以选择清理掉。

etcd mini-transactions

img

在 etcd 内部会保证整个事务操作的原子性。也就是说 If 操作所有的比较条件,其看到的视图,一定是一致的。同时它能够确保在争执条件中,多个操作的原子性不会出现 etc 仅执行了一半的情况。

通过 etcd 提供的事务操作,我们可以在多个竞争中去保证数据读写的一致性,比如说前面已经提到过的 Kubernetes 项目,它正是利用了 etcd 的事务机制,来实现多个 KubernetesAPI server 对同样一个数据修改的一致性。

lease 租约机制

lease 是分布式系统中一个常见的概念,用于代表一个租约。通常情况下,在分布式系统中需要去检测一个节点是否存活的时候,就需要租约机制。

image-20220102200612703

创建了一个 10s 的租约,如果创建租约后不做任何的操作,那么 10s 之后,这个租约就会自动过期。

在这里,接着将 key1 和 key2 两个 key value 绑定到这个租约之上,这样当租约过期时,etcd 就会自动清理掉 key1 和 key2 对应的数据。

如果希望这个租约永不过期,比如说需要检测分布式系统中一个进程是否存活,那么就会在这个分布式进程中去访问 etcd 并且创建一个租约,同时在该进程中去调用 KeepAlive 的方法,与 etcd 保持一个租约不断的续约。试想一下,如果这个进程挂掉了,这时就没有办法再继续去开发 Alive。租约在进程挂掉的一段时间就会被 etcd 自动清理掉。所以可以通过这个机制来判定节点是否存活

在 etcd 中,允许将多个 key 关联在统一的 lease 之上,这个设计是非常巧妙的,事实上最初的设计也不是这个样子。通过这种方法,将多个 key 绑定在同一个lease对象,可以大幅地减少 lease 对象刷新的时间。试想一下,如果大量的 key 都需要绑定在同一个 lease 对象之上,每一个 key 都去更新这个租约的话,这个 etcd 会产生非常大的压力。通过支持加多个 key 绑定在同一个 lease 之上,它既不失灵活性同时能够大幅改进 etcd 整个系统的性能。

典型场景

元数据存储-Kubernetes

因为 Kubernetes 将自身所用的状态设备存储在 etcd 中,其状态数据的存储的复杂性将 etcd 给 cover 掉之后,Kubernetes 系统自身不需要再树立复杂的状态流转,因此自身的系统设立架构也得到了大幅的简化。

img

Server Discovery (Naming Service)

第二个场景是 Service Discovery,也叫做名字服务。在分布式系统中,通常会出现的一个模式就是需要后端多个,可能是成百上千个进程来提供一组对等的服务,比如说检索服务、推荐服务。

img

对于这样一种后端服务,通常情况下为了简化后端服务的运维成本,后端的这一进程会被类似 Kubernetes 这样的集群管理系统所调度,也就是说,我们事先无法知道这一组进程分布在哪里。为了解决这个问题,可以利用 etc来解决资源注册的问题,当这一组后端进程被调度,在进程内部启动之后,可以将自身所在的地址注册到 etcd。

从而使得 API 网关能够通过 etcd 及时感知到后端进程的地址,这样当后端进程发生故障迁移的时候,会重新注册到 etcd 中,使得 API 网关能够及时地感知到新的集群地址。同时,因为 etcd 提供的 Lease 操作,可以及时感知到进程状态的变化,如果进程运行过程中死掉了,那么网关可以及时感知到进程状态的变化,从而将流量自动地切到其他的进程。

通过这种方式,整个状态数据被 etcd 接管,那么 API 网关本身也是无状态的,它可以水平地扩展来服务更多的客户。同时得益于 etcd 的良好性能,可以支持上万个后端进程的节点,基本上这种架构可以服务于非常大型的企业。

Distributed Coordination: leader election

第三个场景是分布式场景中比较常见的一个选主的场景。

我们知道在分布式系统中,有一种典型的设计模式就是 Master+Slave。通常情况下,Slave 提供了 CPU 内存磁盘以及网络的各种资源 ,而 Master 用来控制这些资源的协同,Master 内部会存储一些状态数据,以及实现一组和业务逻辑相关的控制器,而 Slave 之间会和 Master 保持一个心跳的交互。

典型的分布式存储服务以及分布式计算服务,比如说 Hadoop,HDFS 等等,它们都是采用了类似这样的设计模式。这样的设计模式会有一个典型的问题:Master 节点的可用性。当 Master 故障以后,整个集群的服务就被停掉了,没有办法再服务用户的请求。

为了解决这个问题,需要去启动多个 Master。那么多个 Master 就会面临一个问题:谁来提供这个服务?因为通常情况下分布式系统和 Master 都是有状态逻辑的,无法允许多个 Master 同时运行。

img

可以通过 etcd 来实现选主,将其中的一个 Master 选主成 Leader,负责控制整个集群中所有 Slave 的状态。被选主的 Leader 可以将自己的 IP 注册到 etcd 中,使得 Slave 节点能够及时获取到当前组件的地址,从而使得系统按照之前单个 Master 节点的方式继续工作。当 Leader 节点发生异常之后,通过 etcd 能够选取出一个新的节点成为主节点,并且注册新的 IP 之后,Slave 又能够拉取新的主节点的 IP,从而会继续恢复服务。这一架构,在分布式系统中,被广泛使用。

Distributed Coordination 分布式系统并发控制

最后来看分布式协同中的另外一个问题。在分布式系统中,当我们去执行一些任务,比如说去升级 OS、或者说升级 OS 上的软件的时候、又或者去执行一些计算任务的时候,通常情况下需要控制任务的并发度。因为任务到了后端服务,通常是有容量瓶颈的。无法在同一个时刻,同时拉起成千上万的任务。这样后端的存储系统或者是网络资源都是吃不消的。

img

因此需要控制任务执行的并发度。在这个设计模式的进程中通过 etcd 提供的一些基本 API 操作来完成分布式的协同。这样当第一个进程执行完毕以后,第二个进程就可以开始执行。同时利用 etcd 的进程存活性检测机制可以做到:当将任务分发给一个进程,但这个进程没有执行完就已经死掉之后,可以继续换下一个进程继续工作。

也就是说,在这个模式中通过 etcd 去实现一个分布式的信号量,并且它支持能够自动地剔除掉故障节点。在进程执行过程中,如果进程的运行周期比较长,我们可以将进程运行过程中的一些状态数据存储到 etcd,从而使得当进程故障之后且需要恢复到其他地方时,能够从 etcd 中去恢复一些执行状态,而不需要重新去完成整个的计算逻辑,以此来加速整个任务的执行效率。

etcd 性能优化

image-20220102224054714

image-20220102224119600

https://coreos.com/docs/latest/op-guide/hardware.html

image-20220102230907682

image-20220103103831760

image-20220103103931236

安全通信

1
2
3
4
5
ETCDCTL_API=3 etcdctl --endpoints 10.2.0.9:2379 \
  --cert=/etc/kubernetes/pki/etcd/client.crt \
  --key=/etc/kubernetes/pki/etcd/client.key \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  member list

快照

1
2
3
4
5
6
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=<trusted-ca-file> --cert=<cert-file> --key=<key-file> \
  snapshot save <backup-file-location>


ETCDCTL_API=3 etcdctl --data-dir=/var/lib/etcd snapshot restore snapshotdb