看过之前文章的朋友们,相信已经对Eureka的运行机制已经有了一定的了解。为了更深入的理解它的运作和配置,下面我们结合源码来分别看看服务端和客户端的通信行为是如何实现的。另外写这篇文章,还有一个目的,还是希望鼓励大家能够学会学习和研究的方法,由于目前Spring Cloud的中文资料并不多,并不是大部分的问题都能找到现成的答案,所以其实很多问题给出一个科学而慎重的解答也都是花费研究者不少精力的。
在看具体源码前,我们先回顾一下之前我们所实现的内容,从而找一个合适的切入口去分析。首先,服务注册中心、服务提供者、服务消费者这三个主要元素来说,后两者(也就是Eureka客户端)在整个运行机制中是大部分通信行为的主动发起者,而注册中心主要是处理请求的接收者。所以,我们可以从Eureka的客户端作为入口看看它是如何完成这些主动通信行为的。
我们在将一个普通的Spring Boot应用注册到Eureka Server中,或是从Eureka Server中获取服务列表时,主要就做了两件事:
-
在应用主类中配置了
@EnableDiscoveryClient
注解
-
在
application.properties
中用eureka.client.serviceUrl.defaultZone
参数指定了服务注册中心的位置
顺着上面的线索,我们先查看@EnableDiscoveryClient
的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12
|
* Annotation to enable a DiscoveryClient implementation. * @author Spencer Gibb */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient { }
|
从该注解的注释我们可以知道:该注解用来开启DiscoveryClient
的实例。通过搜索DiscoveryClient
,我们可以发现有一个类和一个接口。通过梳理可以得到如下图的关系:
其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient
是Spring
Cloud的接口,它定义了用来发现服务的常用抽象方法,而org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient
是对该接口的实现,从命名来就可以判断,它实现的是对Eureka发现服务的封装。所以EurekaDiscoveryClient
依赖了Eureka的com.netflix.discovery.EurekaClient
接口,EurekaClient
继承了LookupService
接口,他们都是Netflix开源包中的内容,它主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient
类。
那么,我们就看看来详细看看DiscoveryClient
类。先解读一下该类头部的注释有个总体的了解,注释的大致内容如下:
1 2 3 4 5 6 7 8 9
|
这个类用于帮助与Eureka Server互相协作。 Eureka Client负责了下面的任务: - 向Eureka Server注册服务实例 - 向Eureka Server为租约续期 - 当服务关闭期间,向Eureka Server取消租约 - 查询Eureka Server中的服务实例列表 Eureka Client还需要配置一个Eureka Server的URL列表。
|
在具体研究Eureka Client具体负责的任务之前,我们先看看对Eureka Server的URL列表配置在哪里。根据我们配置的属性名:eureka.client.serviceUrl.defaultZone
,通过serviceUrl
我们找到该属性相关的加载属性,但是在SR5版本中它们都被@Deprecated
标注了,并在注视中可以看到@link
到了替代类com.netflix.discovery.endpoint.EndpointUtils
,我们可以在该类中找到下面这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public static Map<String, List<String>> getServiceUrlsMapFromConfig( EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); String region = getRegion(clientConfig); String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); if (availZones == null || availZones.length == 0) { availZones = new String[1]; availZones[0] = DEFAULT_ZONE; } …… int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); if (serviceUrls != null) { orderedUrls.put(zone, serviceUrls); } …… return orderedUrls; }
|
Region、Zone
在上面的函数中,我们可以发现客户端依次加载了两个内容,第一个是Region,第二个是Zone,从其加载逻上我们可以判断他们之间的关系:
-
通过
getRegion
函数,我们可以看到它从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region,如果不特别配置,就默认为default。若我们要自己设置,可以通过eureka.client.region
属性来定义。
1 2 3 4 5 6 7 8
|
public static String getRegion(EurekaClientConfig clientConfig) { String region = clientConfig.getRegion(); if (region == null) { region = DEFAULT_REGION; } region = region.trim().toLowerCase(); return region; }
|
-
通过
getAvailabilityZones
函数,我们可以知道当我们没有特别为Region配置Zone的时候,将默认采用defaultZone,这也是我们之前配置参数eureka.client.serviceUrl.defaultZone
的由来。若要为应用指定Zone,我们可以通过eureka.client.availability-zones
属性来进行设置。从该函数的return
内容,我们可以Zone是可以有多个的,并且通过逗号分隔来配置。由此,我们可以判断Region与Zone是一对多的关系。
1 2 3 4 5 6 7
|
public String[] getAvailabilityZones(String region) { String value = this.availabilityZones.get(region); if (value == null) { value = DEFAULT_ZONE; } return value.split(","); }
|
ServiceUrls
在获取了Region和Zone信息之后,才开始真正加载Eureka Server的具体地址。它根据传入的参数按一定算法确定加载位于哪一个Zone配置的serviceUrls。
1 2 3
|
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
|
具体获取serviceUrls的实现,我们可以详细查看getEurekaServerServiceUrls
函数的具体实现类EurekaClientConfigBean
,该类是EurekaClientConfig
和EurekaConstants
接口的实现,用来加载配置文件中的内容,这里有非常多有用的信息,这里我们先说一下此处我们关心的,关于defaultZone
的信息。通过搜索defaultZone
,我们可以很容易的找到下面这个函数,它具体实现了,如何解析该参数的过程,通过此内容,我们就可以知道,eureka.client.serviceUrl.defaultZone
属性可以配置多个,并且需要通过逗号分隔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public List<String> getEurekaServerServiceUrls(String myZone) { String serviceUrls = this.serviceUrl.get(myZone); if (serviceUrls == null || serviceUrls.isEmpty()) { serviceUrls = this.serviceUrl.get(DEFAULT_ZONE); } if (!StringUtils.isEmpty(serviceUrls)) { final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls); List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length); for (String eurekaServiceUrl : serviceUrlsSplit) { if (!endsWithSlash(eurekaServiceUrl)) { eurekaServiceUrl += "/"; } eurekaServiceUrls.add(eurekaServiceUrl); } return eurekaServiceUrls; } return new ArrayList<>(); }
|
当客户端在服务列表中选择实例进行访问时,对于Zone和Region遵循这样的规则:优先访问同自己一个Zone中的实例,其次才访问其他Zone中的实例。通过Region和Zone的两层级别定义,配合实际部署的物理结构,我们就可以有效的设计出区域性故障的容错集群。
服务注册
在理解了多个服务注册中心信息的加载后,我们再回头看看DiscoveryClient
类是如何实现“服务注册”行为的,通过查看它的构造类,可以找到它调用了下面这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private void initScheduledTasks() { ... if (clientConfig.shouldRegisterWithEureka()) { ...
instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
... instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info("Not registering with Eureka server per configuration"); } }
|
在上面的函数中,我们可以看到关键的判断依据if
(clientConfig.shouldRegisterWithEureka())
。在该分支内,创建了一个InstanceInfoReplicator
类的实例,它会执行一个定时任务,查看该类的run()
函数了解该任务做了什么工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public void run() { try { discoveryClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoveryClient.register(); instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } }
|
相信大家都发现了discoveryClient.register();
这一行,真正触发调用注册的地方就在这里。继续查看register()
的实现内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
boolean register() throws Throwable { logger.info(PREFIX + appPathIdentifier + ": registering service..."); EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e); throw e; } if (logger.isInfoEnabled()) { logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == 204; }
|
通过属性命名,大家基本也能猜出来,注册操作也是通过REST请求的方式进行的。同时,这里我们也能看到发起注册请求的时候,传入了一个com.netflix.appinfo.InstanceInfo
对象,该对象就是注册时候客户端给服务端的服务的元数据。
服务获取与服务续约
顺着上面的思路,我们继续来看DiscoveryClient
的initScheduledTasks
函数,不难发现在其中还有两个定时任务,分别是“服务获取”和“服务续约”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) {
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS);
…… } }
|
从源码中,我们就可以发现,“服务获取”相对于“服务续约”更为独立,“服务续约”与“服务注册”在同一个if逻辑中,这个不难理解,服务注册到Eureka Server后,自然需要一个心跳去续约,防止被剔除,所以他们肯定是成对出现的。从源码中,我们可以清楚看到了,对于服务续约相关的时间控制参数:
1 2
|
eureka.instance.lease-renewal-interval-in-seconds=30 eureka.instance.lease-expiration-duration-in-seconds=90
|
而“服务获取”的逻辑在独立的一个if判断中,其判断依据就是我们之前所提到的eureka.client.fetch-registry=true
参数,它默认是为true的,大部分情况下我们不需要关心。为了定期的更新客户端的服务清单,以保证服务访问的正确性,“服务获取”的请求不会只限于服务启动,而是一个定时执行的任务,从源码中我们可以看到任务运行中的registryFetchIntervalSeconds
参数对应eureka.client.registry-fetch-interval-seconds=30
配置参数,它默认为30秒。
继续循序渐进的向下深入,我们就能分别发现实现“服务获取”和“服务续约”的具体方法,其中“服务续约”的实现较为简单,直接以REST请求的方式进行续约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
boolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); if (httpResponse.getStatusCode() == 404) { REREGISTER_COUNTER.increment(); logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName()); return register(); } return httpResponse.getStatusCode() == 200; } catch (Throwable e) { logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e); return false; } }
|
而“服务获取”则相对复杂一些,会根据是否第一次获取发起不同的REST请求和相应的处理,具体的实现逻辑还是跟之前类似,有兴趣的读者可以继续查看服务客户端的其他具体内容,了解更多细节。
服务注册中心处理
通过上面的源码分析,可以看到所有的交互都是通过REST的请求来发起的。下面我们来看看服务注册中心对这些请求的处理。Eureka Server对于各类REST请求的定义都位于:com.netflix.eureka.resources
包下。
以“服务注册”请求为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
@POST
@Consumes({"application/json", "application/xml"}) public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) { logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
...
DataCenterInfo dataCenterInfo = info.getDataCenterInfo(); if (dataCenterInfo instanceof UniqueIdentifier) { String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId(); if (isBlank(dataCenterInfoId)) { boolean experimental = "true".equalsIgnoreCase( serverConfig.getExperimental("registration.validation.dataCenterInfoId")); if (experimental) { String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id"; return Response.status(400).entity(entity).build(); } else if (dataCenterInfo instanceof AmazonInfo) { AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo; String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId); if (effectiveId == null) { amazonInfo.getMetadata().put( AmazonInfo.MetaDataKey.instanceId.getName(), info.getId()); } } else { logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass()); } } } registry.register(info, "true".equals(isReplication)); return Response.status(204).build();
}
|
在对注册信息进行了一大堆校验之后,会调用org.springframework.cloud.netflix.eureka.server.InstanceRegistry
对象中的register(InstanceInfo
info, int leaseDuration, boolean isReplication)
函数来进行服务注册:
1 2 3 4 5 6 7 8 9 10 11
|
public void register(InstanceInfo info, int leaseDuration, boolean isReplication) { if (log.isDebugEnabled()) { log.debug("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication "
+ isReplication); } this.ctxt.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication)); super.register(info, leaseDuration, isReplication); }
|
在注册函数中,先调用publishEvent
函数,将该新服务注册的事件传播出去,然后调用com.netflix.eureka.registry.AbstractInstanceRegistry
父类中的注册实现,将InstanceInfo
中的元数据信息存储在一个ConcurrentHashMap<String,
Map<String, Lease<InstanceInfo>>>
对象中,它是一个两层Map结构,第一层的key存储服务名:InstanceInfo
中的appName
属性,第二层的key存储实例名:InstanceInfo
中的instanceId
属性。
分享到:
相关推荐
《Spring Cloud Eureka源码分析》 Spring Cloud Eureka是Netflix公司开源的一个服务发现组件,它是基于REST的服务,用于在分布式系统中定位服务,以实现负载均衡和中间层服务器的故障转移。Eureka的设计目标是提供...
【标题】"狂神Spring Cloud源码"是一个关于Spring Cloud深度学习和实践的资源集合,提供了免费的源码下载,旨在帮助开发者深入了解Spring Cloud的工作原理和实现细节。 【描述】"狂神Spring Cloud源码 免费下载!!!...
《Spring Cloud项目源码深度解析》 在当前的微服务架构领域,Spring Cloud以其强大的功能和易用性,成为开发者构建分布式系统的重要选择。本文将深入探讨基于Spring Cloud的项目源码,帮助读者理解其核心原理,提升...
1. **SpringCloud源码解析**: SpringCloud的源码分析有助于开发者了解其实现机制,从而更好地定制和优化自己的服务。源码中包含了Eureka服务发现、Zuul边缘服务、Hystrix断路器、 Ribbon客户端负载均衡、Feign声明...
SpringCloud和SpringBoot是Java开发领域中非常流行的微服务框架,Eureka则是SpringCloud生态中的一个关键组件,用于实现服务发现。在这个项目中,我们将会深入探讨这三个技术的结合使用,以及Eureka的源码分析。 ...
为什么要看源码: 1、提升技术功底:学习源码里的优秀设计思想,比如一些疑难问题的解决思路,还有一些优秀的设计模式,整体提 升自己的技术功底 2、深度掌握技术框架:源码看多了,对于一个新技术或框架的掌握速度会有...
《SpringCloud源码解析——构建微服务架构的关键组件》 SpringCloud作为当今最热门的微服务框架之一,深受广大开发者喜爱。它集成了众多优秀的开源项目,为开发人员提供了便捷的微服务开发工具。本篇将围绕Spring...
这个"SpringCloud源码demo"可能是为了帮助开发者深入理解SpringCloud的工作原理而创建的,通过实际的代码示例来探索和学习其内部机制。 首先,我们从SpringCloud的核心组件开始。Eureka是服务注册与发现的组件,它...
这些视频课程结合源码分析和课件学习,可以帮助开发者深入理解SpringBoot和SpringCloud的核心原理,并掌握实际应用中的最佳实践。对于想要在微服务领域深入发展的IT专业人士来说,这是一个非常有价值的资源。通过...
9. **源码分析**: 分享的源码是作者手敲并通过编译验证的,意味着你可以深入研究每个服务的实现细节,理解Spring Cloud的原理和最佳实践。通过阅读和运行这些代码,开发者可以提升自己在实际项目中的应用能力。 ...
### 一、SpringCloud简介与核心组件 #### 1.1 SpringCloud概述 SpringCloud是一套基于Spring Boot实现的微服务云应用开发工具集,它提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线...
Spring Cloud 源码分析可以帮助我们更深入地理解其工作原理,从而更好地利用这个框架来构建我们的微服务架构。 首先,让我们从 `lcg0124-clouddo-view-master.zip` 和 `lcg0124-clouddo-master.zip` 这两个文件开始...
SpringCloud是中国Java开发者广泛使用的微服务框架,它包含了一系列组件,用于构建分布式系统。这个压缩包文件"SpringCloud 15个完整例子"提供了一系列从基础到进阶的示例项目,帮助用户深入理解并实践SpringCloud的...
Spring Cloud提供了一个快速构建分布式系统工具集,包括服务发现(Eureka)、配置中心(Config Server)、API网关(Zuul或Gateway)、服务间调用(Ribbon和Feign)以及断路器(Hystrix)。这些组件都是Spring Boot的...
这个"springcloud demo项目源码"是用于展示Spring Cloud功能的一个实例,旨在帮助开发者更好地理解和应用Spring Cloud到实际项目中。 **1. Spring Cloud核心组件** - **Eureka**:服务注册与发现,每个微服务启动...
本项目为基于Java的SpringCloud注册中心Eureka设计源码,包含54个文件,其中Java源文件26个,PNG图片文件11个,Git忽略文件6个,XML和YAML配置文件各5个,Markdown文档1个。该源码旨在构建一个适用于SpringCloud...
在标签 "spring boot springcloud 源码" 中,Spring Boot 是 Spring Cloud 的基础,它简化了创建独立的、生产级别的基于 Spring 的应用程序。Spring Boot 自动配置了许多常见的 Spring 组件,并且提供了开箱即用的...
综上所述,"尚硅谷周阳老师SpringCloud学习源码"涵盖了SpringCloud的多个关键组件,通过学习这些源码,开发者能够深入理解微服务架构中的服务治理、API网关、断路器、配置中心、消息驱动等核心概念,进一步提升...
基于SpingBoot+SpringCloud+Maven+Eureka+Vue的分布式架构网上商城系统源码+数据已获导师指导。 本项目是一套基于SpringCloud的分布式架构网上商城系统,主要针对计算机相关专业的正在做毕设的学生和需要项目实战...
在压缩包文件中,`SpringCloud-Consumer`可能包含了服务消费者项目的源代码,`EureKaserver`则是Eureka Server的源码,而`SpringCloud-Service`则可能是服务提供者的源码。通过分析这些代码,你可以更深入地理解...