服务发现Zookeeper vs etcd vs Consul

【编者的话】本文对比了Zookeeper、etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考。

如果使用预定义的端口,服务越多,发生冲突的可能性越大,毕竟,不可能有两个服务监听同一个端口。管理一个拥挤的比方说被几百个服务所使用的所有端口的列表,本身就是一个挑战,添加到该列表后,这些服务需要的数据库和数量会日益增多。因此我们应该部署无需指定端口的服务,并且让Docker为我们分配一个随机的端口。唯一的问题是我们需要发现端口号,并且让别人知道。

single-node-docker.png

当我们开始在一个分布式系统上部署服务到其中一台服务器上时,事情会变得更加复杂,我们可以选择预先定义哪台服务器运行哪个服务的方式,但这会导致很多问题。我们应该尽我们所能尽量利用服务器资源,但是如果预先定义每个服务的部署位置,那么要实现尽量利用服务器资源是几乎不可能的。另一个问题是服务的自动伸缩将会非常困难,更不用说自动恢复了,比方说服务器故障。另一方面,如果我们将服务部署到某台只有最少数量的容器在运行的服务器上,我们需要添加IP地址到数据列表中,这些数据需要可以被发现并存储在某处。

multi-node-docker.png

当我们需要存储和发现一些与正在工作的服务相关的信息时,还有很多其他的例子。

为了能够定位服务,我们需要至少接下来的两个有用的步骤。

  • 服务注册——该步骤存储的信息至少包括正在运行的服务的主机和端口信息
  • 服务发现——该步骤允许其他用户可以发现在服务注册阶段存储的信息。

除了上述的步骤,我们还需要考虑其他方面。如果一个服务停止工作并部署/注册了一个新的服务实例,那么该服务是否应该注销呢?当有相同服务的多个副本时咋办?我们该如何做负载均衡呢?如果一个服务器宕机了咋办?所有这些问题都与注册和发现阶段紧密关联。现在,我们限定只在服务发现的范围里(常见的名字,围绕上述步骤)以及用于服务发现任务的工具,它们中的大多数采用了高可用的分布式键/值存储。

服务发现工具

服务发现工具的主要目标是用来服务查找和相互对话,为此该工具需要知道每个服务,这不是一个新概念,在Docker之前就已经存在很多类似的工具了,然而,容器带给了这些工具一个全新水平的需求。

服务发现背后的基本思想是对于服务的每一个新实例(或应用程序),能够识别当前环境和存储相关信息。存储的注册表信息本身通常采用键/值对的格式,由于服务发现经常用于分布式系统,所以要求这些信息可伸缩、支持容错和分布式集群中的所有节点。这种存储的主要用途是给所有感兴趣的各方提供最起码诸如服务IP地址和端口这样的信息,用于它们之间的相互通讯,这些数据还经常扩展到其它类型的信息服务发现工具倾向于提供某种形式的API,用于服务自身的注册以及服务信息的查找。

比方说我们有两个服务,一个是提供方,另一个是第一个服务的消费者,一旦部署了服务提供方,就需要在服务发现注册表中存储其信息。接着,当消费者试图访问服务提供者时,它首先查询服务注册表,使用获取到的IP地址和端口来调用服务提供者。为了与注册表中的服务提供方的具体实现解耦,我们常常采用某种代理服务。这样消费者总是向固定IP地址的代理请求信息,代理再依次使用服务发现来查找服务提供方信息并重定向请求,在本文中我们稍后通过反向代理来实现。现在重要的是要理解基于三种角色(服务消费者、提供者和代理)的服务发现流程。

服务发现工具要查找的是数据,至少我们应该能够找出服务在哪里?服务是否健康和可用?配置是什么样的?既然我们正在多台服务器上构建一个分布式系统,那么该工具需要足够健壮,保证其中一个节点的宕机不会危及数据,同时,每个节点应该有完全相同的数据副本,进一步地,我们希望能够以任何顺序启动服务、杀死服务或者替换服务的新版本,我们还应该能够重新配置服务并且查看到数据相应的变化。

