一、前言在上一篇文章中,我们讲解了客户端第一次获取注册表时,需要从注册表中拉取完整的注册表进行保存本地。如果将来有客户端注册或下线,则注册表必须已更改。这时候客户端就得更新本地注册表了。如何更新?下面我就带大家看一下第二个客户端第二次获取注册表的方法(这里指的是获取全额后的下一次)。题外话:之前写过一篇Redis主从同步架构原理的文章,里面也涉及到一次同步和二次同步。其实原理差不多,只是Redis主从同步的原理比较复杂。强烈推荐一起看:镜|5维度深度剖析“主从架构”原理二、增量获取带来的问题上文我们提到,第一次获取全量信息时,本地会有注册信息。如果服务端的注册表有更新,比如服务注册或下线,则客户端必须重新获取注册表信息。是否可以再次全额拉取?是的,但是如果注册表信息很大怎么办?例如,如果注册了数百个微服务,那么拉取非常耗时并且占用网络带宽,性能差,这个解决方案是不可靠的。所以我们需要采用增量拉取注册信息表的方式,也就是只拉取变化的数据,这样数据量比较小。如下图所示:增量访问注册表从源码中我们可以看出,EurekaClient通过调用getAndUpdateDelta方法获取增量变化的注册表数据,EurekaServer将变化后的数据返回给Client。这里有几个问题:(1)客户多久进行一次增量获取?(2)服务器把变化的数据存放在哪里?(3)客户端如何将变化的数据合并到本地注册中心?回答以上问题。3、同步间隔多久?3.1默认间隔是默认30s同步一次,如下图:默认30s同步一次,这个30s是通过变量client.refresh.interval定义的。Eureka每30秒会调用一个后台线程来拉取增量注册表。这个后台线程的名字是:cacheRefresh。如下图:间隔时间源码3.2Client通过调用getDelta方法发送请求拉取registry,发送HTTP请求调用jersey的restful接口,然后由server端的Jersey框架处理要求。发送请求getDelta的方法如下:eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());restful接口的地址是这样的:http://localhost:8080/v2/apps/delta那么server端如何过滤掉delta的注册表信息量呢?我们可以找到这个方法:getContainerDifferential。如下图所示:该方法的主要工作是获取最近变化的数据。接下来,让我们看看最近更改的数据存储在哪里。4、修改后的数据存储在哪里?4.1数据结构其实是放在这个队列中的:recentlyChangedQueue。它的数据结构是一个并发安全链表队列ConcurrentLinkedQueue。链表中存储的元素是最近更改的注册信息RecentlyChangedItem。ConcurrentLinkedQueue当客户端注册时,一个对象将被添加到链表的末尾。关于ConcurrentLinkedQueue,大家还记得我之前写的18种队列吗?不记得就看这篇文章:45张图片,18种Queue,你知道有多少吗?出无界队列。如下图所示:ConcurrentLinkedQueue原理4.2内部结构这个队列的结构我觉得还是值得学习的。我们来看看这个队列的结构,如下图所示:增量数据的内部结构这个队列中存储的对象是最近的Changed对象RecentlyChangedItem。RecentlyChangedItem包含三个元素:实例信息、操作类型和最后更新时间。Instance信息:使用Lease保存一个client的registry信息,在第四章讲解registry结构时已经介绍过。ActionType:当客户端发起注册、更新注册表或下线时,都会设置actionType,对应三个枚举值:add、update、delete。最后更新时间:当客户端注册信息发生变化时,需要同时更新最后更新时间。4.3最近的数据既然上面说了只有最近变化的数据才会放进去,那么最近是多少?1分钟?2分钟?我们通过源码找到这个默认配置,每三分钟刷新一次,也就是180s刷新一次。那么什么是刷新?刷新其实遍历了这个队列:recentlyChangedQueue。遍历队列中的所有元素,比较每个对象的最后更新时间是否超过三分钟,如果是,则移除这个元素。如下图所示:比较上次更新时间当一个元素的上次更新时间超过3分钟没有更新时,移除该元素。如下图所示:RemoveElement4.4CheckInterval服务器会将最近3分钟更新过的注册信息放入队列,超过3分钟未更新的数据将被移除。那么多久检查一次呢?我们通过源码发现checking任务每隔30s就会调用一次。如下图所示:CheckInterval4.5Summary客户端每30秒调用一次增量获取注册表的接口。服务器每30秒调用一次检查队列。如果队列中有一个元素在3分钟内没有更新,则将该元素从队列中移除。5、客户端注册表合并有问题:客户端第一次获取的完整注册表存储在本地。第二次拿到增量注册表,如何把两个数据合并在一起?如下图所示:Registry合并下面我们来看一下clientregistry合并的原理。当client调用request获取deltaregistry时,registry会返回增量信息,然后client会调用本地的merge方法:updateDelta。合并注册表的原理图如下:合并注册表的原理首先遍历增量注册表,检查其中的每一项,不管actionType是new、deleted还是update,如果本地存在,则执行follow-up类型的判断逻辑。如果实例信息的名称在本地不存在,则先在本地registry中添加一个注册信息。那么本地肯定有注册信息,执行后续的判断逻辑。当type字段actionType等于add或update时,先删除再添加。当type字段actionType等于delete时,直接delete。经过这一系列的逻辑,增量注册中心和本地注册中心就合并了。6、比较注册表经过多次判断+合并操作,客户端终于完成了对本地注册表的刷新。理论上,此时client的registry应该和registry的registry一致。但是如何确保它是一致的呢?这里我们考虑几个选项:再次完整拉取注册表并与本地注册表进行比较。但是既然又来了一次fullpull,那么之前的incrementalpull就没有必要了。拉取增量registry,服务端返回全量registry的instanceid,client比较每个instanceid是否存在,检查本地是否有冗余,若能匹配则认为一致。但是这里也有一个问题。对于新增和更新的注册实例,需要将更新后的实例信息的字段一一对比,判断是否一致,比较麻烦。另外还有一个致命的问题:如果客户端因为网络故障而下线,最后3分钟的增量数据没有拉取,就相当于丢失了一个增量数据。此时,还不是一个完整的注册表信息。有没有方便准确的比较方法?对,就是哈希比较。哈希比较是指两个对象通过哈希算法计算得到两个哈希值。如果两个哈希值相等,则认为两个对象相等。这种方式在代码中也很常见,比如一个类的hashcode()方法。从源码中我们可以看到,EurekaServer在返回注册中心的时候,会返回一个哈希值,也就是对整个注册中心进行哈希计算后的值。此方法称为:getReconcileHashCode()。如下图,获取增量注册表的接口,会返回增量注册表和hashcode。然后本地registry合并后,计算一个hashcode,与服务器返回的hashcode进行比较。如果一致,则说明本地注册中心与服务器一致。如果没有,将执行完整拉取。下面我们画个示意图来理解一下上面说的原理:七、总结这篇文章可以用一张图来概括一下,直接上图:客户端注册同步原理客户端每30s获取增量数据,注册中心返回注册信息发生变化最近3分钟,包括新注册、更新和下线的服务实例。然后返回增量注册表+全量注册表的hash值。客户端合并本地注册表+增量注册表。合并完成后,计算一个哈希值,与服务器返回的哈希值进行比较。如果相等,说明客户端的注册表与注册中心的注册表一致,同步完成。如果没有,则需要全额提取一次。问一个问题:为什么hash比较不一致?答案在文中!下一篇注册中心的缓存架构开始!