Etcd是CoreOS基于Raft协议开发的分布式key-value存储,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。
2013 年,一个叫做作 CoreOS 的创业团队认为,所有的安全问题,都可以归结为软件更新能力的问题,如果可以构建一个自动更新的服务器(自动化),那么系统的安全问题将迎刃而解。为了达到这个目的,必须将应用程序打包成一个独立单元,达到基本操作系统和应用程序隔离的目的,CoreOS 通过容器技术来达到目的。同时,还共了集群化的管理方案,让用户管理集群就像管理单机一样方便。 对于管理集群,CoreOS 团队面临的第一个问题就是,如何保证集群的高可用?换句话说,CoreOS 团队希望在重启任意一个集群中的某个节点时,其他的应用程序不会因此而宕机。因此,需要运行多个副本。那多个副本,就需要进行协调服务。CoreOS 首先在开源社区去寻找优秀的、相关的开源协调服务项目,它们希望这个开源项目满足 5 个目标:
有了理想目标后,CoreOS 开始寻找这样的协调服务了,不过很遗憾,在 2013 年并没有,因此 CoreOS 决定自己造轮子,那接下来就是为了实现这个目标而技术选型了。 在解决数据一致性的问题上,CoreOS 选择引入一个共识算法 Raft。该算法是易于理解的,它将一致性问题拆解成了 Leader 选举、日志同步、安全性三个相对独立的子问题。 再来看看低容量的问题,CoreOS 参考了 ZooKeeper 的数据模型与 API,使用基于目录的层级模式,而 API 相比 ZooKeeper 来说,使用了更加简单的 REST API,来对节点进行增删改查以及监听操作。 对于在存储引擎上的节点,etcd 选择的是简单内存树,将节点的数据结构进行了精简,节点中包含路径、值、孩子节点信息。这是一个典型的低容量设计。 最后是可维护性,Raft 算法提供了成员变更算法,基于此来监听成员的在线、变更状态。 基于以上的技术选型,CoreOS 团队在 2013 年发布了基于 Go 编写的 etcd 的第一个版本 beta v0.1 。 2014 年 6 月,Google 的 Kubernetes 项目横空出世,它选择了 etcd 作为服务协调的基石,因为 etcd 的目标与 Kubernetes 所需要的特性不谋而合,例如:高可用、Watch 机制。Kubernetes 项目使用 etcd,除了技术因素,也与当时的商业竞争有关,CoreOS 是 Kubernetes 容器生态圈的核心成员之一。在 Kubernetes 的带领下,etcd 也进入了快速发展的时代。因此在 2015 年 1 月,etcd 的 v2.0 版本正式发布,此时它被广泛应用于配置存储、服务发现、主备选举等场景。不过此时也出现了若干性能、功能不足的问题,如下:
面对这些问题,CoreOS 团队积极听取了开源社区的声音并积极改进,面对功能局限中提到的分页、范围查询查询不支持,那么它就去支持;面对内存开销大的问题,那它就通过引入 B-tree、boltdb 实现一个 MVCC 数据库,将数据模型从层次性改为扁平型,基于 boltdb 实现持久化存储,降低 etcd 的内存占用;对于性能瓶颈,它引入了 gRPC,通过 protobuf 定义消息,解码性能得到了大幅的提高,并通过 HTTP/2.0 多路复用机制,减少了大量 watcher 等场景下的连接数。基于这些解决方案,在 2016 年 6 月,etcd v3.0 正式发布了。
如今,etcd已然成为了云原生时代首选的元数据存储产品。
etcd集群本身是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过Raft协议保证每个节点维护的数据是一致的,在ETCD集群中任意时刻至多存在一个有效的主节点,由主节点处理所有来自客户端写操作,通过Raft协议保证写操作对状态机的改动会可靠的同步到其他节点,Raft协议如下图所示:
Raft协议主要分为三个部分:选举,复制日志,安全性。
Raft协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群,并且有一个主节点来对外提供服务。当集群初始化,或者主节点挂掉后,面临一个选举问题。集群中每个节点,任意时刻处于Leader、Follower、Candidate这三个角色之一,选举特点如下:
为了避免陷入选举失败循环,每个节点未收到心跳发起选举的时间是一定范围内的随机值,这样能够避免2个节点同时发起选举。
日志复制是指主节点将每次操作形成日志条目,并持久化到本地磁盘,然后通过网络IO发送给其他节点。其他节点根据日志的逻辑时钟(TERM)和日志编号(INDEX)来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回,那么认为该日志是可提交的(committed),并将日志输入到状态机,将结果返回给客户端。
这里需要注意的是,每次选举都会形成一个唯一的TERM编号,相当于逻辑时钟,每一条日志都有全局唯一的编号。
主节点通过网络IO向其他节点追加日志。若某节点收到日志追加的消息,首先判断该日志的TERM是否过期,以及该日志条目的INDEX是否比当前以及提交的日志的INDEX更早。若已过期,或者比提交的日志更早,那么就拒绝追加,并返回该节点当前的已提交的日志的编号。否则将日志追加,并返回成功。
当主节点收到其他节点关于日志追加的回复后,若发现有拒绝,则根据该节点返回的已提交日志编号,发生其编号下一条日志。
主节点向其他节点同步日志,还作了拥塞控制。主节点发现日志复制的目标节点拒绝了某次日志追加消息,将进入日志探测阶段,一条一条发送日志,直到目标节点接受日志,然后进入快速复制阶段,可进行批量日志追加。
按照日志复制的逻辑,我们可以看到,集群中慢节点不影响整个集群的性能。另外一个特点是,数据只从主节点复制到Follower节点,这样大大简化了逻辑流程。Raft日志复制路程如下图所示:
选举和复制日志并不能保证节点间数据一致。当一个某个节点挂掉了,一段时间后再次重启,并刚好当选为主节点。而在其挂掉这段时间内,集群若有超过半数节点存活,集群会正常工作,那么会有日志提交,这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选举节点,它将缺失部分已提交的日志。在这样场景下,按Raft协议,它将自己日志复制给其他节点,会将集群已经提交的日志给覆盖掉,这显然是不可接受的,对于出现这种问题解决办法:
ETCD服务发现示意图如下图所示:
服务发现是分布式系统中最常见的需要解决的问题之一,即在同一个分布式集群中的进程或服务,客户端通过名字就可以查找和连接服务端。要解决服务发现的问题,需要有下面三点:
(1) 在微服务中使用etcd服务发现
随着Docker容器的流行,多种微服务共同协作,构成一个相对功能强大的组织架构。使用etcd服务发现机制,在etcd中注册某个服务名字的目录,在该目录下存储可用的服务节点的IP。服务使用者从etcd目录下查找可用的服务节点IP来连接和调用,达到透明化的动态添加这些服务目的,示意图如下图所示:
(2) 在PaaS平台中使用etcd服务发现
PaaS平台中的应用一般都有多个实例,通过域名不仅可以透明的对多个实例进行访问,而且还可以做到负载均衡。但是应用的某个实例随时都有可能故障重启,这时就需要动态的配置域名解析(路由)信息,通过etcd的服务发现功能就可以轻松解决这个动态配置的问题,实现多实例与实例故障重启透明化目的,示意图如下图所示:
etcd的发布订阅消息示意图如下图所示:
在分布式系统中,消息发布与订阅最适合使用用在组件之间通信。使用etcd发布订阅功能可以实现一个配置共享中心,数据提供者在配置中心发布消息,消息消费者订阅他们关心的主题,一旦主题有新消息发布,就会实时通知订阅者,通过这种方式可以做到分布式系统配置的集中式管理与动态更新。
etcd发布订阅最典型应用在kubernetes上,其他场景应用:
etcd的负载均衡示意图如下图所示:
etcd本身分布式架构存储的信息访问支持负载均衡,etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择。 etcd可以监控一个集群中多个节点的状态,利用etcd维护一个负载均衡节点表,当有一个请求发过来后,可以轮询式的把请求转发给存活着的节点。
分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
这里说到的分布式通知与协调,与消息发布和订阅有些相似。都用到了etcd中Watche机制,通过注册与异步通知机制,实现分布式环境下不同系统之间 的通知与协调,从而对数据变更做到实时处理。实现方式:不同系统都在etcd上对同一个目录进行注册,同时设置Watcher观测该目录的变化(如果对子目录的变化也有需要,可以设置递归模式),当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。
使用etcd完成分布式协同工作原理如下图所示:
因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁,锁有两种使用方式:
从etcd获取的分布式锁如下图所示:
分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,创建一个先进先出的队列,保证顺序。另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点,condition可以表示信息如下:
使用etcd来实现集群的实时性的监控,可以第一时间检测到各节点的健康状态,以完成集群的监控要求。etcd本身就有自带检点健康监控功能,实现起来也比较简单。
使用Watcher机制,当某个节点消失或有变动时,Watcher会第一时间发现并告知用户。
节点可以设置TTL key,比如每隔30s发送一次心跳使代表该机器存活的节点继续存在,否则节点消失。