Client端

Hoxton版本的springcloud已经不需要注解@EnableEurekaClient了,spring-cloud-netflix-eureka-client 包里面spring.factories中有配置类EurekaClientAutoConfiguration,其内部类RefreshableEurekaClientConfiguration 中的eurekaClient方法会向工厂注册一个DiscoveryClient的子类CloudEurekaClient(因为有@lazy注解,所以没有实例化,还是beanDefinition)。而它的eurekaAutoServiceRegistration方法会注册一个EurekaAutoServiceRegistration,由于EurekaAutoServiceRegistration实现了SmartLifecycle接口,在ApplicaitonContext.refresh()--->finishRefresh()--->getLifecycleProcessor().onRefresh()--->startBeans的流程中,EurekaAutoServiceRegistration调用start方法---> this.serviceRegistry.register(this.registration)--->maybeInitializeClient(reg)--->reg.getEurekaClient()--->注意到CloudEurekaClient是被@GenericScope修饰的,所以reg中注入的eurekaclient是代理类,直接调用代理类的getTargetSource().getTarget()会实例化一个CloudEurekaClient。

在CloudEurekaClient的父类DiscoveryClient的构造方法中,fetchRegistry获取服务列表,initScheduledTasks方法启动两个定时任务(延迟30秒):心跳 + 获取服务列表,其中,CacheRefreshThread 发出/apps/update请求获取增量,HeartbeatThread发出/apps/appName/appId请求发起心跳。注册的逻辑是:initScheduledTasks方法在启动两个定时任务之后,把statusChangeListener添加到applicationInfoManager中,而上面说的maybeInitializeClient方法的下面,setInstanceStatus方法,会调用statusChangeListener的notify--->--->InstanceInfoReplicator.this.run--->注册,注册的url是"apps/" + info.getAppName()。如果这次的注册没有成功,也没关系,HeartbeatThread的run方法中的renew方法中,如果心跳返回的是没有这个任务,说明之前没有注册过,会重新开始注册

如果注册中心有多个,client默认是使用第一个注册中心,如果第一个不可用了,切换到下一个。看源码:心跳的http方法是eurekaTransport.queryClient.getApplications,获取任务是eurekaTransport.registrationClient.sendHeartBeat,queryClient和registrationClient都是SessionedEurekaHttpClient的实例,这里用的是装饰者模式,SessionedEurekaHttpClient---RetryableEurekaHttpClient---RedirectingEurekaHttpClient---MetricsCollectingEurekaHttpClient。依次调用getAppliction和execute方法,其中在RetryableEurekaHttpClient的execute方法中,在numberOfRetries次数内,用delegate这个变量,实现请求失败时候的url切换,这个思路可以用到网关的高可用上:前端维护一个网关列表,失败了自动切换到下一个。

 

Server端:

server端处理请求有两种方式:1、在控制页面,用的是springMVC,也就是dispatchServlet-->controller。2、响应注册、心跳、获取服务这些请求,是在tomcat的请求链filterChain中加入一个filter:ServletContainer拦截请求,在其dofilter方法中,根据各种rule,匹配url,调用处理方法,其中处理注册的是AbstractInstanceRegistry的register方法,心跳是renew方法,获取服务全量是ApplicationsResource的getContainers方法,增量是getContainerDifferential方法。