让我们看一下一些常用的选项来完成我们上面设定的目标。

手动配置

大多数服务仍然是需要手动管理的,我们预先决定在何处部署服务、如何配置和希望不管什么原因,服务都将继续正常工作,直到天荒地老。这样的目标不是可以轻易达到的。部署第二个服务实例意味着我们需要启动全程的手动处理,我们需要引入一台新的服务器,或者找出哪一台服务器资源利用率较低,然后创建一个新的配置集并启动服务。情况或许会变得越来越复杂,比方说,硬件故障导致的手动管理下的反应时间变得很慢。可见性是另外一个痛点,我们知道什么是静态配置,毕竟是我们预先准备好的,然而,大多数的服务有很多动态生成的信息,这些信息不是轻易可见的,也没有一个单独的地方供我们在需要时参考这些数据。

反应时间会不可避免的变慢,鉴于存在许多需要手动处理的移动组件,故障恢复和监控也会变得非常难以管理。

尽管在过去或者当服务/服务器数量很少的时候有借口不做这项工作,随着服务发现工具的出现,这个借口已经不存在了。

Zookeeper

Zookeeper是这种类型的项目中历史最悠久的之一,它起源于Hadoop,帮助在Hadoop集群中维护各种组件。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。其数据存储的格式类似于文件系统,如果运行在一个服务器集群中,Zookeper将跨所有节点共享配置状态,每个集群选举一个领袖,客户端可以连接到任何一台服务器获取数据。

Zookeeper的主要优势是其成熟、健壮以及丰富的特性,然而,它也有自己的缺点,其中采用Java开发以及复杂性是罪魁祸首。尽管Java在许多方面非常伟大,然后对于这种类型的工作还是太沉重了,Zookeeper使用Java以及相当数量的依赖使其对于资源竞争非常饥渴。因为上述的这些问题,Zookeeper变得非常复杂,维护它需要比我们期望从这种类型的应用程序中获得的收益更多的知识。这部分地是由于丰富的特性反而将其从优势转变为累赘。应用程序的特性功能越多,就会有越大的可能性不需要这些特性,因此,我们最终将会为这些不需要的特性付出复杂度方面的代价。

Zookeeper为其他项目相当大的改进铺平了道路,“大数据玩家“在使用它,因为没有更好的选择。今天,Zookeeper已经老态龙钟了,我们有了更好的选择。

etcd

etcd是一个采用HTTP协议的健/值对存储系统,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。其很容易部署、安装和使用,提供了可靠的数据持久化特性。它是安全的并且文档也十分齐全。

etcd比Zookeeper是比更好的选择,因为它很简单,然而,它需要搭配一些第三方工具才可以提供服务发现功能。

etcd.png

现在,我们有一个地方来存储服务相关信息,我们还需要一个工具可以自动发送信息给etcd。但在这之后,为什么我们还需要手动把数据发送给etcd呢?即使我们希望手动将信息发送给etcd,我们通常情况下也不会知道是什么信息。记住这一点,服务可能会被部署到一台运行最少数量容器的服务器上,并且随机分配一个端口。理想情况下,这个工具应该监视所有节点上的Docker容器,并且每当有新容器运行或者现有的一个容器停止的时候更新etcd,其中的一个可以帮助我们达成目标的工具就是Registrator。

Registrator

Registrator通过检查容器在线或者停止运行状态自动注册和去注册服务,它目前支持etcd、Consul和SkyDNS 2。

Registrator与etcd是一个简单但是功能强大的组合,可以运行很多先进的技术。每当我们打开一个容器,所有数据将被存储在etcd并传播到集群中的所有节点。我们将决定什么信息是我们的。

etcd-registrator.png

上述的拼图游戏还缺少一块,我们需要一种方法来创建配置文件,与数据都存储在etcd,通过运行一些命令来创建这些配置文件。

Confd

