Eureka 由浅入深解读,7W+篇幅,再也不想学下去了(一)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 上篇我对微服务方案实现的其中之一SpringCloud进行了介绍,相信大家已经对它有个大致的了解了。👉《小白学微服务》之,什么是Spring Cloud Netflix比如,SpringCloud不是一个技术,而是一个技术的集合体;再比如SpringCloud组件中的各个技术都是以SpringBoot为基础实现的等。那既然知道了这些,我就开始介绍SpringCloud中的第一个组件了-Eureka。


一、是什么


SpringCloud Eureka 是 SpringCloud Netflix 微服务套件的一部分,基于 Netflix Eureka 做了二次封装,主要负责实现微服务架构中的服务治理功能。


SpringCloud Eureka 是一个基于 REST 的服务,并且提供了基于 Java 的客户端组件,能够非常方便地将服务注册到 SpringCloud Eureka 中进行统一管理。


在微服务架构体系中,服务治理是必不可少的。在服务治理中,服务注册中心是一定要有的,而除了用 Eureka 作为注册中心之外,还可以用,Consul、Etcd、Zookeeper 等来作为服务的注册中心。


当然,在常见的可以作为服务注册中心的组件中,Zookeeper 和 Eureka 一样都是使用的比较多的。那为什么SpringCloud不用 Zookeeper 当作注册中心而选用 Eureka呢!


这就不得不提一个在分布式领域中的著名定理CAP了。


1.1 CAP定理

CAP定理又称CAP原则,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),最多只能同时满足三个特性中的两个,三者不可兼得。


image.png


C:Consisteny(一致性),比如数据库是主从模式,一个写库请求进来了,master库完成了写入操作,但是再slave同步数据之前,另一个用户查了这条数据,结果没查到,但是也没报错,这就不是强一致性。虽然最终会同步成功,但这是最终一致性的体现。强一致性的体现在于我不管你因为什么没同步成功(可能网络延迟或其他等),只要没同步成功,我这个slave就不能对外提供服务。必须主从数据一致才可以提供服务。(很少有做到这点的)

A:Availability(可用性),还是上面的例子,就是保证了可用性。因为虽然主从没同步完成,但是我从库照样能提供服务而且及时响应结果。也就是说可用性保证服务可用,而不在乎数据是否一致。明显和C是冲突的,那CA怎么还能组合到一起?其实是可以的(单机部署)。

P:Partition Tolerance(分区容错性),集群部署了三台服务。挂了一台,其他两台还能继续对外提供服务,这时候我就认为他是没问题的,也就是我能容忍你挂了一台,只要还有服务能对外提供请求即可。所以一般分区容忍性是必须的,一般都需要从C和A之间做选择。

那么现在两两组合的话就有下面三种情况:


CP:满足一致性和分区容忍性,常见的就是 Zookeeper 集群。


AP:满足可用性和分区容忍性,创建的就是 Eureka 集群。


CA:满足一致性和可用性,适用单机部署系统,扩展性不强。


至于在分布式微服务系统中如何抉择,就没有很好的定论了。所以在设计系统架构的时候,我们应该根据系统具体的业务场景来权衡CAP。


只有适合的才是最好的,切不可为了追求完美而浪费太多的时间精力在CAP的抉择上。


更多CAP内容可看扩展阅读一1


1.2 为什么SpringCloud注册中心选择Eureka

根据上面的CAP理论,我们知道 zookeeper 和 Eureka 在设计的时候就已经对分布式场景作出了一些取舍。


zookeeper的CP保证


当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s,且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。


Eureka的AP保证


Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:


Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务

Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)

当网络稳定时,当前实例新的注册信息会被同步到其它节点中


结论:Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个注册服务瘫痪。


1.3 Eureka架构

Eureka1.x版本架构图


image.png


从架构图中我先来说一下Eureka的分区。


Eureka 提供了 Region(区域) 和 Availability Zone(可用区) 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:


region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制,根据项目具体的情况,可以自行合理划分 region。图中us-east-1c、us-east-1d、us-east-1e就是一个个不同的区域。


Availability Zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。


了解分区之后,可以知道 Eureka 之间的行为非常之多,具体如下:


  • Application Service:由名字可知,它是一个服务的提供者,同时也是一个Eureka Client。作用,扮演服务提供角色,提供业务服务,向 Eureka Server 注册和更新自己的信息,同时能从 Eureka Server 注册表中获取到其它服务信息。
  • Eureka Server:扮演服务注册中心的角色,提供服务注册和发现的功能。每个 Eureka Client 向 Eureka Server 注册自己的信息,也可以通过 Eureka 获取到其它服务的信息达到发现和调用其它服务的目的。
  • Application Client:是一个 Eureka Client ,扮演服务消费者的角色,通过 Eureka Server 获取注册到其上的其它服务信息,从而根据信息找到所需的服务发起远程调用。
  • Replicate:Eureka Server 之间注册表信息的同步复制,使 Eureka Server 集群中不同注册表中的服务实例信息保持一致。
  • Make Remote Call:服务之间的远程调用。
  • Register:注册服务实例,Client 端向 Server 端注册自身的元数据以提供服务发现。
  • Renew:续约,通过发送心跳到 Server 以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server 没有收到 Client 的心跳信息,将默认服务下线,会把服务实例信息从注册表中删除。
  • Cancel:服务下线,Client 再关闭时主动向 Server 注销服务实例元数据,这是 Client 的服务实例数据信息将从注册表中删除。
  • Get Registry:获取注册表,Client 向 Server 请求注册表信息,用于服务发现,从而发起服务之间的远程调用。


这些行为在 Eureka 中发挥着重要的作用,那了解了以上的这些内容相信Eureka中各位的脑海中有了一个大致的轮廓的。比如Eureka是一个服务治理框架,用于服务注册发现,强调AP、用分区的架构来实现集群等。


二、基础应用


好记性不然烂笔头,虽然用在这里不恰当,但明白我的意思就行,讲了很多,下面就来实操搭建一下Eureka的Server,Client端应用程序。


一点说明:


SpringCloud版本:Finchley.SR3


SpringBoot版本:2.0.3.RELEASE


所有项目都是基于SpringBoot开发


2.1 项目基础模块规划

因为 Eureka 是 SpringCloud 的一个组件,所以前期肯定是搭建一个微服务项目,而对于微服务项目,依照本人的搭建习惯,会将项目的目录结构做如下调整:

spring-cloud-study(父工程)
|
----server(业务模块)
  ----server-user(业务具体模块)
  ----server-XXXX
|
----spring-cloud(cloud模块)
  ----spring-cloud-eureka(cloud的各个组件)
  ----spring-cloud-config
  ----spring-cloud-XXXX


既然目录结构清晰了,那先创建父工程,业务模块,cloud 模块出来


image.png


1)父工程 pom 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.baiqi</groupId>
    <artifactId>springcloud-study</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <!--父工程名称-->
    <name>springcloud-study</name>
    <!--聚合子模块-->
    <modules>
        <module>spring-cloud</module>
        <module>server</module>
    </modules>
    <!--版本控制-->
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR3</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>
    <dependencies>
        <!--web启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


2)业务模块 pom 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父级依赖-->
    <parent>
        <artifactId>springcloud-study</artifactId>
        <groupId>cn.baiqi</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>server</name>
    <!--打包方式:pom-->
    <packaging>pom</packaging>
    <!--业务模块下的子业务模块-->
    <modules>
    </modules>
    <dependencies>
        <!-- Eureka客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


3)cloud 模块 pom 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父级依赖-->
    <parent>
        <artifactId>springcloud-study</artifactId>
        <groupId>cn.baiqi</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>spring-cloud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud</name>
    <!--打包方式:pom-->
    <packaging>pom</packaging>
    <!--cloud模块下的子组件模块-->
    <modules>
    </modules>
    <dependencies>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


到此项目的架子算是搭建起来了,创建聚合工程的时候我们要注意如下几点


父级项目只做聚合,不写任何代码,打包方式为 pom

父级项目的 pom 文件中要聚合子项目将其下的子项目聚合到<modules>标签中

子模块的<parent>依赖要写父级坐标,打包方式为 jar

现在可以往项目中添加一个个的组件模块了。


2.2 Eureka注册中心搭建

首先我们在 spring-cloud 模块中添加一个子模块,用来当作 Eureka 注册中心,子模块名:spring-cloud-eureka


1)创建 SpringBoot 项目,目录结构如下:


image.png


2)编写 pom 文件,并导入 Eureka 服务端依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父项目依赖-->
    <parent>
        <artifactId>spring-cloud</artifactId>
        <groupId>cn.baiqi</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>spring-cloud-eureka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!--模块名-->
    <name>spring-cloud-eureka</name>
    <dependencies>
        <!--Eureka服务端依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


3)编写配置文件:application.yml

server:
  port: 9000 #端口
spring:
  application:
    name: spring-cloud-eureka #应用名称
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka #暴露的服务注册中心地址,用于Server和Client之间交互
    register-with-eureka: false #因为本服务是服务注册中心,所以不需要注册自己
    fetch-registry: false #不需要向注册中心获取注册表信息


4)启动类上添加@EnableEurekaServer注解

@EnableEurekaServer // 开启Eureka服务端
@SpringBootApplication
public class SpringCloudEurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudEurekaApplication.class, args);
    }
}


5)启动 Eureka 注册中心服务,访问:http://localhost:9000/,看到如下页面即注册中心搭建完成。


image.png


注册中心搭建好之后,其它服务就可以往这个注册中心注册服务了,按照习惯我准备了两个业务服务:


server-user(调用者)

server-order(提供者)

下面分别搭建一下把。


2.3 Eureka Client搭建-服务提供者

业务服务名:server-order


1)创建 SpringBoot 项目,目录结构如下:


image.png


2)因为 server-order 的父项目是 server ,所以只需在 server 中的 pom 里添加 Eureka 的客户端依赖,这样只要是 server 下面的项目,都会因为依赖传递而获取到相关的依赖,记住别忘了将子项目聚合到父项目中(<modules>标签里)


server-order 的 pom 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
        <!--父级依赖-->
    <parent>
        <artifactId>server</artifactId>
        <groupId>cn.baiqi</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>server-order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>server-order</name>
    <dependencies>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


server 的 pom 文件


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父级依赖-->
    <parent>
        <artifactId>springcloud-study</artifactId>
        <groupId>cn.baiqi</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>server</name>
    <!--打包方式:pom-->
    <packaging>pom</packaging>
    <!--业务模块下的子业务模块-->
    <modules>
        <module>server-user</module>
        <module>server-order</module>
    </modules>
    <dependencies>
        <!-- Eureka客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


3)配置文件:application.yml

server:
  port: 9001 #端口
eureka:
  instance:
    lease-renewal-interval-in-seconds: 2 #每间隔2s,向服务端发送一次心跳,证明自己依然"存活"
    lease-expiration-duration-in-seconds: 10 #告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我踢出掉
    prefer-ip-address: true #告诉服务端,服务实例以IP作为链接,而不是取机器名
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka #注册中心地址
spring:
  application:
    name: server-order #服务名
logging:
  level:
    org.springframework: debug #对应模块的日志级别


4)添加一个 Controller,对外提供调用

/**
 * @author J3-白起
 * @package cn.baiqi.serverorder.controller
 * @createTime 2021/5/7 - 14:38
 * @description 订单控制器
 */
@RestController
@RequestMapping("/order")
public class OrderController {
    @GetMapping("/getAll/{value}")
    public String getAll(@PathVariable String value) {
        return "order服务提供了相关数据:" + value;
    }
}


该接口被调用后会返回相应的数据


5)Eureka Client 端在启动类中不需要添加 @EnableXXXX 注解,只要添加了 Client 依赖,它就是一个 Eureka Client 应用,直接启动就行了。


2.4 Eureka Client搭建-服务调用者

业务服务名:server-user


1)创建 SpringBoot 项目,目录结构如下:


image.png


2)server-user 模块的 pom 文件同服务提供者一样,不赘述,server 的 pom 文件只需聚合子项目模块就行。


3)配置文件:application.yml

server:
  port: 9002 #端口
eureka:
  instance:
    lease-renewal-interval-in-seconds: 2 #每间隔2s,向服务端发送一次心跳,证明自己依然"存活"
    lease-expiration-duration-in-seconds: 10 #告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我踢出掉
    prefer-ip-address: true #告诉服务端,服务实例以IP作为链接,而不是取机器名
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka #注册中心地址
spring:
  application:
    name: server-user #服务名
logging:
  level:
    org.springframework: debug #对应模块的日志级别


4)添加一个调用 Controller,获取相关数据

