【重识云原生】第六章容器6.3.4节——etcd组件

1 etcd概述

1.1 etcd是什么

        这里有一个ETCD的相关视频讲解:分布式注册服务中心etcd

        Etcd是CoreOS基于Raft协议开发的分布式key-value存储系统,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等),授权协议为Apache。 在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现和注册而涉及,它提供了数据TTL失效、数据改变监视、多值、目录监听、分布式锁原子操作等功能,可以方便的跟踪并管理集群节点的状态。

        当下,提供配置共享和服务发现的系统比较多,其中最为大家熟知的是[Zookeeper](后文简称ZK),而ETCD可以算得上是后起之秀了。不过,在项目实现、一致性协议易理解性、运维、安全等多个维度上,ETCD相比Zookeeper都占据优势。

        etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点:

  1. 简单:基于HTTP+JSON的API让你用curl就可以轻松使用。
  2. 安全:可选SSL客户认证机制。
  3. 快速:每个实例每秒支持一千次写操作。
  4. 可信:使用Raft算法充分实现了分布式。

1.2 ETCD vs ZK

        本文选取ZK作为典型代表与ETCD进行比较,而不考虑[Consul]项目作为比较对象,原因为Consul的可靠性和稳定性还需要时间来验证(项目发起方自身服务并未使用Consul, 自己都不用)。

  • 一致性协议: ETCD使用[Raft]协议, ZK使用ZAB(类PAXOS协议),前者容易理解,方便工程实现;
  • 运维方面:ETCD方便运维,ZK难以运维;
  • 项目活跃度:ETCD社区与开发活跃,ZK已经快死了;
  • API:ETCD提供HTTP+JSON, gRPC接口,跨平台跨语言,ZK需要使用其客户端;
  • 访问安全方面:ETCD支持HTTPS访问,ZK在这方面缺失;

1.3 etcd主要功能

  1. 基本的 key-value 存储
  2. 监听机制
  3. key 的过期及续约机制,用于监控和服务发现
  4. 原子性操作(CAS 和 CAD),用于分布式锁和 leader 选举

Tips: CAS的原理:CAS操作需要输入两个数值,一个旧值(期望操作之前的值)和一个新值,在操作期间先比较旧值有没有变化,如果没有发生变化,才交换新值,发生了变化则不交换。

1.4 ETCD的使用场景

        分布式系统中的数据分为控制数据和应用数据。etcd的使用场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。应用场景有如下几类:

  1. 场景一:服务发现(Service Discovery)
  2. 场景二:消息发布与订阅
  3. 场景三:负载均衡
  4. 场景四:分布式通知与协调
  5. 场景五:分布式锁、分布式队列
  6. 场景六:集群监控与Leader竞选

        举个最简单的例子,如果你需要一个分布式存储仓库来存储配置信息,并且希望这个仓库读写速度快、支持高可用、部署简单、支持http接口,那么就可以使用etcd。目前,cloudfoundry使用etcd作为hm9000的应用状态信息存储,kubernetes用etcd来存储docker集群的配置信息等。

1.5 ETCD读写性能

        按照官网给出的[Benchmark],在2CPU/1.8G内存/SSD磁盘这样的配置下,单节点的写性能可以达到16K的QPS,而先写后读也能达到12K的QPS。这个性能还是相当可观的。

2 ETCD工作原理

        ETCD使用Raft协议来维护集群内各个节点状态的一致性。简单说,ETCD集群是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过Raft协议保证每个节点维护的数据是一致的。

        如图所示,每个ETCD节点都维护了一个状态机,并且,任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端的写操作,通过Raft协议保证写操作对状态机的改动会可靠的同步到其他节点。

2.1 Etcd 基于 RAFT 的一致性原理

        ETCD工作原理核心部分在于Raft协议。本节接下来将简要介绍Raft协议,具体细节请参考其[论文]。Raft协议正如论文所述,确实方便理解。主要分为三个部分:选主,日志复制,安全性。

