服务发现之 Etcd VS Consul

在分布式微服务架构中,一个应用可能由一组职责单一化的服务组成。这时候就需要一个注册服务的机制,注册某个服务或者某个节点是可用的,还需要一个发现服务的机制来找到哪些服务或者哪些节点还在提供服务。

在实际应用中,通常还都需要一个配置文件告诉我们一些配置信息,比如数据连接的地址,redis 的地址等等。但很多时候,我们想要动态地在不修改代码的情况下得到这些信息,并且能很好地管理它们。

有了这些需求,于是发展出了一系列的开源框架,比如 ZooKeeper, Etcd, Consul 等等。

这些框架一般会提供这样的服务:

服务注册

主机名,端口号,版本号,或者一些环境信息。

服务发现

可以让客户端拿到服务端地址。

一个分布式的通用 k/v 存储系统

基于 Paxos 算法或者 Raft 算法

领导者选举 (Leader Election)

其它一些例子:

分布式锁 (Distributed locking)

原子广播 (Atomic broadcast)

序列号 (Sequence numbers)

我们都知道 CAP 是 Eric Brewer 提出的分布式系统三要素:

强一致性 (Consistency)

可用性 (Availability)

分区容忍性 (Partition Tolerance)

几乎所有的服务发现和注册方案都是 CP 系统,也就是说满足了在同一个数据有多个副本的情况下,对于数据的更新操作达到最终的效果和操作一份数据是一样的,但是在出现网络分区(分区间的节点无法进行网络通信)这样的故障的时候,在节点之间恢复通信并且同步数据之前,一些节点将会无法提供服务(一些系统在节点丢失的情况下支持 stale reads )。

ZooKeeper 作为这类框架中历史最悠久的之一,是由 Java 编写。Java 在许多方面非常伟大,然而对于这种类型的工作还是显得有些沉重,也正因为其复杂性和对新鲜事物的向往,我们第一时间就放弃了选择它。

本文将从协议和应用层来比较 Etcd 和 Consul,并最终给出了笔者的偏好。

Etcd

Etcd 是一个使用 go 语言写的分布式 k/v 存储系统。考虑到它是 coreos 公司的项目,以及在 GitHub 上的 star 数量 (9000+),Google 著名的容器管理工具 Kuberbetes 就是基于 Etcd 的。我们最先考虑的就是它。

在介绍 Etcd 之前,我们需要了解一些基本的概念。我们知道在单台服务器单个进程中维护一个状态是很容易的,读写的时候不会产生冲突。即便在多进程或者多线程环境中,使用锁机制或者协程(coroutine)也可以让读写有序地进行。但是在分布式系统中,情况就会复杂很多,服务器可能崩溃,节点间的机器可能网络不通等等。所以一致性协议就是用来在一个集群里的多台机器中维护一个一致的状态。

很多的分布式系统都会采用 Paxos 协议,但是 Paxos 协议难以理解,并且在实际实现中差别比较大。所以 Etcd 选择了 Raft 作为它的一致性协议。Raft 是 Diego Ongaro 和 John Ousterhout 在 ‘In Search of an Understandable Consensus Algorithm’ 中提出的。它在牺牲很少可用性,达到相似功能的情况下,对 Paxos 做了很大的优化,并且比 Paxos 简单易懂很多。

它主要集中在解决两个问题:

领导者选举(Leader Election)

Raft 先通过领导选举选出一个 Leader,后续的一致性维护都由 Leader 来完成,这就简化了一致性的问题。Raft 会保证一个时间下只会有一个 Leader,并且在超过一半节点投票的情况下才会被选为 Leader。当 Leader 挂掉的时候,新的 Leader 将会被选出来。

日志复制 (Log Replication)

为了维护状态,系统会记录下来所有的操作命令日志。Leader 在收到客户端操作命令后,会追加到日志的尾部。然后 Leader 会向集群里所有其它节点发送AppendEntriesRPC请求,每个节点都通过两阶段提交来复制命令,这保证了大部分的节点都能完成。

在实际的应用中,一般 Etcd 集群以 5 个或者 7 个为宜,可以忍受 2 个或者 3 个节点挂掉,为什么不是越多越好呢?是出于性能的考虑,因为节点多了以后,日志的复制会导致 CPU 和网络都出现瓶颈。

