Eureka注册中心源码分析及在生产环境中的优化

一、简介

EurekaSpring Cloud Netflix生态中的服务注册与发现组件。在平常开发过程中,我们经常搭建一个简单的eureka节点或者是集群就直接在生产环境中就使用了,但是通过对Eureka源码的分析,其中还有很多可以优化的地方。今天本篇文章来具体分析一下Eureka Server和Eureka Client的源码。

版本介绍:
SpringBoot : 2.3.0.RELEASE
SpringCloud : Hoxton.SR4

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

server端和client端的依赖:

   <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
   </dependency>

二、快速搭建

  1. 在pom.xml文件引入server端的依赖。
  2. 主启动类:通过@EnableEurekaServer注解开启Server服务端。
    在这里插入图片描述
    单机版和集群版主要在配置文件不同:
    可以参考官方文档:https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/#spring-cloud-eureka-server-peer-awareness

2.1 单节点版

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false #是否将自己注册到eureka中
    fetchRegistry: false #是否从eureka中获取信息
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

2.2 双节点版

双节点配置的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/ #在第二个节点配置第一个节点

2.3 三节点版

三节点则需要将三个节点地址全部写上(参考官方文档)。

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/
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
三节点的注册中心搭建完毕。

三、Server端源码分析

接下来对eureka server 端的源码进行分析,首先从自动配置类入手,在spring-cloud-netflix-eureka-server jar包META-INF目录下的spring.factories文件找到自动配置的类 org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

在这里插入图片描述在这里插入图片描述
EurekaServerAutoConfiguration这个类就是启动服务端的自动配置类,接下来我们从这个类入手。

3.1 启动服务端

在主启动类上使用@EnableEurekaServer注解即可开启服务端,进入@EnableEurekaServer注解类,可以看到该注解类通过 @Import 注解引入了 EurekaServerMarkerConfiguration 类,在EurekaServerMarkerConfiguration这个类中仅仅只是创建了一个Marker标记类。
在这里插入图片描述

在这里插入图片描述

而在Eureka的自动配置类EurekaServerAutoConfiguration中可以看到:
在这里插入图片描述
@ConditionalOnBean({Marker.class})这个注解,表示这个Marker标记类是作为条件的,所以可以把Marker当做一个开关,使用@EnableEurekaServer创建了Marker类,表示打开开关,启动服务端配置。

3.2 自我保护源码分析

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,剔除服务;如果最后一分钟续约数小于阈值,则不剔除服务,自我保护正式开启。

3.3 三级缓存

从CAP理论看,Eureka是一个AP系统,其优先保证可用性(A)和分区容错性( P),不保证强一致性 ( C)。

缓存名称类型
一级缓存registryConcurrentHashMap<String, Map<String, Lease>> registry = new ConcurrentHashMap();
二级缓存readWriteCacheMapLoadingCache<Key, ResponseCacheImpl.Value> readWriteCacheMap 谷歌缓存框架guava
三级缓存readOnlyCacheMapConcurrentMap<Key, ResponseCacheImpl.Value> readOnlyCacheMap = new ConcurrentHashMap();

在这里插入图片描述

3.3.1 初始化缓存

在EurekaServerAutoConfiguration自动配置类中的eurekaServerContext方法,构造了一个DefaultEurekaServerContext类:
在这里插入图片描述
DefaultEurekaServerContext这个类中的initialize()方法中调用了PeerAwareInstanceRegistryImplinit()方法:

在这里插入图片描述
在**init()**这个方法中调用this.initializedResponseCache();进行缓存初始化:
在这里插入图片描述

在这里插入图片描述
在initializedResponseCache()方法中,构造了一个ResponseCacheImpl对象,在继续看这个对象的构造方法:
在这里插入图片描述
通过ResponseCacheImpl的构造方法,创建出了二级缓存readWriteCacheMap, 使用的是谷歌的guava缓存框架,并且设置 二级缓存失效时间为180秒:
在这里插入图片描述
通过ResponseCacheImpl.Value value = ResponseCacheImpl.this.generatePayload(key);方法可以看出,如果从二级缓存没有获得到信息,则会直接从一级缓存中获得信息:
在这里插入图片描述
最后执行二级缓存(readWriteCacheMap)和三级缓存(readOnlyCacheMap)之间的缓存更新任务,responseCacheUpdateIntervalMs 为定时任务间隔时间,即表示缓存更新的时间间隔,默认为30秒:
在这里插入图片描述

在继续看这个缓存更新任务,进入getCacheUpdateTask() 方法:
在这里插入图片描述

3.3.2 服务注册时将缓存失效

当有Eureka client启动的时候,就会执行到Eureka server端的com.netflix.eureka.resources.ApplicationResource类中的addInstance方法,即添加实例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意: 源码跟踪到最后发现,是将缓存失效,表示如果一个新的实例注册上来,则需要首先将原来的readWriteCacheMap缓存失效掉。

3.3.3 使用缓存

ApplicationResource 类中的getApplication方法:

在这里插入图片描述
进入get()方法一路向下最后找到getValue()方法():
useReadOnlyCache 的值即为是否使用缓存,在配置文件中用eureka.server.use-read-only-response-cache进行配置,false表示不开启readOnlyCacheMap缓存。
在这里插入图片描述
注意:在服务注册的时候将readWriteCacheMap中的缓存失效掉,所以如果从readWriteCacheMap缓存中获取不到信息,则会直接从一级缓存(registry)获取信息。

3.4 集群同步

如果在第二个server节点启动后,会拉取第一个server节点的注册表信息,拉取动作只会发生一次,此时如果第一个server节点之后又来了一个client实例,则只能通过集群同步的方式同步到第二个节点。
这一过程发生在com.netflix.eureka.resources.ApplicationResourceaddInstance()方法里:
在这里插入图片描述
在这里插入图片描述

3.5 服务续约、下线

当服务启动并成功注册到Eureka服务器后,Eureka客户端会默认以每隔30秒的频率向Eureka服务器发送一次心跳。发送心跳起始就是执行服务续约(Renew)操作,避免自己的注册信息被Eureka服务器剔除。续约的处理逻辑和与服务注册逻辑基本一致:首先更新时间戳,然后同步到其他Eureka服务器节点。

com.netflix.eureka.resources.InstanceResource 类的renewLease() 方法:更新lastUpdateTimestamp时间,并且进行集群同步
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到服务续约服务下线服务上线都是更新时间戳,并且进行集群同步
在这里插入图片描述

3.6 拉取注册表(全量和增量)

服务注册表的全量拉取很好理解,就是第一次的时候,从注册表全量拉取注册表到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

在这里插入图片描述同样的增量拉取对应的url是:apps/delta
在这里插入图片描述

而在server端的源码com.netflix.eureka.resources.ApplicationsResource中可以看到:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在服务端源码com.netflix.eureka.registry.ResponseCacheImpl中的构造方法中可以看到执行定时任务删除recentlyChangedQueue中过期的数据:
在这里插入图片描述

在这里插入图片描述
定时任务默认30秒:
在这里插入图片描述

全量和增量数据都是先从缓存中获取:
在这里插入图片描述

在这里插入图片描述
recentlyChangedQueue获取增量数据:
在这里插入图片描述

3.7 unavailable-replicas 副本不可用问题

在这里插入图片描述
根据源码分析:
在自动配置类中找到eurekaController方法
在这里插入图片描述

在这里插入图片描述
进入getStatusInfo()方法
在这里插入图片描述
进入关键判断方法isReplicaAvailable(node.getServiceUrl())
在这里插入图片描述
第二个判断条件:
在这里插入图片描述

结论:

  1. 需要将eureka.client.register-with-eureka 设置为true,注册到eureka中;
  2. eureka.client.service-url.defaultZone配置的域名需要和eureka.instance.hostname的值一样。
    在这里插入图片描述

四、Client端源码分析

客户端源码的分析主要从com.netflix.discovery.DiscoveryClient这个类进行:
在这里插入图片描述
客户端拉取注册表
在这里插入图片描述
客户端注册服务:
在这里插入图片描述
有3个定时任务执行,分别是:

  1. 缓存刷新(定时拉取注册表)
  2. 发送心跳(定时发送心跳,服务端对其续约,如果长时间没有心跳,服务端则会剔除)
  3. 状态改变监听(按需注册,如果我自己的实例和server端的不一样,则会重新进行注册)
    在这里插入图片描述

在这里插入图片描述
服务下线,在销毁之前将服务下线:在这里插入图片描述
注意:

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端主要做的事情:

  1. 三级缓存机制
  2. 剔除服务定时任务(接收心跳、服务续约、自我保护机制)
  3. 集群同步(server节点记一次启动拉取注册表,之后通过集群同步拉取注册表)

Eureka Client端主要做的事情:

  1. 拉取注册表(第一次全量拉取,之后通过增量拉取)
  2. 注册服务
  3. 服务下线(在服务停止之前执行服务下线)
  4. 3个定时任务(发送心跳、更新缓存、状态改变监听)

Server端配置

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

Client端配置

# 应用名称
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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liu_Shihao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值