Confd是一个轻量级的配置管理工具,常见的用法是通过使用存储在etcd、consul和其他一些数据登记处的数据保持配置文件的最新状态,它也可以用来在配置文件改变时重新加载应用程序。换句话说,我们可以用存储在etcd(或者其他注册中心)的信息来重新配置所有服务。

etcd-registrator-confd.png

对于etcd、Registrator和Confd组合的最后的思考

当etcd、Registrator和Confd结合时,可以获得一个简单而强大的方法来自动化操作我们所有的服务发现和需要的配置。这个组合还展示了“小”工具正确组合的有效性,这三个小东西可以如我们所愿正好完成我们需要达到的目标,若范围稍微小一些,我们将无法完成我们面前的目标,而另一方面如果他们设计时考虑到更大的范围,我们将引入不必要的复杂性和服务器资源开销。

在我们做出最后的判决之前,让我们看看另一个有相同目标的工具组合,毕竟,我们不应该满足于一些没有可替代方案的选择。

Consul

Consul是强一致性的数据存储,使用gossip形成动态集群。它提供分级键/值存储方式,不仅可以存储数据,而且可以用于注册器件事各种任务,从发送数据改变通知到运行健康检查和自定义命令,具体如何取决于它们的输出。

与Zookeeper和etcd不一样,Consul内嵌实现了服务发现系统,所以这样就不需要构建自己的系统或使用第三方系统。这一发现系统除了上述提到的特性之外,还包括节点健康检查和运行在其上的服务。

Zookeeper和etcd只提供原始的键/值队存储,要求应用程序开发人员构建他们自己的系统提供服务发现功能。而Consul提供了一个内置的服务发现的框架。客户只需要注册服务并通过DNS或HTTP接口执行服务发现。其他两个工具需要一个亲手制作的解决方案或借助于第三方工具。

Consul为多种数据中心提供了开箱即用的原生支持,其中的gossip系统不仅可以工作在同一集群内部的各个节点,而且还可以跨数据中心工作。

consul1.png

Consul还有另一个不错的区别于其他工具的功能,它不仅可以用来发现已部署的服务以及其驻留的节点信息,还通过HTTP请求、TTLs(time-to-live)和自定义命令提供了易于扩展的健康检查特性。

Registrator

Registrator有两个Consul协议,其中consulkv协议产生类似于etcd协议的结果。

除了通常的IP和端口存储在etcd或consulkv协议中之外,Registrator consul协议存储了更多的信息,我们可以得到服务运行节点的信息,以及服务ID和名称。我们也可以借助于一些额外的环境变量按照一定的标记存储额外的信息。

consul-registrator1.png

Consul-template

confd可以像和etce搭配一样用于Consul,不过Consul有自己的模板服务,其更适配Consul。

通过从Consul获得的信息,Consul-template是一个非常方便的创建文件的途径,还有一个额外的好处就是在文件更新后可以运行任意命令,正如confd,Consul-template也可以使用Go模板格式。

consul-registrator-consul-template1.png

Consul健康检查、Web界面和数据中心

监控集群节点和服务的健康状态与测试和部署它们一样的重要。虽然我们应该向着拥有从来没有故障的稳定的环境努力,但我们也应该承认,随时会有意想不到的故障发生,时刻准备着采取相应的措施。例如我们可以监控内存使用情况,如果达到一定的阈值,那么迁移一些服务到集群中的另外一个节点,这将是在发生“灾难”前执行的一个预防措施。另一方面,并不是所有潜在的故障都可以被及时检测到并采取措施。单个服务可能会齿白,一个完整的节点也可能由于硬件故障而停止工作。在这种情况下我们应该准备尽快行动,例如一个节点替换为一个新的并迁移失败的服务。Consul有一个简单的、优雅的但功能强大的方式进行健康检查,当健康阀值达到一定数目时,帮助用户定义应该执行的操作。