2.1.1 选主算法

        Raft协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群,并且有一个主节点来对外提供服务。当集群初始化,或者主节点挂掉后,面临一个选主问题。集群中每个节点,任意时刻处于Leader, Follower, Candidate这三个角色之一。选举特点如下:

  • 集群初始启动时,每个节点都是Follower角色,节点处于 follower 状态并被设定一个 election timeout,如果在这一时间周期内没有收到来自 leader 的 heartbeat,节点将发起选举:将自己切换为 candidate 之后,向集群中其它 follower 节点发送请求,询问其是否选举自己成为 leader。
  • 当收到来自集群中过半数节点的接受投票后,节点即成为 leader,开始接收保存 client 的数据并向其它的 follower 节点同步日志。如果没有达成一致,则 candidate 随机选择一个等待间隔(150ms ~ 300ms)再次发起投票,得到集群中半数以上 follower 接受的 candidate 将成为 leader。
  • leader 节点依靠定时向 follower 发送 heartbeat 来保持其地位。
  • 任何时候如果其它 follower 在 election timeout 期间都没有收到来自 leader 的 heartbeat,同样会将自己的状态切换为 candidate 并发起选举(出现这种情况,是由于多个节点同时选举,所有节点均为获得过半选票)。每成功选举一次,新 leader 的任期(Term)都会比之前 leader 的任期大 1。
  • Candidate节点收到来自主节点的信息后,会立即终止选举过程,进入Follower角色。为了避免陷入选主失败循环,每个节点未收到心跳发起选举的时间是一定范围内的随机值,这样能够避免2个节点同时发起选主。

2.1.2 日志复制

        所谓日志复制,是指主节点将每次操作形成日志条目,并持久化到本地磁盘,然后通过网络IO发送给其他节点。其他节点根据日志的逻辑时钟(TERM)和日志编号(INDEX)来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回,那么认为该日志是可提交的(committed),并将日志输入到状态机,将结果返回给客户端。

        这里需要注意的是,每次选主都会形成一个唯一的TERM编号,相当于逻辑时钟。每一条日志都有全局唯一的编号。

        主节点通过网络IO向其他节点追加日志。若某节点收到日志追加的消息,首先判断该日志的TERM是否过期,以及该日志条目的INDEX是否比当前以及提交的日志的INDEX跟早。若已过期,或者比提交的日志更早,那么就拒绝追加,并返回该节点当前的已提交的日志编号。否则,将日志追加,并返回成功。

        当主节点收到其他节点关于日志追加的回复后,若发现有拒绝,则根据该节点返回的已提交日志编号,发生其编号下一条日志。

        主节点向其他节点同步日志,还作了拥塞控制。具体地说,主节点发现日志复制的目标节点拒绝了某次日志追加消息,将进入日志探测阶段,一条一条发送日志,直到目标节点接受日志,然后进入快速复制阶段,可进行批量日志追加。

        按照日志复制的逻辑,我们可以看到,集群中慢节点不影响整个集群的性能。另外一个特点是,数据只从主节点复制到Follower节点,这样大大简化了逻辑流程。

2.1.3 安全性

        截止此刻,选主以及日志复制并不能保证节点间数据一致。试想,当一个某个节点挂掉了,一段时间后再次重启,并当选为主节点。而在其挂掉这段时间内,集群若有超过半数节点存活,集群会正常工作,那么会有日志提交。这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选主节点,它将缺失部分已提交的日志。在这样场景下,按Raft协议,它将自己日志复制给其他节点,会将集群已经提交的日志给覆盖掉。

        这显然是不可接受的。

        其他协议解决这个问题的办法是,新当选的主节点会询问其他节点,和自己数据对比,确定出集群已提交数据,然后将缺失的数据同步过来。这个方案有明显缺陷,增加了集群恢复服务的时间(集群在选举阶段不可服务),并且增加了协议的复杂度。

        Raft解决的办法是,在选主逻辑中,对能够成为主的节点加以限制,确保选出的节点已经包含了集群已经提交的所有日志。如果新选出的主节点已经包含了集群所有提交的日志,那就不需要和其他节点比对并同步数据了。简化了流程,缩短了集群恢复服务的时间。

        这里存在一个问题,加以这样限制之后,还能否选出主呢?答案是:只要仍然有超过半数节点存活,这样的主一定能够选出。因为已经提交的日志必然被集群中超过半数节点持久化,显然前一个主节点提交的最后一条日志也被集群中大部分节点持久化。当主节点挂掉后,集群中仍有大部分节点存活,那这存活的节点中一定存在一个节点包含了已经提交的日志了。

