Eureka
是Spring Cloud Netflix
生态中的服务注册与发现组件。在平常开发过程中,我们经常搭建一个简单的eureka节点或者是集群就直接在生产环境中就使用了,但是通过对Eureka源码的分析,其中还有很多可以优化的地方。今天本篇文章来具体分析一下Eureka Server和Eureka Client的源码。
版本介绍:
SpringBoot : 2.3.0.RELEASE
SpringCloud : Hoxton.SR4
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.0.RELEASEversion>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR4spring-cloud.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
server端和client端的依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
@EnableEurekaServer
注解开启Server服务端。server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false #是否将自己注册到eureka中
fetchRegistry: false #是否从eureka中获取信息
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
双节点配置的eureka.client.service-url.defaultZone
则为互相注册:
---
spring:
profiles: peer1
eureka:
instance:
hostname: peer1
client:
serviceUrl:
defaultZone: https://peer2/eureka/ # 在第一个节点配置第二个节点
---
spring:
profiles: peer2
eureka:
instance:
hostname: peer2
client:
serviceUrl:
defaultZone: https://peer1/eureka/ #在第二个节点配置第一个节点
三节点则需要将三个节点地址全部写上(参考官方文档)。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:7900/eureka/,http://localhost:7901/eureka/,http://localhost:7902/eureka/
---
server:
port: 7900
spring:
profiles: 7900
eureka:
instance:
hostname: eureka-7900
---
server:
port: 7901
spring:
profiles: 7901
eureka:
instance:
hostname: eureka-7901
---
server:
port: 7902
spring:
profiles: 7902
eureka:
instance:
hostname: eureka-7902
将7000启动复制两分,修改为7901和7902:
分别启动即可:
浏览器地址访问:http://localhost:7900/ 、http://localhost:7901/、http://localhost:7902/
三节点的注册中心搭建完毕。
接下来对eureka server 端的源码进行分析,首先从自动配置类入手,在spring-cloud-netflix-eureka-server jar包META-INF目录下的spring.factories文件找到自动配置的类 org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
。
EurekaServerAutoConfiguration这个类就是启动服务端的自动配置类,接下来我们从这个类入手。
在主启动类上使用@EnableEurekaServer
注解即可开启服务端,进入@EnableEurekaServer注解类,可以看到该注解类通过 @Import 注解引入了 EurekaServerMarkerConfiguration 类,在EurekaServerMarkerConfiguration这个类中仅仅只是创建了一个Marker
标记类。
而在Eureka的自动配置类EurekaServerAutoConfiguration
中可以看到:
@ConditionalOnBean({Marker.class})
这个注解,表示这个Marker标记类是作为条件的,所以可以把Marker当做一个开关,使用@EnableEurekaServer创建了Marker类,表示打开开关,启动服务端配置。
Eureka Server 有自我保护机制:
默认情况下:
如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障(延迟、卡顿和拥挤)发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
开启自我保护机制:
Eureka server将会尝试保护其服务注册表中的信息,不再删除其服务注册表中的数据,也就是不会注销任何微服务。即某一时刻当某一个微服务不可用了,Eureka不会立刻清理它,依旧会对该微服务的信息进行保存。属于CAP里面的AP
分支。
总结:
所以当服务很多的情况下,出现网络问题是很正常的情况,所以需要开启自我保护机制;而在服务少的情况下,如果出现服务不可用的时候,很大概率就是服务真的不可用了,服务调用方调用服务就会出现问题,所以这个时候不要开启自我保护机制。
自我保护配置:
server:
#Eureka自我保护机制
enable-self-preservation: false
#自我保护阈值 ,默认为0.85
renewal-percent-threshold: 0.85
在自动配置类中通过 @Import 注解导入了EurekaServerInitializerConfiguration类,
会自动运行start() 方法,进入 contextInitialized() 方法:
进入initEurekaServerContext()
方法:
进入postInit()
方法:
这段代码的意思是在server端,会定期的将没有心跳的服务剔除。
this.serverConfig.getEvictionIntervalTimerInMs()
是表示剔除的时间间隔的毫秒数
,默认是60秒。
可以通过配置eureka.server.eviction-interval-timer-in-ms
设置为1000
(单位毫秒),实现将不可用服务快速下线。
看到postInit() 方法中最后执行了一个定时任务。
我们进入这个定时任务类 EvictionTask看看这个任务是执行什么的:
进入evict()
(evict 意思为驱逐)方法:
是否剔除服务的条件:isLeaseExpirationEnabled()
if (!this.isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
} else {
//如果isLeaseExpirationEnabled结果为true,则剔除服务
}
进入isLeaseExpirationEnabled()
:
public boolean isLeaseExpirationEnabled() {
if (!this.isSelfPreservationModeEnabled()) {
return true;
} else {
return this.numberOfRenewsPerMinThreshold > 0 && this.getNumOfRenewsInLastMin() > (long)this.numberOfRenewsPerMinThreshold;
}
}
所以即表示:
如果没有开启自我保护机制,返回为true,则执行剔除服务。
如果开启了自我保护机制,但是如果最后一分钟的续约数大于阈值,返回true,剔除服务;如果最后一分钟续约数小于阈值,则不剔除服务,自我保护正式开启。
从CAP理论看,Eureka是一个AP系统,其优先保证可用性(A)和分区容错性( P),不保证强一致性 ( C)。
缓存 | 名称 | 类型 |
---|---|---|
一级缓存 | registry | ConcurrentHashMap |
二级缓存 | readWriteCacheMap | LoadingCache |
三级缓存 | readOnlyCacheMap | ConcurrentMap |
在EurekaServerAutoConfiguration自动配置类中的eurekaServerContext
方法,构造了一个DefaultEurekaServerContext
类:
在DefaultEurekaServerContext
这个类中的initialize()
方法中调用了PeerAwareInstanceRegistryImpl 的init()
方法:
在**init()**这个方法中调用this.initializedResponseCache();
进行缓存初始化:
在initializedResponseCache()方法中,构造了一个ResponseCacheImpl
对象,在继续看这个对象的构造方法:
通过ResponseCacheImpl
的构造方法,创建出了二级缓存readWriteCacheMap
, 使用的是谷歌的guava
缓存框架,并且设置 二级缓存失效时间为180
秒:
通过ResponseCacheImpl.Value value = ResponseCacheImpl.this.generatePayload(key);
方法可以看出,如果从二级缓存没有获得到信息,则会直接从一级缓存中获得信息:
最后执行二级缓存(readWriteCacheMap)和三级缓存(readOnlyCacheMap)之间的缓存更新任务,responseCacheUpdateIntervalMs
为定时任务间隔时间,即表示缓存更新的时间间隔,默认为30
秒:
在继续看这个缓存更新任务,进入getCacheUpdateTask()
方法:
当有Eureka client启动的时候,就会执行到Eureka server端的com.netflix.eureka.resources.ApplicationResource类中的addInstance
方法,即添加实例:
注意: 源码跟踪到最后发现,是将缓存失效,表示如果一个新的实例注册上来,则需要首先将原来的readWriteCacheMap缓存失效掉。
在ApplicationResource 类中的getApplication
方法:
进入get()
方法一路向下最后找到getValue()
方法():
useReadOnlyCache 的值即为是否使用缓存,在配置文件中用eureka.server.use-read-only-response-cache
进行配置,false表示不开启readOnlyCacheMap缓存。
注意:在服务注册的时候将readWriteCacheMap
中的缓存失效掉,所以如果从readWriteCacheMap缓存中获取不到信息,则会直接从一级缓存(registry)获取信息。
如果在第二个server节点启动后,会拉取第一个server节点的注册表信息,拉取动作只会发生一次,此时如果第一个server节点之后又来了一个client实例,则只能通过集群同步的方式同步到第二个节点。
这一过程发生在com.netflix.eureka.resources.ApplicationResource
的addInstance()
方法里:
当服务启动并成功注册到Eureka服务器后,Eureka客户端会默认以每隔30秒的频率向Eureka服务器发送一次心跳。发送心跳起始就是执行服务续约(Renew)操作,避免自己的注册信息被Eureka服务器剔除。续约的处理逻辑和与服务注册逻辑基本一致:首先更新时间戳,然后同步到其他Eureka服务器节点。
在com.netflix.eureka.resources.InstanceResource
类的renewLease()
方法:更新lastUpdateTimestamp
时间,并且进行集群同步
。
可以看到服务续约、服务下线、服务上线都是更新时间戳,并且进行集群同步
。
服务注册表的全量拉取
很好理解,就是第一次的时候,从注册表全量拉取注册表到eureka client,后面的话就是增量拉取
了。
增量拉取注册表
的实现借助了一个ConcurrentLinkedQueue类型的变量recentlyChangedQueue
,通过名称我们就能知道这个变量的含义,最近改变的队列,默认情况下recentlyChangedQueue
里面存放的是180
秒内修改的服务实例信息,后台会有一个定时任务(30秒)来维护recentlyChangedQueue
,只有最近3分钟
内有变更的服务实例才会在里面。
在增量拉取注册表时,会将本地的注册表和recentlyChangedQueue中的服务实例进行一个合并,将有变化的服务实例信息进行一个修改,保证本地的服务注册表信息和eureka server的服务注册表一致,在获取增量注册表信息的时候,同时获取了一个eureka server全量注册表的hash值,在eureka client增量同步注册表完成之后,也会计算一个hash值,然后将自己计算出来的这个hash值和eureka server全量注册表的hash值进行比对,如果是一致的,说明增量数据同步没问题,反之则说了增量数据同步出现了不一致,那么就会重新从eureka server全量拉取一份最新的服务注册表。
客户端启动是会进行拉取注册表:在client端的源码com.netflix.discovery.DiscoveryClient
类的fetchRegistry
方法中:
在全量拉取getAndStoreFullRegistry()
方法中:
EurekaHttpResponse<Applications> httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get());
进入getApplications()
方法:可以看到全量拉取
的URL是/apps
而在server端的源码com.netflix.eureka.resources.ApplicationsResource
中可以看到:
在服务端源码com.netflix.eureka.registry.ResponseCacheImpl
中的构造方法中可以看到执行定时任务删除recentlyChangedQueue
中过期的数据:
根据源码分析:
在自动配置类中找到eurekaController方法
:
进入getStatusInfo()方法
:
进入关键判断方法isReplicaAvailable(node.getServiceUrl())
:
第二个判断条件:
结论:
客户端源码的分析主要从com.netflix.discovery.DiscoveryClient
这个类进行:
客户端拉取注册表
:
客户端注册
服务:
有3个定时任务执行,分别是:
eureka:
client:
service-url:
defaultZone: http://eureka-7900:7900/eureka/,http://eureka-7901:7901/eureka/,http://eureka-7902:7902/eureka/
客户端注册只会向第一个server注册,第一个注册不成功才会向第二个server注册。
另外:如果集群配置了4个节点,则不会想第四个server进行注册,因为默然参数是3个。
在源码com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient
中可以找到:
注意:客户端在拉取注册表也只会从第一个server端拉取。
所以在实际生产环境中,可以把客户端配置的eureka server url 顺序打乱,防止一个server的压力过大。
Eureka Server端主要做的事情:
Eureka Client端主要做的事情:
spring:
application:
name: cloud-eureka
# CAP原则 :一致性(Consistency)/可用性(Availability)/分区容忍性(Partition tolerance)
# Eureka实现了AP ,存在三级缓存:register --> readWriteCaCheMap --> readOnlyCacheMap 30s同步一次 use-read-only-response-cache
eureka:
client:
register-with-eureka: true #是否将自己注册到eureka中
fetch-registry: false #是否从eureka中获取信息()
service-url:
# 单节点 就写一个;两个节点只写另一个节点 ;三个以上节点:全部写上 。逗号分隔
#defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
# defaultZone: http://localhost:7900/eureka/
defaultZone: http://eureka-7900:7900/eureka/,http://eureka-7901:7901/eureka/,http://eureka-7902:7902/eureka/
registry-fetch-interval-seconds: 20 # 拉取注册表间隔时间 单位秒
server:
enable-self-preservation: false #Eureka自我保护机制 看服务的多少:服务很多建议开启;服务较少 则关闭
renewal-percent-threshold: 0.85 #自我保护阈值 ,默认为0.85
eviction-interval-timer-in-ms: 1000 # 剔除服务间隔(eureka server将长时间没有心跳的服务从注册表剔除)可以设置参数为1秒实现服务快速下线,单位:毫秒,默认60秒,
use-read-only-response-cache: false # 是否开启从readOnly读注册表
response-cache-update-interval-ms: 1000 #缓存同步时间(readONLYCache和readWriteCache同步时间间隔) 单位:毫秒,默认30秒,
---
server:
port: 7900
spring:
profiles: 7900
eureka:
instance:
# host文件配置
hostname: eureka-7900
---
server:
port: 7901
spring:
profiles: 7901
eureka:
instance:
# host文件配置
hostname: eureka-7901
---
server:
port: 7902
spring:
profiles: 7902
eureka:
instance:
# host文件配置
hostname: eureka-7902
# 应用名称
server:
port: 8080
spring:
application:
name: api-passenger
eureka:
client:
# eureka client 功能开关
# enabled: false
service-url:
#注意,生产中如果配置集群,则集群地址顺序要打乱,因为拉取注册表只从第一个地址拉取
defaultZone: http://localhost:7900/eureka
# 拉取注册表间隔时间
registry-fetch-interval-seconds: 30
instance:
# 心跳间隔时间(续约时间,即:client发送心跳,server对该服务续约) 默认30
lease-renewal-interval-in-seconds: 30