前一段时间的项目里用到了 Etcd, 所以研究了一下它的源码以及实现。网上关于 Etcd 的使用介绍的文章不少,但分析具体架构实现的文章不多,同时 Etcd v3 的文档也非常稀缺。本文通过分析 Etcd 的架构与实现,了解其优缺点以及瓶颈点,一方面可以学习分布式系统的架构,另外一方面也可以保证在业务中正确使用 Etcd,知其然同时知其所以然,避免误用。最后介绍 Etcd 周边的工具和一些使用注意事项。
阅读对象:分布式系统爱好者,正在或者打算在项目中使用 Etcd 的开发人员。
Etcd 按照官方介绍
Etcd is a distributed, consistent key-value store for shared configuration and service discovery
是一个分布式的,一致的 key-value 存储,主要用途是共享配置和服务发现。Etcd 已经在很多分布式系统中得到广泛的使用,本文的架构与实现部分主要解答以下问题:
所有的分布式系统,都面临的一个问题是多个节点之间的数据共享问题,这个和团队协作的道理是一样的,成员可以分头干活,但总是需要共享一些必须的信息,比如谁是 leader, 都有哪些成员,依赖任务之间的顺序协调等。所以分布式系统要么自己实现一个可靠的共享存储来同步信息(比如 Elasticsearch ),要么依赖一个可靠的共享存储服务,而 Etcd 就是这样一个服务。
Etcd 主要提供以下能力,已经熟悉 Etcd 的读者可以略过本段。
更详细的使用场景不在这里描述,有兴趣的可以参看文末 infoq 的一篇文章。
说到这个就不得不说起 raft 协议。但这篇文章不是专门分析 raft 的,篇幅所限,不能详细分析,有兴趣的建议看文末原始论文地址以及 raft 协议的一个动画。便于看后面的文章,我这里简单做个总结:
Etcd 实现 raft 的时候,充分利用了 go 语言 CSP 并发模型和 chan 的魔法,想更进行一步了解的可以去看源码,这里只简单分析下它的 wal 日志。
wal 日志是二进制的,解析出来后是以上数据结构 LogEntry。其中第一个字段 type,只有两种,一种是 0 表示 Normal,1 表示 ConfChange(ConfChange 表示 Etcd 本身的配置变更同步,比如有新的节点加入等)。第二个字段是 term,每个 term 代表一个主节点的任期,每次主节点变更 term 就会变化。第三个字段是 index,这个序号是严格有序递增的,代表变更序号。第四个字段是二进制的 data,将 raft request 对象的 pb 结构整个保存下。Etcd 源码下有个 tools/etcd-dump-logs,可以将 wal 日志 dump 成文本查看,可以协助分析 raft 协议。
raft 协议本身不关心应用数据,也就是 data 中的部分,一致性都通过同步 wal 日志来实现,每个节点将从主节点收到的 data apply 到本地的存储,raft 只关心日志的同步状态,如果本地存储实现的有 bug,比如没有正确的将 data apply 到本地,也可能会导致数据不一致。
Etcd v2 和 v3 本质上是共享同一套 raft 协议代码的两个独立的应用,接口不一样,存储不一样,数据互相隔离。也就是说如果从 Etcd v2 升级到 Etcd v3,原来 v2 的数据还是只能用 v2 的接口访问,v3 的接口创建的数据也只能访问通过 v3 的接口访问。所以我们按照 v2 和 v3 分别分析。
Etcd v2 是个纯内存的实现,并未实时将数据写入到磁盘,持久化机制很简单,就是将 store 整合序列化成 json 写入文件。数据在内存中是一个简单的树结构。比如以下数据存储到 Etcd 中的结构就如图所示。
- /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。
这里有几个影响使用的细节问题:
从而可以看出,Etcd v2 的一些限制:
Etcd v3 将 watch 和 store 拆开实现,我们先分析下 store 的实现。
Etcd v3 store 分为两部分,一部分是内存中的索引,kvindex,是基于 google 开源的一个 golang 的 btree 实现的,另外一部分是后端存储。按照它的设计,backend 可以对接多种存储,当前使用的 boltdb。boltdb 是一个单机的支持事务的 kv 存储,Etcd 的事务是基于 boltdb 的事务实现的。Etcd 在 boltdb 中存储的 key 是 reversion,value 是 Etcd 自己的 key-value 组合,也就是说 Etcd 会在 boltdb 中把每个版本都保存下,从而实现了多版本机制。
举个例子: 用 etcdctl 通过批量接口写入两条记录:
- etcdctl txn << <'
- put key1 "v1"
- put key2 "v2"
- '
再通过批量接口更新这两条记录:
- etcdctl txn << <'
- put key1 "v12"
- put key2 "v22"
- '
boltdb 中其实有了 4 条数据:
- 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"
reversion 主要由两部分组成,第一部分 main rev,每次事务进行加一,第二部分 sub rev,同一个事务中的每次操作加一。如上示例,第一次操作的 main rev 是 3,第二次是 4。当然这种机制大家想到的第一个问题就是空间问题,所以 Etcd 提供了命令和设置选项来控制 compact,同时支持 put 操作的参数来精确控制某个 key 的历史版本数。
了解了 Etcd 的磁盘存储,可以看出如果要从 boltdb 中查询数据,必须通过 reversion,但客户端都是通过 key 来查询 value,所以 Etcd 的内存 kvindex 保存的就是 key 和 reversion 之前的映射关系,用来加速查询。
然后我们再分析下 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,方便设置统一的过期时间,以及实现批量续约。
相比 Etcd v2, Etcd v3 的一些主要变化:
这三个产品是经常被人拿来做选型比较的。 Etcd 和 Zookeeper 提供的能力非常相似,都是通用的一致性元信息存储,都提供 watch 机制用于变更通知和分发,也都被分布式系统用来作为共享信息存储,在软件生态中所处的位置也几乎是一样的,可以互相替代的。二者除了实现细节,语言,一致性协议上的区别,最大的区别在周边生态圈。Zookeeper 是 apache 下的,用 java 写的,提供 rpc 接口,最早从 hadoop 项目中孵化出来,在分布式系统中得到广泛使用(hadoop, solr, kafka, mesos 等)。Etcd 是 coreos 公司旗下的开源产品,比较新,以其简单好用的 rest 接口以及活跃的社区俘获了一批用户,在新的一些集群中得到使用(比如 kubernetes)。虽然 v3 为了性能也改成二进制 rpc 接口了,但其易用性上比 Zookeeper 还是好一些。 而 Consul 的目标则更为具体一些,Etcd 和 Zookeeper 提供的是分布式一致性存储能力,具体的业务场景需要用户自己实现,比如服务发现,比如配置变更。而 Consul 则以服务发现和配置变更为主要目标,同时附带了 kv 存储。 在软件生态中,越抽象的组件适用范围越广,但同时对具体业务场景需求的满足上肯定有不足之处。
自动上次 Elasticsearch 的文章之后,给自己安排了一个作业,每次分析源码后需要提出几个发散思维的想法,开个脑洞。
Etcd 在 Zookeeper 已经奠定江湖地位的情况下,硬是重新造了一个轮子,并且在生态圈中取得了一席之地。一方面可以看出是社区的形态在变化,沟通机制和对用户反馈的响应越来越重要,另外一方面也可以看出一个项目的易用的重要性有时候甚至高于稳定性和功能。新的算法,新的语言都会给重新制造轮子带来了机会。
答:v2 的大多数功能,用 v3 都能实现,比如用 prefix 模拟原来的目录结构,用 txn 模拟 CAS,一般不会有什么问题。但因为 v2 和 v3 的数据是互相隔离的,所以迁移起来略麻烦。建议先在业务中封装一层,将 etcd v2,v3 的差异封装起来,然后通过开关切换。
答:metad 的 watch 实现的比较简单,因为 metad 的 watch 返回的不是变更事件,而是最新的结果。所以 metad 只维护了一个全局版本号,只要发现客户端 watch 的版本小于等于全局版本号,就直接返回最新结果。
etcd 和 zk 二者大多数情况下可以互相替代,都是通用的分布式一致性 kv 存储。二者之间选择建议选择自己的开发栈比较接近并且团队成员比较熟悉的,比如一种是按语言选择,go 语言的项目用 etcd,java 的用 zk,出问题要看源码也容易些。如果是新项目,纠结于二者,那可以分装一层 lib,类似于 docker/libkv,同时支持两种,有需要可以切换。
答:etcd 和 zk 的选型前面讲到了,二者的定位都是通用的一致性 kv 存储,而 eureka 和 consul 的定位则是专做服务注册和发现。前二者的优势当然是通用性,应用广泛,部署运维的时候容易和已有的服务一起共用,而同时缺点也是太通用了,每个应用的服务注册都有自己的一套元数据格式,互相整合起来就比较麻烦了,比如想做个通用的 api gateway 就会遇到元数据格式兼容问题。这也成为后二者的优势。同时因为后二者的目标比较具体,所以可以做一些更高级的功能,比如 consul 的 DNS 支持,consul-template 工具,eureka 的事件订阅过滤机制。Eureka 本身的实现是一个 AP 系统,也就是说牺牲了一致性,它认为在服务发现和配置中心这个场景下,可用性和分区容错比一致性更重要。 我个人其实更期待后二者的这种专门的解决方案,要是能形成服务注册标准,那以后应用之间互相交互就容易了。但也有个可能是这种标准由集群调度系统来形成事实标准。
后二者我了解的也不深入,感觉可以另起一篇文章了。
答:这个坑的概念比较太广泛了,更详细的可以翻 bug 列表。但使用中的大多数坑一般有几种:
想要少踩坑,一个办法就是我文中提到的,研究原理知其然同时知其所以然,另外一个问题就是多试验,出了问题有预案。
我们实现了基于 Arm 的分布式互联的硬件集群(方法参考的是 https://edcashin.wordpress.com/2013/12/29/trying-etcd-on-android-mac-and-raspberry-pi/comment-page-1/ 将 etcd 跑在 Arm 开发板上),将 Etcd 当作一个分布式的数据库使用(但是 Etcd 本身运行在这些硬件之上),然后参考 go-rpiohttps://github.com/stianeikeland/go-rpio 实现基于 etcd 的 key-value 同步硬件的信息,控制某些 GPIO。
问题 1:目前已知 Etcd 可以为别的服务提供服务发现,在这个场景下假设已经存在 5 个运行 Etcd 节点的硬件,当一个新的 Etcd 硬件节点被安装时,Etcd 能否为自己提供服务发现服务,实现 Etcd 节点的自动发现与加入?
问题 2:随着硬件安装规模的增加,Etcd 的极限是多少,raft 是否会因为节点的变多,心跳包的往返而导致同步一次的等待时间变长?
问题 3:当规模足够大,发生网络分区时,是否分区较小的一批硬件之间的数据是无法完成同步的?
答:这个案例挺有意思,我一个一个回答。
答:这个要看跨机房的场景。如果是完全无关联需要公网连接的两个机房,服务之间一般也不需要共享数据吧?部署两套互不相干的 etcd,各用各的比较合适。但如果是类似于 aws 的可用区的概念,两个机房内网互通,搭建两套集群为了避免机房故障,可以随时切换。这个 etcd 当前没有太好的解决办法,建议的办法是跨可用区部署一个 etcd cluster,调整心跳以及选举超时时间,这个办法如果有 3 个可用区机房,每个机房 3 个节点,挂任何一个机房都不影响整个集群,但两个机房就比较尴尬。还有个办法是两个集群之间同步,这个 etcdv3 提供了一个 mirror 的工具,但还是不太完善,不过感觉用 etcd 的 watch 机制做一个同步工具也不难。这个机制 consul 倒是提供了,多数据中心的集群数据同步,互相不影响可用性。
扫码关注 @谢工 的新项目 gitchat。本文最初在 gitchat 上分享,并在付费交流群中进行交流。本文原计划 10 月份发布,结果一拖再拖,在 gitchat 上发布题目后,订阅达标,才在 gitchat 编辑的『监督』下完成。 感谢 gitchat 提供这样的知识分享平台。
来源: http://www.tuicool.com/articles/ZBRNNri