课时

课时 16 深入理解 etcd:基于原理解析(曾凡松)

本文将主要分享以下三方面的内容:

  1. 第一部分,会为大家介绍 etcd 项目发展的整个历程,从诞生至今 etcd 经历的那些重要的时刻;
  2. 第二部分,会为大家介绍 etcd 的技术架构以及其内部的实现机制,通过对技术架构和内部机制的学习,帮助我们正确地使用 etcd;
  3. 第三部分,结合具体的使用场景,为大家介绍在分布式系统中 etcd 所扮演的角色。

etcd 项目的发展历程

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

项目当前隶属于 CNCF 基金会,被 AWS、Google、Microsoft、Alibaba 等大型互联网公司广泛的使用。

avatar

最初,在 2013 年 6 月份由 CoreOS 公司向 GitHub 中提交了第一个版本的初始代码。

到了 2014 年的 6 月,社区发生了一件事情,Kubernetes v0.4 版本发布。这里有必要介绍一下 Kubernetes 项目,它首先是一个容器管理平台,由谷歌开发并贡献给社区,因为它集齐了谷歌在容器调度以及集群管理等领域的多年经验,从诞生之初就备受瞩目。在 Kubernetes v0.4 版本中,它使用了 etcd 0.2 版本作为实验核心元数据的存储服务,自此 etcd 社区得到了飞速的发展。

很快,在 2015 年 2 月份,etcd 发布了第一个正式的稳定版本 2.0。在 2.0 版本中,etcd 重新实践了 Raft 一致性算法,另外用户提供了一个简单的树形数据视图,在 2.0 版本中 etcd 支持每秒超过 1000 次的写入性能,满足了当时绝大多数的应用场景需求。2.0 版本发布之后,经过不断的迭代与改进,其原有的数据存储方案逐渐成为了新世纪的性能瓶颈,因此 etcd 启动了 v3 版本的方案设计。

2017 年 1 月份的时候,etcd 发布了 3.1 版本,基本上标志着 v3 版本方案的全面成熟。在 v3 版本中 etcd 提供了一套全新的 API,并且重新实践了更有效的一致性读取方法,同时提供了一个 gRPC 接口。gRPC 的 proxy 用于扩展 etcd 的读取性能,同时在 v3 版本的方案中包含了大量的 GC 的优化,极大地提高了 etcd 的性能。在该版本中 etcd 可以支持每秒超过 10000 次的写入。

2018 年,CNCF 基金会下的众多项目都使用了 etcd 作为其核心的数据存储。据不完全统计,使用 etcd 的项目超过了 30 个,在同年 11 月份,etcd 项目自身也成为了 CNCF 旗下的孵化项目。进入 CNCF 基金会后,etcd 拥有了超过 400 个贡献组,其中包含了来自 AWS、Google、Alibaba 等 8 个公司的 9 个项目维护者。

2019 年,etcd 即将发布全新的 3.4 版本,该版本由 Google、Alibaba 等公司联合打造,将进一步改进 etcd 的性能及稳定性,以满足在超大型公司使用中苛刻的场景要求。

架构及内部机制解析

内部机制解析

etcd 是一个分布式的、可靠的 key-value 存储系统,它用于存储分布式系统中的关键数据,这个定义非常重要。

avatar

一个 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 之间采用这份最新的数据去完成数据的同步,从而保证整个集群向前衍进的过程中其数据保持一致。

avatar

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

etcd API 接口

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

avatar

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

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,是其数据被操作的时候对应的版本号;

  • 第三个 version 就是一个计数器,代表了 KeyValue 被修改了多少次。

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

avatar

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

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

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

etcd mvcc & streaming watch

了解 etcd 的版本号控制后,接下来介绍一下 etcd 多版本号的并发控制以及 watch 的使用方法。

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

avatar

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

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

avatar

在 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

在理解了 mvcc 机制及 watch 机制之后,来介绍一下 etcd 提供的 mini-transactions 机制。etcd 的 transaction 机制比较简单,基本可以理解为一段 if-else 程序,在 if 中可以提供多个操作。

avatar

在上图的示例中,if 里面写了两个条件。当 Value(key1) 大于“bar”,并且 Version(key1) 的版本等于 2 的时候,执行 Then 里面指定的操作:修改 Key2 的数据为 valueX,同时删除 Key3 的数据。如果不满足条件,则执行另外一个操作:Key2 修改为 valueY。

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

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

etcd lease 的概念及用法

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

avatar

上图示例中首先创建了一个 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 整个系统的性能。

实例演示

etcd 基本的读写操作

这里为大家以实际 demo 的方式来演示一些 etcd 的基本的操作。首先启动一个 etcd,会发现 etcd 已经成功启动了。

avatar

启动 etcd 之后,就可以通过 etcd 客户端来操作 etcd 的数据了。比如说向 etcd 中插入一条数据,在插入一条数据后,可以通过 get 方法来查询到这条数据。如下图所示:

avatar

为了展示 etcd 查询的基本内容,这里直接创建 10 个 key value,分别为 key0~key9,所以我们就向 etcd 中插入了 10 条数据。

avatar

可以查询其中的 key5,同时也支持查询 key 的范围。指定一个 key 的起始的范围,以及 key 的结束范围。注意:结束范围是不包含的,也就是说当查询 key2~key6 的时候,etcd 会返回 key2、key3、key4、key5 四条数据。

avatar

etcd 还支持指定前缀的查询。通过这种方式,可以发现 etcd 会返回 key0~key9 所有的数据。

