前言
服务发现是一个古老的话题,当应用开始脱离单机运行和访问时,服务发现就诞生了。目前的网络架构是每个主机都有一个独立的IP地址,那么服务发现基本上都是通过某种方式获取到服务所部署的IP地址。DNS协议是最早将一个网络名称翻译为网络IP的协议,在最初的架构选型中,DNS+LVS+Nginx基本可以满足所有的RESTful服务的发现,此时服务的IP列表通常配置在Nginx或者LVS。后来出现了RPC服务,服务的上下线更加频繁,人们开始寻求一种能够支持动态上下线并且推送IP列表变化的注册中心产品。
ZooKeeper是一款经典的服务注册中心产品(虽然它最初的定位并不在于此),在很长一段时间里,它是国人在提起RPC服务注册中心时心里想到的唯一选择,这很大程度上与Dubbo在中国的普及程度有关。Consul和Eureka都出现于2014年,Consul在设计上把很多分布式服务治理上要用到的功能都包含在内,可以支持服务注册、健康检查、配置管理、Service Mesh等。而Eureka则借着微服务概念的流行,与SpringCloud生态的深度结合,也获取了大量的用户。去年开源的Nacos,则携带着阿里巴巴大规模服务生产经验,试图在服务注册和配置管理这个市场上,提供给用户一个新的选择。
图1 服务发现
当市面上有多种相似功能的产品出现时,人们往往希望知道这些产品相比较的优劣。产品本身的定位会决定它包含了哪些功能,而产品架构的设计,则会影响产品的性能和可用性等。开源产品的一个优势是开发人员可以去阅读源代码,理解产品的功能设计和架构设计,同时也可以通过本地部署来测试性能,随之而来的是各种产品的对比文章。不过当前关于注册中心的对比,往往停留在表面的功能对比上,对架构或者性能并没有非常深入的探讨。
另一个现象是服务注册中心往往隐藏在服务框架背后,作为默默支持的产品。优秀的服务框架往往会支持多种配置中心,但是注册中心的选择依然强关联与服务框架,一种普遍的情况是一种服务框架会带一个默认的服务注册中心。这样虽然免去了用户在选型上的烦恼,但是单个注册中心的局限性,导致用户使用多个服务框架时,必须部署多套完全不同的注册中心,这些注册中心之间的数据协同也是一个问题。
本文是一篇来自Nacos项目组的文章,虽然是来自Nacos,我们依然力求公正和客观的去看待服务发现所有产品的各个维度。本文不仅仅包含常见服务注册中心产品的对比,还试图从我们的经验和调研中总结和阐述服务注册中心产品设计上应该去遵循和考虑的要点。
数据模型
注册中心的核心数据是服务的名字和它对应的网络地址,当服务注册了多个实例时,我们需要对不健康的实例进行过滤或者针对实例的一些特征进行流量的分配,那么就需要在实例上存储一些例如健康状态、权重等属性。随着服务规模的扩大,渐渐的又需要在整个服务级别设定一些权限规则、以及对所有实例都生效的一些开关,于是在服务级别又会设立一些属性。再往后,我们又发现单个服务的实例又会有划分为多个子集的需求,例如一个服务是多机房部署的,那么可能需要对每个机房的实例做不同的配置,这样又需要在服务和实例之间再设定一个数据级别。
Zookeeper没有针对服务发现设计数据模型,它的数据是以一种更加抽象的树形K-V组织的,因此理论上可以存储任何语义的数据。而Eureka或者Consul都是做到了实例级别的数据扩展,这可以满足大部分的场景,不过无法满足大规模和多环境的服务数据存储。Nacos在经过内部多年生产经验后提炼出的数据模型,则是一种服务-集群-实例的三层模型。如上文所说,这样基本可以满足服务在所有场景下的数据存储和管理。
图2 服务的分级模型
Nacos的数据模型虽然相对复杂,但是它并不强制你使用它里面的所有数据,在大多数场景下,你可以选择忽略这些数据属性,此时可以降维成和Eureka和Consul一样的数据模型。
另外一个需要考虑的是数据的隔离模型,作为一个共享服务型的组件,需要能够在多个用户或者业务方使用的情况下,保证数据的隔离和安全,这在稍微大一点的业务场景中非常常见。另一方面服务注册中心往往会支持云上部署,此时就要求服务注册中心的数据模型能够适配云上的通用模型。Zookeeper、Consul和Eureka在开源层面都没有很明确的针对服务隔离的模型,Nacos则在一开始就考虑到如何让用户能够以多种维度进行数据隔离,同时能够平滑的迁移到阿里云上对应的商业化产品。
图3 服务的逻辑隔离模型
Nacos提供了四层的数据逻辑隔离模型,用户账号对应的可能是一个企业或者独立的个体,这个数据一般情况下不会透传到服务注册中心。一个用户账号可以新建多个命名空间,每个命名空间对应一个客户端实例,这个命名空间对应的注册中心物理集群是可以根据规则进行路由的,这样可以让注册中心内部的升级和迁移对用户是无感知的,同时可以根据用户的级别,为用户提供不同服务级别的物理集群。再往下是服务分组和服务名组成的二维服务标识,可以满足接口级别的服务隔离。
Nacos 1.0.0介绍的另外一个新特性是:临时实例和持久化实例。在定义上区分临时实例和持久化实例的关键是健康检查的方式。临时实例使用客户端上报模式,而持久化实例使用服务端反向探测模式。临时实例需要能够自动摘除不健康实例,而且无需持久化存储实例,那么这种实例就适用于类Gossip的协议。右边的持久化实例使用服务端探测的健康检查方式,因为客户端不会上报心跳,那么自然就不能去自动摘除下线的实例。
图4 临时实例和持久化实例
在大中型的公司里,这两种类型的服务往往都有。一些基础的组件例如数据库、缓存等,这些往往不能上报心跳,这种类型的服务在注册时,就需要作为持久化实例注册。而上层的业务服务,例如微服务或者Dubbo服务,服务的Provider端支持添加汇报心跳的逻辑,此时就可以使用动态服务的注册方式。
数据一致性
数据一致性是分布式系统永恒的话题,Paxos协议的艰深更让数据一致性成为程序员大牛们吹水的常见话题。不过从协议层面上看,一致性的选型已经很长时间没有新的成员加入了。目前来看基本可以归为两家:一种是基于Leader的非对等部署的单点写一致性,一种是对等部署的多写一致性。当我们选用服务注册中心的时候,并没有一种协议能够覆盖所有场景,例如当注册的服务节点不会定时发送心跳到注册中心时,强一致协议看起来是唯一的选择,因为无法通过心跳来进行数据的补偿注册,第一次注册就必须保证数据不会丢失。而当客户端会定时发送心跳来汇报健康状态时,第一次的注册的成功率并不是非常关键(当然也很关键,只是相对来说我们容忍数据的少量写失败),因为后续还可以通过心跳再把数据补偿上来,此时Paxos协议的单点瓶颈就会不太划算了,这也是Eureka为什么不采用Paxos协议而采用自定义的Renew机制的原因。
这两种数据一致性协议有各自的使用场景,对服务注册的需求不同,就会导致使用不同的协议。在这里可以发现,Zookeeper在Dubbo体系下表现出的行为,其实采用Eureka的Renew机制更加合适,因为Dubbo服务往Zookeeper注册的就是临时节点,需要定时发心跳到Zookeeper来续约节点,并允许服务下线时,将Zookeeper上相应的节点摘除。Zookeeper使用ZAB协议虽然保证了数据的强一致,但是它的机房容灾能力的缺乏,无法适应一些大型场景。
Nacos因为要支持多种服务类型的注册,并能够具有机房容灾、集群扩展等必不可少的能力,在1.0.0正式支持AP和CP两种一致性协议并存。1.0.0重构了数据的读写和同步逻辑,将与业务相关的CRUD与底层的一致性同步逻辑进行了分层隔离。然后将业务的读写(主要是写,因为读会直接使用业务层的缓存)抽象为Nacos定义的数据类型,调用一致性服务进行数据同步。在决定使用CP还是AP一致性时,使用一个代理,通过可控制的规则进行转发。
目前的一致性协议实现,一个是基于简化的Raft的CP一致性,一个是基于自研协议Distro的AP一致性。Raft协议不必多言,基于Leader进行写入,其CP也并不是严格的,只是能保证一半所见一致,以及数据的丢失概率较小。Distro协议则是参考了内部ConfigServer和开源Eureka,在不借助第三方存储的情况下,实现基本大同小异。Distro重点是做了一些逻辑的优化和性能的调优。
图5 Nacos一致性协议
负载均衡
负载均衡严格的来说,并不算是传统注册中心的功能。一般来说服务发现的完整流程应该是先从注册中心获取到服务的实例列表,然后再根据自身的需求,来选择其中的部分实例或者按照一定的流量分配机制来访问不同的服务提供者,因此注册中心本身一般不限定服务消费者的访问策略。Eureka、Zookeeper包括Consul,本身都没有去实现可配置及可扩展的负载均衡机制,Eureka的负载均衡是由ribbon来完成的,而Consul则是由Fabio做负载均衡。
图6 客户端侧负载均衡
在阿里巴巴集团内部,却是使用的相反的思路。服务消费者往往并不关心所访问的服务提供者的负载均衡,它们只关心以最高效和正确的访问服务提供者的服务。而服务提供者,则非常关注自身被访问的流量的调配,这其中的第一个原因是,阿里巴巴集团内部服务访问流量巨大,稍有不慎就会导致流量异常压垮服务提供者的服务。因此服务提供者需要能够完全掌控服务的流量调配,并可以动态调整。
服务端的负载均衡,给服务提供者更强的流量控制权,但是无法满足不同的消费者希望使用不同负载均衡策略的需求。而不同负载均衡策略的场景,确实是存在的。而客户端的负载均衡则提供了这种灵活性,并对用户扩展提供更加友好的支持。但是客户端负载均衡策略如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务提供者。
图7 服务端侧负载均衡
抛开负载均衡到底是在服务提供者实现还是在服务消费者实现,我们看到目前的负载均衡有基于权重、服务提供者负载、响应时间、标签等策略。其中Ribbon设计的客户端负载均衡机制,主要是选择合适现有的IRule、ServerListFilter等接口实现,或者自己继承这些接口,实现自己的过滤逻辑。这里Ribbon采用的是两步负载均衡,第一步是先过滤掉不会采用的服务提供者实例,第二步是在过滤后的服务提供者实例里,实施负载均衡策略。Ribbon内置的几种负载均衡策略功能还是比较强大的,同时又因为允许用户去扩展,这可以说是一种比较好的设计。
基于标签的负载均衡策略可以做到非常灵活,Kubernetes和Fabio都已经将标签运用到了对资源的过滤中,使用标签几乎可以实现任意比例和权重的服务流量调配。但是标签本身需要单独的存储以及读写功能,不管是放在注册中心本身或者对接第三方的CMDB。
在Nacos 0.7.0版本中,我们除了提供基于健康检查和权重的负载均衡方式外,还新提供了基于第三方CMDB的标签负载均衡器,具体可以参考CMDB功能介绍文章。使用基于标签的负载均衡器,目前可以实现同标签优先访问的流量调度策略,实际的应用场景中,可以用来实现服务的就近访问,当您的服务部署在多个地域时,这非常有用。使用这个标签负载均衡器,可以支持非常多的场景,这不是本文要详细介绍的。虽然目前Nacos里支持的标签表达式并不丰富,不过我们会逐步扩展它支持的语法。除此以外,Nacos定义了Selector,作为负载均衡的统一抽象。关于Selector,由于篇幅关系,我们会有单独的文章进行介绍。
理想的负载均衡实现应该是什么样的呢?不同的人会有不同的答案。Nacos试图做的是将服务端负载均衡与客户端负载均衡通过某种机制结合起来,提供用户扩展性,并给予用户充分的自主选择权和轻便的使用方式。负载均衡是一个很大的话题,当我们在关注注册中心提供的负载均衡策略时,需要注意该注册中心是否有我需要的负载均衡方式,使用方式是否复杂。如果没有,那么是否允许我方便的扩展来实现我需求的负载均衡策略。
健康检查
Zookeeper和Eureka都实现了一种TTL的机制,就是如果客户端在一定时间内没有向注册中心发送心跳,则会将这个客户端摘除。Eureka做的更好的一点在于它允许在注册服务的时候,自定义检查自身状态的健康检查方法。这在服务实例能够保持心跳上报的场景下,是一种比较好的体验,在Dubbo和SpringCloud这两大体系内,也被培养成用户心智上的默认行为。Nacos也支持这种TTL机制,不过这与ConfigServer在阿里巴巴内部的机制又有一些区别。Nacos目前支持临时实例使用心跳上报方式维持活性,发送心跳的周期默认是5秒,Nacos服务端会在15秒没收到心跳后将实例设置为不健康,在30秒没收到心跳时将这个临时实例摘除。
不过正如前文所说,有一些服务无法上报心跳,但是可以提供一个检测接口,由外部去探测。这样的服务也是广泛存在的,而且以我们的经验,这些服务对服务发现和负载均衡的需求同样强烈。服务端健康检查最常见的方式是TCP端口探测和HTTP接口返回码探测,这两种探测方式因为其协议的通用性可以支持绝大多数的健康检查场景。在其他一些特殊的场景中,可能还需要执行特殊的接口才能判断服务是否可用。例如部署了数据库的主备,数据库的主备可能会在某些情况下切换,需要通过服务名对外提供访问,保证当前访问的库是主库。此时的健康检查接口,可能就是一个检查数据库是否是主库的MYSQL命令了。
客户端健康检查和服务端健康检查有一些不同的关注点。客户端健康检查主要关注客户端上报心跳的方式、服务端摘除不健康客户端的机制。而服务端健康检查,则关注探测客户端的方式、灵敏度及设置客户端健康状态的机制。从实现复杂性来说,服务端探测肯定是要更加复杂的,因为需要服务端根据注册服务配置的健康检查方式,去执行相应的接口,判断相应的返回结果,并做好重试机制和线程池的管理。这与客户端探测,只需要等待心跳,然后刷新TTL是不一样的。同时服务端健康检查无法摘除不健康实例,这意味着只要注册过的服务实例,如果不调用接口主动注销,这些服务实例都需要去维持健康检查的探测任务,而客户端则可以随时摘除不健康实例,减轻服务端的压力。
图8 Nacos的健康检查
Nacos既支持客户端的健康检查,也支持服务端的健康检查,同一个服务可以切换健康检查模式。我们认为这种健康检查方式的多样性非常重要,这样可以支持各种类型的服务,让这些服务都可以使用到Nacos的负载均衡能力。Nacos下一步要做的是实现健康检查方式的用户扩展机制,不管是服务端探测还是客户端探测。这样可以支持用户传入一条业务语义的请求,然后由Nacos去执行,做到健康检查的定制。
性能与容量
虽然大部分用户用到的性能不高,但是他们仍然希望选用的产品的性能越高越好。影响读写性能的因素很多:一致性协议、机器的配置、集群的规模、存量数据的规模、数据结构及读写逻辑的设计等等。在服务发现的场景中,我们认为读写性能都是非常关键的,但是并非性能越高就越好,因为追求性能往往需要其他方面做出牺牲。Zookeeper在写性能上似乎能达到上万的TPS,这得益于Zookeeper精巧的设计,不过这显然是因为有一系列的前提存在。首先Zookeeper的写逻辑就是进行K-V的写入,内部没有聚合;其次Zookeeper舍弃了服务发现的基本功能如健康检查、友好的查询接口,它在支持这些功能的时候,显然需要增加一些逻辑,甚至弃用现有的数据结构;最后,Paxos协议本身就限制了Zookeeper集群的规模,3、5个节点是不能应对大规模的服务订阅和查询的。
在对容量的评估时,不仅要针对企业现有的服务规模进行评估,也要对未来3到5年的扩展规模进行预测。阿里巴巴的中间件在内部支撑着集团百万级别服务实例,在容量上遇到的挑战可以说不会小于任何互联网公司。这个容量不仅仅意味着整体注册的实例数,也同时包含单个服务的实例数、整体的订阅者的数目以及查询的QPS等。Nacos在内部淘汰Zookeeper和Eureka的过程中,容量是一个非常重要的因素。
Zookeeper的容量,从存储节点数来说,可以达到百万级别。不过如上面所说,这并不代表容量的全部,当大量的实例上下线时,Zookeeper的表现并不稳定,同时在推送机制上的缺陷,会引起客户端的资源占用上升,从而性能急剧下降。
Eureka在服务实例规模在5000左右的时候,就已经出现服务不可用的问题,甚至在压测的过程中,如果并发的线程数过高,就会造成Eureka crash。不过如果服务规模在1000上下,几乎目前所有的注册中心都可以满足。毕竟我们看到Eureka作为SpringCloud的注册中心,在国内也没有看到很广泛的对于容量或者性能的问题报告。
Nacos在开源版本中,服务实例注册的支撑量约为100万,服务的数量可以达到10万以上。在实际的部署环境中,这个数字还会因为机器、网络的配置与JVM参数的不同,可能会有所差别。图9展示了Nacos在使用1.0.0版本进行压力测试后的结果总结,针对容量、并发、扩展性和延时等进行了测试和统计。
图9 Nacos性能与容量报告
完整的测试报告可以参考Nacos官网:
https://nacos.io/en-us/docs/nacos-naming-benchmark.html
https://nacos.io/en-us/docs/nacos-config-benchmark.html
易用性
易用性也是用户比较关注的一块内容。产品虽然可以在功能特性或者性能上做到非常先进,但是如果用户的使用成本极高,也会让用户望而却步。易用性包括多方面的工作,例如API和客户端的接入是否简单,文档是否齐全易懂,控制台界面是否完善等。对于开源产品来说,还有一块是社区是否活跃。在比较Nacos、Eureka和Zookeeper在易用性上的表现时,我们诚邀社区的用户进行全方位的反馈,因为毕竟在阿里巴巴集团内部,我们对Eureka、Zookeeper的使用场景是有限的。从我们使用的经验和调研来看,Zookeeper的易用性是比较差的,Zookeeper的客户端使用比较复杂,没有针对服务发现的模型设计以及相应的API封装,需要依赖方自己处理。对多语言的支持也不太好,同时没有比较好用的控制台进行运维管理。
Eureka和Nacos相比较Zookeeper而言,已经改善很多,这两个产品有针对服务注册与发现的客户端,也有基于SpringCloud体系的starter,帮助用户以非常低的成本无感知的做到服务注册与发现。同时还暴露标准的HTTP接口,支持多语言和跨平台访问。Eureka和Nacos都提供官方的控制台来查询服务注册情况。不过随着Eureka 2.0宣布停止开发,Eureka在针对用户使用上的优化后续应该不会再有比较大的投入,而Nacos目前依然在建设中,除了目前支持的易用性特性以外,后续还会继续增强控制台的能力,增加控制台登录和权限的管控,监控体系和Metrics的暴露,持续通过官网等渠道完善使用文档,多语言SDK的开发等。
从社区活跃度的角度来看,目前由于Zookeeper和Eureka的存量用户较多,很多教程以及问题排查都可以在社区搜索到,这方面新开源的Nacos还需要随着时间继续沉淀。
集群扩展性
集群扩展性和集群容量以及读写性能关系紧密。当使用一个比较小的集群规模就可以支撑远高于现有数量的服务注册及访问时,集群的扩展能力暂时就不会那么重要。从协议的层面上来说,Zookeeper使用的ZAB协议,由于是单点写,在集群扩展性上不具备优势。Eureka在协议上来说理论上可以扩展到很大规模,因为都是点对点的数据同步,但是从我们对Eureka的运维经验来看,Eureka集群在扩容之后,性能上有很大问题。
集群扩展性的另一个方面是多地域部署和容灾的支持。当讲究集群的高可用和稳定性以及网络上的跨地域延迟要求能够在每个地域都部署集群的时候,我们现有的方案有多机房容灾、异地多活、多数据中心等。
图10 Nacos的多机房部署和容灾
首先是双机房容灾,基于Leader写的协议不做改造是无法支持的,这意味着Zookeeper不能在没有人工干预的情况下做到双机房容灾。在单机房断网情况下,使机房内服务可用并不难,难的是如何在断网恢复后做数据聚合,Zookeeper的单点写模式就会有断网恢复后的数据对账问题。Eureka的部署模式天然支持多机房容灾,因为Eureka采用的是纯临时实例的注册模式:不持久化、所有数据都可以通过客户端心跳上报进行补偿。上面说到,临时实例和持久化实例都有它的应用场景,为了能够兼容这两种场景,Nacos支持两种模式的部署,一种是和Eureka一样的AP协议的部署,这种模式只支持临时实例,可以完美替代当前的Zookeeper、Eureka,并支持机房容灾。另一种是支持持久化实例的CP模式,这种情况下不支持双机房容灾。
在谈到异地多活时,很巧的是,很多业务组件的异地多活正是依靠服务注册中心和配置中心来实现的,这其中包含流量的调度和集群的访问规则的修改等。机房容灾是异地多活的一部分,但是要让业务能够在访问服务注册中心时,动态调整访问的集群节点,这需要第三方的组件来做路由。异地多活往往是一个包含所有产品线的总体方案,很难说单个产品是否支持异地多活。
多数据中心其实也算是异地多活的一部分。从单个产品的维度上,Zookeeper和Eureka没有给出官方的多数据中心方案。Nacos基于阿里巴巴内部的使用经验,提供的解决方案是才有Nacos-Sync组件来做数据中心之间的数据同步,这意味着每个数据中心的Nacos集群都会有多个数据中心的全量数据。Nacos-Sync是Nacos生态组件里的重要一环,不仅会承担Nacos集群与Nacos集群之间的数据同步,也会承担Nacos集群与Eureka、Zookeeper、Kubernetes及Consul之间的数据同步。
图11 Nacos的多数据中心方案
用户扩展性
在框架的设计中,扩展性是一个重要的设计原则。Spring、Dubbo、Ribbon等框架都在用户扩展性上做了比较好的设计。这些框架的扩展性往往由面向接口及动态类加载等技术,来运行用户扩展约定的接口,实现用户自定义的逻辑。在Server的设计中,用户扩展是比较审慎的。因为用户扩展代码的引入,可能会影响原有Server服务的可用性,同时如果出问题,排查的难度也是比较大的。设计良好的SPI是可能的,但是由此带来的稳定性和运维的风险是需要仔细考虑的。在开源软件中,往往通过直接贡献代码的方式来实现用户扩展,好的扩展会被很多人不停的更新和维护,这也是一种比较好的开发模式。Zookeeper和Eureka目前Server端都不支持用户扩展,一个支持用户扩展的服务发现产品是CoreDNS。CoreDNS整体架构就是通过插件来串联起来的,通过将插件代码以约定的方式放到CoreDNS工程下,重新构建就可以将插件添加到CoreDNS整体功能链路的一环中。
那么这样的扩展性是否是有必要的呢?举一个上文提到过的例子,假如要添加一种新的健康检查方式,连接数据库执行一条MySQL命令,通常的方式是在代码里增加MySQL类型的健康检查方法、构建、测试然后最终发布。但是如果允许用户上传一个jar包放到Server部署目录下的某个位置,Server就会自动扫描并识别到这张新的健康检查方式呢?这样不仅更酷,也让整个扩展的流程与Server的代码解耦,变得非常简单。所以对于系统的一些功能,如果能够通过精心的设计开放给用户在运行时去扩展,那么为什么不做呢?毕竟增加扩展的支持并不会让原有的功能有任何损失。
所有产品都应该尽量支持用户运行时扩展,这需要Server端SPI机制设计的足够健壮和容错。Nacos在这方面已经开放了对第三方CMDB的扩展支持,后续很快会开放健康检查及负载均衡等核心功能的用户扩展。目的就是为了能够以一种解耦的方式支持用户各种各样的需求。
尾声
本文并没有把完整介绍Nacos的所有功能,包括Nacos支持的DNS协议,打自定义标等能力。Consul也并没有在本文做重点比较,因为Consul实际上是和Nacos比较相似的产品,虽然Consul目前的主要发展方向放在了Service Mesh,但是Consul最初支持的服务发现和配置管理,也是Nacos的两大功能。与Consul的详细比较将会通过另外一篇文章单独介绍,虽然Nacos在Consul之后以与之相似的部署架构开源,但这并不意味着Nacos在功能和架构上也模仿Consul,Nacos的架构和功能是由阿里巴巴内部十年的运行演进经验得来,所以二者的比较也一定会让大家更加了解他们的定位和演进方向是完全不一样的。
为了能让大家对注册中心产品整体的差异有一个快速的总览,我们制作了下面这个表格:
Nacos已经在4月10号发布GA版本,后续将会以和社区共建的方式,持续输出新的功能,在服务发现和配置管理这两大领域继续深耕,期待与大家一起建设出最好用的服务发现和配置管理平台。
|