2.1.4 失效处理

  • Leader 失效:其他没有收到 heartbeat 的节点会发起新的选举,而当 Leader 恢复后由于步进数小会自动成为 follower(日志也会被新 leader 的日志覆盖)。
  • follower 节点不可用:follower 节点不可用的情况相对容易解决。因为集群中的日志内容始终是从 leader 节点同步的,只要这一节点再次加入集群时重新从 leader 节点处复制日志即可。
  • 多个 candidate:冲突后 candidate 将随机选择一个等待间隔(150ms ~ 300ms)再次发起投票,得到集群中半数以上 follower 接受的 candidate 将成为 leader。

        至此,关于Raft的一致性方案就全部结束了。

2.2 网络层实现

2.2.1 概述

        在目前的实现中,ETCD通过HTTP协议对外提供服务,同样通过HTTP协议实现集群节点间数据交互。

        网络层的主要功能是实现了服务器与客户端(能发出HTTP请求的各种程序)消息交互,以及集群内部各节点之间的消息交互。

2.2.2 ETCD-SERVER整体架构

        ETCD-SERVER 大体上可以分为网络层,Raft模块,复制状态机,存储模块,架构图如图1所示。

 ETCD-SERVER架构图

  • 网络层:提供网络数据读写功能,监听服务端口,完成集群节点之间数据通信,收发客户端数据;
  • Raft模块:完整实现了Raft协议;
  • 存储模块:KV存储,WAL文件,SNAPSHOT管理
  • 复制状态机:这个是一个抽象的模块,状态机的数据维护在内存中,定期持久化到磁盘,每次写请求会持久化到WAL文件,并根据写请求的内容修改状态机数据。

 2.2.3 节点之间网络拓扑结构

        ETCD集群的各个节点之间需要通过HTTP协议来传递数据,表现在:

  • Leader 向Follower发送心跳包, Follower向Leader回复消息;
  • Leader向Follower发送日志追加信息;
  • Leader向Follower发送Snapshot数据;
  • Candidate节点发起选举,向其他节点发起投票请求;
  • Follower将收的写操作转发给Leader;

        各个节点在任何时候都有可能变成Leader, Follower, Candidate等角色,同时为了减少创建链接开销,ETCD节点在启动之初就创建了和集群其他节点之间的链接。

        因此,ETCD集群节点之间的网络拓扑是一个任意2个节点之间均有长链接相互连接的网状结构。如下图所示。

 ETCD集群节点网络拓扑图

        需要注意的是,每一个节点都会创建到其他各个节点之间的长链接。每个节点会向其他节点宣告自己监听的端口,该端口只接受来自其他节点创建链接的请求。

2.2.4 节点之间消息交互

        在ETCD实现中,根据不同用途,定义了各种不同的消息类型。各种不同的消息,最终都通过google protocol buffer协议进行封装。这些消息携带的数据大小可能不尽相同。例如 传输SNAPSHOT数据的消息数据量就比较大,甚至超过1GB, 而leader到follower节点之间的心跳消息可能只有几十个字节。

        因此,网络层必须能够高效地处理不同数据量的消息。ETCD在实现中,对这些消息采取了分类处理,抽象出了2种类型消息传输通道:Stream类型通道和Pipeline类型通道。这两种消息传输通道都使用HTTP协议传输数据。

