服务发现
服务发现
容器化思维推荐以微服务的形式构建容器化应用。在以微服务方式构建的应用中,服务发现、服务发布和订阅等模块发挥了连接各个微服务的重要作用。在同一个分布式集群中的进程或服务,互相感知并建立连接,这就是服务发现。目前,有很多工具已经实现了服务发现的功能,例如Zookeeper、etcd、Consul等,基本上所有的SOA和微服务架构的分布式应用都是利用这些工具为分布式系统提供服务发现功能的。随着Docker容器的流行,多种微服务共同协作构成一个功能相对强大的架构的案例越来越多,透明化地动态添加这些服务的需求也日益强烈,关于Docker做服务发现,较为常见的解决方案是使用etcd
服务发现在SOA架构中是一个很重要的概念,是支撑大规模 SOA 的核心服务,在应用Docker容器集群的实践中也是非常重要的功能。对于Docker容器之间跨主机访问这个难题,服务发现是目前较为实用的解决方案。
服务发现并不是Docker特有的概念,但是容器带给了这些工具一个全新水平的需求,在目前主流的系统架构中服务发现也越来越受关注。使用服务发现很明显的优点就是零配置,不用使用硬编码的网络地址,只需服务的名字(有时甚至连名字都不用)就能使用服务。在现代的体系架构中,单个服务实例的启动和销毁很常见,所以应该做到无需了解整个架构的部署拓扑,就能找到这个实例。
服务发现不仅与SOA和微服务相关,在现代计算环境越来越趋向动态计算的系统架构中,服务发现更是尤为重要。除了Docker这种容器引擎,基于VM的EC2也在慢慢支持更高级别的动态计算框架,例如Amazon已经实现了Mesos服务在EC2集群中自动化实现。本文将会介绍服务发现的应用场景及解决方案,特别是与Docker相关的解决方案。
什么是服务发现
服务发现的功能是管理一个集群中的进程或服务之间的通信,涉及到服务列表,在服务列表中注册服务,并且能够在服务列表中查找并连接服务。服务发现的基本思想是任何一个应用的实例能够以编程的方式获取当前环境的细节,这是为了让新的实例可以嵌入到现有的应用环境而不需要人工干预。当有任何进程监听TCP或UDP端口时,服务发现组件都应该能够察觉,并且能够根据名字查找到并连接上这个端口。复杂系统的服务发现组件要提供更多的功能,例如,服务元数据存储、健康监控、多种查询和实时更新等。总的来说,服务发现提供了一种协调机制,方便服务的发布和查找。
为什么需要服务发现
传统应用使用的服务通常是一些遵循标准协议的服务,这些服务都占用的端口都是预定义的,比如HTTP占用80端口。如果类似于SOA中定义的这种服务也是使用预定义的端口,那么服务越多,发生冲突的可能性越大,毕竟,不可能允许两个服务监听同一个端口,尤其是像Docker的应用场景中,很多应用都是运行在同一个主机之上。管理一个拥挤的比方说被几百个服务所使用的所有端口的列表,本身有难度,添加到该列表后,这些服务需要的数据库和数量会日益增多。因此在实际的Docker应用场景中应该部署无需指定端口的服务,并且让Docker为我们分配一个随机的端口,那么就需要发现端口号,并且让其他服务或应用知道。
在一个分布式系统上部署服务到其中一台服务器上时,事情会变得更加复杂,如果预先定义哪台服务器运行哪个服务,会简单很多,但这会导致很多问题。而且如果预先定义每个服务的部署位置,那么要实现充分利用服务器资源的目标几乎是不可能的。另一方面,服务的自动伸缩会非常困难,更不用说自动恢复了,例如服务器故障的情况,预定义的服务部署方式就无法解决,因为服务的IP、端口等基本信息是固定的,其他应用只会根据这个固定的IP和端口连接该服务。所以更好的实现是将服务自动部署到某台只有最少数量的容器在运行的服务器上,充分利用服务器的资源,并做到负载均衡,而这样实现就需要添加IP地址到数据列表中,这些数据需要存储在某处并可以被其他服务或应用发现,也就是配置共享。实现服务发现的关键技术就是高可用的配置中心。
服务发现的应用场景
为了能够定位服务,至少需要两个的步骤,服务注册和服务发现。服务注册就是存储的信息至少包括正在运行的服务的主机和端口信息服务发现就是保证其他用户可以发现在服务注册阶段存储的信息。除此之外,还需要考虑其他方面。例如,如果一个服务停止工作并注册了一个新的服务实例,那么该服务是否应该注销呢?如果相同的服务有多个副本,该如何做负载均衡呢?如果一个服务器宕机了又该怎么处理?所有这些问题都与服务注册和发现阶段紧密关联。
基本场景
服务发现的基本功能就是在同一个分布式集群中的进程或服务互相感知并建立连接。从本质上说,服务发现就是想要了解集群中是否有进程在监听 UDP或TCP端口,并且通过名字信息就可以进行查找和连接。如下图所示,服务提供者在服务发现仓库注册,服务请求者在仓库中查找,最后通过查找的细节建立链接。
服务动态添加
在微服务协同工作的架构中,需要做到透明化地动态添加服务。通过服务发现机制,在服务注册仓库中注册某个服务名字的目录,在该目录下存储可用的服务节点IP。在使用服务的过程中,只要从服务目录中查找可用的服务节点使用即可。
PaaS平台中应用多实例
PaaS平台中的应用一般都有多个实例,实例故障重启透明化与负载均衡都与服务发现密切相关。通过服务发现机制,不仅可以透明的对多个实例进行访问,而且还可以实现负载均衡。但是应用的某个实例随时都有可能故障重启,这时就需要动态的配置域名解析(路由)中的信息。服务发现功能就可以解决这个动态配置的问题。
服务发现的实现
设计思想
服务发现的基本思想是对于服务的每一个新实例,能够识别当前环境和存储的相关信息,这是为了让新的实例在不需要人工干预的情况下可以嵌入到现有的应用环境。存储的注册表信息本身通常采用键/值对的格式,这种存储的主要目的是给所有访问这个服务的实例提供服务IP地址和端口这种基本信息,用于它们之间的相互通讯,这些数据一般还会包含服务相关的其他信息。
通常服务发现工具会提供某种形式的API,用于服务自身的注册以及目标服务信息的查找。当每一个服务实例启动之后,他们通过服务发现工具提供的API来注册自身的信息,这些信息记录了一个相关组件若想使用某服务时的全部必要信息。例如,一个MySQL数据库服务会在这注册它运行的ip和端口。当一个服务的调用者需要调用服务时,它可以通过服务发现工具提供的API,查找目标服务的相关信息,然后它就可以基于查到的信息与其需要的组件进行交互。负载均衡就是一个很好的例子,它可以通过查询服务发现得到各个后端节点承受的流量数,然后根据这个信息来调整配置。另外,为了与注册表中的服务提供者的具体实现解耦,实现上通常采用代理服务。这样所有的消费者总是向固定IP地址的代理发送请求,代理再依次使用服务发现来查找服务提供方信息并重定向请求。
设计目标
服务发现工具要查找的是数据,至少应该能够定位到服务的主机和端口,跟踪服务的健康状态,即时更新服务的配置信息;针对一个构建在多台服务器上的分布式系统,该工具需要足够健壮,保证其中一个节点的宕机不会危及数据;同时,每个节点应该有完全相同的数据副本,始终保持一致性;进一步地,服务发现还应该能够以任何顺序启动服务、杀死服务或者替换服务的新版本;最后,服务发现工具还应该能够重新配置服务并且查看到数据相应的变化。
名称解析与DNS
服务名称解析是服务发现组件中重要功能,使其他服务可以通过名字找到并连接目标服务的地址及端口,这个功能听起来很像DNS。然而,有很多原因导致单独的DNS并没有太大意义。一个关键原因是DNS原本就没有对名称解析的实时更新做任何优化,所以在需要实时更新名称解析的系统中,DNS不能够及时地找到正确的目标服务。当然,把DNS的TTL值(Time-To-Live)设为0,使DNS服务器一直更新缓存数据,可以达到实时更新的效果。那么你就需要管理你自己内部的DNS服务,这样就会涉及到你用什么数据存储来保证DNS解析数据的高可用性,又应该怎么样创建和销毁DNS记录,因为这些操作将会是非常频繁的,必须考虑在内。如果这些都能完成,那就相当于自己实现了一套DNS服务,所以这种方案是不可行的。
DNS做服务发现还有一个致命的缺点,当初DNS就是为遵循标准协议的服务而设计的,这些服务都占用特定的端口,例如HTTP协议占用80端口,SSH协议占用22端口等,所以对于DNS服务,只需要知道相应服务所在主机的IP地址就可以了,没有任何端口相关的信息。那么就如SOA中定义的服务,这些服务监听的端口是不可预知的,有时候还可能是随机的,尤其是像Docker的应用场景中,很多应用都是运行在同一个主机之上。端口必须作为重要信息存储在服务列表中,SRV记录(service record)可以为DNS解决这个问题,SRV记录提供目标服务主机IP的同时,也能够提供对应端口。至少在数据模型方面,SRV记录使DNS的功能更接近服务发现。但是当一个普通的应用要连接一个服务的时候,传统的DNS通过服务名称仍然是只解析出IP地址,那么这个普通应用应该怎么查询或者是用什么工具来查询SRV记录。也就是说除非这个应用支持查询SRV记录,要不然DNS仍然只是一个解析IP地址的工具。目前,有关服务发现的实现,大多数采用高可用的分布式键/值存储这种解决方案。
Zookeeper
Zookeeper是最早做服务发现的开源项目之一,起源于Hadoop,帮助在Hadoop集群中维护各种组件。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。其数据存储的格式类似于文件系统,如果运行在一个服务器集群中,Zookeeper实现了跨所有节点共享配置状态。
然而,Zookeeper的缺点也很明显。首先,它采用Java开发,尽管Java在许多方面非常优秀,但对于服务发现这种工作还是太沉重了。java本身就偏向于重型应用,会引入大量的依赖,而运维人员也普遍希望机器集群尽可能地简单,维护起来也不易出错;其次, Zookeeper的部署、维护和使用都很复杂,管理员需要掌握一系列知识和技能,一方面Zookeeper是功能丰富,但从另一个角度分析,丰富的特性反而将其从优势转变为累赘;最后,Apache基金会结构庞大,导致Zookeeper项目发展缓慢。
Zookeeper为其他项目的改进铺平了道路,现在,服务发现工具有了更好的选择。
etcd
etcd是一个年轻的项目,正在高速迭代和开发中,这样也带来一个缺点,就是版本的迭代导致其使用的可靠性无法保证,无法得到大项目长时间使用的检验。然而,目前CoreOS、Kubernetes和Cloud Foundry等知名项目均在生产环境中使用了etcd。
实际上,etcd作为一个受到Zookeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,还具有自身的优势:使用简单,基于HTTP+JSON的API,用curl命令就可以使用;安全,可选SSL客户认证机制;快速,每个实例每秒支持一千次写操作;可信,使用Raft算法充分实现了分布式,保证分布式系统数据的一致性。
etcd的架构并不复杂,etcd主要分为四个部分,如下图所示。
- HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
- Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
- Raft:Raft强一致性算法的具体实现,是etcd的核心。
- WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。
通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。Consul
Consul是强一致性的数据存储,与Zookeeper和etcd不一样,Consul内嵌实现了服务发现系统,所以这样就不需要构建自己的系统或使用第三方系统,另外,Consul还包括节点健康检查和运行在其上的服务。
Zookeeper和etcd只提供原始的键/值队存储,要求应用程序开发人员构建他们自己的系统提供服务发现功能。而Consul提供了一个内置的服务发现的框架。客户只需要注册服务并通过DNS或HTTP接口执行服务发现。其他两个工具需要一个亲手制作的解决方案或借助于第三方工具。
Consul使用Raft算法来保证一致性,与etcd相同,而zookeeper 采用的是 Paxos。相比较而言,Raft算法比复杂的 Paxos 算法更直接。Consul支持多数据中心,内外网的服务采用不同的端口进行监听。多数据中心集群可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟, 分片等情况等。zookeeper 和 etcd 均不提供多数据中心功能的支持。Consul支持健康检查,而etcd不提供此功能。而且Consul支持 http 和 dns 协议接口,比zookeeper 的集成更为简单, etcd只支持 http 协议。另外,Consul官方提供web管理界面, etcd 则没有此功能。
值得注意的是,Consul为服务提供了DNS查询的接口,DNS那一节内容中提到过DNS做服务发现的不足就是它只能提供主机的IP,并不知道端口,除非是使用SRV记录,但是查询SRV记录又需要调用者来支持。Consul为服务解决了查询SRV记录这个难题。一般的环境中DNS是不会查询SRV记录的,但Consul内置了DNS服务器,并提供标准的API支持SRV记录,这就意味着任何轻量级的客户端都可以很方便地查询SRV记录,从而利用DNS实现服务查询的功能。Docker服务发现解决方案
解决方案:Consul与etcd
Consul和etcd都声称自己是一个服务发现的解决方案。然而,它们并不算是一个完整的解决方案, Consul和etcd的核心是使用Raft算法来保证数据一致性,从而实现高可用的配置中心,所以Consul和etcd都实现了很好的服务存储目录,但这仅仅是服务发现解决方案中的一部分。
解决服务发现不能忽略一个重要问题,就是分布式系统中的所有组件要怎么去得到,是自定义服务还是使用现有的软件来集成并使用服务目录。这在Docker社区是一个很受关注的话题,也就是为所有能跑在容器中的应用设计出便携式的解决方案。
Consul确实尝试为服务发现提供一个全面的解决方案,虽然理想的服务发现可能还不只是Consul提供的这些功能,但Consul应该是目前最全面的解决方案,但是Consul在Docker生态系统中并不如etcd受欢迎,比如大名鼎鼎的Kubernetes就是使用etcd。可能是因为HashiCorp这家公司对Docker的关注较少,远不如维护etcd的CoreOS公司。至少目前的情况是etcd是Docker界主流的解决方案。
要解决服务发现的问题,需要有三大支柱,缺一不可。以下结合etcd来详细说明。 - 一个强一致性、高可用的服务存储目录。这一点,Consul和etcd都是基于Raft算法实现,都是很好的服务存储目录。
- 一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
- 一种查找和连接服务的机制。通过在etcd指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个Proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接。
etcd:微服务协同工作
随着Docker容器的流行,多种微服务共同协作构成一个功能相对强大的架构的案例越来越多,透明化地动态添加这些服务的需求也日益强烈。通过服务发现机制,在etcd中注册某个服务名字的目录,在该目录下存储可用的服务节点的IP。在使用服务的过程中,只要从服务目录下查找可用的服务节点去使用即可。如下图所示,以Docker为承载的前端在服务发现的目录下查找可用的中间件,中间件再找到服务后端,以此快速构建起一个动态的高可用的架构。etcd:PaaS平台应用多实例
上面章节介绍了服务发现在PaaS平台的一种应用场景就是实现应用多实例、实例故障重启透明化以及负载均衡。这种情况下,通常采用某种代理服务,这样消费者总是向固定IP地址或动态配置的IP地址的代理发送请求,代理再依次使用服务发现来查找服务提供方信息并重定向请求。这样就可以应对故障恢复和多实例访问的场景:首先,当一个服务实例发生故障时,代理可以从服务实现的列表中选择其他的实例,然后重定向请求到这个健康的实例。其次,应对多实例的情况,代理服务可以从服务实例的列表中选择一个适当的实例发送请求,具体的选择算法由代理服务实现,使用优秀的选择算法就可以实现负载均衡。同时,理论上负载均衡也可以由调用者客户端实现,由客户端决定流量分配到哪个实例。详细架构如下图所示: