万字长文,助你吃透 Eureka 服务发现机制!

用户头像
攀岩飞鱼
关注
发布于: 2020 年 06 月 02 日
万字长文,助你吃透Eureka服务发现机制!

微服系列文章



第1篇: “四个维度” 讲明白什么是微服务!

第2篇: 微服务涉及的技术生态有哪些?

第3篇: 微服务-为什么要有服务发现与注册?

第4篇: 万字长文,助你吃透Eureka服务发现机制!



前言



在前面几篇系列文章中,我们对微服务的基本概念微服务的技术生态等,做了简单的阐述。对于分布式系统或微服务,服务发现是最基础、最核心、最不可缺的能力。然而许多人对服务发现有一种说不清,道不明,似懂非懂的感觉。



这都无可厚非,毕竟服务发现涉及到分布式相关的知识,而分布式的知识浩如烟海,晦涩难懂,如:分布式协议、分布式事务,资源协调,Zookeeper等,光这一堆的概念就让人望而却步。我们知道,学习一门技术,如果不深入理解背后要解决的问题与设计原理,就很难做到灵活运用,触类旁通。



这篇文章我们就从服务发现的基本概念入手,再结合Eureka的具体实现,来深入分析服务发现背后要解决的问题与设计原理。这不仅是我们阅读这个系列文章,学习微服务,学习分布式系统的必经之路,同时很多的设计理论也可以为我们学习其他技术(如:Hadoop,区块链等)打下坚实的基础。



文章比较长,大约有1万3千多字,建议大家打开目录进行阅读,现在我们就以下面四个问题为线索,来全面解析Eureka服务发现的原理机制。



  • 1)如何理解服务发现这一概念?

  • 2)服务发现要解决的问题是什么?

  • 3)Eureka是如何设计服务发现?

  • 4)Eureka如何保证高可用与数据一致?



一、如何理解服务发现这一概念?



一个概念往往是对某一个领域,某个问题的高度概括,要理解服务发现这个概念,我们 从分布式系统说起,这里我不想引用百度词条或专业的术语给分布式系统下一个定义,这容易让人产生困惑,我引用一句更通俗易懂的古语来对分布式系统做个比喻:



“当一头牛拉不动车的时候,不要试图去寻找一头更强壮的牛,而是要用两头牛来拉”



因此,从狭义上来讲分布式系统可以理解为:由诸多应用程序协同合作来完成任务的一种工作模式系统。这里的任务可能是一个商品的下单操作,可能是一个超大集合的排序计算,可能是存储一个超大的文件,也可能是绘制团战游戏的实时画面。总之,这个任务由于各种条件限制,很难由单个程序独立完成,而需要由多个程序一起协同完成,这种工作模式我们用计算机语言总结有4个特征:



  • 1)协同工作的各个程序运行在由区域、机房、机柜,交换机、网线连接的计算机上;

  • 2)程序之间通过一次或者多次远程调用或数据传递来完成某项任务;

  • 3)程序可能会奔溃,硬盘可能会损坏,CPU可能会跑满,网络可能会抖动;

  • 4)任务成功与否由多个程序共同决定,一个程序失败,可能整个任务都失败;



看了以上4个特征,是不是觉得分布式系统的工作模式有些不靠谱,这种工作模式太复杂、充满不确定性,非常脆弱这有点像马戏团小丑的杂技表演,脚下踩着独轮车,头上顶着碗,双手还不停的抛着皮球,这种表演很脆弱,只要任何一个小地方出现闪失,比如某个球没接住掉到地上,那表演就算失败。





分布式系统也是如此,所有环境,所有程序只要某个环节出现一点问题,都会影响整个任务的执行结果。面对这种不确定性与复杂性,计算机科学家们无论在学术理论还是工程实践方面,想了很多办法也沉淀了很多成果,包括:



  • 抽象提炼问题,如互斥性问题,幂等性问题,拜占庭将军问题等;

  • 总结升华定律,如CAP理论,BASE理论等;

  • 解决设计算法,如Paxos算法,ZAB算法,分布式哈希算法等;

  • 规范定义概念,如服务发现,分布式事务,负载均衡,配置中心等;

  • 开发工程工具,如Zookeeper,Eureka,GTS,Apollo等;



因此我们看到,服务发现在整个分布式生态中的位置,它是整个分布式生态中一个十分重要的概念,这个概念代表着分布式中某一个具体的问题域,围绕这一概念还涉及分布式生态中其他很多知识,如:CAP的理论基础,ZAB的算法思想,Zookeepr或Eureka的工程工具等等。



二、服务发现要解决的问题是什么?



上面我们说到,服务发现是为解决分布式环境中某问题域而产生的一个概念,那具体是针对哪些问题域,具体要解决哪些问题呢?



服务发现要解决的第一个问题就是--解耦、屏蔽程序之间 IP与端口的依赖。 在分布式系统工作特征中我们说过“程序之间通过一次或者多次远程调用或数据传递来完成某项任务"因此,调用程序方首先要知道被调用程序在网络中的标识,我们知道在计算机网络里程序的标识是用IP+端口来实现,但这种方式存在两个问题。



  • IP与端口没有任何特殊含义不便于理解、不便于记忆;

  • 当程序部署的IP与端口改变,程序的调用者也要同步修改;



而通过服务发现,可以将服务之间IP与端口的依赖转化为服务名的依赖,服务名可以根据具体微服务业务特征来命名,因此,屏蔽、解耦服务之间IP与端口的依赖是服务发现要解决的第一个问题。



服务发现要解决的另一个问题是--动态管理程序的状态在分布式系统工作特征中我们说过,“程序可能会奔溃,硬盘可能会损坏,CPU可能会跑满,网络可能会抖动",因此,在分布式环境中程序的状态是随时发生变化的,而且从某种程度上来说这种变化不可预测,谁也不知道下一秒某个程序会不会挂掉,一旦某个程序出问题,调用者还未得到通知的话,可能会发生多米勒骨牌式的连锁反应。



而服务发现可以对服务的状态进行实时管理,当程序状态发生改变时,可以第一时间通知到程序的使用者,我们可以从如下两个方面去理解这种管理能力。



  • 首先,程序要能彼此知晓对方的信息与状态;

  • 其次,当对方信息与状态发生变化时要能及时知晓;

三、Eureka是如何设计服务发现的?



前面我们对服务发现的概念与要解决的问题做了简单的介绍,这都是为理解服务发现的设计原理做的铺垫。下面我们就以Eureka服务发现为例,来分析一下服务发现是如何设计的。



1、引用一个统一管理中心



服务发现的目标是要抽象程序标识并对程序状态进行动态管理。一旦涉及到管理,我们第一个想到的是什么?集权、集中、统一,设置一个机构等等。Eureka也是如此,要管理这些程序,先要有一个统一的管理中心,这个统一的管理中心就是--注册中心。



因此,注册中心可以认为是Eureka的大脑,肩负着Eureka的各项管理,协调职能



使用统一的管理中心这一设计思想,在分布式架构中几乎无处不在,如Hadoop,Zookeeper等等。而作为对比的非统一中心设计思想,就是比特币,比特币中的节点程序除了在功能上有所区分(如钱包节点,挖矿节点)在职能上没有任何特殊的中心节点。



2、定义四个基础概念



理清服务端、客户端、生产者,消费者四个基本概念



将注册中心作为服务发现中的管理者是个不错的主意。就像公司一样,从职责上对员工进行划分,比如:出钱的--老板,找活干的--产品经理,监督干活的--项目经理,正真干活的--程序员,背锅的--初级程序员。



为了理解沟通方便,服务发现对管理者与非管理者也进行了划分,负责管理的注册中心--服务端,负责具体完成任务的程序--客户端这就是服务器端与客户端的概念。



通过服务端与客户端的概念,从职责上对程序进行了区分,但是,对于具体干活的客户端,还有一个问题:那就是它们之间的调用关系错综复杂,能不能从调用关系这个维度对客户端再进行区分呢?



答案是肯定的,因此,对于客户端又分为生产者,消费者的概念。例如,一个任务需要两个程序协调合作,程序A调用程序B,调用方也就是程序A叫--消费者,被调用方也就是程序B叫--生产者。



生产者与消费者只是概念上的区别,它们并不是单独的两类程序而是一类,都属于客户端。一个程序是不是生产者与消费者并不是绝对的,可能是生产者,可能是消费者,也可能既是生产者也是消费者,这取决于我们看待的角度。



服务器端、客户端、生产者、消费者



服务端,客户端、生产者、消费者这四个概念非常重要,在涉及服务发现的文章中(包括本文,网上或者相关书籍)都会被大量重复提及。我们要结合上下文去理解这些概念,否则碰到一段话就很难看懂,甚至会被误导。



3、抽象七个基本运行流程



理顺了相关的概念之后,下面我们重点来介绍Eureka服务发现的基本运行流程,这会使我们对Eureka有一个初步的整体认识,这是理解后面更复杂内容的前提,也是进一步研究源码的基础。



我将Eureka基本运行流程抽象为七个步骤,当然这七个步骤并不是严格的顺序执行,更不是官方的说法,只是让我们更好的理解Eureka的工作流程,这7个步骤分别如下:



  • 1、客户端发起服务注册;

  • 2、服务端保存信息至注册表;

  • 3、客户端定时发送心跳请求;

  • 4、服务端服务剔除与自我保护

  • 5、客服端发送服务下线请求

  • 6、客户端获取服务端注册表;

  • 7、客户端整合服务发现;



Eureka整体运行流程



1、客户端发起服务注册



客户端向服务器发送请求,将自身的相关信息提交给服务端;



对于这一步骤很好理解,即当客户端程序启动时,先要加入到分布式系统中,第一步就是要向服务器提供自身的信息并告诉服务端我来了。这跟婚介所有点像,你想在婚介所找对象,就得先到婚介所登记提供自己的详细信息,只有这样女孩子才可能了解到你。在服务发现中这一过程就叫 --服务注册



那Eureka是如何实现服务注册这一过程的呢?



服务端提供一个对外注册接口,用来接收客户端的注册请求,服务端启动后会一直监听这个接口,等待客户端调用;



客户端在启动时,先找到服务端与自身的配置信息(分区,名称,IP,端口等),再调用服务端的注册接口将相关信息发送给服务端;



关于配置信息可能在客户端程序的配置文件中,也可能在统一的配置中心,配置中心在这个系列文章中我们会详细介绍。



客户端服务注册子流程



2、服务端保存信息至注册表



服务端将客户端提交的注册信息保存至本地内存注册表;



这一步骤很简单,也没有任何流程与通信。但这里的注册表可能是Eureka中最核心,最关键的概念,注册表的作用就跟数据库一样,不管是我们正在讨论的7个基本运行流程,还有后面要说的数据同步操作等,其本质就是围绕着对注册表添加、更新、删除、获取,同步等一系列操作。



注册表这么重要,那注册表到底长什么样,存储了哪些数据呢?其实,Eureka注册表是一个双字典结构的数据, 服务发现的目标是标识与管理服务的状态,因此注册表里面存储的有服务标识,服务基本信息,服务状态等数据,如下图所示:



注册表双字典结构



本篇文章,我本不打算加入任何源代码,后面计划是写一篇专门对Eureka源码分析的文章。但是,这里要理解注册表的双字典结构,最高效的方式还是直接看看源码的定义,如下代码:

ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();



为什么是字典结构,而且还是双字典结构呢?在大型系统中一个业务服务由多个实例程序进行负载,当对某个实例程序信息进行操作时,只要通过服务标识Key找到多个服务实例,再通过服务实例标识Key找到该实例信息。



这种操作的时间复杂度为O(1)效率非常高。当然,Eureka为了追求极致的效率,还配合使用了一些缓存机制,有机会我们会详细分析。



3、客服端定时发送心跳请求



客户端定时向服务端发送请求告诉服务端我还活着



为什么要有这一步骤?如果客户端只是在启动时注册自己,在后续的程序运行中,程序宕机或者其他异常,服务端是不知道的,服务端就达不到实时状态管理的目的。



因此,需要客户端不时的主动发送请求告诉服务端-我还活着,服务端再根据上报情况进行判断,哪些程序正常,哪些程序不正常,这种主动上报的方式在Eureka中叫 --服务续约。



在具体实现上,Eureka在服务端开放一个续约接口,用来接收客户端的心跳请求。客户端启动一个定时器,定时调用服务端的续约接口,服务端接收到请求后,更新自己注册表中服务实例的续约时间,如下图所示:



心跳续约机制



在分布式系统中这种定时心跳续约的方式叫--Lease 机制,Lease机制有很多应用场景,比如Eureka中服务剔除,密钥更新,分布式文件系统的顺序号等。



其实在大型的分布式环境下,这种简单的心跳续约还不够,比如有一个程序CPU跑到了98%,这台服务器已经在崩溃的边缘,但是它并没有挂掉,心跳续约也正常,服务端会判断是正常服务,这样在负载分配策略上就会有很大的问题,关于负载均衡,在这个系列文章中我们会单独介绍。



4 、服务端服务剔除与自我保护



服务端在一段时间内未收到客户端心跳请求,就从注册表中移除该客户端



上面我们说了心跳续约,客户端主动续约自己状态,服务器端维护最后的续约时间,这样做目的只有一个,就是让服务端来判断客户端服务是否运行正常。



如果服务器端在一定时间(默认90秒)内没有收到客户端的心跳续约,那么服务端就认为该客户端可能挂掉了,就将客户端实例从注册表中移除,消费者客户端便拿不到异常的服务实例,这个过程就叫--服务剔除。



服务自我保护



关于服务剔除还有一种特殊情况,当网络发生了故障,客户端与服务端暂时无法正常通信。但是客户端实例运行正常,生产者与消费者之间通信也正常,服务消费者也可以调用服务生产者。如下图所示:



如果按正常的服务剔除逻辑,那些没有正常续约的客户端,会被从注册表中剔除掉,这样会影响那些正常网络内的客户端。对此,Eureka有一种自我保护模式,默认是打开状态,具体操作我总结为如下几点:



服务端判断15分钟之内超过85%的客服端都没有心跳续约,则自动进入自我保护状态;

进入自我保护状态后,服务端不再剔除没有继续续约客户端;

进入自我保护状态后,服务端只接受新客户端的注册操作与服务查询操作;



自我保护模式是Eureka设计的一种自我容错策略,它将网络异常或抖动对系统的影响降到最低,增强了系统的稳定性与健壮性。



5、客服端发送服务下线请求



如果客户端正常关闭,则向服务端发送已下线请求,服务端直接从注册表中移除该客户端



Eureka运用服务心跳续约、服务剔除来鉴别、排除异常的客户端服务。那对正常关闭的客户端呢?比如做灰度发布时主动关停部分服务。



在这种情况下,如果等待服务续约失败再执行服务剔除操作,整个流程就太复杂、效率也太低,毕竟从客户端程序关闭,到心跳续约停止,再到服务器端检测异常剔除,会有一定的延迟时间。



因此, 当客户端正常或主动关闭时,则主动向服务端发送下线请求,服务端收到请求后从注册表中移除该客户端,这一系列操作就--服务下线。



6 、客户端获取服务端注册表



客服端定时向服务端获取注册表信息,并保存至本地注册表中



以上说的几个步骤,都是在讲生产者如何与服务端进行通信。那对于消费者呢?消费者跟服务端具体有哪些操作?



对消费者来说与服务端之间的通信非常简单,即通过服务端获取生产者的服务注册信息,来完成后续的业务调用。



在设计上消费者本地也有一个注册表,还有一个定时器,每隔一段时间客户端向服务端发起请求,用来获取服务端注册表信息,并将获取来的注册表信息,同步到自己的本地注册表中,如下图所示:





这里还有一个细节问题,如果客户端本地注册表中的数据,跟服务端获取来的数据存在冲突怎么办?关于这些问题我们在后面章节高可用与数据一致性时会讲到。



7 、客户端整合服务发现



客户端消费者从本地内存中获取生产者服务信息,并执行后续的调用操作;



对于这一步骤,严格来讲不算是Eureka的范畴,但如果少了这一步,我们理解服务发现就会大打折扣。



文章的开始我们就提到服务发现要解决的问题,但我们一直在讲Eureka有哪些概念、是如何设计的等等,却没有讲服务发现具体是如何解决这些问题,即应用程序如何整合服务发现,最终完成服务间相互调用。



这好比我们有一把斧头,我们总是夸它用了多好的钢铁,是如何一步一步制造出来的,但却不告诉人们如何用它来砍树,也好比画了一条龙,最后忘记画眼睛。



大家目前只要知道有这么回事就行,在这个系列文章中,我会结合服务网关Zuul来讲解这块内容。



最后,我将Eureka整体运行流程的7个步骤画到一张图上,如下图所示:



Eureka整体运行流程



四、Eureka如何保证高可用与数据一致?



前面的篇幅,我们从服务发现运行流程的角度,将Eureka核心设计抽象为7个流程步骤,并对每一步骤进行了简单的阐述。但是,我们忽略了一个重要问题,我们讨论的所有流程中,注册中心只有一个,并未涉及到多个注册中心的内容。



事实上Eureka的注册中心是支持集群运行的,即运行多个服务端实例,这些实例可以部署到不同的区域、机房。这样可以横向扩展服务发现的规模,当某个服务端实例挂掉之后,整个服务发现还能正常运行,保证系统的高可用。



集群运行,高可用无论在设计上还是实现上都不是一件简单事情,要付出相当大的代价,在某些条件下甚至只能选择妥协,如google在最早的hadoop1.x的版本中,就只设计了单个NameNode,Hadoop1.x存在单点故障问题。



那针对Eureka支持多注册中心的集群运行方式,具体有哪些挑战呢?至少有两个问题要解决:



  • 1、从服务器端来说,如何保证各个服务端数据的一致性;

  • 2、从客户端来说,应该选择哪个服务端进行通信;



接下来的篇幅我们都将围绕着这两个问题展开。



1、一个关于CAP理论的故事



我们刚才说到了系统高可用,数据一致性,这些话题不仅仅是Eureka服务发现要面临的问题,而是所有分布式系统都要面临的问题,为此我们要先理解一个定律--CAP定律



为什么有这个理论?这个定律说的到底是什么?让我们从下面的一个故事说起。





在一个大型分布式系统中,多个节点程序部署在网络连通的5个机房内,节点们各司其职,系统就这样一直安静、稳定的运行着.......



然而,一只老鼠的出现打破了一切美好的现状。



某天夜里,一只饥饿的老鼠寻找食物,闯进了A机房防潮隔离层内,并把机房连通外界的光缆啃断了。



显然,A机房与其他机房失去了连接,整个网络也被分成了两块区域,一块A机房区域,一块非A机房区域(如下图所示)





情况出现之后,团队围绕系统是否还要正常工作,展开了一场头脑风暴。



老板说:“ 老鼠啃网线这很正常,我们不能对老鼠有过分的期望,必须要应对这种突发事件,因此,系统必须要正常工作,用户不关心老鼠与机房的任何事情”。叮叮......老板电话响了,“你们先研究讨论一下,相信你们一定能解决这个问题”,于是,老板拿着手机走出了会议室。



程序员小周眉头紧皱:“这可能有点困难,因为A机房有个程序,里面存放着非常重要的数据,其他程序都得用到这个数据,缺少了这些数据,整个系统肯定是跑不起来”



“我不管,这是老板的要求!今天必须解决这个Bug”。有人卖掉一个肾,切掉一半胃,都能活得好好的,为什么我们系统被老鼠吃掉一段网线就不能运行呢?”。产品经理理直气壮的说到。



会议室被一股尴尬的气氛笼罩着,程序员小周实在忍不住想笑。 "产品经理居然认为这是一个bug,关键是他的这个比喻.....莫非是得到了郭德纲的真传?" 。但是 ,看着产品经理一本正经的表情,他用拳头捂住嘴巴故意咳嗽了几声,硬是把笑意憋了回去。



架构师老陈翘着二郎腿,不停的摸着他那非洲稀疏草原似的头顶,一副若有所思的样子,大家把目光都投向他,等待着他的回答:“解决方案也是有的,只要我们将A机房的程序,在每个机房都部署一份,同时保证这些程序之间的数据一致,如果A机房不可用,那我们就用其他机房程序里的数据”。



小周仰望着天花板,突然,他猛的拍打了一下自己的大腿,从椅子上跃了起来,撸起衬衫的袖子,这件蓝色方格子衬衫,他连续穿了不少于3个星期,他说到:“是啊,我怎么没想到呢?我们把数据同步到其他机房不就OK了吗?”



还没等大家反映过来,小周已冲出会议室,只听见办公室里“嗒嗒......嗒嗒......“,青轴键盘发出悦耳的歌声。



10个小时之后,程序修改大功告成,发布上线,与此同时,客服办公室的电话已被打爆.......



用户反映系统某个操作完全没有反应,团队又开始了新一轮的头脑风暴。



这次老陈先说话了,“小周,你那个代码是怎么写的,改出bug了啊”。 程序员小周开始为自己辩解到:“没理由!不至于啊! 你不是说要将A机房的数据同步到其他机房,还要保证数据一致性吗,我就在处理A机房数据操作时,加入了同步其他机房的代码,为了确保数据一致,我让所有的机房更新数据返回成功之后,再执行下面的操作。



老陈继续摸着他那非洲稀疏草原似的头顶,接着问了第二个问题,那假如C机房的网线也被老鼠啃了,对C机房接口的调用不会成功也不会失败,而是出现第三种状态--超时,那你的处理有什么问题呢?



程序员小周工作不到2年,有点愣头青,干活积极,思维敏捷,通过老陈的一番点拨,他立马就明白了问题所在。他回应到:“是啊,为了保证数据一致性,我将所有接口返回成功作为条件,结果就是,一旦有某个接口返回失败或者超时,整个业务操作就没法进行了”。



小周接着说道:“为了保证网络分区后系统能正常使用,就得将数据同步到更多的机房并保证数据一致,为了保证数据一致,就得保证每个机房都同步操作成功,但是,反过来假如有一个机房返回失败或超时,整个操作就算失败,这又影响了系统的可用性 ”。分区可用,数据一致,可用性这三者 就像先有蛋,还是先有鸡的问题,好南啊”!



老陈满意的点了点头心想到--浪可谓啊!他又清了清嗓子对小周说到:“你理解得很到位,在分布式系统中,分区容错性,数据一致性,可用性这三者是一个矛盾体,就好像我们不能“又想当什么什么,又想立牌坊”,在这三者之间,我们必须要有所取舍。而且,这是有理论作为依据的,那就是CAP理论。



故事到这里就讲完了,纯属瞎编,我们再来看看CAP理论的内容:



  • 一致性(Consistency) (数据在多副本的情况下,对数据进行更改操作时,可能因为机器,网络或其他原因,一部分副本更改成功,一部分副本更改失败,导致数据的不一致)



  • 可用性(Availability)(在任何时候客户端对集群进行读写数据操作时,集群必须在一个规定的延迟内返回结果——但是,不能保证返回的数据是最新的)



  • 分区容错性(Partition tolerance)(所谓分区容错是指:当网络发生故障,整个集群被划分为多个无法相互通信的区域时,系统仍然可用)





总结来说就是,数据存在的节点越多,分区容忍性越高,但是复制更新的节点就要越多,复制的节点越多一致性就越难保证,为了保证一致性,更新所有节点数据所需要的时间就会越长,时间越长系统可用性又会降低。



对于一致性、可用性、分区容忍性这三者,我们很难同时都满足,只能有所侧重的去满足其中1点或者2点,这就是CAP理论的核心内容。



CAP理论为分布式系统设计提供了理论依据,它告诉我们不可能设计一个十全十美,包罗万象的分布式系统,就跟能量守恒定律告诉我们不可能制造出永动机一样。系统架构设计的精髓在于权衡,取舍,与适应。



2、可用性与一致性的取舍



刚才我们通过一个故事对CAP理论做了简单的分析,那Eureka的设计是如何遵循CAP定律的呢?



首先对于服务发现,网络分区客观存在,我们没法改变,出现网络分区后系统肯定也要能正常运行,也就是上面故事中老板的要求。因此,这里我们讨论CAP理论时,是以分区容错性为前提,然后再来谈系统是CP类型,还是AP类型,即在分区容错性基础上再权衡一致性(C)与可用性(A)。



Eureka选择的是AP优于CP的原则,即可用性优于一致性,为了更好的理解Eureka的AP设计原则,以及CP与AP之间的差异,我们将Eureka与Zookeeper做一个对比分析,看看它们在CP与AP之间是如何取舍的。



​Zookeeper CP系统



如上图所示,整个ZK集群部署在两个机房内,其中ZK Leader部署在机房1,两个机房还分别部署了ZK Follower、生产者与消费者。



由于某些原因导致两个机房的网络中断,即出现了网络分区。



对于Zookeeper CP系统,此时当机房2中的生产者B进行注册时不会成功,因为,CP系统要求的是强一致性,ZK要保证数据成功同步到其他的Follower节点,也就是机房2中Follower的数据要成功同步到机房1 的Leader之后,才认为注册成功。显然,此时做不到,因为两个机房的网络中断了。



然而,机房2中的生产者与消费者之间连接正常,它们应该是可以在机房2中注册成功,并完成远程调用。但是,Zookeeper为了集群数据的强一致性,牺牲了机房2的可用性,这就是CP系统的特征,优先考虑数据一致性。



我们再来看Eureka AP 可用性优先会如何处理?当网络分区后,Eureka会先允许机房2内的服务注册成功,机房2中消费者也可以调用生产者,网络分区后机房2中的可用性丝毫不受影响, 其代价就是机房1与机房2中的数据会不一致,这就是AP系统的特征,为了可用性牺牲部分数据一致性。



那Eureka为什么要选择AP呢?个人认为,服务发现中服务器的数据属于客户端的元数据(IP,端口、状态),元数据并不会像业务数据那样频繁变化,导致数据不一致的几率没那么高,拿到一份过期或不一致的数据,总比拿不到任何数据要强。并且,当客户端拿到服务数据进行服务调用时,自身也会做相关其他的处理,比如:重试、熔断、负载等,这些我们也会在这个系列文章中介绍。



3、服务端如何实现数据同步?



我们绕了一大圈讲CAP理论,讲Eurkea如何选择AP等等,下面我们就具体来分析Eureka是如何进行服务端数据同步的。

1、Peer To Peer 同步模式



一般而言,分布式系统中的数据同步模式可分为两种:主从模式,对等模式。



  • 主从模式(Master-Slave):在集群有一个主副本与多个从副本,所有数据的写操作都提交到主副本上,最后再由主副本更新到其他从副本,而从副本负责所有的读操作。该模式主副本节点要承担所有的写入压力,可能会成为系统的瓶颈,但是数据的一致性能得到保证。



  • 对等模式(Peer To Peer):在集群中不分主从,任何副本都可以进行读写操作,副本与副本之间实现数据同步,这样的优点是:不存在任何单点的写操作压力,缺点是:每个副本都可以进行写操作,数据同步与解决数据冲突就成了一个棘手的问题。



2、数据同步操作



上面讨论的两种数据同步模式中,Eureka选择对等模式来实现服务端集群注册表的信息同步。具体来看一个例子,假如Eureka服务端集群由4个服务端构成,当某个客户端跟服务端A进行通信时(注册,下线,剔除),该服务端A注册表会发生变化,此时服务端A会将这些操作同步到其他B、C、D3个服务端,从而保证整个集群的数据一致,如下图所示:



还有一种情况,当新的服务端准备加入集群时,集群中其他服务端注册表已经有完整的注册信息,如果新服务端再等待其他服务端的数据同步操作,那只能获取部分数据,新服务端的数据就会丢失,因此,当新的服务端加入集群时,先要去某个服务端获取全部的注册表信息。





这里的操作是不是很熟悉,数据同步的操作跟我们讲Eureka7个核心步骤的操作很相似,是的,服务端集群虽然在职责上叫服务端,但是为了实现集群数据同步,在操作上跟客户端的那些操作(如:服务获取,注册、下线、剔除等)又是一样的。



其实,Eureka服务端要依赖客户端,但是这里讲的客户端不能直接等同于上面在“定义四个基础概念”所说的客户端。



那里所讲的客户端是从业务角度去理解,即客户端是包含业务代码的程序,具体干活做业务操作的,而这里的客户端是从集群通信的角度来说,因为Peer To Peer 模式 ,没有中心服务端,也就没有客户端,所以,从通信上来说Eureka的服务端同时又是客户端。



某些概念放在不同的上下文中,所代表的内容与含义会完全不一样,因此要理解这些概念,我们一定要先看看上下文说的是什么。



3、如何避免死循环?



我们还是回到刚才的数据同步问题,大家是否思考过这些问题,假如:客户端对服务端A进行了下线操作,服务端A将操作同步到B、C、D其他3个服务端,当服务端B接收到同步过来的下线请求后,会不会再将该操作又同步到其他的服务端,从而使同步陷入死循环呢?



答案是不会,Eureka会区分正常的客户端请求与服务端发起的数据同步请求,对于任何服务端发起的数据同步请求,Eureka不会再进行其他同步操作,从而避免数据同步出现死循环。



具体的做法是Eureka在http请求头中加入特殊的标识,用来区分正常的客户端请求与数据同步请求。



4、如何解决数据冲突?



刚才说了数据同步的流程与如何避免数据同步陷入死循环,但是在数据同步的过程中还有一个重要的问题要解决--那就是数据冲突。数据冲突有两层含义:



  • 第一是:如何标识数据冲突,即谁的数据新,谁的数据旧;

  • 第二是:当发生了冲突后怎么使用数据,如何解决冲突;



1)使用版本号解决数据冲突



对于第一个问题很好解决,一般我们给数据加一个版本号就行,如时间戳,只要有任何数据更新操作,就更新时间戳,最后更新数据的时间戳最大,也就是最新的数据,上面讲Eureka中注册表中时间信息就是做此用途的。



对于第二问题就要稍微麻烦一些,我们可以从一个场景来说:当服务端A向服务端B同步数据时,发现A,B都有这条数据,但是各自数据的时间戳不一样,即发生了冲突,那无非就两种情况 A新B旧,B新A旧



先看第一种情况 A新B旧, 如下图:

当服务端A向服务端B发起同步请求时,服务端B发现自己不是最新的数据,于是返回一个404状态,服务端A发现返回状态是404时,则从新发起一个服务注册同步请求,服务端B接收到注册请求后则同步数据。



再来看第二种情况 B新A旧 ,如下图:



当服务端A向服务端B发送同步请求时,服务端B发现自己的数据比服务端A的要新,于是返回409状态,告诉服务端A我的数据是最新的,服务端A收到返回409时,则向服务端B发起获取数据的请求,服务端B返回最新的数据。



2)客户端定时获取注册表并检测数据



刚才我们说的是通过版本号来解决数据冲突问题,这有点像事后补救措施,在同步过程中发现数据冲突再解决,但是如果某个数据同步操作失败了,数据冲突还是会存在。



其实Eureka还有一种更主动的方式来解决数据冲突,那就是在客户端通过心跳请求来获取服务端注册表数据,如果发现与本地数据存在冲突,则让有冲突的客户端从新执行注册操作。



4、客户端选择哪个服务端通信?



我们再来回答这个章节的第二个问题 :“当有多个服务器端时客户端选择与哪个服务端进行通信?”



优先使用默认配置



首先,客户端可以配置默认优先的服务端,比如客户端与服务端在同一分区或同一机房时,我们可以指定客户端优先与这些服务端通信。



从可用服务端列表中选择



如果没有配置默认优先的服务端,客户端同时还维护着两个服务端列表,一个不可用列表,一个可用列表,当要与服务端通信时,先从可用列表中选择一个服务端,并尝试与该服务端进行通信,如果连续通信3次都失败,则从可用列表中选择一个新的服务端从新尝试。



随机打乱可用服务端列表



服务端列表的顺序默认是配置文件的顺序,每次取服务也变成有顺序的了,这样导致客户端会选择排在前面的服务端,从而导致服务负载严重失衡,为此客服端设置了一个定时器,定时器会定时随机打乱服务列表的顺序,从而保证客服端随机选择服务端,使各个服务端的负载保持均衡。



最后,我将Eureka的整体运行机制放到一张图上,如下图:



Eureka整体运行机制



5、Eureka分区(Region与Zone)



关于Eureka的高可用内容,还有一个问题需要讨论,那就是当系统规模比较大,用户在地理上分布广泛时,一般通过将服务部署到多个区域,多个机房,来保证系统高可用、容灾,数据靠近用户等,那Eureka具体是如何支撑分区域,分机房部署与调用的呢?



当服务调用发生在多个机房之间,我们希望同一个机房内的服务优先调本机房内的服务,只有同机房服务不可用时,再考虑调用其他机房的服务,这样可以最大限度的降低延迟与开销。



Eureka通过Region与Zone实现了服务的分区管理,Region与Zone这两个概念都来自亚马逊的AWS。



  • region:可以理解为地理上的分区,比如亚洲地区、华北地区,北京地区等等,地区没有具体大小的限制。根据具体的情况自行合理划分region。



  • zone:可以理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2分别代表机房1,机房2.



对于Eureka的分区管理(Region、Zone)可以总结归类如下几点:



  • Eureka通过分区管理,实现不同区域不同机房服务之间的就近调用,以降低延迟;

  • Region表示区域、Zone代表机房,它们之间是一对多的关系;

  • 可以在服务端与客户端配置Region与 Zone,将服务指定到不同的区域与机房;

  • 在服务调用时,优先顺序选择配置的Region与Zone;



五、总结



本文从几个问题出发,说明了服务发现的概念,来源于分布式系统,并简单介绍了服务发现要解决的问题,还重点介绍Eureka是如何设计服务发现的,并将服务发现的运行流程,抽象为7个具体的步骤。后面又介绍了Eureka如何保证高可用与数据一致,同时还通过一个故事讲解了CAP理论,最后,还谈到了Eureka如何实现数据同步,如何实现分区调用等话题;



这篇文章的主要内容我总结如下:



  • 服务发现是在分布式系统环境下产生的一个基本概念;

  • 服务发现要解决的是服务标识与服务状态管理的问题;

  • 服务端,客户端,消费者、生产者这四个基本概念,是从不同的角度来描述服务发现;

  • 服务发现基本的运行流程,可以抽象为7个基本运行步骤;

  • Eureka服务端支持集群部署与运行,支撑高可用与可扩展;

  • Eureka服务端集群采用AP优于CP的设计原则;

  • Eureka使用点对点模式来完成服务端集群的数据同步;

  • Eureka使用Region与Zone的概念对服务分区进行管理,解决服务就近调用的问题;



分布式与Eureka的内容繁多,而本人水平有限,如有不到之处还请大家谅解。



如果你喜欢本文,请记得点赞,关注哦!!!谢谢!!!



微服系列文章



第1篇: “四个维度” 讲明白什么是微服务!

第2篇: 微服务涉及的技术生态有哪些?

第3篇: 微服务-为什么要有服务发现与注册?

第4篇: 万字长文,助你吃透Eureka服务发现机制!



发布于: 2020 年 06 月 02 日 阅读数: 1541
用户头像

攀岩飞鱼

关注

任何一步都不会决定成功,但可以决定失败! 2017.10.19 加入

记录技术,项目,管理,读书,挣钱,养娃等点点滴滴可以记录的事情。

评论 (4 条评论)

发布
用户头像
感谢作者分享。 有一个不理解得点:如何解决数据冲突。12两种解决方法,分别是对应服务端之间同步冲突和客户端与服务端同步冲突,对吗?如果是,那2中又说版本号解决是亡羊补牢,意思是想用2中方法前置避免这种问题,这是两码事吧?
2020 年 06 月 02 日 17:02
回复
用户头像
先点赞再看
2020 年 06 月 02 日 10:50
回复
用户头像
感谢分享长文干货,InfoQ首页推荐。
2020 年 06 月 02 日 09:01
回复
感谢小编姐姐推荐,继续加油!
2020 年 06 月 02 日 09:13
回复
没有更多了
万字长文,助你吃透Eureka服务发现机制!