节点之间建立消息传输通道图

        集群启动之初,就创建了这两种传输通道,各自特点:

  • Stream类型通道:点到点之间维护HTTP长链接,主要用于传输数据量较小的消息,例如追加日志,心跳等;
  • Pipeline类型通道:点到点之间不维护HTTP长链接,短链接传输数据,用完即关闭。用于传输数据量大的消息,例如snapshot数据。

        如果非要做做一个类别的话,Stream就向点与点之间维护了双向传输带,消息打包后,放到传输带上,传到对方,对方将回复消息打包放到反向传输带上;而Pipeline就像拥有N辆汽车,大消息打包放到汽车上,开到对端,然后在回来,最多可以同时发送N个消息。

2.2.4.1 Stream类型通道

        Stream类型通道处理数据量少的消息,例如心跳,日志追加消息。点到点之间只维护1个HTTP长链接,交替向链接中写入数据,读取数据。

        Stream 类型通道是节点启动后主动与其他每一个节点建立。Stream类型通道通过Channel 与Raft模块传递消息。每一个Stream类型通道关联2个Goroutines, 其中一个用于建立HTTP链接,并从链接上读取数据, decode成message, 通过Channel传给Raft模块中,另外一个通过Channel 从Raft模块中收取消息,然后写入通道。

        具体点,ETCD使用golang的http包实现Stream类型通道:

  • 1)被动发起方监听端口, 并在对应的url上挂载相应的handler(当前请求来领时,handler的ServeHTTP方法会被调用)
  • 2)主动发起方发送HTTP GET请求;
  • 3)监听方的Handler的ServeHTTP访问被调用(框架层传入http.ResponseWriter和http.Request对象),其中http.ResponseWriter对象作为参数传入Writter-Goroutine(就这么称呼吧),该Goroutine的主循环就是将Raft模块传出的message写入到这个responseWriter对象里;http.Request的成员变量Body传入到Reader-Gorouting(就这么称呼吧),该Gorutine的主循环就是不断读取Body上的数据,decode成message 通过Channel传给Raft模块。

2.2.4.2 Pipeline类型通道

        Pipeline类型通道处理数量大消息,例如SNAPSHOT消息。这种类型消息需要和心跳等消息分开处理,否则会阻塞心跳。

        Pipeline类型通道也可以传输小数据量的消息,当且仅当Stream类型链接不可用时。

        Pipeline类型通道可用并行发出多个消息,维护一组Goroutines, 每一个Goroutines都可向对端发出POST请求(携带数据),收到回复后,链接关闭。

        具体地,ETCD使用golang的http包实现的:

  • 1)根据参数配置,启动N个Goroutines;
  • 2)每一个Goroutines的主循环阻塞在消息Channel上,当收到消息后,通过POST请求发出数据,并等待回复。

2.2.5 网络层与Raft模块之间的交互

        在ETCD中,Raft协议被抽象为Raft模块。按照Raft协议,节点之间需要交互数据。在ETCD中,通过Raft模块中抽象的RaftNode拥有一个message box, RaftNode将各种类型消息放入到messagebox中,有专门Goroutine将box里的消息写入管道,而管道的另外一端就链接在网络层的不同类型的传输通道上,有专门的Goroutine在等待(select)。

        而网络层收到的消息,也通过管道传给RaftNode。RaftNode中有专门的Goroutine在等待消息。也就是说,网络层与Raft模块之间通过Golang Channel完成数据通信。这个比较容易理解。

2.2.6 ETCD-SERVER处理请求(与客户端的信息交互)

        在ETCD-SERVER启动之初,会监听服务端口,当服务端口收到请求后,解析出message后,通过管道传入给Raft模块,当Raft模块按照Raft协议完成操作后,回复该请求(或者请求超时关闭了)。

2.3 etcd v2与v3比较

        Etcd v2 和 v3 本质上是共享同一套 raft 协议代码的两个独立的应用,接口不一样,存储不一样,数据互相隔离。也就是说如果从 Etcd v2 升级到 Etcd v3,原来 v2 的数据还是只能用 v2 的接口访问,v3 的接口创建的数据也只能访问通过 v3 的接口访问。所以我们按照 v2 和 v3 分别分析。

