# 第14章 服务发现
服务发现是一种容许计算机网络上的任意服务均可以找到它所需要通信的其他服务的机制。它是大多数分布式系统的核心组件。如果用户的基础设施是运行或者遵循的面向服务的[架构](https://en.wikipedia.org/wiki/Service-oriented_architecture)(Service Oriented Architecture,SOA),那么毫无疑问,用户需要部署某种服务发现方案。同时,服务发现也是软件应用设计方面的一个新兴概念,这和传统的SOA有许多的相似之处,而如今它有一个更广为人知的名字叫做[微服务](http://martinfowler.com/articles/microservices.html)。
服务发现的定义相当简单(如图14-1所示):**一个客户端如何才能找到它想要与之通信的IP地址和服务端口**?
![图像说明文字](https://box.kancloud.cn/71f374e9caaed0963206892e37fd7e75_700x485.jpeg)
图14-1
针对这个问题,不同的解决方案往往隐藏了很多用户不知道的微妙之处。为了能更好地理解服务发现的理念,我们首先至少需要为它约定一些基本的要求。一个服务发现方案必须遵循的最低要求如下。
- **服务注册/服务声明**:即服务在它所属的网络上声明自己存在的过程。其通常做法是在某种服务数据库里加上这行记录,该数据库通常也被称为**服务中心**或**服务目录**。服务中心里的每行条目必须至少包含服务的IP地址和端口信息,最好还包含服务的一些元数据,如使用的协议、具体的环境、版本等。
- **服务查找/服务发现**:即从网络上找出想要与之通信的服务的具体连接信息的过程。该过程具体可以归结为查询服务目录数据库然后找出给定服务的具体IP地址和端口信息。理想情况下,应该可以通过不同的维度如上述所提到的几个元数据,来查询服务目录里的数据。
服务注册的过程其实比我们上述所提到的情况要复杂许多。一般来说,有以下两种实现方式。
- 把服务注册的模块直接嵌入到应用服务的源代码里。
- 使用一个伙伴进程(或者说是协同进程)来帮忙处理注册的任务。
将服务注册嵌入到应用源代码中,这会对客户端的库有一定的要求。一般来说,一个特定编程语言可用的客户端的库也是有限的,因此为了实现这一点,用户可能会写很多额外的代码,这可能会给用户的代码库引入很多不必要的复杂度。在应用的源代码里嵌入服务发现的代码常常会导致产生所谓的**胖客户端**,这部分代码不易编写,并且常常会造成调试方面的困扰。这一方案会将用户绑定到一个特定的服务发现方案上,这可能会导致用户的应用和服务缺乏一定的可移植性。最后,如果用户想要在自己的基础设施上运行和集成一些第三方的服务,如`redis`,那么可能需要费些力气。但是,如果能够建立一套稳定的、积极维护的方案,用户可以从这里面得到很多好处,如客户端服务的负载均衡、连接的池化、自动的服务心跳检测和故障迁移等。
另一种被广泛采用的服务注册的方案是在应用服务一侧运行一个伙伴进程,然后用它来代表应用服务的注册。这一方案的好处也是显而易见的。它非常实用的一点在于不需要应用程序的作者为此编写任何额外的代码。通过把伙伴进程集成到init系统[\[1\]](part0020.xhtml#anchor141)里,可以确保(一定程度上来说)服务只有当它完全启动并且正常运行的时候方才注册。采用这种方案来注册服务同样也使它可以轻松地和第三方服务整合到一起。但是,它的缺点在于用户不得不为每个需要注册的服务运行一个单独的进程。此外,使用伙伴进程也会对实际的服务注册过程提出额外的要求:你怎么为自己的伙伴进程提供你想要注册的服务的配置呢?由于服务注册经常需要更新应用服务的一些元数据,因此了解应用的配置是非常有必要的。显然,伙伴进程这个方法会带来一些好处,但是它也同样会引入一些运维方面的挑战。
基础设施越复杂,我们要求放到服务目录数据库里的数据也会越多。一个更加全面的解决方案理应该可以提供更多的内容(与之前的两种方案相比),特别是在以下几个方面:
- 服务目录数据库的高可用性;
- 能够轻松将服务目录数据库扩展到多台主机,同时保证一定级别的数据一致性;
- 告知相关服务的可用性,以便在服务不可用时及时地将它从服务目录中删除;
- 告知一些特定服务目录条目的变更,如当一些服务的元数据变更时。
什么,使用Docker是不是这些事情都得做?好吧,实际上Docker会使服务发现的问题更加明显。特别是在开始将容器基础设施扩展到多台宿主机的时候这一点更为关键。一旦用户开始使用Docker来打包和运行应用,用户会发现自己在不停地查找运行在Docker容器里的特定服务正在监听的端口和IP地址。幸运的是,Docker使得查找服务连接信息变得很简单:用户需要做的只是通过Docker守护进程去查询一下远程API暴露给自己的信息。很多时候用户最终选择的是编写一些简单的shell脚本,然后将服务连接的具体信息以环境变量的形式通过Docker API传入新容器里。虽然对于本地环境而言这是一个不错的便捷方法,但是它不是一个易于扩展和可持续的方案。这不仅在于定制脚本的维护的艰难,更主要的是当应用或服务实例分布到多台宿主机或者多个数据中心时情况会变得更为糟糕。这个问题在云环境这样的服务更替频繁的场景下会暴露得更加明显。
在本章中,我们会尽力覆盖到服务发现这个话题的方方面面,并且从实际情况出发,探讨在运行Docker容器时可以采用的多种开源方案。那么,打起精神来,让我们开始吧。
DNS是支撑起[万维网](https://en.wikipedia.org/wiki/World_Wide_Web)的核心技术之一。从整体上来说,DNS是一个主要用于将人类可读的名称解析为机器可读的IP地址的一致性的分布式数据库。众所周知,它也被用于查找负责一个指定域的电子邮件服务器。用DNS做服务发现似乎是一个很自然的选择,因为它一个已经被充分检验过、被广泛部署并且非常易于理解的技术。对此,业界有丰富的可供选择的开源服务的实现,与此同时几乎能想到的任何一门编程语言都会提供相当不错的全套客户端库。DNS支持客户端缓存和基础的域名代理,这也使得它成为一个可扩展性很强的解决方案。
DNS最易于理解的部分莫过于早前所提到的通过DNS的[A记录](https://en.wikipedia.org/wiki/List_of_DNS_record_types#A)实现从域名解析到IP地址的过程。然而只使用A记录做服务发现是远远不够的,因为它们不会提供指定服务所监听端口的任何信息。此外,A记录无法提供关于运行在用户的基础设施里的服务的任何元数据信息。为了能够用DNS实现全功能的服务发现,我们至少需要用到以下两个额外的DNS记录:
- [**SRV**](https://en.wikipedia.org/wiki/SRV_record)——通常用来提供网络上的服务位置信息,如端口号等;
- [**TXT**](https://en.wikipedia.org/wiki/TXT_Record)——通常用于提供多个服务的元数据信息,如环境,版本等。
这些资源记录可以说是使用DNS作为服务发现的解决方案的最低要求。当然也可以根据国际惯例在公知端口号[\[2\]](part0020.xhtml#anchor142)下运行服务,这样一来便可以简单地只使用A记录来实现服务发现,然而,这样一来用户将无法完成一些自己本来需要的复杂查询操作。
注册和注销服务时需要在DNS服务器配置里添加或删除指定的DNS记录,并且常常需要重新加载一次服务以使得变动生效。用户必须实现一定程度的自动化,以保持DNS服务端的配置和线上自己基础设施里正在运行的应用服务的配置是一致的。DNS诞生于一个相对“静态”的互联网时代,与如今新的“云时代”下服务器和服务的更替都非常频繁的情况相比,当时它并不需要频繁地去更新DNS记录。由于DNS基础设施采取的是多层缓存机制,因此DNS记录的修改也需要一定的传播时间才能使之完全生效。用户常常试图采取把[TTL](https://en.wikipedia.org/wiki/Time_to_live)值降低到最小的方法来解决这个传播时间的问题,但这样做的话可能会产生太多不必要的网络流量,进而拖慢通信的速度,并且会因为服务的查找动作过于频繁而给DNS服务器增加额外的不必要的负载。
与经过实战检验的[bind](https://www.isc.org/downloads/bind/)服务相比,虽然业界一些著名的[DNS服务器实现](http://bind-dlz.sourceforge.net)在配置方面采取了更加灵活的方式,但是用户常常不愿意运行他们自己的DNS服务器,因为除了原有方案已经实现的大量的自动化工作以外,他们还需要花费相当大的运营和维护成本来维护它们。云服务商们还提供了很丰富的带有简单API控制的DNS服务,如AWS的[Route53](http://aws.amazon.com/cn/route53/),使用它们的话可能需要花费额外的精力来编写和维护代码,这样看来这些方案也不是非常可取的,并且它们始终没有解决服务繁杂的问题。的确,天上可不会掉馅饼。
随着Docker和**微服务**架构的兴起,DNS服务器也从现有模式中衍生出了新品种。这些DNS服务器解决了部分之前讨论过的问题,而且常常可以被用来提供简单的服务发现,并且它的服务对象还不只是运行在Docker容器里的服务。在下一节里,我们将介绍一些最广为人知的实现方案,并且探讨下应该如何在基础设施里使用它们。
最广为人知的“新生代”DNS服务实现之一是一款名为[SkyDNS](https://github.com/skynetservices/skydns)的软件,它可以被用在基础设施里实现服务发现。可以从源码编译它也可以将它打包成Docker容器部署。可以从[Docker Hub](https://hub.docker.com/r/skynetservices/skydns/)上找到它的Docker镜像。SkyDNS的最新版本采用的是`etcd`服务来存储它的DNS记录。我们会在本章的稍后部分详细介绍`etcd`。现在,让我们姑且认为`etcd`是一个分布式的键值对存储吧。
SkyDNS提供了一个远程的JSON API,它允许用户通过发送`HTTP POST`请求到指定的API端点的方式来动态地完成服务的注册。这样的话它会自动创建一个`SRV`DNS记录,正如我们之前所了解的,它可以被用于发现在网络上运行的服务的连接端口信息。自己也可以将`TTL`设置成任意值,如此一来,一旦`TTL`过期了它会自动注销该服务。SkyDNS也提供了[DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions)的功能。要想设置它的话自己需要做一些额外的配置。可以在GitHub上阅读该项目的[文档](https://github.com/skynetservices/skydns/blob/master/README.md)。
如果想把SkyDNS和Docker一起使用,就得写一个专门的SkyDNS客户端库来帮忙完成服务的注册。幸运的是,正如前面提到的,多亏SkyDNS本身提供远程API,这应该不会是一个大问题。一旦服务完成了注册,便可以用任一DNS库来查询它的具体信息。丰富的开源DNS库在这时体现出了巨大的价值。一个更简单的使用SkyDNS集成到Docker的方案便是使用Docker公司的[Michael Crosby](https://twitter.com/crosbymichael)开发的[skydock](https://github.com/crosbymichael/skydock),虽然目前来说它的可扩展性还不是很强。Skydock将会监听Docker API的事件,然后自动地为用户处理所有Docker容器的服务注册工作:会帮用户自动完成服务的注册和注销。Skydock的唯一问题便是它目前只能用在一台Docker宿主机上,然而,这一点可能在以后会得到改善。Skydock的确是一款值得密切关注的工具。如果想找到更多关于Skydock以及它是如何应用到Docker容器的内容,这里有一个非常棒的由Michael贡献的[YouTube视频](https://www.youtube.com/watch?v=Nw42q1ofrV0),他会带你领略所有的Skydock特性。
Docker DNS服务器领域的另外一员便是[`weave-dns`](https://github.com/weaveworks/weave/tree/master/weavedns)。`weave-dns`最大的好处在于用户不必花费太多精力就能使用它提供的一些开箱即用的功能。和Skydock一样,它会监听Docker API的事件然后通过自动添加和删除特定的DNS记录来完成容器服务的注册。与Skydock只能在一台Docker宿主机上使用不同,`weave-dns`允许跨多台Docker宿主机工作。然而,如果想要充分发挥它的优势,就必须在专有的`weave`覆盖网络上使用它,这对于一些用户而言可能是一个不太好的消息。`weave`网络是一个**软件定义网络**([SDN](https://en.wikipedia.org/wiki/Software-defined_networking)),它可以为用户提供一个简单而安全的跨多台Docker宿主机的覆盖网络,然而,如果用户已经使用了其他的SDN方案,那么`weave-dns`也许不是一个最佳选择。此外,目前`weave-dns`依赖于用户使用公知端口号来提供服务,而不是查询`SRV`或`TXT`记录。
随着Docker生态系统的快速扩张和成长,以后可能还会有更多可供使用的DNS服务器实现,有的可能作为独立的服务,有的可能是作为成熟的SDN产品的一部分提供。由于篇幅的限制,本书不可能覆盖到所有可供选择的方案,其他的备选方案就留待读者自己继续挖掘和探索。接下来我们将介绍一些已经成为分布式系统领域事实标准的服务发现方案。我们将从著名的Zookeeper项目开始。
Zookeeper是一个[Apache基金会](http://www.apache.org)的项目,它为分布式系统提供分布式的协同服务。它还提供了**简单的基本授权**功能,允许客户端建立更加复杂的协同功能。Zookeeper旨在成为一个分布式服务的核心协调组件或者是搭建强大的分布式应用时的一个基础组件。关于Zookeeper的介绍我们甚至可以写一整本书来讲解,而且实际上这样的[书](http://www.amazon.com/ZooKeeper-Distributed-Coordination-Flavio-Junqueira/dp/1449361307)已经有了。在这里我们将不再探讨那些基础的概念了,取而代之的是,我们将会把重点放在该如何在基础设施里采用Zookeeper作为用户的服务发现方案。
从整体上来说,Zookeeper提供了一个名为`znode`的分布式*内存*数据存储寄存器。它们以类似标准文件系统的组织方式存放在分级的命名空间里。znode这样的层次结构通常被称为“数据树”。`znode`通常有两种类型:
- **普通的**——可以被客户端显式创建和删除;
- **临时的**——与普通的一样,此外客户端还可以选择委托,一旦客户端会话终止便会自动把它们从集群里删除。
客户端可以在集群里的任意`znode`上设置**监听**,它可以让Zookeeper自动地通知客户端任何数据上的更改或删除。可以说,Zookeeper最大的优势之一在于它提供的API的简洁性:它只提供了7种[`znode`操作](http://zookeeper.apache.org/doc/r3.2.1/zookeeperOver.html#Simple+API)。借助于[Zookeeper原子广播](http://web.stanford.edu/class/cs347/reading/zab.pdf)(ZAB)一致性算法的实现,Zookeeper提供了健壮的数据一致性保证和分区容忍性。简单来说,ZAB定义了一个领导者和一群追随者(他们可以共同选举出领导者)。所有的写请求都会转发到领导者这边,随后领导者会将它们应用到系统中。读请求则可以被追随者们消费。Zookeeper只能在服务器的法定人数(大多数)都正常的情况下正常工作,因此用户必须保证Zookeeper集群的部署数量总是保持在3、5等这样的奇数单位。或者更官方地说,用户必须保证运行的集群有`2n+1`个节点(*n*是一个代表服务器数量的正整数)。这样规模的集群可以容忍*n*个节点的故障。前面所提到的属性对Zookeeper的可扩展性有一定的影响。增加新节点可以提高读的吞吐量,但是会降低写的吞吐能力。此外,当仲裁发生时,它必须等待远程站点选举领导者的投票结束,这样也就导致写的速度会有所下降,因此,如果想跨多个数据中心运行Zookeeper集群,用户应该事先考虑好这些问题的应对措施。
可以通过利用Zookeeper原生提供的临时`znode`特性来实现服务发现。服务在注册时会在集群里的一个在其启动时给定的命名空间下创建一个临时`znode`,然后将它在网络上的位置信息(IP地址和端口)填充进去。Zookeeper层级式的命名空间为同类服务的集合提供了一个简单的实现机制,当基础设施里运行了多个同种服务的实例时,这个实现机制相当有用。
服务注册必须要嵌入到相关服务的源代码中,或者用户也可以编写一个简单的伙伴服务,它将使用Zookeeper协议并处理服务本身的注册工作。事实上,无论选择哪种方式都无可避免地需要编写一些额外的代码。客户端可以通过检索特定的Zookeeper `znode`的命名空间里的信息来发现已注册的服务数据。和之前提到的一样,只要被注册的服务创建的`TCP`会话仍然是活动状态,那么临时`znode`便不会被删除。而一旦服务从Zookeeper断开连接,`znode`就会被删除并且该服务马上会被注销。客户端可以在他们想要被告知相关情况的[`znode`端设置监听](http://zookeeper.apache.org/doc/r3.2.1/zookeeperOver.html#Conditional+updates+and+watches)。当`znode`发生变化时监听会被**马上触发然后删除**。
Zookeeper是完全使用Java编写的。该项目也提供了一个完备的Java客户端库来完成和Zookeeper集群的交互。虽然也有通过其他[编程语言实现](https://cwiki.apache.org/confluence/display/ZOOKEEPER/ZKClientBindings)的客户端,但是并不是所有的客户端都会提供全面的功能特性的支持,而且他们的实现也各有差异,这有时候会使终端用户产生一些困扰。客户端必须处理服务发现和自动服务故障迁移两方面的负载平衡,以防出现客户端查找时一些发现的服务不再响应或它们的Zookeeper会话已经关闭的情况。如果用的是Java编程语言,那么这里有一个很棒的库,它在Zookeeper客户端库的基础上进行了一下包装,并且提供了很多额外的开箱即用的功能。它叫做[Curatorr](http://curator.apache.org)。同Zookeeper一样,它也是一个Apache基金会项目。关于如何使用Curator,可以查看Curator的[入门文档](http://tomaszdziurko.pl/2014/07/zookeeper-curator-and-microservices-load-balancing/)。
Zookeeper的确能够提供一个健壮的、经过实战检验的服务发现方案。然而,在用户的基础设施里运行Zookeeper集群会引入一定程度的复杂性,它要求用户具备一些基本的Zookeeper运维经验并且会带来一些额外的维护成本。由于Zookeeper提供了一个强一致性的保证,当网络出现阻断时位于非仲裁方的服务即使仍然正常工作也将无法完成注册或寻找已经注册的服务。就服务更替非常频繁的写负载较重的环境而言,Zookeeper也许不是一个最佳选择。使用Zookeeper来完成服务发现的最棘手的问题之一便是它依赖已注册服务创建的TCP会话的持久性来保证服务发现的可用性。而仅仅只存在TCP会话也不能保证服务一定是健康的。应用服务可以执行的任务非常多元化。只检查TCP会话是否存活的话很难验证这些任务的健康性。因此用户不能**只靠TCP连接的活跃度**来判断服务是否健康!很多用户低估了这一点并且因此引发了很多意想不到的事情。
如果把Zookeeper当做IT架构里的一个基础组件,它也许会让用户眼前一亮。虽然用户无法直接将其运用到Docker基础设施里,但是它可以通过其他构建在它之上的系统“悄然”贡献自己的一份力。经典案例莫过于[Apache Mesos](http://mesos.apache.org),用户可以使用它的一个插件然后借助Zookeeper来完成Docker宿主机上Mesos集群的容器之间的调度。如果想把Zookeeper作为一个独立的服务发现方案来使用,可能需要编写一个简单的伙伴客户端,它会和“Docker化”的应用服务一起运行并且处理服务自身的注册工作。然而,还有更简单的方法。用户可以使用一些基于Zookeeper之上实现的一些解决方案,如Smartstack,关于这块内容我们将在本章的稍后部分详细介绍。
接下来,我们将涉足的是分布式键值存储领域,相对于新人而言,它可以说是比Zookeeper更容易的、用作Docker基础设施里服务发现的方案。我们讨论的第一个主题即是这其中一款名为`etcd`的工具。
`etcd`是[CoreOS](https://coreos.com)团队使用[Go语言](https://golang.org)编写的一款分布式键值存储软件。它和Zookeeper有许多的相似之处。我将先介绍它的一些基本特性然后再讲述如何将它用于服务发现。
同Zookeeper类似,`etcd`将数据存放在层级的命名空间里。它定义了目录和键的概念(这并不是`etcd`独创的)。任何目录都可以包含多个键,实际上它们是用来查找存储在`etcd`中的数据的唯一标识符。存储在`etcd`中的数据可以是临时性的也可以是持久化的。`etcd`和Zookeeper主要不同之处在于针对临时数据的实现方式。在Zookeeper里,临时数据的生命周期等同于客户端创建的TCP会话的寿命,而`etcd`采取的是和DNS的做法有些相似的一个方案。任何一个存放在`etcd`里的键都会设置一个`TTL`(Time to Live,存活时间)值。该TTL值定义了对应键在被设置值以后多长时间会过期,过期的同时该键值即被永久删除。客户端可以在任意时刻刷新TTL从而延长存储数据的寿命。客户端还可以在任意键或者目录上设置监听,这样一来当这些数据发生变更时它们便能获取到相应的通知。`etcd`的监听机制是通过[HTTP长轮询](https://en.wikipedia.org/wiki/Push_technology#Long_polling)来实现的。
使用`etcd`的最大的好处之一在于它通过提供一个远程的JSON API抽象了底层的数据操作。这对于应用开发人员来说是一个天大的喜讯,他们不再需要使用特定的编程语言客户端来实现这些操作。所有需要与`etcd`交互的操作都可以通过一个简单的HTTP客户端来实现(另外,每个`etcd`发行版都默认自带一个名为**etcdctl**的命令行客户端工具)。正如[官方文档](https://github.com/coreos/etcd/blob/master/Documentation/api.md)中的很多例子中展示的,用户甚至可以简单地使用`curl`命令来和`etcd`集群进行交互。`etcd`使用[Raft](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf)一致性算法,通过在集群之间同步日志来管理数据。和Zookeeper使用的ZAB类似,Raft定义了领导者和追随者节点。所有的写请求都必须经由通过领导者使之生效,随后它会将操作以日志形式同步/重演到其余的追随者节点。
为了使`etcd`能够正常工作,集群里必须部署2*n*+1个节点,即3、5、7等。可以通过https://coreos.com/ os/docs/latest/cluster-architectures.html来查看推荐的生产环境集群设置方案。同Zookeeper一样,`etcd`也提供了强大的数据一致性保证和分区容忍。此外,`etcd`还建立了一套强大的[安全模型](https://github.com/coreos/etcd/blob/master/Documentation/security.md),它使得客户端与集群之间以及集群中各节点之间的通信都可以采用SSL/TLS作为认证方式来完成客户端的授权。虽然说在`etcd`集群的管理方面可能会遇到一些小小的挑战。但值得庆幸的是,`etcd`官方提供了很棒的[管理](https://github.com/coreos/etcd/blob/master/Documentation/admin_guide.md)和[集群部署](https://github.com/coreos/etcd/blob/master/Documentation/clustering.md)指导手册,在将`etcd`集群部署到基础设施之前,**必须得去读一读**这些手册。当然,`etcd`不只限于这里简要介绍的内容,因此我建议如果感兴趣的读者不妨去看看它的官方文档。接下来,我们将讨论该如何将`etcd`用于服务发现。
可以通过利用`etcd`的TTL特性来实现服务发现的功能。注册服务会在`etcd`集群里创建一个新的键值对,然后把连接的详细信息填到里面。这里,可以创建一个单独的键也可以插入到一个现有的键的目录里。`etcd`的目录提供了一个很好的分组方式,它可以将运行在基础设施里的相同服务的多个实例归类到一起(目录本身也是以一个键的形式存在)。该服务随后可以在一个给定的键上设置一个TTL,这样一来用户无需对它做任何进一步的操作,存放在其中的数据便会在达到TTL的值后自动过期。借助于TTL,`etcd`实现了一个简单的自动注销服务的机制。用户还可以通过更新TTL的方式来延迟数据的过期时间,这也是一些长期运行的服务必须做的,它们可以借助这一手段来避免不断的注册/注销。客户端则可以通过查询`etcd`集群里的特定键或者目录来找出已注册服务的连接信息。就像之前提到的那样,客户端可以在任意一个键上设置监听,然后可以在该键存储的数据发生变化时收到通知。
多亏了有`etcd`提供的远程JSON API,服务的注册甚至还可以嵌入到服务的源代码里,或者也可以使用一个伙伴进程,通过实现一个简单的HTTP客户端——`etcdctl`或者`curl`就能办到——来和`etcd`集群交互。由于使用简单的命令行工具便能完成服务的注册,这使得集成第三方服务也变得相当简单:可以在服务启动时在`etcd`里添加一个新条目,然后在服务关闭时删掉该键。借助于[SystemD](http://www.freedesktop.org/wiki/Software/systemd/),可以很容易地实现伙伴进程同主服务进程一起启动、停止。
在使用Docker时用户也可以轻松地采取相同的策略来实现服务的注册。伙伴进程会去检索服务容器的信息,然后将解析后的IP地址和端口数据填充到一个特定的`etcd`键里。这个键随后可以被其他Docker容器通过查询`etcd`集群的方式来读取。
如果采用伙伴进程来实现服务注册,最终可能在维护方面会比较伤脑筋,因为用户需要不断地更新`TTL`值并监控已注册服务的健康状况。用户一般是通过一个简单的shell脚本来实现这一点,它会运行一个无限循环,并且以一定的时间间隔不断检查正在运行的服务的健康性,然后据此更新特定的`etcd`键对应的值。可以通过一个简单的[实例](https://coreos.com/fleet/docs/latest/launching-containers-fleet.html)来了解这个方案。
至于客户端,业界已经有大量现成的、不同的编程语言实现的工具和[库可供选择](https://github.com/coreos/etcd/blob/master/Documentation/libraries-and-tools.md),这里面的大多数工具的社区仍然相当活跃。再者说,同其他工具一样,`etcd`原生的[Go语言库](https://github.com/coreos/go-etcd)应该会提供所有功能特性的支持。遗憾的是,它仍然无法提供一些服务负载均衡或者故障转移方面的功能支持,因此用户需要自己去解决这些问题。
`etcd`提供了一个非常不错的针对服务发现的解决方案。因此,尽管它仍然还在不断的迭代完善,很多企业已经开始将它应用到他们的生产环境中。具备编程语言无关性的远程API接口对于应用开发者而言有很大的推动作用,因为它给了他们更多的选择空间。然而,同Zookeeper一样,采用`etcd`的话会给用户的基础设施引入额外的管理复杂度。用户需要了解如何操作`etcd`集群,而这一点并不是那么容易就能办到。通常来说,缺乏对`etcd`内部原理的理解,往往可能导致一些意想不到的事情发生,有些情况下甚至可能会丢失数据。
因此,对于一个初学者而言,根据用户存储在集群里的数据容量来完成etcd的扩容工作可能会是一个不小的挑战。服务目录里的记录必须有它们各自的TTL值,然后用户需要通过已注册的服务不断地刷新该值,这需要开发者投入一些额外的精力。如果所处环境里运行的服务本身生命周期很短,那么频繁更新TTL值会产生相当大的网络流量。我们这里所提到的`etcd`实际上是许多其他开源项目实现的基石,像之前提到过的SkyDNS或者是[Kubernetes](http://kubernetes.io),并且它已经默认被内置到了CoreOS Linux发行版里。该项目很有可能会得到进一步的发展和显著的提高。
`etcd`已然成为Docker基础设施里一个非常流行的、用来实现服务发现解决方案的基本构建组件。一些新的、全套的解决方案都受到了它的启发。值得一提的是,这里面有一个项目是[Jason Wilder](https://twitter.com/jaswilder)本人创建的。它把`etcd`和另外一款非常流行的名为HAProxy的开源软件结合了起来。它采取伙伴进程的方式来实现服务发现并且利用Docker API发送相应的事件,随后用它来生成HAproxy的配置,借此完成Docker容器里运行的服务与其他服务之间请求的路由和负载均衡工作。关于这部分内容,读者可以在http://jasonwilder.com/blog/2014/07/15/docker-service-discovery/了解更多详细内容。再强调一次,业界可能已经有很多方案是基于`etcd`实现的服务发现,然而Jason的这个项目把服务发现本身的一些基础概念讲解得非常透彻,并且定义了一个已经在Docker社区里被反复验证的服务发现模型。图14-2对该项目进行了简单地讲解说明。
![图像说明文字](https://box.kancloud.cn/ccacbeb22b0bc5dacee50fbce0808cc2_700x400.jpeg)
图14-2
在以上设定中,HAproxy直接运行在宿主机上(即不是运行在Docker容器里)并且为所有运行在Docker容器里的服务提供了一个单一的入口。运行在Docker容器里的服务会在服务启动时通过在`etcd`里创建一个特定的键条目来完成注册。我们可以利用一个特殊的服务进程(如[conf.d](https://github.com/kelseyhightower/confd))来监控`etcd`集群里的键命名空间,一旦发生变化的话它会马上为之生成新的HAproxy配置并随后重新加载HAproxy服务使之生效。针对该服务的请求会自动被路由和负载均衡到运行在Docker容器里的其他服务。这有点像Smartstack所推崇的模式,它是另外一款服务发现的解决方案,我们将在本章的后面部分详细介绍。
由于篇幅有限,关于Docker生态圈里其他那些围绕`etcd`建立的服务发现方案便留待读者朋友探索。接下来,我们将介绍一款比`etcd`更加年轻也可以说更加强大的兄弟软件:`consul`。
consul是一款由[HashiCorp公司](https://www.hashicorp.com)编写的多功能分布式系统工具。同之前介绍过的`etcd`一样,consul也是使用Go语言实现。consul很好地将它所有的特性集成为一个可定制化软件,并易于使用和运维。在这里,我们不会花太多篇幅去介绍什么是consul,关于这一点读者可以在它的[官方网站](https://www.consul.io/docs/index.html)上找到一个非常全面的文档,里面包含了大量的实际案例。取而代之的是,我们将会去总结它的主要特性并且探讨在Docker基础设施里如何借助它来实现服务发现。最后,在本章的末尾我们将会介绍一个实际案例,它利consul提供的功能特性,将consul作为一个插件式的后端服务,为运行在Docker容器里的应用提供了即插即用的服务发现功能。
我们选择用**多功能**一词来描述consul的目的正是在于consul的确可以无需花费其他任何额外的精力,作为一款单独运行的工具提供下述任意一项功能:
- 分布式键值存储;
- 分布式监控工具;
- DNS服务器。
上述功能及其易用性使得consul成为DevOps社区里一款非常强大和流行的工具。让我们快速过目一下,看看consul的背后究竟隐藏了些什么奥秘使得它可以用在如此多的场合。
consul,同`etcd`类似,也是基于Raft一致性算法实现的,这也就是说,它所在的集群里的节点部署数量同样应该满足2*n*+1(*n*表示一个正整数)以保证其正常工作。和`etcd`一样,consul也提供了一个远程的JSON API,这使得各种不同的编程语言实现的客户端访问该服务更加简单。通过提供远程API的支持,consul允许用户自行在其之上构建新服务或直接使用它原生提供的开箱即用的功能。
就部署而言,consul定义了一个代理(agent)的概念。该代理能够在以下两种模式运行:
- **服务端**——提供分布式键值存储和DNS服务器;
- **客户端**——提供服务的注册、运行健康监测以及转发请求给服务器。
服务端和客户端代理共同组成一个完整的集群。consul通过利用HashiCorp编写的另外一款名为[serf](https://www.serfdom.io)的工具来实现集群成员身份和节点发现。serf是基于[SWIM](http://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf)一致性协议实现的,并且在一些性能方面做了优化。您可以通过[consul官网](https://www.consul.io/docs/internals/gossip.html)来了解consul中的gossip详细的内部实现原理。利用gossip协议并通过将它和本地服务的健康检测结合到一起,这使得consul可以实现一个简单但是异常强大的分布式故障检测机制。这对于开发者和运维人员而言实在是一个巨大的福音。开发人员可以在他们的应用程序里公开健康检测的端点然后轻松地将应用服务添加到consul的分布式服务集合里。运维人员也可以编写简单的工具,使用consul的API来监控服务的健康性,或者他们只是使用consul原生提供的[Web UI](https://github.com/hashicorp/consul/blob/master/ui/README.md)去操作。
这里讨论到的只是consul一些基本的内容,实际上它所提供的还远不止这些,因此**强烈建议**读者去细读一下consul强大的官方文档。如果想知道consul和市面上其他工具的差异,不要犹豫,赶紧去看一下专门讨论这一话题的[官方文档](https://www.consul.io/intro/vs/index.html)吧。接下来,让我们一起来看看我们该如何将consul用于基础设施里的服务发现。
迄今为止,在我们介绍过的工具中,作为一款可定制的服务发现解决方案,consul无疑是最容易上手的一个。用户可以通过以下几种方式将自己的服务注册到consul的服务目录里:
- 利用consul的远程API将服务注册嵌入到用户的应用代码里;
- 使用一个简单的伙伴脚本/客户端工具,在应用服务启动时通过远程API来完成注册;
- 创建一个简单的声明服务的[配置文件](https://www.consul.io/intro/getting-started/services.html),consul代理可以在服务启动或重新加载服务后读取该配置。
已注册的服务可以通过consul的远程API直接去查找,当然用户也可以使用consul提供的开箱即用的DNS服务来检索它们的信息。这一点尤其方便,因为用户不必再受限于一个特定的服务查找方案,而且甚至可以不费任何力气地同时使用这两套方案。此外,consul还允许用户为自己的应用服务设计一些自定义的健康检测机制。如此一来,用户不必再像之前的Zookeeper那样和TCP会话周期绑定到一起,也不必像`etcd`那样和TTL值挂钩。consul代理程序会持续不断地在本地监控已注册服务的健康性并且一旦健康检测失败它会立马自动将其从服务目录中抹除。
Consul提供了一个非常完备的服务发现解决方案,并且令人意外的是它的成本其实非常低。在consul中,应用服务可以通过远程API或DNS来定位和检索。为了完成服务的注册,需要避免使用远程API而可以采用更简单的基于JSON的配置文件来实现这一点。这使得consul能够很方便地和传统配置管理工具集成在一起。使用consul会给用户的基础设施引入一些额外的复杂度,但是作为回报,用户也从中获得了大量的收益。与`etcd`相比,consul集群无疑是更易于维护和管理的。Consul在多数据中心方面也具备很好的扩展能力,事实上,consul提供了一些额外的工具专门负责多数据中心的扩容工作。Consul可以作为一个单独的工具使用,也可以作为构建一个复杂的分布式系统的基本组件。如今,业界围绕它已然形成了一个新的完整的工具生态圈。在下一节里,我们将会介绍一款名为`registrator`的工具,它使用consul作为它的一个可插拔的后端服务,它为运行在Docker容器里的应用提供了一个非常易于上手的、自动服务注册的解决方案。
从整体上来说,`registrator`会去监听Docker的Unix套接字来获取Docker容器启动和消亡时的事件,并且它会通过在事先配置好的一个可插拔的后端服务中创建新记录的形式自动完成容器的服务注册。这就意味着它必须以一个Docker容器的身份来运行。读者可以在[Docker Hub](https://registry.hub.docker.com/u/gliderlabs/registrator/)上找到`registrator`的Docker镜像。`registrator`提供了相当多的配置参数选择,因此,尽情去[GitHub项目页面](https://github.com/gliderlabs/registrator)的文档库里去查找关于它们的详细解释吧。
下面,让我们一起来看一个简短的实际案例。我们将会使用`registrator`把`redis`内存数据库打包到Docker容器里运行,用户可以很方便地通过consul发现它在网络上所提供的服务。当然,用户也可以使用类似的方法在Docker基础设施里运行任意的应用服务。
回到这个例子,首先我们需要启动一个consul容器。这里,需要用到`registrator`之父[Jeff Lindsay](https://twitter.com/progrium)创建的镜像来实例化具体的容器:
```
# docker run -d -p 8400:8400 -p 8500:8500 -p 8600:53/udp -h
node1 progrium/consul -server -bootstrap
37c136e493a60a2f5cef4220f0b38fa9ace76e2c332dbe49b1b9bb596e3ead39
#
```
现在,后端的发现服务已经开始运行,接下来我们将会启动一个`registrator`容器,并且同时传给它一个consul的连接URL作为参数:
```
# docker run -d -v /var/run/docker.sock:/tmp/docker.sock -h
$HOSTNAME gliderlabs/registrator consul://$CONSUL_IP:8500
e2452c138dfa9414e907a9aef0eb8a473e8f6e28d303e8a374245ea6cd0e9cdd
```
如下所示,我们可以看到容器均已成功启动,并且我们假定所有容器都是注册的`redis`服务:
```
docker ps
CONTAINER ID IMAGE COM-
MAND CREATED STATUS
PORTS
NAMES
e2452c138dfa gliderlabs/registrator:latest "/bin/regis-
trator co 3 seconds ago Up 2 sec-
onds
distracted_sammet
37c136e493a6 progrium/consul:latest "/bin/start
-server 2 minutes ago Up 2 minutes 53/tcp,
0.0.0.0:8400->8400/tcp, 8300-8302/tcp, 8301-8302/udp,
0.0.0.0:8500->8500/tcp, 0.0.0.0:8600->53/udp furious_kirch
```
考虑到整个例子的完整性,我们不妨介绍一下最初的情况,下列命令展示了我们正在运行的只有一个节点的consul集群并且在该时刻没有任何已注册的服务运行:
```
# curl $CONSUL_IP:8500/v1/catalog/nodes
[{"Node":"consul1","Address":"172.17.0.2"}]
# curl $CONSUL_IP:8500:8500/v1/catalog/services
{"consul":[]}
```
现在,让我们先启动一个`redis`容器,然后公开它所有需要对外提供服务的端口:
```
# docker run -d -P redis
55136c98150ac7c44179da035be1705a8c295cd82cd452fb30267d2f1e0830d6
```
如果一切顺利,我们应该可以在consul的服务目录里找到该`redis`服务的信息:
```
# curl -s localhost:8500/v1/catalog/service/redis |python -
mjson.tool
[
{
"Address": "172.17.0.6",
"Node": "node1",
"ServiceAddress": "",
"ServiceID": "docker-hacks:hungry_archimedes:6379",
"ServiceName": "redis",
"ServicePort": 32769,
"ServiceTags": null
}
]
```
从上面的输出可以看到`registrator`定义服务所采用的格式。关于这一点可以转到[项目文档](https://github.com/gliderlabs/registrator#how-it-works)了解更多的细节。正如我们在前面章节所了解到的,consul提供了一个原生的开箱即用的DNS服务的支持,因此所有已注册的服务可以很轻松地通过DNS来查找和定位。要验证这一点也非常简单。首先,我们需要找出consul提供的DNS服务器将哪些端口映射到了宿主机上:
```
# docker port 37c136e493a6
53/udp -> 0.0.0.0:8600
8400/tcp -> 0.0.0.0:8400
8500/tcp -> 0.0.0.0:8500
```
太棒了,我们可以看到容器的DNS服务被映射到了宿主机的所有网络接口上,并且监听了8600端口。现在,我们可以使用Linux上著名的`dig`工具来完成一些DNS的查询操作。从consul的官方文档中我们可以了解到,consul里已注册服务对应的默认的DNS记录会以NAME.service.consul的格式命名。因此,在这个例子中,当注册一个新服务时`registrator`使用的Docker镜像名便会是`redis.service.consul`(当然,必要的话也可以修改这个设置)。
那么,现在让我们来试着运行一下DNS的查询吧:
```
# dig @172.17.42.1 -p 8600 redis.service.consul +short
172.17.0.6
```
如今我们已经获得了`redis`服务器的IP地址,但是同该服务通信所需的信息还远不止这些。我们还需要找出该服务器监听的TCP端口。幸运的是,这一点很容易办到。我们需要做的只是通过查询查询consul的DNS来寻找对应的使用相同的DNS名称的`SRV`记录。如果一切顺利,我们应该可以看到返回的端口号是32769,当然我们也可以通过它提供的远程API以检索consul服务目录的方式来获取这个信息:
```
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul +short
1 1 32769 node1.node.dc1.consul.
```
真的是太棒了!借助consul,我们成功地为我们的Docker容器实施了一整套完备的服务发现方案,而且所有我们需要做的配置只是运行两个简单的命令而已!我们甚至无需编写任何代码。
如果我们现在停止`redis`容器,consul会将它标记为已停止的状态,如此一来,它将不会再响应我们的任何请求。这一点同样也非常容易验证:
```
# docker stop 55136c98150a
55136c98150a
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul +short
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul
; <<>> DiG 9.9.5-3ubuntu0.1-Ubuntu <<>> @172.17.42.1 -p 8600 -t
SRV redis.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 56543
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDI-
TIONAL: 0
;; QUESTION SECTION:
;redis.service.consul. IN SRV
;; Query time: 3 msec
;; SERVER: 172.17.42.1#8600(172.17.42.1)
;; WHEN: Tue May 05 17:59:35 EDT 2015
;; MSG SIZE rcvd: 38
```
如果正在寻找一个简单而容易上手的服务发现的解决方案,`registrator`无疑是一个非常省力的选择,尽管它仍然需要用户运行一些像`consul`或者`etcd`这样的存储后端。然而,由于其本身具备简单部署的优势以及它提供的对Docker的原生集成支持,选用它无疑是利大于弊的。
现在,我们将结束这一节,接着介绍一个新的不依赖任何一致性算法来保证数据的强一致性的解决方案,然而它仍然提供了一些有趣的特性,因此不妨将它当作实现用户的基础设施里的服务发现的一个备选方案。
在过去的几年里,[Netflix的工程师](http://techblog.netflix.com)团队开发了大量的开源工具来帮助他们管理微服务云基础架构,以方便那些以前必须创建他们自己的[开源软件中心](http://netflix.github.io)心才能消费他们的用户快速上手。大部分的工具都是用Java编程语言编写的,如果用户没有复用其OSS工具箱里的一些其他工具往往会很难将其集成到私有的基础设施里。在这一节里,我们将会一起来看看[Eureka](https://github.com/Netflix/eureka)究竟提供了怎样的一个有意思的服务发现的选择。
Eureka是一个基于REST的服务,它在设计之初最主要的目的是用来提供“中间层”的负载均衡、服务发现和服务的故障转移。官方推荐的用例场景是,如果用户在AWS云中运行自己的基础设施,AWS云是Eureka经过实战检验的地方,而用户的基础设施里又存在大量的内部服务,同时又不希望注册到AWS ELB中或者对外公开这些服务的话,这时候可以考虑采用Eureka实现服务发现。可以说,促成Eureka的最主要的动力便是在于ELB不支持内部服务的负载均衡。理想情况下,由于Eureka本身不提供会话保持的功能,因此通过Eureka发现的服务本身应该都是无状态的。从架构上来说Eureka主要有两个组件:
- **服务端**——提供服务的注册;
- **客户端**——处理服务的注册,并且提供基本的、轮换式的负载均衡和故障转移功能。
官方推荐的部署方案是每个[AWS](http://aws.amazon.com/cn/)地理区域部署一个Eureka的服务集群,或者至少每一个AWS可用区域部署一台服务器。实际上,Eureka服务端根本不知道其他AWS区域有哪些服务器。它保存信息最主要的目的是为了保证**一个AWS区域内**的负载均衡。Eureka集群里的服务器会以异步的形式同步他们彼此间的服务注册信息。这一同步操作可能需要花一些时间才能完全生效。由于服务端可能有缓存,因此该操作有时候甚至要耗费数分钟的时间才能完成。与Zookeeper、etcd或consul相比,**Eureka更偏重于服务的可用性而不是数据的强一致性**,因此客户端往往可能需要处理脏数据的读取。没办法,Eureka本身便是这样设计的。它关注的是经常发生故障的云环境下的弹性伸缩问题。采用这种方案就意味着Eureka甚至可以在集群因为某种故障出现网络分区时仍能工作,不过这样会牺牲数据的一致性。
Eureka客户端会将应用服务的信息注册到服务端,并且随后每30秒更新一次他们的“租约”信息。如果客户端没有在90秒内更新它的租约,那么它会自动从服务端的注册中心里删除,然后必须重新注册。这跟`etcd`里面通过`TTL`实现的机制有些类似,但是如果使用Eureka,用户无法设置注册中心里的记录的生命周期。Eureka客户端对于服务端故障具备一定的弹性耐受能力。它们会将本地的注册信息缓存起来,因此即使注册服务器发生故障,它们依旧可以正常工作,这显然要求客户端这一方可以处理一些服务的故障转移工作。一旦网络分区的故障排除,客户端本地的状态会被合并到服务端。而为了易于排障,Netflix OSS库里提供了另外一款名为\[Ribbon\]{(https://github.com/Netflix/ribbon)的工具专门用来解决这个问题。
Eureka旧版本的客户端主要是基于pull模式的,然而最新发布的版本已经加入了大量对于服务端和客户端两方面的改进。它将集群的[读写](https://github.com/Netflix/eureka/wiki/Eureka-2.0-Architecture-Overview)分离开来,从而改善了性能并提高了可扩展性。Eureka客户端可以订阅一组特定的服务,随后,就像之前介绍过的工具一样,他们便可以在服务端发生任何更改的时候收到通知。Eureka官方还提供了一个简单的仪表盘,使其更容易部署到其他的云服务上。您可以转到如下[链接](https://github.com/Netflix/eureka/wiki/Eureka-2.0-Motivations)来了解新版本设计理念方面的更多内容。
就像之前所提到的那样,Eureka的设计目标是客户端的中间层负载均衡。要实现服务之间的负载均衡,必须首先看看它们是否在服务端的注册中心里。由于Eureka是使用Java编程语言实现的,因此用户可以相当容易地将Java客户端集成到自己的应用代码里。原生的客户端提供了非常全面的功能特性,包括一个简单的基于轮换式的负载均衡的支持。每个应用服务可以在它启动的时候到Eureka服务端注册自己的信息,然后每隔30秒发送一个心跳包到服务端。如果超过90秒该服务仍然没有动作,Eureka会自动将其注销。
Eureka还对外开放了一个REST API,如此一来用户便可以轻松实现自己自己的客户端。虽然开源界目前已经有几个不同编程语言实现的客户端库,但是没有哪个客户端能够真正在功能覆盖面及质量上胜过原生客户端的实现。另外一个选择是用户可以实现一个小型的Java伙伴程序,和自己的服务一起运行,然后由它来调用原生的客户端库帮忙处理服务的注册和心跳汇报工作。这将带来额外的工作量以及不必要的维护复杂度,当然用户将因此获得原生客户端库的全部功能支持。
使用Eureka来实现服务发现的最大优点在于它对故障方面具备一定的容忍能力,这使得它成为云环境下的一个非常不错的选择,当然,前提是用户可以在客户端这一边处理服务的故障转移以及脏数据的读取。事实上,云环境的部署需求正是它设计最主要的动力所在。然而,遗憾的是,用户无法控制服务注册中心里记录的生命周期,而且必须不断地发送心跳包,这样做的话可能会为自己的基础设施带来不小的流量压力,并且会给注册服务器带来一些额外的负载。客户端查询的往往也是全量的服务列表数据,在查找服务时也没有过滤或者搜索的细粒度之分等。关于这一点可能会在2.0版本得到改善,而这一版本也会引入一个服务订阅的概念,即用户可以收到有关服务的通知信息。
Eureka在最开始设计的时候便考虑到了自动扩展,因此它的可扩展性相当不错。此外,Eureka的最新版本对它的扩展能力做了进一步的改善。它的致命要害在于,如果想充分利用Eureka,就必须要用到Netflix的一些其他OSS组件,如之前提到的Ribbon库或Archaius配置服务,该服务又依赖于Zookeeper。这一点也许读者早就意识到了,它可能会为基础设施引入大量不必要的复杂度。
接下来,我们将把视角从Netflix的OSS深渊里挪开,转而讨论一个已经非常流行的不同的变种方案,借助它用户可以非常便捷地实现服务发现,而且它还有一个非常有趣的名字。那么,让我们一起来见识一下Smartstack吧!
Smartstack是一个由[AirBnb](http://nerds.airbnb.com)的工程师团队创建的服务发现解决方案。Smartstack在整个服务发现的生态圈里的地位非常特殊,原因在于它的设计理念启发了其他的解决方案。正如它的名字所暗示的,smartstack真的是一组智能服务组成的**技术栈**,其中包括[Nerve](https://github.com/airbnb/nerve)和[Synapse](https://github.com/airbnb/synapse)两个部分。
Nerve和Synapse都是使用Ruby编程语言编写的,并且以Ruby gem的形式发布。他们可以和[HAproxy](http://www.haproxy.org)以及之前介绍过的Zookeeper交互。http://www.haproxy.org上有一篇很棒的入门性质的博客文章,读者有兴趣的话不妨去读一读,在这里可以了解到更多关于Smartstack背后的一些创造动机。在本节里,我们将介绍它的一些主要特性,并在最后做一个简短的总结。请记住,在打算将Smartstack部署到自己的基础设施之前,千万不要犹豫,多看一些它的在线文档会有很大帮助的。
Smartstack在服务发现的注册和发现方面是伙伴进程模型的拥护者:`synapse`和`nerve`均是以独立进程和应用服务一起运行的,并且代表应用服务自动处理服务的注册及查找工作。在生产环境里,一台宿主机上运行一个Synapse实例应该就够了。Smartstack会利用Zookeeper作为自己服务目录的的后端并且采用HAproxy作为已发现的服务的唯一入口和负载均衡器。Smartstack可以非常轻松地集成到用户的Docker基础设施里。图14-3所示就是采用Smartstack实现服务发现的一个简单架构。
![图像说明文字](https://box.kancloud.cn/78a610c198ec1c3a003a0e51e1ab457f_700x543.jpeg)
图14-3
应用开发人员无需编写任何服务发现的代码并且他们还可以得到免费的开箱即用的负载均衡和服务故障自动转移的功能支持。接下来,让我们进一步看看Smartstack的这两个核心服务,从而更好地理解Smartstack究竟是怎样完成服务发现工作的。
Nerve是一款简单的用来监控机器和服务的健康状况的工具。它将服务的健康信息保存在一些分布式存储里。后端方面目前只完全支持Zookeeper,但是官方也正在努力做针对`etcd`的完全适配工作。Nerve负责服务发现里的**服务注册**部分,它会根据服务的健康状态添加和删除Zookeeper集群里的`znode`。如果使用`etcd`作为后端存储,Nerve将会在`etcd`集群里添加一个键值记录然后设置它的`TTL`为30秒。之后,它会根据服务的健康状况不断地更新该记录。
Nerve给服务部署方面指明了一条道路,即要求应用开发人员提供一些**合适**的机制来监控服务的健康状态。这一点很重要,而这不仅是为了提高服务发现实现的可靠性。Nerve利用应用服务提供的健康检测方案来驱动服务注册流程。最后,Nerve还可以从服务发现解决方案的整体中分离出来,作为单独的监控服务的看门狗来使用。
如果想了解Nerve更多的内容,不妨去GitHub上读一读它的[官方文档](https://github.com/airbnb/nerve)。
Synapse是一款简单的服务发现的实现方案,它定义了服务`watcher`的概念,让用户可以从指定的后端中监听相应的事件。Synapse会根据收到的事件生成相应的HAproxy配置文件。Synapse中提供了一些可用的服务`watcher`:
- **stub**——没有监听的概念,用户只能手动指定服务的列表;
- **zookeeper**——zookeeper会在集群里的特定`znode`节点上注册监听;
- **docker**——监听Docker API的事件;
- **EC2**——根据AWS EC2的标签来监听服务器。
每当监听的服务不可用时,Synapse会重写HAproxy的配置,随后重新加载HAproxy服务使之生效。所有客户端的请求都是通过HAproxy代理的,它负责将请求路由到真正的特定应用服务。这对于应用开发人员和运维人员来说都是一个共赢的局面:
- 开发人员不必编写任何的服务发现代码;
- 运维人员也可以通过一些经过充分验证的解决方案来实现服务的负载均衡以及故障迁移。
再强调一下,这些内容均可以在GitHub上的[官方文档](https://github.com/airbnb/synapse)里找到。
Smartstack是一个具备技术无关性的绝佳方案,借助它,用户可以在基础设施里实现服务发现。它不需要用户编写任何额外的应用代码并且可以轻松地被部署到裸机、虚拟机或Docker容器里。Smartstack本身相当简单,但是整个套件至少需要维护4个不同的部分:Zookeeper、HAproxy、Synapse和Nerve。例如,如果没有在基础设施中事先运行Zookeeper,用户可能觉得运行全套的Smartstack方案会很难。此外,虽然在用户的服务之前运行HAproxy可以为用户提供一个不错的服务层抽象以及负载均衡和服务的故障迁移功能,然而用户需要在每台宿主机上至少管理一个HAproxy实例,这会引入一定的复杂度,并且往往需要一定的维护成本。
在本章的最后,将简单介绍一款由[bitly](http://word.bitly.com)的工程师团队开发的名为`nsqlookupd`的工具。[`nsqlookupd`](http://nsq.io/components/nsqlookupd.html)**并不是一个完整的服务发现解决方案**,它只是提供了一种发现[`nsqd`](http://nsq.io/components/nsqd.html)实例的创新方式,或是在应用运行时跑在基础设施里的一个分布式消息队列。
实际上,`nsqd`守护进程在向那些`nsqlookupd`实例声明自己启动的时候就已经完成了服务注册,随后`nsqd`会定期发送带有它们状态信息的心跳包给每个`nslookupd`实例。
`nsqlookupd`实例担任的角色是直接为客户端提供查询的服务注册中心。它们提供的只是一个网络上周期性同步的`nsqd`实例的数据库。客户端通常需要检索每个可用的实例信息,然后合并这些结果。
如果要找的是一个可以在大规模基础设施这样的拓扑架构里运行的分布式消息队列解决方案,不妨去看一看`nsqd`和`nsqlookupd`项目的官方文档,了解更多详细内容。
在本章里,我们介绍了一系列的服务发现解决方案。服务发现没有捷径可寻,只能根据任务的类型和它必须满足的需求来选择一款合适的工具。和传统的推荐一个特定解决方案的形式不同,我们给出一个包含多种解决方案的概述表(见表14-1),对本章的内容做出总结,希望这能够帮助读者做出正确的选择,选出最适用于自己的基础设施的服务发现工具。
表14-1
名称
注册机制
数据一致性
语言
SkyDNS
客户端
强一致
Go
weave-dns
自动注册
强一致
Go
ZooKeeper
客户端
强一致
Java
etcd
伙伴程序 + 客户端
强一致
Go
consul
客户端 + 配置 + 自动注册
强一致
Go
eureka
客户端
终端一致
Java
nslookupd
客户端
终端一致
Go
在第15章里,我们将介绍Docker的日志采集和监控。
- - - - - -
[\[1\]](part0020.xhtml#ac141) 即像systemd这样的服务管理程序,你可以将自己的应用和伙伴进程关联起来,设定伙伴进程在应用启动之后方才启动。——译者注
[\[2\]](part0020.xhtml#ac142) IANA机构设定了一个Linux下常见服务端口注册列表(https://zh.wikipedia.org/wiki/TCP/UDP%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8),它指定了这些著名服务的默认注册端口号。——译者注
- 版权信息
- 版权声明
- 内容提要
- 对本书的赞誉
- 译者介绍
- 前言
- 本书面向的读者
- 谁真的在生产环境中使用Docker
- 为什么使用Docker
- 开发环境与生产环境
- 我们所说的“生产环境”
- 功能内置与组合工具
- 哪些东西不要Docker化
- 技术审稿人
- 第1章 入门
- 1.1 术语
- 1.1.1 镜像与容器
- 1.1.2 容器与虚拟机
- 1.1.3 持续集成/持续交付
- 1.1.4 宿主机管理
- 1.1.5 编排
- 1.1.6 调度
- 1.1.7 发现
- 1.1.8 配置管理
- 1.2 从开发环境到生产环境
- 1.3 使用Docker的多种方式
- 1.4 可预期的情况
- 为什么Docker在生产环境如此困难
- 第2章 技术栈
- 2.1 构建系统
- 2.2 镜像仓库
- 2.3 宿主机管理
- 2.4 配置管理
- 2.5 部署
- 2.6 编排
- 第3章 示例:极简环境
- 3.1 保持各部分的简单
- 3.2 保持流程的简单
- 3.3 系统细节
- 利用systemd
- 3.4 集群范围的配置、通用配置及本地配置
- 3.5 部署服务
- 3.6 支撑服务
- 3.7 讨论
- 3.8 未来
- 3.9 小结
- 第4章 示例:Web环境
- 4.1 编排
- 4.1.1 让服务器上的Docker进入准备运行容器的状态
- 4.1.2 让容器运行
- 4.2 连网
- 4.3 数据存储
- 4.4 日志
- 4.5 监控
- 4.6 无须担心新依赖
- 4.7 零停机时间
- 4.8 服务回滚
- 4.9 小结
- 第5章 示例:Beanstalk环境
- 5.1 构建容器的过程
- 部署/更新容器的过程
- 5.2 日志
- 5.3 监控
- 5.4 安全
- 5.5 小结
- 第6章 安全
- 6.1 威胁模型
- 6.2 容器与安全性
- 6.3 内核更新
- 6.4 容器更新
- 6.5 suid及guid二进制文件
- 6.6 容器内的root
- 6.7 权能
- 6.8 seccomp
- 6.9 内核安全框架
- 6.10 资源限制及cgroup
- 6.11 ulimit
- 6.12 用户命名空间
- 6.13 镜像验证
- 6.14 安全地运行Docker守护进程
- 6.15 监控
- 6.16 设备
- 6.17 挂载点
- 6.18 ssh
- 6.19 私钥分发
- 6.20 位置
- 第7章 构建镜像
- 7.1 此镜像非彼镜像
- 7.1.1 写时复制与高效的镜像存储与分发
- 7.1.2 Docker对写时复制的使用
- 7.2 镜像构建基本原理
- 7.2.1 分层的文件系统和空间控管
- 7.2.2 保持镜像小巧
- 7.2.3 让镜像可重用
- 7.2.4 在进程无法被配置时,通过环境变量让镜像可配置
- 7.2.5 让镜像在Docker变化时对自身进行重新配置
- 7.2.6 信任与镜像
- 7.2.7 让镜像不可变
- 7.3 小结
- 第8章 存储Docker镜像
- 8.1 启动并运行存储的Docker镜像
- 8.2 自动化构建
- 8.3 私有仓库
- 8.4 私有registry的扩展
- 8.4.1 S3
- 8.4.2 本地存储
- 8.4.3 对registry进行负载均衡
- 8.5 维护
- 8.6 对私有仓库进行加固
- 8.6.1 SSL
- 8.6.2 认证
- 8.7 保存/载入
- 8.8 最大限度地减小镜像体积
- 8.9 其他镜像仓库方案
- 第9章 CI/CD
- 9.1 让所有人都进行镜像构建与推送
- 9.2 在一个构建系统中构建所有镜像
- 9.3 不要使用或禁止使用非标准做法
- 9.4 使用标准基础镜像
- 9.5 使用Docker进行集成测试
- 9.6 小结
- 第10章 配置管理
- 10.1 配置管理与容器
- 10.2 面向容器的配置管理
- 10.2.1 Chef
- 10.2.2 Ansible
- 10.2.3 Salt Stack
- 10.2.4 Puppet
- 10.3 小结
- 第11章 Docker存储引擎
- 11.1 AUFS
- 11.2 DeviceMapper
- 11.3 BTRFS
- 11.4 OverlayFS
- 11.5 VFS
- 11.6 小结
- 第12章 Docker 网络实现
- 12.1 网络基础知识
- 12.2 IP地址的分配
- 端口的分配
- 12.3 域名解析
- 12.4 服务发现
- 12.5 Docker高级网络
- 12.6 IPv6
- 12.7 小结
- 第13章 调度
- 13.1 什么是调度
- 13.2 调度策略
- 13.3 Mesos
- 13.4 Kubernetes
- 13.5 OpenShift
- Red Hat公司首席工程师Clayton Coleman的想法
- 第14章 服务发现
- 14.1 DNS服务发现
- DNS服务器的重新发明
- 14.2 Zookeeper
- 14.3 基于Zookeeper的服务发现
- 14.4 etcd
- 基于etcd的服务发现
- 14.5 consul
- 14.5.1 基于consul的服务发现
- 14.5.2 registrator
- 14.6 Eureka
- 基于Eureka的服务发现
- 14.7 Smartstack
- 14.7.1 基于Smartstack的服务发现
- 14.7.2 Nerve
- 14.7.3 Synapse
- 14.8 nsqlookupd
- 14.9 小结
- 第15章 日志和监控
- 15.1 日志
- 15.1.1 Docker原生的日志支持
- 15.1.2 连接到Docker容器
- 15.1.3 将日志导出到宿主机
- 15.1.4 发送日志到集中式的日志平台
- 15.1.5 在其他容器一侧收集日志
- 15.2 监控
- 15.2.1 基于宿主机的监控
- 15.2.2 基于Docker守护进程的监控
- 15.2.3 基于容器的监控
- 15.3 小结
- DockOne社区简介
- 看完了