Etcd 的 API 比较简单,可以对一个目录或者一个 key 进行 GET,PUT,DELETE 操作,是基于 HTTP 的。Etcd 提供 watch 某个目录或者某个 key 的功能,客户端和 Etcd 集群之间保持着长连接 (long polling)。基于这个长连接,一旦数据发生改变,客户端马上就会收到通知,并且返回的结果是改变后的值和改变前的值,这一点在实际应用中会很有用(这也是后面的 Consul 的槽点之一)。

Etcd 的 watch 和在一般情况下不会漏掉任何的变更。因为 Etcd 不仅存储了当前的键值对,还存储了最近的变更记录,所以如果一个落后于当前状态的 watch 还是可以通过遍历历史变更记录来获取到所有的更新。Etcd 还支持 CompareAndSwap 这个原子操作,首先对一个 key 进行值比较,只有结果一致才会进行下一步的赋值操作。利用这个特性就像利用 x86 的 CAS 实现锁一样可以实现分布式锁。

在 Etcd 中有个 proxy 的概念,它其实是个转发服务器,启动的时候需要指定集群的地址,然后就可以转发客户端的请求到集群,它本身不存储数据。一般来说,在每个服务节点都会启动一个 proxy,所以这个 proxy 也是一个本地 proxy,这样服务节点就不需要知道 Etcd 集群的具体地址,只需要请求本地 proxy。之前提到过一个 k/v 系统还应该支持 leader election,Etcd 可以通过 TTL (time to live) key 来实现。

Consul

Consul 和 Etcd 一样也有两种节点,一种叫 client (和 Etcd 的 proxy 一样) 只负责转发请求,另一种是 server,是真正存储和处理事务的节点。

Consul 使用基于 Serf 实现的 gossip 协议来管理从属关系,失败检测,事件广播等等。gossip 协议是一个神奇的一致性协议,之所以取名叫 gossip 是因为这个协议是模拟人类中传播谣言的行为而来。要传播谣言就要有种子节点,种子节点每秒都会随机向其它节点发送自己所拥有的节点列表,以及需要传播的消息。任何新加入的节点,就在这种传播方式下很快地被全网所知道。这个协议的神奇就在于它从设计开始就没想要信息一定要传递给所有的节点,但是随着时间的增长,在最终的某一时刻,全网会得到相同的信息。当然这个时刻可能仅仅存在于理论,永远不可达。

Consul 使用了两个不同的 gossip pool,分别叫做 LAN 和 WAN,这是因为 Consul 原生支持多数据中心。在一个数据中心内部,LAN gossip pool 包含了这个数据中心所有的节点,包括 proxy 和 servers。WAN pool 是全局唯一的,所有数据中心的 servers 都在这个 pool 中。这两个 pool 的区别就是 LAN 处理的是数据中心内部的失败检测,事件广播等等,而 WAN 关心的是跨数据中心的。除了 gossip 协议之外,Consul 还使用了 Raft 协议来进行 leader election,选出 leader 之后复制日志的过程和 Etcd 基本一致。

回到应用层面上来说,Consul 更像是一个 full stack 的解决方案,它不仅提供了一致性 k/v 存储,还封装了服务发现,健康检查,内置了 DNS server 等等。这看上去非常美好,简直可以开箱即用。于是,我们初步选定了 Consul 作为我们的服务发现和动态配置的框架。但现实往往是冰冷的,在深入研究它的 API 之后发现了比较多的坑,可能设计者有他自己的考虑。

在使用获取所有 services 的 API 的时候返回的只是所有服务的名字和 tag,如果想要每个服务的具体信息,你还需要挨个去请求。这在客户端就会十分不方便,因为在笔者看来,获取所有服务的列表以及具体信息应该是一个很常见的需求,并且请求的次数越少越好。

如果 watch 服务是否有变化,当值发生改变的时候,返回的结果居然是相当于重新读取所有 services,没有能够体现出服务信息的变化,这会导致客户端很难进行更新操作。

健康检查是 Consul 的内置功能,在注册一个服务的时候可以加上自定义的健康检查,这听上去很不错。但是如果你忘记给你某个服务加上健康检查,那它在各种 API 的返回结果中变得难以预料。

结语

在折腾了数天之后,最终我们决定回归 Etcd,事实证明这个决定是正确的。我们基于 Etcd 实现了稳定的服务发现和动态配置功能,当然还有一些比较细节的问题没有在文中阐述,欢迎一起探讨。

本文作者:张陈丞,点融 social 团队高级工程师,对很多技术领域都有兴趣,目前专注于分布式系统和机器学习。

推荐阅读更多精彩内容