两点细节需要注意的是:1、ServletContainer注册到工厂的逻辑是@EnableEurekaServer--->@Import(EurekaServerConfiguration)-->@Bean 返回name为jerseyFilterRegistration的FilterRegistrationBean,jerseyFilterRegistration是一个ServletContextInitializer,springboot生成tomcat的时候,会借助TomcatStarter获取工厂所有的ServletContextInitializer,其中就会在Context的 startInternal方法中,调用所有ServletContextInitializer的onStartup方法,向ServletContext中注册jerseyFilterRegistration初始化的时候传入ServletContainer。注意,因为tomcat的filterchain是动态组装的,当前request的url不匹配过滤器的话,这个过滤器不会被放入过滤链,所以非/eureka/*类型的请求的过滤器链中就没有ServletContainer,可以一直到达controller。

存储服务信息用的是三个Map或者类似Map的结构:readOnlyCacheMap,readWriteCacheMap,registry。registry的数据结构是一个map,key是appName,value还是一个map,值是appid(url+port),value是leaseInfo。readOnlyCacheMap和readWriteCacheMap的key都是全量/增量/appName + zip/full(压缩还是未压缩)的组合,值就是所对应的服务的信息。

这样的三层缓存,自然要讨论失效的问题,默认情况下,获取服务都是从readOnlyCacheMap获取,如果里面没有,再去readWriteCacheMap取,然后readOnlyCacheMap存一份,如果readWriteCacheMap没有,那么从registry取(逻辑是responseCache.get--->getValue--->readWriteCacheMap.get--->localCache.getOrLoad--->loadSync--->loader.load),readWriteCacheMap同样缓存一份。而服务的注册或者离线都是反映到最后的registry上,不管是增加一个服务(register)还是优雅关闭离线一个服务(internalCancel),最后都会在registry上增减,然后把readWriteCacheMap里面的缓存全部清空。上面说的是服务正常通信时候的增删,有的服务并没有被优雅关闭而是直接被杀掉了进程,又或者该服务连接到注册中心的网络断掉了,那么这个时候需要eureka定时的检测registry上的服务的租期是否已经过了,这个过程用的是evict方法:@EnableEurekaServer--->@Import(EurekaServerConfiguration.class)--->@Import(EurekaServerInitializerConfiguration.class)--->实现SmartLifecycle接口,start方法中调用eurekaServerBootstrap.contextInitialized--->initEurekaServerContext()--->this.registry.openForTraffic--->AbstractInstanceRegistry.post--->evictionTimer.schedule(EvictionTask)--->evict(compensationTimeMs)。evict方法的思路是:定期的清除过期服务,但是要防止因为eureka自身网络不好导致的大面积的服务心跳收不到,判断标准是:短时间内所有服务发起的心跳数小于预期值(根据当前应该受到的心跳数*系数0.85等等计算得出),在这种情况下,自我保护模式开启,不会剔除服务(前提是没有通过属性配置强制关闭自我保护模式)。自我保护模式还有一层保护就是,即使满足上面的条件,进入剔除环节,剔除的服务总量,也只能小于总服务数的0.15(1-0.85)倍。剔除服务的逻辑就比较简单了,和注册还有离线一样。

有一个间隔30秒的定时任务timer.schedule(getCacheUpdateTask())会定期对比writeMap和readMap中的服务,当发现writeMap中的值因为上述原因清空后,会从registry中取服务信息,再放到write/readMap,这样就使得注册或离线的服务反映到readMap上了。

看到这里,就会发现server端可能会有很长一段时间保存离线的服务。假设一种可能的情况:一个服务掉线了,在租期到期之后(上次的renew心跳,把lastUpdateTimestamp设置成当前时间加上duration--90秒,判断的时候,要判断当前时间大于lastUpdateTimestamp再加duration,那么需要180秒),之后假设幸运的(通常没有那么幸运)通过了自我保护模式,30秒后被定时任务剔除出了registry,再过30秒被另一个定时任务复制给了readOnlyCacheMap,再过30秒被eurekaClient的获取服务的请求获取到,再过30秒,被客户端的ribbon和eurekaClient之间的定时拷贝任务给拷贝过去供ribbon请求的时候轮训。这样要五分钟才能被感知(当然ribbon有自己的熔断机制,不会五分钟一直访问这个掉线服务)。

这样的话导致的后果就是经常会调用不通服务,这对于要求高可用的系统是不能容忍的。解决的思路有两个:

第一:重试,尊重eureka的设计思想:服务不可用是偶然的、短期可以恢复的、网络抖动导致的情况,那么离线的服务占总服务数的比例就很小,多次重试还连接不到正确服务的概率很小,而且ribbon有熔断机制,一个服务三次超时,就会把它从列表内过滤掉一段时间,默认post服务不重试,如果一定要重试,要注意超时时间和去重,稳定的线上运行可以用这种模式。

第二:把上面的自我保护模式关闭,禁用readOnlyCacheMap,定时任务3秒(可以更短)就剔除掉线服务,lease的duration设置为3秒,3秒钟客户端就获取一次服务,客户端eureka和ribbon之间的服务复制也设置成3秒一次。这样就可以很快的得到服务掉线的信息,但是增加了eureka的负担和客户端的负担,在开发测试或者刚上线经常需要改动重新发布的阶段可以用这种模式。

第三:在假设服务稳定是常态的基础上,利用ribbon的熔断,服务集群之间,发起频繁的空方法调用,检测到一个服务集群中的某台机器掉线,可以很快的从ribbon列表中剔除,上线之后,也能很快的发现。这样做还有一个优点就是,能很快的在原有服务和新启动的服务之间,建立长链接。减少真正请求时,第一次建立长链接的时间。

多个注册中心,信息之间的拷贝

有的文章说走的是syncUp方法,这个方法默认是不会进入的,因为registrySyncRetries的默认值是0,如果手动改为大于0让它起作用的话,server第一次启动的时候,会把其他已经启动的注册中心的服务列表拷贝到自己这里。每次重试,要休眠一段时间,这是为了等待client那边去获取服务,所以想要用syncUp方法的话,eureka.client.fetchRegistry要设置为true。

我个人建议不要开启syncUp方法,因为节点之间是存在可持续、有保障的拷贝机制的,原理是每次一个注册中心收到注册、心跳、取消等请求的时候,都会通过replicateToPeers把事件发送给集群的其他节点。在replicateToPeers方法中,根据类型,最后生成replicationTask,batchingDispatcher.process--->acceptorExecutor.process---> acceptorQueue.add放到队列里。而DefaultEurekaServerContext的@PostConstruct注解的方法中有peerEurekaNodes.start()--->updatePeerEurekaNodes方法首次把配置的service-url.defaultZone中的地址列表用createPeerEurekaNode方法封装成PeerEurekaNode,在PeerEurekaNode的构造方法中,TaskDispatchers.createBatchingTaskDispatcher---->TaskExecutors.batchExecutors---->new TaskExecutors方法中一口气建了20个线程,执行BatchWorkerRunnable的run方法,逻辑是getWork,取出tasks,放到processor.process方法中,发起http请求,也就是上面说的把注册心跳等请求发送给其他节点。看getWork方法, workQueue其实是batchWorkQueue。而在上面说的createBatchingTaskDispatcher方法中,new AcceptorExecutor会启动线程运行AcceptorRunner,run方法中在while循环中,drainInputQueues--->appendTaskHolder,先把上面说的acceptorQueue中的task放到processingOrder中,然后assignBatchWork中从processingOrder中取出来放到batchWorkQueue中。