推荐在 Kubernetes 集群中使用 Etcd v3,v2 版本已在 Kubernetes v1.11 中弃用。

2.3.1 Etcd v2 存储、Watch 以及过期机制

        Etcd v2 是个纯内存的实现,并未实时将数据写入到磁盘,持久化机制很简单,就是将 store 整合序列化成 json 写入文件。数据在内存中是一个简单的树结构。比如以下数据存储到 Etcd 中的结构就如图所示。

代码语言:javascript
复制
/nodes/1/name node1

/nodes/1/ip 192.168.1.1

        store 中有一个全局的 currentIndex,每次变更,index 会加 1。然后每个 event 都会关联到 currentIndex。

        当客户端调用 watch 接口(参数中增加 wait 参数)时,如果请求参数中有 waitIndex,并且 waitIndex 小于 currentIndex,则从 EventHistroy 表中查询 index 大于等于 waitIndex,并且和 watch key 匹配的 event,如果有数据,则直接返回。如果历史表中没有或者请求没有带 waitIndex,则放入 WatchHub 中,每个 key 会关联一个 watcher 列表。 当有变更操作时,变更生成的 event 会放入 EventHistroy 表中,同时通知和该 key 相关的 watcher。

        这里有几个影响使用的细节问题:

  1. EventHistroy 是有长度限制的,最长 1000。也就是说,如果你的客户端停了许久,然后重新 watch 的时候,可能和该 waitIndex 相关的 event 已经被淘汰了,这种情况下会丢失变更。
  2. 如果通知 watcher 的时候,出现了阻塞(每个 watcher 的 channel 有 100 个缓冲空间),Etcd 会直接把 watcher 删除,也就是会导致 wait 请求的连接中断,客户端需要重新连接。
  3. Etcd store 的每个 node 中都保存了过期时间,通过定时机制进行清理。

        从而可以看出,Etcd v2 的一些限制:

  1. 过期时间只能设置到每个 key 上,如果多个 key 要保证生命周期一致则比较困难。
  2. watcher 只能 watch 某一个 key 以及其子节点(通过参数 recursive),不能进行多个 watch。
  3. 很难通过 watch 机制来实现完整的数据同步(有丢失变更的风险),所以当前的大多数使用方式是通过 watch 得知变更,然后通过 get 重新获取数据,并不完全依赖于 watch 的变更 event。

2.3.2 Etcd v3 存储、Watch以及过期机制

        Etcd v3 将 watch 和 store 拆开实现:

2.3.2.1 etcd v3 store

        Etcd v3 store 分为两部分,一部分是内存中的索引kvindex,是基于 google 开源的一个 golang 的 btree 实现的,另外一部分是后端存储。按照它的设计,backend 可以对接多种存储,当前使用的 boltdb。boltdb 是一个单机的支持事务的 kv 存储,Etcd 的事务是基于 boltdb 的事务实现的。Etcd 在 boltdb 中存储的 key 是 revision,value 是 Etcd 自己的 key-value 组合,也就是说 Etcd 会在 boltdb 中把每个版本都保存下,从而实现了多版本机制。

        举个例子: 用 etcdctl 通过批量接口写入两条记录:

代码语言:javascript
复制
etcdctl txn <<<

put key1 "v1"

put key2 "v2"

再通过批量接口更新这两条记录:

代码语言:javascript
复制
etcdctl txn <<<
put key1 "v12"
put key2 "v22"

boltdb 中其实有了 4 条数据:

代码语言:javascript
复制
rev={3 0}, key=key1, value="v1"
rev={3 1}, key=key2, value="v2"
rev={4 0}, key=key1, value="v12"
rev={4 1}, key=key2, value="v22"

        revision 主要由两部分组成,第一部分 main rev,每次事务进行加一;第二部分 sub rev,同一个事务中的每次操作加一。如上示例,第一次操作的 main rev 是 3,第二次是 4。当然这种机制大家想到的第一个问题就是空间问题,所以 Etcd 提供了命令和设置选项来控制 compact,同时支持 put 操作的参数来精确控制某个 key 的历史版本数。

        了解了 Etcd 的磁盘存储,可以看出如果要从 boltdb 中查询数据,必须通过 revision,但客户端都是通过 key 来查询 value,所以 Etcd 的内存 kvindex 保存的就是 key 和 revision 之前的映射关系,用来加速查询。

2.3.2.2 watch 机制的实现

        然后我们再分析下 watch 机制的实现。Etcd v3 的 watch 机制支持 watch 某个固定的 key,也支持 watch 一个范围(可以用于模拟目录的结构的 watch),所以 watchGroup 包含两种 watcher,一种是 key watchers,数据结构是每个 key 对应一组 watcher,另外一种是 range watchers, 数据结构是一个 IntervalTree(不熟悉的参看文文末链接),方便通过区间查找到对应的 watcher。

        同时,每个 WatchableStore 包含两种 watcherGroup,一种是 synced,一种是 unsynced,前者表示该 group 的 watcher 数据都已经同步完毕,在等待新的变更,后者表示该 group 的 watcher 数据同步落后于当前最新变更,还在追赶。

        当 Etcd 收到客户端的 watch 请求,如果请求携带了 revision 参数,则比较请求的 revision 和 store 当前的 revision,如果大于当前 revision,则放入 synced 组中,否则放入 unsynced 组。同时 Etcd 会启动一个后台的 goroutine 持续同步 unsynced 的 watcher,然后将其迁移到 synced 组。也就是这种机制下,Etcd v3 支持从任意版本开始 watch,没有 v2 的 1000 条历史 event 表限制的问题(当然这是指没有 compact 的情况下)。

        另外我们前面提到的,Etcd v2 在通知客户端时,如果网络不好或者客户端读取比较慢,发生了阻塞,则会直接关闭当前连接,客户端需要重新发起请求。Etcd v3 为了解决这个问题,专门维护了一个推送时阻塞的 watcher 队列,在另外的 goroutine 里进行重试。

        Etcd v3 对过期机制也做了改进,过期时间设置在 lease 上,然后 key 和 lease 关联。这样可以实现多个 key 关联同一个 lease id,方便设置统一的过期时间,以及实现批量续约。

2.3.3 相比 Etcd v2, Etcd v3 的一些主要变化

  1. 接口通过 grpc 提供 rpc 接口,放弃了 v2 的 http 接口。优势是长连接效率提升明显,缺点是使用不如以前方便,尤其对不方便维护长连接的场景。
  2. 废弃了原来的目录结构,变成了纯粹的 kv,用户可以通过前缀匹配模式模拟目录。
  3. 内存中不再保存 value,同样的内存可以支持存储更多的 key。
  4. watch 机制更稳定,基本上可以通过 watch 机制实现数据的完全同步。
  5. 提供了批量操作以及事务机制,用户可以通过批量事务请求来实现 Etcd v2 的 CAS 机制(批量事务支持 if 条件判断)。

2.4 etcd 的周边工具

2.4.1 Confd

        在分布式系统中,理想情况下是应用程序直接和 Etcd 这样的服务发现 / 配置中心交互,通过监听 Etcd 进行服务发现以及配置变更。但我们还有许多历史遗留的程序,服务发现以及配置大多都是通过变更配置文件进行的。Etcd 自己的定位是通用的 kv 存储,所以并没有像 Consul 那样提供实现配置变更的机制和工具,而 Confd 就是用来实现这个目标的工具。

        Confd 通过 watch 机制监听 Etcd 的变更,然后将数据同步到自己的一个本地存储。用户可以通过配置定义自己关注哪些 key 的变更,同时提供一个配置文件模板。Confd 一旦发现数据变更就使用最新数据渲染模板生成配置文件,如果新旧配置文件有变化,则进行替换,同时触发用户提供的 reload 脚本,让应用程序重新加载配置。

        Confd 相当于实现了部分 Consul 的 agent 以及 consul-template 的功能,作者是 kubernetes 的 Kelsey Hightower,但大神貌似很忙,没太多时间关注这个项目了,很久没有发布版本,我们着急用,所以 fork 了一份自己更新维护,主要增加了一些新的模板函数以及对 metad 后端的支持。