/**
 * @author J3-白起
 * @package cn.baiqi.serveruser.controller
 * @createTime 2021/5/7 - 14:46
 * @description 用户控制器
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/getUserOrder/{userId}")
    public String getUserOrder(@PathVariable Long userId) {
        // 调用地址
        String url = "http://server-order/order/getAll/{userId}";
        // 调用
        String body = restTemplate.getForEntity(url, String.class, 1L).getBody();
        return body;
    }
    /**
     * 注入一个负载均衡的 restTemplate 用于服务调用
     *
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}


访问该接口就可以调用对应的服务提供者获得相关数据。


5)Eureka Client 端在启动类中不需要添加 @EnableXXXX 注解,只要添加了 Client 依赖,它就是一个 Eureka Client 应用,直接启动就行了。


2.5 启动测试

依次启动应用,访问 Eureka 的注册中心主页,界面如下:


image.png


访问:http://localhost:9002/user/getUserOrder/2,结果如下

order服务提供了相关数据:2



RestTemplate 将根据服务名 server-order 通过预先从 spring-cloud-eureka 缓存到本地的注册表中获取到 server-order 服务的具体地址 ,从而发起服务间调用。


三、Eureka Client 源码解析


通过上面的服务调用案例,我们基本上不用做过多的复杂操作,即可完成一个远程的服务调用。


所以 Eureka Client 为了简化开发人员的工作,将很多与 Eureka Server 交互的工作都隐藏起来了,自主完成。


下面看看各个阶段,Eureka Client 在后台自动完成的工作图:


image.png


对于 Eureka Client 而言,它没有在启动类上加 @EnableXXXX 注解依然可以正常使用其相关的功能,那么我们肯定就想到了 SpringBoot 的自动装配原理。


找到 Eureka Client 源码包中的 spring.factories 文件就可以知道在启动的时候,自动配置了那些功能。


image.png


在自动装配类中,我们关注下面三个配置类:


EurekaClientAutoConfiguration:Eureka Client 自动配置类,负责Eureka Client 中关键Beans的配置和初始化,如ApplicationManager 和 EurekaClientConfig 等。

RibbonEurekaAutoConfiguration:Ribbon 负载均衡相关配置。

EurekaDiscoveryClientConfiguration:配置自动注册和应用的健康检测器。


3.1 读取应用自身配置


EurekaClientAutoConfiguration 配置类中定义了非常多的Bean,在 SpringBoot 启动的时候就会完成这些Bean的读取和配置,主要的配置如下。


image.png


EurekaClientAutoConfiguration 配置类代码:

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
@AutoConfigureBefore({NoopDiscoveryClientAutoConfiguration.class,
                      CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class})
@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",
                            "org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",
                            "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})
public class EurekaClientAutoConfiguration {
    // ......
}


看到这么多注解不要慌,有些不是我们的关注点,我们只需要关注下面几点就可:


关注点一:@ConditionalOnBean注解作用,该类在加载之前环境中必须要有EurekaClientConfig.class,EurekaDiscoveryClientConfiguration.Marker.class两个类,否则不加载。


关注点二:@ConditionalOnProperty注解作用,配置文件中eureka.client.enabled属性的值为 ture 时,该配置类才生效,而该属性默认值就为true,所以该属性配置可以忽略。这也是我们不用在 Eureka 客户端程序的启动类上添加@EnableEurekaClient注解,程序依然生效的原因。


关注点三:@AutoConfigureBefore和@AutoConfigureAfter注解作用,该类的加载的顺序为@AutoConfigureAfter注解中定义的配置类之后,然后再是该配置类,最后再是@AutoConfigureBefore注解中定义的类加载。


加载顺序:@AutoConfigureAfter ==> EurekaClientAutoConfiguration ==> @AutoConfigureBefore

在加载@AutoConfigureAfter注解时,当加载到EurekaDiscoveryClientConfiguration后,EurekaClientAutoConfiguration配置类就满足了所有定义的条件,即生效了。


image.png


3.2 客户端发现

先来了解一下DiscoveryClient接口,和DiscoveryClient类。


DiscoveryClient 接口是 SpringCloud 框架提供的,主要为了扩展 Netflix 提供的 Eureka 客户端而提供的,该接口的实现类通过组合的方式引入了 Netflix 提供的 DiscoveryClient 类,然后进行了进一步封装,让开发者更加容易使用 SpringBoot 进行基于 Eureka 的开发。


DiscoveryClient 类是 Netflix 开源框架提供的,主要用于与Eureka服务端(即注册中心)进行交互。


DiscoveryClient接口和其默认实现类EurekaDiscoveryClient代码


DiscoveryClient.java

public interface DiscoveryClient {
    // 获取实现类的描述
    String description();
  // 通过服务id获取服务实例信息
    List<ServiceInstance> getInstances(String serviceId);
    // 获取所有服务实例id
    List<String> getServices();
}


EurekaDiscoveryClient.java # getInstances()

public class EurekaDiscoveryClient implements DiscoveryClient {
    // ......
    private final EurekaClient eurekaClient;
    // ......
    @Override
    public List<ServiceInstance> getInstances(String serviceId) {
        List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
                                                                              false);
        List<ServiceInstance> instances = new ArrayList<>();
        for (InstanceInfo info : infos) {
            instances.add(new EurekaServiceInstance(info));
        }
        return instances;
    }
    // ......
}


EurekaDiscoveryClient 实现了 DiscoveryClient 接口,通过 getInstances() 方法会发现,其中又调用了 Netflix 提供的 EurekaClient 接口这种组合形式的方式使得我们非常容易的进行 Eureka 开发。


image.png


通过组合形式,我们就将代码拉回到了 Netflix 包中。


EurekaClient 接口来自于 com.netflix.discovery 包中,默认实现为 com.netflix.discovery.DiscoveryClient 它属于 eureka-client的源代码。


DiscoveryClient 可以说时功能非常的强大,提供了 Eureka Client 注册到 Server上、续租、下线、及获取Server中注册表信息等诸多关键功能。


下面继续分析这个类。


3.3 DiscoveryClient类分析

DiscoveryClient 是 Eureka Client 的核心类,负责与 Eureka Server 交互的关键逻辑,具体功能如下:


注册服务实例到 Eureka Server 中

发送心跳更新与 Eureka Server 的续约

在服务关闭时从 Eureka Server 中取消续约,服务下线

查询在 Eureka Server 中注册的服务实例列表

上面有该类的类图👆。


DiscoveryClient 继承了 LookupService 接口, LookupService作用是发现活跃的服务实例, 主要方法如下:

public interface LookupService<T> {
    // 根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器
    Application getApplication(String appName);
    // 返回当前注册表中所有的服务实例信息
    Applications getApplications();
    // 根据服务实例的 id 获取服务实例信息
    List<InstanceInfo> getInstancesById(String id);
    // ......
}


Application:持有服务实例信息列表,它可以理解成同一个服务的集群信息,这些服实例都挂在同一个服务名 appName 下。

Applications:表示注册表中所有服务实例信息的集合

InstanceInfo:表示一个服务实例信息。

Application和Applications类中,对InstanceInfo的操作都是同步操作,防止出错。


EurekaClient在LookupService接口的基础上扩充了更多的接口,并提供了非常丰富的获取服务实例的方式,下面主要关注两个接口。

@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {
    // ......
    // 为Eureka Client注册健康检查处理器
    public void registerHealthCheck(HealthCheckHandler healthCheckHandler);
    // 为Eureka Client注册一个EurekaEventListener(事件监听器)监听client服务器实力信息的更新
    public void registerEventListener(EurekaEventListener eventListener);
    // ......
}


在 Eureka Server 中一般通过心跳(heartbeats)来识别一个实例的状态。Eureka Client 中存在一个定时任务定时通过 HealthCheckHandler 检测当前 Client 的状态,如果 Client 的状态发生改变,将会触发新的注册事件,更新 Eureka Server 的注册表中该服务的相关信息。


HealthCheckHandler代码如下:

public interface HealthCheckHandler {
    InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);
}


Eureka 中的事件监听模式属于观察者模式,事间监听器将监听 Client 的服务实例信息变化,触发对应的处理事件,EurekaEvent类图如下。


image.png


3.4 DiscoveryClient构造函数

咱们再回到EurekaClientAutoConfiguration类中。

当程序加载该类的时候,关注如下代码:

public class EurekaClientAutoConfiguration {
    // ......
    @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
    @org.springframework.cloud.context.config.annotation.RefreshScope
    @Lazy
    public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance,
        @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
      //If we use the proxy of the ApplicationInfoManager we could run into a problem
      //when shutdown is called on the CloudEurekaClient where the ApplicationInfoManager bean is
      //requested but wont be allowed because we are shutting down.  To avoid this we use the
      //object directly.
      ApplicationInfoManager appManager;
      if (AopUtils.isAopProxy(manager)) {
        appManager = ProxyUtils.getTargetObject(manager);
      }
      else {
        appManager = manager;
      }
            // 创建Eureka Client
      CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs,
          this.context);
      cloudEurekaClient.registerHealthCheck(healthCheckHandler);
      return cloudEurekaClient;
    }
    // ......
}


该类加载时,Eureka Client 的初始化入口就是从这里开始进入的,具体的说,可以是下面这都段代码:

// 创建Eureka Client
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs,
                                                            this.context);


进入 CloudEurekaClient 类的构造器,可以确认最终它调用的是 DiscoveryClient类的构造器。


DiscoveryClient 构造器所做的功能非常多,包含:


Eureka Client 从 Eureka Server 中拉取注册表信息

服务注册

初始化发送心跳

缓存刷新(重新拉去注册表信息)

按需注册定时任务

上面所列的功能,可以说是贯穿了 Eureka Client 启动阶段的各项工作。


构造器代码如下:

@Inject // java自带的自动注入注解
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider) {
    // ......
}


简单介绍一下构造参数:


ApplicationInfoManager:上面提到过,它是应用信息管理器。


EurekaClientConfig:上面提到过,封装了 Client 和 Server 交互配置信息的类。

AbstractDiscoveryClientOptionalArgs:注入一些可选参数,和一些像 jersey1,jersey2等通用过滤器。


BackupRegistry:备份注册中心的职责,当 Eureka Client 无法从任何一个Eureka Server 中获取注册表信息时,BackupRegistry将被调用以获取注册表信息。


从构造器代码往下看,忽略一些参数校验,属性赋值等之后,先来关注下面几行代码:

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider) {
    // ......
    this.applicationInfoManager = applicationInfoManager;
    InstanceInfo myInfo = applicationInfoManager.getInfo();
    // 客户端配置 大部分信息采用DefaultEurekaClientConfig
    clientConfig = config;
    // 已经过时,主要为了兼容遗留客户端问题
    staticClientConfig = clientConfig;
    // 传输层如http请求超时、重试等信息
    transportConfig = config.getTransportConfig();
    // 该eureka实例的信息 如主机信息,健康检测接口等
    instanceInfo = myInfo;
    if (myInfo != null) {
        appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
    } else {
        logger.warn("Setting instanceInfo to a passed in null value");
    }
    // 备份注册表信息,当服务端不可用时,客户度可以从这里获取注册表信息
    this.backupRegistryProvider = backupRegistryProvider;
    // 如果eureka server的地址来源dns服务,则随机获取urls
    this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
    // 采用cas Applications存放的时服务器返回的存储客户端注册信息的
    localRegionApps.set(new Applications());
    // cas 递增版本,防止客户端注册旧的信息
    fetchRegistryGeneration = new AtomicLong(0);
    remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
    remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
    // 判断是否需要从eureka server 获取注册表信息 并初始化相应的度量信息
    if (config.shouldFetchRegistry()) {
        this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
    } else {
        this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }
    // 是否需要将信息注册到eureka server上,通过这个开关可以实现,
    // 只获取其他实例的信息,而不将自己的信息给其他客户端发现
    if (config.shouldRegisterWithEureka()) {
        this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
    } else {
        this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }
    // 如果不需要注册信息到server和拉取注册信息表,则初始化完成。
    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
        // ......
        return;  // no need to setup up an network tasks and we are done
    }
    // ......
}


上面两个 if 判断语句中,就对应着 Eureka 拉取注册列表和注册服务,如果两个都为 flase 得话,那 Discovery 初始化直接结束,表示该客户端既不进行服务注册也不进行服务发现。


config.shouldFetchRegistry()对应的配置:eureka client.fetch.register


为 true 则表示将从 Eureka Server 中拉去注册表信息

config.shouldRegisterWithEureka()对应的配置:eureka.client.register.with-eureka


为 true 则表示将注册到 Eureka Server 中


往下看,接着定义了一个线程池 ScheduledExecutorService ,线程池大小为2,一个线程用于发送心跳,另一个线程用于缓存刷新,同时定义了发送心跳和缓存刷新线程池。

scheduler = Executors.newScheduledThreadPool(2,
                                             new ThreadFactoryBuilder()
                                             .setNameFormat("DiscoveryClient-%d")
                                             .setDaemon(true)
                                             .build());
// 发送心跳线程池
heartbeatExecutor = new ThreadPoolExecutor(...);  // use direct handoff
// 缓存刷新线程池
cacheRefreshExecutor = new ThreadPoolExecutor(...);  // use direct handoff


往下看,初始化了 Eureka Client 和 Eureka Service 进行 HTTP 交互的 Jersey 客户端,将 AbstractDiscoveryClientOptionalArgs 中的属性用来构建 EurekaTransport。

eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);


往下看,接着从 Eureka Server 中拉取注册表信息。

if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    fetchRegistryFromBackup();
}


如果 EurekaClientConfig # shouldFetchRegistry 为 ture 时,fetchRegistry 方法将被调用。


在 Eureka Client 向 Eureka Server 注册前,需要先从 Eureka Server 中拉取注册表中的信息,这是服务发现的前提。通过将 Eureka Server 中的注册表信息缓存到本地,就可以就近获取其它服务的相关信息,减少与 Eureka Server 的网络通信。


拉取完 Eureka Server 中的注册表信息后,将对服务实例进行注册,代码如下:

if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
    try {
        // 发起服务注册
        if (!register() ) {
            // 失败抛出异常
            throw new IllegalStateException("Registration error at startup. Invalid server response.");
        }
    } catch (Throwable th) {
        logger.error("Registration error at startup: {}", th.getMessage());
        throw new IllegalStateException(th);
    }
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 最后初始化定时任务
initScheduledTasks();


在服务注册之前会进行注册预注册,Eureka 没有对此提供默认实现。构造函数的最后将初始化并启动发送心跳,刷新缓存和按需注册等定时任务。


看到这里,DiscoveryClient 的构造函数中重要的功能步骤都讲解了一下,那咱们来总结一下在这里面所做的所有功能:


  1. 相关属性的赋值,如:ApplicationInfoManager,EurekaClientConfig等。
  2. 备份注册中心的初始化,默认没有实现。
  3. 拉取 Eureka Server 注册表中的信息
  4. 注册前的预处理
  5. 向 Eureka Server 注册自身
  6. 初始化心跳定时任务,缓存刷新和按需注册等定时任务


来一个清晰的流程图:


微信图片_20220427000857.png


整体的 Eureka Client 初始化的功能都分析完了,下面根据具体的功能进行分析,如拉取注册信息,续约,心跳,注册等。


3.5 拉取注册表信息

拉取注册表的入口在,DiscoveryClient类的 fetchRegistry 方法,相关代码如下:

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
    try {
        // If the delta is disabled or if it is the first time, get all
        // applications
        // 如果增量式拉取被禁止,或者Applications为null,进行全量拉取
        Applications applications = getApplications();
        if (clientConfig.shouldDisableDelta()
            || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
            || forceFullRegistryFetch
            || (applications == null)
            || (applications.getRegisteredApplications().size() == 0)
            || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
        {
            logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
            logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
            logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
            logger.info("Application is null : {}", (applications == null));
            logger.info("Registered Applications size is zero : {}",
                        (applications.getRegisteredApplications().size() == 0));
            logger.info("Application version is -1: {}", (applications.getVersion() == -1));
            // 全量拉取注册表信息
            getAndStoreFullRegistry();
        } else {
            // 增量拉取注册表信息
            getAndUpdateDelta(applications);
        }
        // 计算应用集合一致性哈希码
        applications.setAppsHashCode(applications.getReconcileHashCode());
        // 打印注册表上所有服务实例的总数量
        logTotalInstances();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
        return false;
    } finally {
        if (tracer != null) {
            tracer.stop();
        }
    }
    // Notify about cache refresh before updating the instance remote status
    // 在更新远程实例状态之前推送缓存刷新事件,但是Eureka 中并没有提供默认的事件监听器
    onCacheRefreshed();
    // Update remote status based on refreshed data held in the cache
    // 基于缓存中被刷新的数据更新远程实例状态
    updateInstanceRemoteStatus();
    // registry was fetched successfully, so return true
    // 注册表拉取成功,返回true
    return true;
}


当 Eureka 客户端第一次注册到 Eureka 服务端时才会进行全量拉取,其他时候都只进行增量拉取,下面来看看这两个拉取注册表信息的具体逻辑。


DiscoveryClient # getAndStoreFullRegistry():全量拉取

DiscoveryClient # getAndUpdateDelta():增量拉取


3.5.1 全量拉取注册表信息

DiscoveryClient # getAndStoreFullRegistry()代码如下:


private void getAndStoreFullRegistry() throws Throwable {
    // 获取拉取的注册表版本,防止拉取版本落后(由其他的线程引起)
    long currentUpdateGeneration = fetchRegistryGeneration.get();
    logger.info("Getting all instance registry info from the eureka server");
    Applications apps = null;
    // 全量拉取注册表
    EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
            ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
            : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
    // 获取成功
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        apps = httpResponse.getEntity();
    }
    logger.info("The response status is {}", httpResponse.getStatusCode());
    if (apps == null) {
        logger.error("The application is null for some reason. Not storing this information");
        // 检查currentUpdateGeneration的更新版本是否发生改变,无改变得华说明版本拉取是最新得
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        // 从apps中筛选出状态为 UP 得实例,同时打乱实例得顺序,方式同一个服务得不同实例在其同时接受流量
        localRegionApps.set(this.filterAndShuffle(apps));
        logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
    } else {
        logger.warn("Not updating applications as another thread is updating it already");
    }
}


全量拉取的方式将会从 Eureka Server 中拉取注册表中所有得服务实例信息(封装在 Applications中),并经过处理后替换掉本地注册表缓存Applications。


通过 debug 启动方式启动,我们可以看到向 Eureka Server 全量拉取服务注册表信息的 url 为:http://localhost:9000/eureka/apps,前面“http://localhost:9000/eureka”是配置文件配置的注册中心地址,后面“/apps/”是程序自己拼接的。


具体代码:AbstractJerseyEurekaHttpClient # getApplicationsInternal,截图如下。


image.png


getAndStoreFullRegistry() 方法是有可能被多个线程同时调用的,这会导致拉取的注册表被旧的注册表所覆盖(有可能出现先拉取注册表信息的线程在覆盖 apps 时被阻塞,被后拉取注册表信息的线程抢先设置了 apps,被阻塞的线程恢复后在次设置了 apps,导致 apps 数据版本落后),产生脏数据,对此,Eureka 通过类型为 AtomicLong 的 currentUpdateGeneration 对 apps 的更新版本进行跟踪。如果跟新版本不一致,说明本次拉取注册表信息已过时,不需要缓存到本地。


最后对拉取到的注册表信息 apps 进行了筛选,只保留状态为 UP 的服务实例信息。


3.5.2 增量拉取注册表信息

DiscoveryClient # getAndUpdateDelta()代码如下:


private void getAndUpdateDelta(Applications applications) throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();
    Applications delta = null;
    // 增量拉取注册表
    EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        delta = httpResponse.getEntity();
    }
  // 拉取失败,进行全量拉取
    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.");
        // 全量拉取
        getAndStoreFullRegistry();
        // 检查currentUpdateGeneration的更新版本是否发生改变,无改变得华说明版本拉取是最新得
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
        String reconcileHashCode = "";
        if (fetchRegistryUpdateLock.tryLock()) {
            try {
                // 更新本地缓存
                updateDelta(delta);
                // 计算应用集合一致性哈希码
                reconcileHashCode = getReconcileHashCode(applications);
            } finally {
                fetchRegistryUpdateLock.unlock();
            }
        } else {
            logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
        }
        // 比较应用集合一致性哈希码,如果不一致将认为本次增量式拉取数据已脏,将发起全量拉取更新本地缓存注册表信息
        // There is a diff in number of instances for some reason
        if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
            reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
        }
    } 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());
    }
}


增量更新注册表信息,访问 Eureka Server 的 url 为:http://localhost:9000/eureka/apps/delta,前面“http://localhost:9000/eureka”是配置文件配置的注册中心地址,后面“/apps/delta”是程序自己拼接的。


具体代码:AbstractJerseyEurekaHttpClient # getApplicationsInternal,截图如下。


image.png


由于更新的过程时间比较久,时间成本为O(N^2),所以需要通过同步代码快防止多个线程同时进行更新,污染数据。


在根据从 Eureka Server 拉取的 delta 信息更新本地缓存的时候,Eureka 定义了 ActionType 来标记变更状态,代码在 InstanceInfo 类中,代码如下:

public enum ActionType {
    // 添加 Eureka Server
    ADDED, // Added in the discovery server
    // 在 Eureka Server 中的信息已经修改
    MODIFIED, // Changed in the discovery server
    // 被从 Eureka Server 中删除
    DELETED // Deleted from the discovery server
}


在更新本地缓存的时候,根据 InstanceInfo # ActionType 的不同,对 delta 中的 InstanceInfo 采取不同的操作,其中 ADDED 和 MODIFIED 状态变更的服务实例信息将添加到本地注册标中,DELETED 状态变更的服务实例将从本地注册表中删除,具体代码如下:

private void updateDelta(Applications delta) {
    // ......
    // 变更类型为 ADDED
    if (ActionType.ADDED.equals(instance.getActionType())) {
        Application existingApp = applications.getRegisteredApplications(instance.getAppName());
        if (existingApp == null) {
            applications.addApplication(app);
        }
        logger.debug("Added instance {} to the existing apps in region {}", instance.getId(), instanceRegion);
        // 添加到本地注册表
        applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);
        // 变更类型为 MODIFIED
    } else if (ActionType.MODIFIED.equals(instance.getActionType())) {
        Application existingApp = applications.getRegisteredApplications(instance.getAppName());
        if (existingApp == null) {
            applications.addApplication(app);
        }
        logger.debug("Modified instance {} to the existing apps ", instance.getId());
        // 添加到本地注册表
        applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);
        // 变更类型为 DELETED
    } else if (ActionType.DELETED.equals(instance.getActionType())) {
        Application existingApp = applications.getRegisteredApplications(instance.getAppName());
        if (existingApp == null) {
            applications.addApplication(app);
        }
        logger.debug("Deleted instance {} to the existing apps ", instance.getId());
        // 从本地注册表中移除
        applications.getRegisteredApplications(instance.getAppName()).removeInstance(instance);
    }
    // ......
}


在更新完本地缓存的注册表中之后,Eureka Client 会通过 DiscoveryClient # getReconcileHashCode(Applications applications)方法计算合并后的 Applications 的 appHashCode (应用集合一直性哈希码),和 Eureka Server 传递的 delta 上的 appsHashCode 进行比较(delta 中携带的 appsHashCode 通过 Eureka Server 的全量注册表计算得出),比对客户端和服务端上注册表的差异。如果哈希值不一致的话将再次调用一次 getAndStoreFullRegistry 获取全量数据保证 Eureka Client 与 Eureka Server 之间注册表数据的一致。

private void getAndUpdateDelta(Applications applications) throws Throwable {
    // ......
    // There is a diff in number of instances for some reason
    if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
        reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
    }
    // ......
}


在 reconcileAndLogDifference() 方法中将会执行全量拉取注册表信息操作


appsHashCode 的一般表示为:


appsHashCode = ${status}_ ${count} _


它通过将应用状态和数量拼接成字符串,表示了当前注册表中服务实例状态的统计信息。比如:有 10 个应用实例的状态为 UP ,有 5 个应用实例状态为 DOWN ,其中它的状态数量为 0 (不进行表示),那么 appsHashCode 的形式如下是:


appsHashCode = UP_ 10 _ DOWN _ 5 _


3.4.3 拉取注册表流程图


image.png

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
目录
相关文章
|
3天前
|
XML Java 数据格式
🚀今天,我们来详细的聊一聊SpringBoot自动配置原理,学了这么久,你学废了吗?
🚀今天,我们来详细的聊一聊SpringBoot自动配置原理,学了这么久,你学废了吗?
75 0
|
6月前
|
存储 设计模式 编译器
千万不要错过的后端【纯干货】面试知识点整理 I
千万不要错过的后端【纯干货】面试知识点整理 I
|
6月前
|
存储 编译器 Linux
千万不要错过的后端【纯干货】面试知识点整理 I I
千万不要错过的后端【纯干货】面试知识点整理 I I
|
8月前
|
缓存 Java 中间件
谈谈你对Nacos配置动态更新原理的理解,这道题一定要会
Nacos作为阿里的开源中间件在Spring Cloud生态以后,不管是作为配置中心,还是作为注册中心,因为它简单易用的特性,在互联网公司被广泛运用。随后,大家会发现Nacos相关的面试题也就越来越多了。
189 0
|
缓存 网络协议 安全
前端八股文(未完待续······)
前端八股文(未完待续······)
|
JSON API Nacos
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(3)
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(3)
129 0
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(3)
|
算法 网络协议 NoSQL
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(1)
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(1)
305 0
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(1)
|
缓存 API Nacos
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(2)
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(2)
128 0
面试官:Nacos 为什么这么强?讲讲实现原理?我懵了。。(2)
|
监控 NoSQL 安全
Spring Boot 怎么做监控?这篇总算整明白了。。。
Spring Boot 怎么做监控?这篇总算整明白了。。。
2037 8
Spring Boot 怎么做监控?这篇总算整明白了。。。
|
JSON Prometheus 监控
Spring Boot 怎么做监控?这篇总算整明白了。。。(2)
Spring Boot 怎么做监控?这篇总算整明白了。。。(2)
174 0
Spring Boot 怎么做监控?这篇总算整明白了。。。(2)