如果用户Google搜索“etcd ui”或者“etec dashboard”时,用户可能看到只有几个可用的解决方案,可能会问为什么我们还没有介绍给用户,这个原因很简单,etcd只是键/值对存储,仅此而已。通过一个UI呈现数据没有太多的用处,因为我们可以很容易地通过etcdctl获得这些数据。这并不意味着etcd UI是无用的,但鉴于其有限的使用范围,它不会产生多大影响。

Consu不仅仅是一个简单的键/值对存储,正如我们已经看到的,除了存储简单的键/值对,它还有一个服务的概念以及所属的数据。它还可以执行健康检查,因此成为一个好的候选dashboard,在上面可以看到我们的节点的状态和运行的服务。最后,它支持了多数据中心的概念。所有这些特性的结合让我们从不同的角度看到引入dashboard的必要性。

通过Consul Web界面,用户可以查看所有的服务和节点、监控健康检查状态以及通过切换数据中心读取设置键/值对数据。

consul-nodes.png

对于Consul、Registrator、Template、健康检查和Web UI的最终思考

Consul以及上述我们一起探讨的工具在很多情况下提供了比etcd更好的解决方案。这是从内心深处为了服务架构和发现而设计的方案,简单而强大。它提供了一个完整的同时不失简洁的解决方案,在许多情况下,这是最佳的服务发现以及满足健康检查需求的工具。

结论

所有这些工具都是基于相似的原则和架构,它们在节点上运行,需要仲裁来运行,并且都是强一致性的,都提供某种形式的键/值对存储。

Zookeeper是其中最老态龙钟的一个,使用年限显示出了其复杂性、资源利用和尽力达成的目标,它是为了与我们评估的其他工具所处的不同时代而设计的(即使它不是老得太多)。

etcdRegistratorConfd是一个非常简单但非常强大的组合,可以解决大部分问题,如果不是全部满足服务发现需要的话。它还展示了我们可以通过组合非常简单和特定的工具来获得强大的服务发现能力,它们中的每一个都执行一个非常具体的任务,通过精心设计的API进行通讯,具备相对自治工作的能力,从架构和功能途径方面都是微服务方式。

Consul的不同之处在于无需第三方工具就可以原生支持多数据中心和健康检查,这并不意味着使用第三方工具不好。实际上,在这篇博客里我们通过选择那些表现更佳同时不会引入不必要的功能的的工具,尽力组合不同的工具。使用正确的工具可以获得最好的结果。如果工具引入了工作不需要的特性,那么工作效率反而会下降,另一方面,如果工具没有提供工作所需要的特性也是没有用的。Consul很好地权衡了权重,用尽量少的东西很好的达成了目标。

Consul使用gossip来传播集群信息的方式,使其比etcd更易于搭建,特别是对于大的数据中心。将存储数据作为服务的能力使其比etcd仅仅只有健/值对存储的特性更加完整、更有用(即使Consul也有该选项)。虽然我们可以在etcd中通过插入多个键来达成相同的目标,Consul的服务实现了一个更紧凑的结果,通常只需要一次查询就可以获得与服务相关的所有数据。除此之外,Registrator很好地实现了Consul的两个协议,使其合二为一,特别是添加Consul-template到了拼图中。Consul的Web UI更是锦上添花般地提供了服务和健康检查的可视化途径。

我不能说Consul是一个明确的赢家,而是与etcd相比其有一个轻微的优势。服务发现作为一个概念,以及作为工具都很新,我们可以期待在这一领域会有许多的变化。秉承开放的心态,大家可以对本文的建议持保留态度,尝试不同的工具然后做出自己的结论。

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

在实际应用中,通常还都需要一个配置文件告诉我们一些配置信息,比如数据连接的地址,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 会向集群里所有其它节点发送 AppendEntries RPC 请求,每个节点都通过两阶段提交来复制命令,这保证了大部分的节点都能完成。

在实际的应用中,一般 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 实现了稳定的服务发现和动态配置功能,当然还有一些比较细节的问题没有在文中阐述,欢迎一起探讨。

 

原文链接:Service Discovery: Zookeeper vs etcd vs Consul(翻译:胡震)

发表评论