前面入门系列已经讲了eureka的简单使用,这篇对于eureka的原理进行解析说明
官方给的架构图。至于各个流程是怎么样的,看下面
服务注册(register):Eureka Client会通过发送REST请求的方式向Eureka Server注册自己的服务,提供自身的元数 据,比如ip地址、端口、运行状况指标的url、主页地址等信息。Eureka Server接收到注册请求后,就会把这些元数 据信息存储在一个双层的Map中(注册表)。
服务续约(renew):在服务注册后,Eureka Client会维护一个心跳来持续通知Eureka Server,说明服务一直处于可 用状态,防止被剔除。Eureka Client在默认的情况下会每隔30秒 (eureka.instance.leaseRenewallIntervalInSeconds)发送一次心跳来进行服务续约。
服务同步(replicate):Eureka Server之间会互相进行注册,构建Eureka Server集群,不同Eureka Server之间会进 行服务同步,用来保证服务信息的一致性。
获取服务(get registry):服务消费者(Eureka Client)在启动的时候,会发送一个REST请求给Eureka Server,获 取上面注册的服务清单,并且缓存在Eureka Client本地,默认缓存30秒 (eureka.client.registryFetchIntervalSeconds)。同时,为了性能考虑,Eureka Server也会维护一份只读的服务清 单缓存,该缓存每隔30秒更新一次。
服务调用:服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行 远程调用。Eureka有Region和Zone的概念,一个Region可以包含多个Zone,在进行服务调用时,优先访问处于同 一个Zone中的服务提供者。
服务下线(cancel):当Eureka Client需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前 先发送REST请求给Eureka Server,告诉Eureka Server自己要下线了,Eureka Server在收到请求后,就会把该服务 状态置为下线(DOWN),并把该下线事件传播出去。
服务剔除(evict):有时候,服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给 Eureka Server来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server在启动的时候会创建一个定时任 务,每隔一段时间(默认60秒),从当前服务清单中把超时没有续约(默认90秒, 但实际代码中是180秒(eureka的bug,因为eureka已经大量使用所以没去修改)eureka.instance.leaseExpirationDurationInSeconds)的服务剔除。
自我保护:既然Eureka Server会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了 异常,所有的服务都没能够进行续约,Eureka Server就把所有的服务都剔除了,这样显然不太合理。所以,就有了 自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下, Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enableself-preservation: false)(默认15分钟低于85%可用则进入自我保护机制,可以设置不开启)
上面是对于eureka的一些核心知识进行字面表达,此时会不会觉得记忆点比较少?有点模糊?那么下面我会对于整个eureka进行形象化的解释描述。
该图就是对于client端是怎么和server做交互从而实现的核心功能
eureka中对外暴露的常用api如下,我们可以手动去调用其api
例如:(http://localhost:8761/eureka/ 是默认的server端地址,你也可以去改相应的地址)
注册新应用实例
POST http://localhost:8761/eureka/apps/SERVICES3
请求内容格式: application/xml
services3:7f411a785a97590bf4ae9086489e3d15 192.168.1.7 SERVICES3 192.168.1.7 UP UNKNOWN 8083 443 1 MyOwn services3 demo-order2 false 1541584436212 1541584436212 常用api枚举
请求方式 url 说明 GET http://localhost:8761/eureka/apps 查询所有应用实例 GET http://localhost:8761/eureka/apps/SERVICES2 根据 AppId 查询 GET http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15 根据 AppId 及 instanceId 查询 GET http://localhost:8761/eureka/instances/services2:7f411a785a97590bf4ae5176489e3d15 根据 instanceId 查询 DELETE http://localhost:8761/eureka/apps/SERVICES3/services3:7f411a785a97590bf4ae9086489e3d15 注销应用实例 POST http://localhost:8761/eureka/apps/SERVICES2 注册新应用实例 PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/status?value=OUT_OF_SERVICE 暂停/下线应用实例 PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/status?value=UP 恢复应用实例 PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15 应用实例发送心跳 PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/metadata?version=1.1.1 修改应用实例元数据
上面说明了eureka分为两个部分,一个是server端、一个client端,而server很像一个web程序,对外提供一些api接口,client端通过调用server端的api接口从而实现对server的注册、获取实例等功能。
而我们需要更加的理解其原理,则需要进一步去了解其源码。下面则对于server端进行简单的代码分析。
下面我们先找到源码的入口:@EnableEurekaServer注解,点进去
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented //这里会导入一个EurekaServerMarkerConfiguration类,该类中会注入一个Marker的bean @Import({EurekaServerMarkerConfiguration.class}) public @interface EnableEurekaServer { }
进入EurekaServerMarkerConfiguration类中,这里要注入一个Maker的bean,该bean的作用是激活EurekaServerAutoConfiguration(重要:即server端的自动配置类)
@Bean public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() { return new EurekaServerMarkerConfiguration.Marker(); } class Marker { Marker() { } }
此时我们激活server端的自动配置类的条件已经满足了,此时进入EurekaServerAutoConfiguration看看里面有什么?
这里我们只关注几个bean,其他代码都被我省略了
@Configuration( proxyBeanMethods = false ) @Import({EurekaServerInitializerConfiguration.class}) @ConditionalOnBean({Marker.class}) @EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class}) @PropertySource({"classpath:/eureka/server.properties"}) public class EurekaServerAutoConfiguration implements WebMvcConfigurer { //(1)加载EurekaController, spring‐cloud 提供了一些额外的接口,用来获取eurekaServer的信息 @Bean @ConditionalOnProperty( prefix = "eureka.dashboard", name = {"enabled"}, matchIfMissing = true ) public EurekaController eurekaController() { return new EurekaController(this.applicationInfoManager); } //(2) 初始化集群注册表 @Bean public PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs) { this.eurekaClient.getApplications(); return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient, this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(), this.instanceRegistryProperties.getDefaultOpenForTrafficCount()); } //(3)配置服务节点信息,这里的作用主要是为了配置Eureka的peer节点,也就是说当有收到有节点注册上来的时候,需要通知给那些服务节点, (互为一个集群) @Bean @ConditionalOnMissingBean public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs, ReplicationClientAdditionalFilters replicationClientAdditionalFilters) { return new EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.applicationInfoManager, replicationClientAdditionalFilters); } //(4) EurekaServer的上下文 @Bean public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) { return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager); } //(5) 这个类的作用是spring‐cloud和原生eureka的胶水代码,通过这个类来启动EurekaSever 后面这个类会在EurekaServerInitializerConfiguration被调用,进行eureka启动 @Bean public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) { return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext); } //(6)配置拦截器,ServletContainer里面实现了jersey框架,通过他来实现eurekaServer对外的restFull接口 @Bean public FilterRegistrationBean> jerseyFilterRegistration(Application eurekaJerseyApp) { FilterRegistrationBean
bean = new FilterRegistrationBean(); bean.setFilter(new ServletContainer(eurekaJerseyApp)); bean.setOrder(2147483647); bean.setUrlPatterns(Collections.singletonList("/eureka/*")); return bean; } } 说到这里,我们需要用一个流程图记录下来我们当前的进度:
主要即注册一个Maker的bean去激活EurekaServerAutoConfiguration,然后EurekaServerAutoConfiguration类中分别注册了多个初始化bean
这里要注意的是eurekaServerBootstrap这个bean,该bean在后面会有用
上面我们的类内容看完了,现在来看看他上面的注解,其中要注意一个EurekaServerInitializerConfiguration类,因为他是服务同步和服务剔除的调用关键,那么我们就进入该类中
@Configuration( proxyBeanMethods = false ) @Import({EurekaServerInitializerConfiguration.class}) @ConditionalOnBean({Marker.class}) @EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class}) @PropertySource({"classpath:/eureka/server.properties"}) public class EurekaServerAutoConfiguration implements WebMvcConfigurer { //代码省略 }
查看发现:其中会使用前面注册的eurekaServerBootstrap去调用contextInitialized方法(该方法回去初始化server端的运行环境和上下文)
public class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered { //因为实现了SmartLifecycle接口,所以会在初始化完后去调用isAutoStartup如果为true,则会去调用该start方法 public void start() { (new Thread(() -> { try { this.eurekaServerBootstrap.contextInitialized(this.servletContext); log.info("Started Eureka Server"); this.publish(new EurekaRegistryAvailableEvent(this.getEurekaServerConfig())); this.running = true; this.publish(new EurekaServerStartedEvent(this.getEurekaServerConfig())); } catch (Exception var2) { log.error("Could not initialize Eureka servlet context", var2); } })).start(); } public boolean isAutoStartup() { return true; } }
所以我们需要进去contextInitialized看看,这里的server端上下文很重要,里面就有关于服务同步和服务剔除的设置。
public void contextInitialized(ServletContext context) { try { //1、初始化server端的运行环境 this.initEurekaEnvironment(); //2、初始化server端的上下文 this.initEurekaServerContext(); context.setAttribute(EurekaServerContext.class.getName(), this.serverContext); } catch (Throwable var3) { log.error("Cannot bootstrap eureka server :", var3); throw new RuntimeException("Cannot bootstrap eureka server :", var3); } }
所以我们需要进入initEurekaServerContext上下文方法中看看,服务同步和服务剔除是怎么进行设置的?
protected void initEurekaServerContext() throws Exception { //1、初始化eureka server上下文 EurekaServerContextHolder.initialize(this.serverContext); log.info("Initialized server context"); // Copy registry from neighboring eureka node // 2、从相邻的eureka节点复制注册表(服务同步方法) int registryCount = this.registry.syncUp(); //3、默认每30秒发送心跳,1分钟就是2次 (服务续约、服务剔除) // 修改eureka状态为up // 同时,这里面会开启一个定时任务,用于清理60秒没有心跳的客户端。自动下线 //至于具体实现我们点进去看一下 this.registry.openForTraffic(this.applicationInfoManager, registryCount); EurekaMonitors.registerAllStats(); }
这里我们补一下流程图
下面我们需要进去这两个方法看看到底是怎么实现的,点进去发现是接口,此时找其实现类PeerAwareInstanceRegistryImpl
syncUp服务同步方法:
public int syncUp() { //省略代码 while(var4.hasNext()) { Application app = (Application)var4.next(); Iterator var6 = app.getInstances().iterator(); while(var6.hasNext()) { InstanceInfo instance = (InstanceInfo)var6.next(); try { if (this.isRegisterable(instance)) { //只需要关注该方法:将其他节点的实例注册到本节点 this.register(instance, instance.getLeaseInfo().getDurationInSecs(), true); ++count; } } catch (Throwable var9) { logger.error("During DS init copy", var9); } } } } return count; }
这里先不讲register方法,这里该方法和clent端注册服务时的方法是同一个,只是这里是将其他节点作为client将其实例注册到本节点从而达到了同步的效果(eureka的各个server是同级别的,各自都把其他节点当成client来看待)
注意:这里的注册节点由于和client注册时是同一个方法,所以等到讲client端时再去说明该方法的实现。
openForTraffic:服务续约、剔除方法
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) { this.expectedNumberOfClientsSendingRenews = count; this.updateRenewsPerMinThreshold(); logger.info("Got {} instances from neighboring DS node", count); logger.info("Renew threshold is: {}", this.numberOfRenewsPerMinThreshold); this.startupTime = System.currentTimeMillis(); if (count > 0) { this.peerInstancesTransferEmptyOnStartup = false; } Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName(); boolean isAws = Name.Amazon == selfName; if (isAws && this.serverConfig.shouldPrimeAwsReplicaConnections()) { logger.info("Priming AWS connections for all replicas.."); this.primeAwsReplicas(applicationInfoManager); } logger.info("Changing status to UP"); // 1、设置实例的状态为UP applicationInfoManager.setInstanceStatus(InstanceStatus.UP); //2、 开启定时任务,默认60秒执行一次,用于清理60秒之内没有续约的实例 //这里是每60秒对90秒内没有续约的数据进行清除 super.postInit(); }
这里我们进入postInit方法:
protected void postInit() { this.renewsLastMin.start(); if (this.evictionTaskRef.get() != null) { ((AbstractInstanceRegistry.EvictionTask)this.evictionTaskRef.get()).cancel(); } this.evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask()); //定时任务中进行服务剔除任务,定时时间是getEvictionIntervalTimerInMs,默认是60秒 //60秒进行一次剔除90秒没续约的服务 this.evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs()); }
至此,我们的server端的主线进行分析。在server端中我们主要对于服务同步(具体实现后面在说)、服务剔除(用定时任务每60秒剔除一次失效的服务(90秒没续约的服务))(默认30秒续约一次)两个核心功能。
最后是对于server具体主线的流程图
上面对于eureka的server端进行简单的解析,这里对于eureka的client端进行分析
这里就不进行太细致的分析,下面是我分析源码的入口:
其中client是在DiscoveryClient中注入EurekaClient(这个为原生Eureka的客户端,用来调用Eureka server的一些api)(实际netfix之前的Eureka在没加入springcloud前是原生的Eureka),这是我们则可以用DiscoveryClient来调用server端的api。
所以我们这里直接进入其中的initScheduledTasks方法,该方法涉及服务获取、续约、注册等核心
private void initScheduledTasks() { int renewalIntervalInSecs; int expBackOffBound; if (this.clientConfig.shouldFetchRegistry()) { //服务注册列表更新的周期时间 renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds(); expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); //1、定时更新服务注册列表(获取服务列表) //其中CacheRefreshThread()方法是该线程执行更新的具体逻辑 this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS); } if (this.clientConfig.shouldRegisterWithEureka()) { //2、服务续约的周期时间 renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs); // 服务定时续约 //其中HeartbeatThread()方法是对续约的具体方法 this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS); this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); this.statusChangeListener = new StatusChangeListener() { public String getId() { return "statusChangeListener"; } public void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) { DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent); } else { DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent); } DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate(); } }; if (this.clientConfig.shouldOnDemandUpdateStatusChange()) { this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener); } //3、start方法是进行服务注册 this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info("Not registering with Eureka server per configuration"); } }
现在先讲第一个核心功能:获取服务(拉取服务列表)
1、拉取服务列表定时的使用DiscoveryClient调用CacheRefreshThread()方法,
class CacheRefreshThread implements Runnable { CacheRefreshThread() { } public void run() { DiscoveryClient.this.refreshRegistry(); } }
再进去refreshRegistry方法:
@VisibleForTesting void refreshRegistry() { //省略代码 //获取注册信息方法 boolean success = this.fetchRegistry(remoteRegionsModified); if (success) { this.registrySize = ((Applications)this.localRegionApps.get()).size(); this.lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis(); } //省略代码 }
进入fetchRegistry
private boolean fetchRegistry(boolean forceFullRegistryFetch) { Stopwatch tracer = this.FETCH_REGISTRY_TIMER.start(); label122: { boolean var4; try { // 取出本地缓存之前获取的服务列表信息 Applications applications = this.getApplications(); //判断多个条件,确定是否触发全量更新,如下任一个满足都会全量更新: //1). 是否禁用增量更新; //2). 是否对某个region特别关注; //3). 外部调用时是否通过入参指定全量更新; //4). 本地还未缓存有效的服务列表信息; if (!this.clientConfig.shouldDisableDelta() && Strings.isNullOrEmpty(this.clientConfig.getRegistryRefreshSingleVipAddress()) && !forceFullRegistryFetch && applications != null && applications.getRegisteredApplications().size() != 0 && applications.getVersion() != -1L) { //1、增量更新 this.getAndUpdateDelta(applications); } else { //2、全量更新 this.getAndStoreFullRegistry(); } //3、重新计算和设置一致性hash码 applications.setAppsHashCode(applications.getReconcileHashCode()); this.logTotalInstances(); break label122; } catch (Throwable var8) { logger.error("DiscoveryClient_{} - was unable to refresh its cache! status = {}", new Object[]{this.appPathIdentifier, var8.getMessage(), var8}); var4 = false; } finally { if (tracer != null) { tracer.stop(); } } return var4; } this.onCacheRefreshed(); this.updateInstanceRemoteStatus(); return true; }
这里有一个重点,如果一个client在对server进行拉取注册表时,获取本地缓存找是否有信息,没有则为第一次去拉取,此时调用全量拉取方法getAndStoreFullRegistry(即拉取所有列表信息),如果本地有则调用增量拉取getAndUpdateDelta方法。注意:每次拉取完后会根据client端的本地缓存的注册表进行计算出一个hashcode值,这个值很重要,再后面会提到
现在我们来看看全量获取的方法getAndStoreFullRegistry:
private void getAndStoreFullRegistry() throws Throwable { long currentUpdateGeneration = this.fetchRegistryGeneration.get(); logger.info("Getting all instance registry info from the eureka server"); //1、这个Applications 就是client本地缓存的注册表 Applications apps = null; //2、eurekaTransport.queryClient.getApplications方法就是调用了server端的全量获取接口 EurekaHttpResponse
httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get()); if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) { //3、如果获取成功,则会放入本地缓存的注册表中 apps = (Applications)httpResponse.getEntity(); } } 全量获取流程在上面代码注释的很明白了,先调用server的全量获取接口(使用默认/配置的地址+接口路径,用http请求调用),然后讲返回的结果放进本地缓存的注册表中。
下面对于增量获取进行分析:
private void getAndUpdateDelta(Applications applications) throws Throwable { long currentUpdateGeneration = this.fetchRegistryGeneration.get(); //1、本地缓存注册表 Applications delta = null; //2、eurekaTransport.queryClient.getDelta调用server端的增量接口 EurekaHttpResponse
httpResponse = this.eurekaTransport.queryClient.getDelta((String[])this.remoteRegionsRef.get()); if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) { //3、将增量合并到注册表中,注意:不管是全量还是增量,server都会传来以一个hashcode值 //其值是根据server中的本地注册表实例计算来的(和我们之前说的client端也会根据本地的注册实例注册一个hashcode值) //此时如果server传来的hashcode值=client接收到实例后新计算的hashcode值,则证明我们拿到的是最新的实例注册表, //如果不等于则表示拿的不是最新的,此时会再进行一次全量拉取请求 delta = (Applications)httpResponse.getEntity(); } //4、如果本地缓存的注册表为空,即第一次请求,此时直接调用全量拉取方法 if (delta == null) { logger.warn("The server does not allow the delta revision to be applied because it is not safe. Hence got the full registry."); this.getAndStoreFullRegistry(); } else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) { logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode()); String reconcileHashCode = ""; if (this.fetchRegistryUpdateLock.tryLock()) { try { this.updateDelta(delta); reconcileHashCode = this.getReconcileHashCode(applications); } finally { this.fetchRegistryUpdateLock.unlock(); } } else { logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta"); } //5、一致性哈希码不同,就在reconcileAndLogDifference方法中做全量更新 if (!reconcileHashCode.equals(delta.getAppsHashCode()) || this.clientConfig.shouldLogDeltaDiff()) { this.reconcileAndLogDifference(delta, reconcileHashCode); } } else { logger.warn("Not updating application delta as another thread is updating it already"); logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode()); } } 该方法的主要步骤:如果本地缓存为空,证明是第一次调用,此时执行全量拉取。如果本地缓存部位null,则将调用增量拉取,拉取的结果合并到本地缓存。最后进行一致哈希码比较,如果传来的哈希码不等于本地新算的哈希码,此时证明拿到的不是最新的,则需要进行一次全量拉取。
至此服务列表拉取代码说到这里结束,太细节对的就不说了。
2、服务续约:(定时任务:每30秒执行一次)
服务续约比较简单,也是通过discoveryClient调用renew()方法对server进行续约操作
server接到请求后会通过client的ip、服务名、请求时间等信息去server的服务列表中修改相应服务实例的信息。
3、服务注册:
服务注册则通过一个线程中的run方法去调用server的一个regiester方法,server端接到请求后会向注册表中添加相应实例信息。到这里我们用一个流程图来总结我们上面的所有步骤:
这里我不打算用源码来解释,这里我直接用流程图的形式来解释其过程:
在说明两个接口的处理逻辑前,我们先需要明白eureka的内部构造,eureka中存在两个缓存
1、只读缓存(concurrenthashmap)(一级缓存):默认30秒失效,失效后会从二级缓存读写缓存中获取(每30秒使用定时任务进行获取),再获取不到则直接到本地注册表中(gMap:也是一个map)获取
2、读写缓存(谷歌的一个map)(二级缓存):默认180秒失效,失效后会去本地注册表中那信息。
其中内层map的value对应的类Lease需要重点理解下,里面会存放这些信息
我们调用续约等接口时就是来修改这些值。
1、注册接口的处理逻辑:
接到请求后,会调用register方法,将所有的实例信息写进注册表中。此时写完后要去清理读写缓存(二级缓存)的信息
2、拉取列表接口:
先从只读缓存获取,获取不到去读写缓存中获取,再读不到去本地注册表拿。
3、续约接口:
直接去注册表中修改相应实例信息即可。
服务注册(register)、服务续约(renew)、服务同步(replicate)、获取服务(get registry)、服务调用、服务下线(cancel)、服务剔除(evict)、自我保护。
1、服务注册(register):client端会根据配置/默认的server地址使用http请求server暴露的注册接口。然后server会在server本地注册表中添加相关的注册实例,并且清除读写缓存的信息。
2、服务续约:client通过server地址访问续约接口,server接到请求后会在本地注册表中修改相应的实例信息。(用定时任务,30秒调用一次接口)
3、服务同步(replicate):各个server将其他的server节点看出client,所以各个其他server节点也是和client一样通过调用注册接口进行注册。
4、获取服务(get registry):client调用拉取服务,分全量拉取和增量拉取(看client的注册表是否为空进行判断使用哪个),拉取服务中为了保证拉取到的是最新的服务实例列表,在server和client都会对各自的注册表中计算出一个哈希码值,如果两者哈希码相同,则会任务拉取到最新的,如过不同则会再触发一次全量拉取。client默认一个定时任务每30秒拉取一次信息。
5、服务调用:server返回给client相应的调用者信息,client根据相应信息进行调用
6、服务下线(cancel):client调用server下线接口,server会去修改相应信息
7、服务剔除(evict):server有一个定时任务,每60秒会去清除server本地的注册表中90秒没有进行续约的服务实例
8、自我保护:默认开启的自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下, Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enableself-preservation: false)(默认15分钟低于85%可用则进入自我保护机制,可以设置不开启)
多级缓存机制的优点:
尽可能保证了内存注册表数据不会出现频繁的读写冲突问题。
并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高
(可以稍微估计下对于一 线互联网公司,内部上千个eureka client实例,每分钟对eureka上千次的访问,一天就是上千万次的访问)
看完多级缓存这块源码我们可以搞清楚一个常见的问题,就是当我们eureka服务实例有注册或下线或有实例发生故 障,内存注册表虽然会及时更新数据,但是客户端不一定能及时感知到,可能会过30秒才能感知到,因为客户端拉 取注册表实例这里面有一个多级缓存机制 (AP)还有服务剔除的不是默认90秒没心跳的实例,剔除的是180秒没心跳的实例(eureka的bug导致)
不信看看这篇文章:https://blog.csdn.net/weixin_42277648/article/details/102813216
到这里可以说对于eureka的基本原理了解的比较请求了,eureka本质就通过client调用server的接口,而且一个30秒、60秒等的都是通过timer的定时任务实现的,整体来说eureka还是比较简单的。
后面会对于hystrix、nacos等的源码进行简单的分析