2.4.2 Metad

        服务注册的实现模式一般分为两种,一种是调度系统代为注册,一种是应用程序自己注册。调度系统代为注册的情况下,应用程序启动后需要有一种机制让应用程序知道『我是谁』,然后发现自己所在的集群以及自己的配置。Metad 提供这样一种机制,客户端请求 Metad 的一个固定的接口 /self,由 Metad 告知应用程序其所属的元信息,简化了客户端的服务发现和配置变更逻辑。

        Metad 通过保存一个 ip 到元信息路径的映射关系来做到这一点,当前后端支持 Etcd v3,提供简单好用的 http rest 接口。 它会把 Etcd 的数据通过 watch 机制同步到本地内存中,相当于 Etcd 的一个代理。所以也可以把它当做 Etcd 的代理来使用,适用于不方便使用 Etcd v3 的 rpc 接口或者想降低 Etcd 压力的场景。

2.5 Etcd 使用注意事项

1)Etcd cluster 初始化的问题

        如果集群第一次初始化启动的时候,有一台节点未启动,通过 v3 的接口访问的时候,会报告 Error: Etcdserver: not capable 错误。这是为兼容性考虑,集群启动时默认的 API 版本是 2.3,只有当集群中的所有节点都加入了,确认所有节点都支持 v3 接口时,才提升集群版本到 v3。这个只有第一次初始化集群的时候会遇到,如果集群已经初始化完毕,再挂掉节点,或者集群关闭重启(关闭重启的时候会从持久化数据中加载集群 API 版本),都不会有影响。

2)Etcd 读请求的机制

        v2 quorum=true 的时候,读取是通过 raft 进行的,通过 cli 请求,该参数默认为 true。

        v3 --consistency=“l” 的时候(默认)通过 raft 读取,否则读取本地数据。sdk 代码里则是通过是否打开:WithSerializable option 来控制。

        一致性读取的情况下,每次读取也需要走一次 raft 协议,能保证一致性,但性能有损失,如果出现网络分区,集群的少数节点是不能提供一致性读取的。但如果不设置该参数,则是直接从本地的 store 里读取,这样就损失了一致性。使用的时候需要注意根据应用场景设置这个参数,在一致性和可用性之间进行取舍。

3)Etcd 的 compact 机制

        Etcd 默认不会自动 compact,需要设置启动参数,或者通过命令进行 compact,如果变更频繁建议设置,否则会导致空间和内存的浪费以及错误。Etcd v3 的默认的 backend quota 2GB,如果不 compact,boltdb 文件大小超过这个限制后,就会报错:”Error: etcdserver: mvcc: database space exceeded”,导致数据无法写入。

3 集群部署

3.1 概述

        想必很多人都知道ZooKeeper,通常用作配置共享和服务发现。和它类似,ETCD算是一个非常优秀的后起之秀了。本文重点不在描述他们之间的不同点。首先,看看其官网关于ETCD的描述1:

A distributed, reliable key-value store for the most critical data of a distributed system.

        在云计算大行其道的今天,ETCD有很多典型的使用场景。常言道,熟悉一个系统先从部署开始。本文接下来将描述,如何部署ETCD集群。

        安装官网说明文档,提供了3种集群启动方式,实际上按照其实现原理分为2类:

  • 通过静态配置方式启动
  • 通过服务发现方式启动

        在部署集群之前,我们需要考虑集群需要配置多少个节点。这是一个重要的考量,不得忽略。