avatar

前面提到的 etcd 中的版本,对于 key0 来说,可以通过 -w 指定 json 的方式来输出。下面格式化一个 json 字符串,发现该操作对应返回了其中的第一条数据,并且在返回体中包含了一个消息头。该消息头描述了 etcd 全局的信息,其中的 class ID 和 member ID 暂时先忽略掉。那么这里的 raft_term 就是我们在前面提到的 etcd 的全局版本号 term,当前 term 值是 2,发现所有的操作中 term 值对应的都是 2。

avatar

假如换成 key5,如下图所示,发现对应的 term 值依然是 2。同时集群全局的版本号 revision 当前是 12,因为我们没有发起任何的数据修改。

avatar

这时可以尝试再插入一条数据。当再次查询 etcd key5 的数据时,会发现全局的 revision 已经变成了 13,因为刚才又新增了一条数据,同时当前的 term 值依然是保持不变的。

在返回的数据中,所有包含的 key value 以外,它还包含着 etcd 提供的三个 revision。这里我们会发现,key5 对应创建的 revision 是 8,同时它修改的 revision 也是 8,因为我们没有对 key5 发起过任何的修改,与此同时,它对应的 version 是 1。

avatar

当对 key5 发起修改的时候,下图中可以看到,其对应的 mod_revision 已经变成了 14,修改的 revision 并不是 +1 的关系,而是采用当前操作对应的 revision。同样,因为已经发生了一次修改,那么 key5 的版本号就会从 1 变成 2。

avatar

那么如果再一次修改 key5 的数据,保持数据不变,还是 x,会发生什么情况呢?

如下图所示,集群全局的 revision 变成了 15,key5 的 mod_revision 也变成了 15,key5 的 version 变成了 3。可以发现:对同一个 key 写入同一个 value,其版本号依然会变化。

avatar

如果删除这个数据,重新产生 key5 这个数据,从下图可以发现,当前的 revision 又增加了一次。因为删除也是一次修改操作,但数据已经没有了。

avatar

如果重新插入 key5,这时全局的 revision 变成了 17、key5 的 create_revision 是 17、对应修改的 revision 也是 17;但 value 值却变成了 1,又重新开始计数,这是因为 value 代表着 key 被修改的次数。

avatar

在同一个任期内的 term 值是不发生变化的。如果尝试停掉 etcd,并且重新启动 etcd。当再次查询 key5 的数据时,将会发现集群已经从 2 变成了 3,但全局的数据的 revision 是不变的。

avatar

如果这时再次修改 key5,会发现跟刚才的规律一样:集群全局的 revision +1、key5 创建的 revision 保持不变。但是修改的 revision 为本次操作的版本,同时,其 value 修改过一次之后 version 变成了2。

这里需要注意到:key 和 value 在 etcd 中的表示并不是字符串的形式,而是二进制的形式,这里输出的时候是以 base64 编码的。

avatar

如果需要知道 etcd 的数据内容,那么可以通过 get key5,或者可以通过反解 base 64 的方式来实现。如下图执行命令后得到的值为 valueX。

avatar

etcd 的 watch操作

接下来看一下 etcd 提供的 watch 操作,来 watch key5 的变化。再开一个窗口去写入 key5 的数据,当数据被写入的时候,原本的 watch 就能及时拿到数据的更新,它可以非常实时地感觉到数据的变化。同时尝试写入 key4 的数据,则 watch 是不会收到数据更新的。

avatar

前面我们提到了 etcd 是支持指定前缀 watch的。如果指定前缀 key,那么在写入 key4 的时候,我们能收到更新;写入 key5 的时候,依然能收到更新。通过这种方式,我们就可以实现 etcd 的数据同步了。

avatar

这里可以给大家留一个课后作业,可以尝试使用 etcd 的客户端,来实现一个同步 etcd 指定前缀的数据版本的功能。

etcd 事务操作

etcd 客户端还提供了一些事务操作的语义。比如说 key5 的数据等于一个不成立的条件,随便写一个“whatever”,那么显然成功操作的时候将会失败;成功操作写一个读取操作,失败的时候尝试将 key5 重新写为 value5,同时将 key4 写为 valueX。当事务成功以后,重新去查 key5 的数据,就会发现 key5 已经被写成 value5 了,key4 也被更新为对应的 valueX。

avatar

demo 的演示到这里就结束了。

三、典型的使用场景介绍

元数据存储-Kubernetes

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

avatar

Server Discovery (Naming Service)

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

avatar

对于这样一种后端服务,通常情况下为了简化后端服务的运维成本,后端的这一进程会被类似 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 同时运行。

avatar

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

Distributed Coordination 分布式系统并发控制

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

avatar

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

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

本节总结

本节课的主要内容就到此为止了,这里为大家简单总结一下:

  • 第一部分,为大家介绍了 etcd 项目是如何诞生的,以及在 etcd 发展的过程中它经历的几个阶段;
  • 第二部分,为大家介绍了 etcd 的架构以及其内部的基本操作接口,在理解 etcd 是如何实现高可用的基础之上,结合了实践的方式展示了 etcd 数据的一些基本操作以及其内部的版本管理机制;
  • 第三部分,介绍了三种典型的 etcd 使用场景,以及在对应的场景下,分布式系统的设计思路。

那么下一节课程,将会继续为大家带来 etcd 的内容,它包含了阿里云在 etcd 生产上面的实践经验,包括如何去构建一个稳定高性能可扩展的 etcd 存储服务。

上一篇
下一篇