3.2 集群节点数量与网络分割

        ETCD使用RAFT协议保证各个节点之间的状态一致。根据RAFT算法原理,节点数目越多,会降低集群的写性能。这是因为每一次写操作,需要集群中大多数节点将日志落盘成功后,Leader节点才能将修改内部状态机,并返回将结果返回给客户端。

        也就是说在等同配置下,节点数越少,集群性能越好。显然,只部署1个节点是没什么意义的。通常,按照需求将集群节点部署为3,5,7,9个节点。

        这里能选择偶数个节点吗? 最好不要这样。原因有二:

  • 偶数个节点集群不可用风险更高,表现在选主过程中,有较大概率或等额选票,从而触发下一轮选举。
  • 偶数个节点集群在某些网络分割的场景下无法正常工作。试想,当网络分割发生后,将集群节点对半分割开。此时集群将无法工作。按照RAFT协议,此时集群写操作无法使得大多数节点同意,从而导致写失败,集群无法正常工作。

        当网络分割后,ETCD集群如何处理的呢?

  • 当集群的Leader在多数节点这一侧时,集群仍可以正常工作。少数节点那一侧无法收到Leader心跳,也无法完成选举。
  • 当集群的Leader在少数节点这一侧时,集群仍可以正常工作,多数派的节点能够选出新的Leader, 集群服务正常进行。

        当网络分割恢复后,少数派的节点会接受集群Leader的日志,直到和其他节点状态一致。

3.3 ETCD参数说明

        这里只列举一些重要的参数,以及其用途。

  • —data-dir 指定节点的数据存储目录,这些数据包括节点ID,集群ID,集群初始化配置,Snapshot文件,若未指定—wal-dir,还会存储WAL文件;
  • —wal-dir 指定节点的was文件的存储目录,若指定了该参数,wal文件会和其他数据文件分开存储。
  • —name 节点名称
  • —initial-advertise-peer-urls 告知集群其他节点url.
  • — listen-peer-urls 监听URL,用于与其他节点通讯
  • — advertise-client-urls 告知客户端url, 也就是服务的url
  • — initial-cluster-token 集群的ID
  • — initial-cluster 集群中所有节点

3.4 通过静态配置方式启动ETCD集群

        按照官网中的文档,即可完成集群启动。这里略。

3.5 通过服务发现方式启动ETCD集群

        ETCD还提供了另外一种启动方式,即通过服务发现的方式启动。这种启动方式,依赖另外一个ETCD集群,在该集群中创建一个目录,并在该目录中创建一个_config的子目录,并且在该子目录中增加一个size节点,指定集群的节点数目。

        在这种情况下,将该目录在ETCD中的URL作为节点的启动参数,即可完成集群启动。使用

--discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83 配置项取代静态配置方式中的--initial-cluster 和inital-cluster-state参数。其中https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83是在依赖etcd中创建好的目录url。

3.6 节点迁移

        在生产环境中,不可避免遇到机器硬件故障。当遇到硬件故障发生的时候,我们需要快速恢复节点。ETCD集群可以做到在不丢失数据的,并且不改变节点ID的情况下,迁移节点。

具体办法是:

  • 1)停止待迁移节点上的etc进程;
  • 2)将数据目录打包复制到新的节点;
  • 3)更新该节点对应集群中peer url,让其指向新的节点;
  • 4)使用相同的配置,在新的节点上启动etcd进程;

参考链接

etcd · Kubernetes指南

ETCD原理详细解析_aa1215018028的博客-CSDN博客_etcd原理剖析

Etcd功能介绍

etcd介绍 - 小吉猫 - 博客园

ETCD实现及原理分析_我的猫叫土豆的博客-CSDN博客_etcd

ETCD介绍—etcd概念及原理方面分析_Linux服务器开发的博客-CSDN博客_etcd

etcd入门详解_钟哥哥实在帅的博客-CSDN博客_etcd

etcd架构原理

etcd介绍

etcd · Kubernetes指南

etcd

Etcd github

Projects using etcd

Etcd 架构与实现解析

etcd:从应用场景到实现原理的全方位解读_语言 & 开发_孙健波_InfoQ精选文章