Skip to content

1.认识 SpringCloud Eureka

1.1、介绍 Eureka

注册发现中心

Eureka 来源于古希腊词汇,意为“发现了”。在软件领域, Eureka 是 Netflix 在线影片 公司开源的一个服务注册与发现的组件,和其他 Netflix 公司的服务组件(例如负载均衡、 熔断器、网关等) 一起,被 Spring Cloud 社区整合为 Spring Cloud Netflix 模块

Eureka 是 Netflix 贡献给 Spring Cloud 的一个框架!Netflix 给 Spring Cloud 贡献了很多框架,后面我们会学习到!

1.2、Spring Cloud Eureka 和 Zookeeper 的区别

1.2.1、什么是 CAP 原则

在分布式 微服务里面 CAP 定理

问:为什么 zookeeper 不适合做注册中心?

CAP 原则又称 CAP 定理,指的是在一个分布式系统中,

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容错性(Partition tolerance)(这个特性是不可避免的)

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

img

因为 zookeeper 遵循 CP 原则,而 eureka 遵循 AP 原则!

也就是说 eureka 是高可用的,这是最重要的!

1.2.2、分布式特征

img

C : 数据的一致性 (A,B,C 里面的数据是一致的)

  • Zk 注重数据的一致性。
  • Eureka 不是很注重数据的一致性!

A: 服务的可用性(若 zk 集群里面的 master 挂了怎么办)Paxos(多数派)

  • 在 zk 里面,若主机挂了,则 zk 集群整体不对外提供服务了,需要选一个新的出来(120s 左右)才能继续对外提供服务!
  • Eureka 注重服务的可用性,当 Eureka 集群只有一台活着,它就能对外提供服务

P:分区的容错性(在集群里面的机器,因为网络原因,机房的原因,可能导致数据短暂不一致),它在分布式中是必须会出现的特性!要允许这个特性!

Zookeeper 注重数据的一致性 CP,zk(注册中心,配置文件中心,协调中心)。

Eureka 注重服务的可用性 AP,eureka(注册中心)。

1.3、Spring Cloud 其他注册中心

Spring Cloud 还有别的注册中心 如 Consul ,以及阿里巴巴提供的 Nacos 都能作为注册中心,我们的选择还是很多。

Consul

https://spring.io/projects/spring-cloud-consulConsul

用的比较少

Nacos(阿里巴巴)

https://nacos.io/zh-cn/

但是我们学习还是选择 Eureka ,因为它的成熟度很高。面试时候问的也是它,不是别人!

img

2.SpringCloud Eureka 快速入门

2.1、创建一个 Eureka-server 服务端

创建并配置

创建一个 SpringBoot 项目,选择 eureka server 依赖即可,这个里面就包含了 web,就不用选择 web 了!

搜索 eureka 即可

img

配置 pom.xml 依赖:springboot 依赖 2.3.12.RELEASE,springcloud 依赖 Hoxton.SR12,我们来自己更改为这个

xml
<?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>
        <!--   指定springboot的版本     -->
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.changlu</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <!--   设置SpringCloud的版本依赖     -->
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!--  依赖管理:并不会把这个依赖注入进来,只是帮你去管理版本号以及其子模块的依赖(也就是说我们在添加springCloud相关依赖的时候,就不需要写版本号了,会自动匹配版本)-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version> <!--用${}取到了Hoxton.SR12版本号-->
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置 appliction.yaml:

yml
# 单机
server:
  port: 8761 # eureka的默认端口为8761
spring:
  application:
    name: eureka-server # 应用名称,必须要给,不要使用特殊字符,一般用-进行连接起名

在 SpringBoot 的启动器上添加一个开启 Eureka 的注解:

java
@SpringBootApplication
@EnableEurekaServer  //开启Eureka注册中心的功能
public class EurekaServerApplication {

    public static void main(String[] args) {

        SpringApplication.run(EurekaServerApplication.class, args);
    }

}

测试

image-20220905224439289

访问网址:localhost:8761/

2.2、Eureka 的服务大屏

当前在 eureka 注册了的实例(服务列表):

此时的实例指的就是一个真正的服务,即一个微服务模块

img

2.3、创建两个 client 注册到 Eureka

同样是创建一个 SpringBoot 项目,依赖如下:这次要选的是 Eureka Client 依赖,同时还要选择 web 依赖,因为 Client 这个

不自带 web 依赖!

imgimg

pom.xml:

xml
<?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>
        <!--   指定springboot的版本     -->
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.changlu</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <!--   设置SpringCloud的版本依赖     -->
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

① 编写对应的 yaml 配置:其中包含当前的服务名,端口号,以及对应客户端注册的目标地址

eureka-client-a 的配置:

yml
server:
  port: 8081 # 客户端的端口没有要求,不重复即可
spring:
  application:
    name: eureka-client-a

# 注册的含义是什么? 就是将自己的一些信息(什么信息ip port...) 发送过去 (发到哪里)
eureka:
  client: # 客户端的相关配置
    service-url: # 指定注册到的地址
      defaultZone: localhost:8761/eureka
  instance:
    hostname: localhost # 应用的主机名称 最好写主机的ip
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
    prefer-ip-address: true # 显示ip
    lease-renewal-interval-in-seconds: 10 # 示例续约的时间

eureka-client-b 的配置:

同上

② 在启动器中都添加注解:

java
@EnableEurekaClient

最后我们将三个服务进行启动:

img

访问地址:localhost:8761/

img

2.4、常用的配置文件设置

img

2.4.1、server 当中常用配置

理解:

image-20220906152204341

这些配置文件的作用就是为了解决上述的问题:

img

img

一般的写法:

yml
#单机
server:
  port: 8761 # eureka的默认端口  6379  8080  3306   8848
spring:
  application:
    name: eureka-server # 应用名称 不要使用特殊字符
eureka: # eureka的配置分为三类 server  client  instance ,其实eureka-server既是服务端又是客户端
  server: #意思是作为server端进行的配置
    eviction-interval-timer-in-ms: 10000 # 服务端间隔多少毫秒做定期删除的操作(10s)
    renewal-percent-threshold: 0.85 # 续约百分比 超过85%的应用没有和你续约 那么eureka会保护服务 不会剔除任何一个
  instance: # 实例的配置
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 主机名称 : 应用名称 : 端口号
    # 使用${}取yml中的其他配置信息,可以解耦!
    hostname: localhost # 应用的主机名称 最好写主机的ip
    prefer-ip-address: true # 允许以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5 # 服务实例的续约的时间间隔(发送心跳的时间间隔)(5s,必须要小于上面的定期删除周期10s)

2.4.2、client 当中常用配置

img

一般的写法:

yml
server:
  port: 8080 # 客户端的端口没有要求
spring:
  application:
    name: eureka-client-a
# 注册的含义是什么? 就是将自己的一些信息(什么信息ip port...) 发送过去 (发到哪里)
eureka:
  client: #意思是作为client端进行的配置
    service-url: # 指定注册的地址
      defaultZone: localhost:8761/eureka
    register-with-eureka: true # 允许 可以不往eureka-server去注册
    fetch-registry: true # 应用是否拉取服务列表到本地缓存(一般要允许)
    registry-fetch-interval-seconds: 10 # 拉取服务列表的时间间隔:为了缓解服务列表的脏读问题 时间越短脏读越少(默认为30s) 但性能消耗越大
  instance:
    hostname: localhost # 应用的主机名称 最好写主机ip
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
    prefer-ip-address: true # 显示ip
    lease-renewal-interval-in-seconds: 5 # 实例续约的时间

3.构建高可用的 Eureka-Server 集群

构建注册中心集群:防止某台服务器挂了就不能服务了,保证高可用!

集群模式:主从集群模式,去中心化模式(更高可用)

我们使用去中心化模式:

eureka 会将数据进行广播和扩散,两两互相守望!

img

3.1、配置三台 Eureka-Server

img

创建工程方式如 2.1 所示,三个配置如下:其中所有的服务名是一致的,端口不同,以及其中的 service-url 设置了另外两个

eureka-server:8761 的

yml
# 集群
server:
  port: 8761 # eureka的默认端口
spring:
  application:
    name: eureka-server # 应用名称 都是一样的
eureka:
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  client: #服务端在此时是作为令两个服务端的客户端
    service-url: # 你不写的话 默认 8761
      defaultZone: localhost:8762/eureka, localhost:8763/eureka
    fetch-registry: true #是否拉取服务列表
    register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
  instance: # 实例的配置
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 主机名称 : 应用名称 : 端口号
    hostname: localhost # 主机名称 或者服务的ip
    prefer-ip-address: true # 以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5 # 服务实例的续约的时间间隔

eureka-server-a:8762 的

yml
# 集群
server:
  port: 8762  # eureka的默认端口  6379  8080  3306   8848
spring:
  application:
    name: eureka-server  # 应用名称 不要使用特殊字符
eureka:
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  client:
    service-url: # 你不写 默认 8761
      defaultZone: localhost:8762/eureka,localhost:8763/eureka
    fetch-registry: true  #是否拉取服务列表
    register-with-eureka: true  #是否注册自己(集群需要注册自己和拉取服务)
  instance: # 实例的配置
    instance-id: ${
   eureka.instance.hostname}:${
   spring.application.name}:${
   server.port}   # 主机名称 : 应用名称 : 端口号
    hostname: localhost  # 主机名称 或者服务的ip
    prefer-ip-address: true # 以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5  # 服务实例的续约的时间间隔

eureka-server-b:8763 的

yml
# 集群
server:
  port: 8763  # eureka的默认端口  6379  8080  3306   8848
spring:
  application:
    name: eureka-server  # 应用名称 不要使用特殊字符
eureka:
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  client:
    service-url: # 你不写 默认 8761
      defaultZone: localhost:8761/eureka,localhost:8762/eureka
    fetch-registry: true  #是否拉取服务列表
    register-with-eureka: true  #是否注册自己(集群需要注册自己和拉取服务)
  instance: # 实例的配置
    instance-id: ${
   eureka.instance.hostname}:${
   spring.application.name}:${
   server.port}   # 主机名称 : 应用名称 : 端口号
    hostname: localhost  # 主机名称 或者服务的ip
    prefer-ip-address: true # 以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5  # 服务实例的续约的时间间隔

此时我们来进行启动三个 server 服务:

img

任意访问一台:localhost:8763/

img

发生错误:

发现并没有出现集群信息,只是同一个服务 server 启动了多台 没有数据交互 不是真正意义上的集群

原因是因为:localhost:8761/eureka/,localhost:8762/eureka/ 这样写的话,eureka 认为

只有一个机器,就是 localhost。

所以这里面主机名不能写成一样。应当写为 peer1,peer2,peer3,写成不同的!

3.2、解决 3.1 中未能构成集群情况

由于我们当前的主机名都是 localhost,那么我们需要修改本地的 hosts 文件,来进行域名映射地址 url 即可。

修改对应路径下的 hosts 文件:C:\Windows\System32\drivers\etc

java
127.0.0.1   peer1
127.0.0.1   peer2
127.0.0.1   peer3

接着执行命令进行刷新:ipconfig /flushdns

然后我们来修改三个服务的配置文件

eureka-server:

java
defaultZone: http://changlu2:8762/eureka,http://changlu3:8763/eureka
hostname: peer1  # 主机名称 或者服务的ip

eureka-server-a:

java
defaultZone: http://changlu1:8762/eureka,http://changlu3:8763/eureka
hostname: peer2  # 主机名称 或者服务的ip

eureka-server-b:

java
defaultZone: http://changlu1:8762/eureka,http://changlu2:8763/eureka
hostname: peer3  # 主机名称 或者服务的ip

接着再来启动服务。

此时我们来访问网址:localhost:8763/

img

  • DS Replicas:表示的是数据同步的地址。

思考:此时我们是通过创建工程的方式来创建多个 Eureka Server,我们能否使用一个工程来实现 Eureka 集群呢?

答案:可以,见最终方案!

3.3、最终方案:在一个 module 里面构建集群

3.3.1、server 配置方案

思路:我们创建一个 Eureka-server 工程,启动三个服务,在每次启动的时候重新指定端口,然后指定的集群包含所

有的服务地址。

我们将 eureka-server 作为我们集群服务的工程,修改其配置文件:

img

yml
#集群的终极方案
server:
  port: 8761 # 这个端口后面要在启动类配置里面修改为8762和8763
spring:
  application:
    name: eureka-server # 应用名称 不要使用特殊字符
eureka:
  client:
    service-url: #修改3:向8761自己也进行注册,即所有的server向所有的server都进行注册,形成了统一
      defaultZone: http://changlu1:8761/eureka, http://changlu2:8762/eureka, http://changlu3:8763/eureka
  instance: # 实例的配置
    # 修改1:不需要hostname了
    # 修改2:无需指定主机名称也可以
    instance-id: ${spring.application.name}:${server.port} #主机名称 : 应用名称 : 端口号
    # hostname: peer1  # 主机名称 或者服务的ip
    prefer-ip-address: true # 以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5 # 服务实例的续约的时间间隔

我们复制对应的 EurekServer 的启动类,把三个启动类写到一个工程里面去:

img

然后我们修改启动类配置:在这里来改端口

img

java
--server.port=8762
--server.port=8763

img

然后我们来访问:localhost:8762/

img

3.3.2、修改 client 配置

img

当 Eureka 配置集群之后,我们只要指定一个注册地址即可:

yml
# 改造:注册到集群eureka-server
server:
  port: 8082 # 客户端的端口没有要求
spring:
  application:
    name: eureka-client-a
# 注册的含义是什么? 就是将自己的一些信息(什么信息ip port...) 发送过去 (发到哪里)
eureka:
  client:  # 客户端的相关配置
    service-url:  # 指定要注册到的地址
      # 指定任意一个,因为每个都是代表了整个集群
      # defaultZone: http://peer1:8761/eureka
      # 终极方案:往三个server里面都注册,这样任意一个挂了,都可以继续使用其他的!
      defaultZone: http://changlu1:8761/eureka, http://changlu2:8762/eureka, http://changlu3:8763/eureka
  instance:
    instance-id: ${
   eureka.instance.hostname}:${
   spring.application.name}:${
   server.port}
    hostname: localhost # 应用的主机名称 最好写主机ip
    lease-renewal-interval-in-seconds: 10 # 示例续约的时间

img

OK,此时我们的 client 服务启动起来了,我们看下 8762 或者其他的集群有没有该服务的信息:

img

3.3.3、测试宕机一台 server 产生的效果

我们宕机掉一个 server 服务:

img

可以看到当前会有些脏数据,也就是会出现不一致的情况,但是服务是依旧可用的:

img

Eureka server 的集群里面,没有主机和从机的概念,节点都是对等的,只有集群里面有一个集群存活,就能保证服

务的可用性。(主机(写)从机(读))

只要有一台存活,服务就能注册和调用!

3.3.4、认识分布式数据一致性协议

协议包含:Paxos、raft ——> 为了解决数据一致性,即分布式共识问题 ——> 只有主从模式的时候才涉及到

  • Zookeeper Paxos 协议
  • Nacos raft 协议

在有主从模式的集群中 一般都要遵循这样的协议 才可以稳定对外提供服务!

两个问题:这也是协议的基本原理

主机该怎么选举?——> 投票

数据要怎么同步?——> 日志复制

一个形象地讲解两个问题的解决方案的网站:http://thesecretlivesofdata.com/raft/

注:eureka 没有分布式数据一致性的机制 节点都是相同的、平等的

4.Eureka 概念的理解

4.1、服务的注册

当微服务的项目启动时(eureka 的客户端),就会向 eureka-server 发送自己的元数据(原始数据),包括运行的 ip,端口 port,健康的状态监控等,因为使用的是 http/ResuFul 请求风格。

eureka-server 会在自己内部保留这些元数据(保存在内存中)。这样在内存中就有一个服务列表,也是 restful 风格,提供以 http 动词的请求方式,完成对 url 资源的操作。

深入理解:

eureka-server 注册中心:可以让别人注册进来(包括 client 还有其他的 server),还可以自己注册自己(默认就会

注册自己)!

也就是说 server 既是 server 也是 client(相对于其他 server 来说)

eureka-client 访问eureka-server 注册中心集群,查找注册中心服务列表,就可以找到对应的其他 client 了!

一个应用可以有多个实例,一个实例有一个唯一的 id

注:这里的 eureka-client 实际上就是一个个的微服务模块项目(在 eureka 中是实例),它只是相对于 eureka 来说是 client,它相对于用户来说实际上是 server 端!一定要理解清除!

4.2、服务的续约(保持心跳)

项目启动成功了,除了向 eureka-server 注册自己成功,还会定时的向 eureka-server 汇报自己的心跳,表示自己还活着。(其实就是修改一个时间)

4.3、服务的下线(主动下线)

当项目关闭时,会给 eureka-server 报告,说明自己要下机了。

4.4、服务的剔除(被动下线,主动剔除)

当项目超过了指定时间没有向 eureka-server 汇报自己,那么 eureka-server 就会认为此节点死掉了,会把它剔

除掉,也不会放流量和请求到此节点了。

5.Eureka 原理分析

5.1、Eureka 运作原理的特点

Eureka-server 对外提供的是 restful 风格的服务 :以 http 动词的形式对 url 资源进行操作:get post put delete。

http 服务 + 特定的请求方式 + 特定的 url 地址:只要利用这些 restful 我们就能对项目实现注册和发现。

只不过,eureka 已经帮我们使用 java 语言写了 client,让我们的项目只要依赖 client 就能实现注册和发现!

只要你能发起 Http 请求,那你就有可能自己实现服务的注册和发现。不管你是什么语言!

5.2、服务注册的源码分析【重点】

img

5.2.1、Eureka-client 发起注册请求

5.2.1.1、源码位置

核心的类:DiscoveryClient

img

5.2.1.2、如何发送信息来进行注册?

img

真正去执行注册的是在一个抽象类中:

img

总结

当 eureka 启动的时候,会向我们指定的 serviceUrl 发送请求,把自己节点的数据以 post 请求的方式,数据以

json 形式发送过去。

当返回的状态码为 204 的时候,表示注册成功。

5.2.2、Eureka-server 实现注册+保存

img

1、接受客户端的请求

com.netflix.eureka.resources.ApplicationResource

img

2、实际服务端的源码位置

img

3、处理请求(注册自己,向其他节点注册)

上面第二段 super.register()

java
public void register(InstanceInfo info, boolean isReplication) {

    int leaseDuration = 90;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {

        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
	//真正注册自己
    super.register(info, leaseDuration, isReplication);
    this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
}

4、真正的注册自己

java
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {

        this.read.lock();

        try {

            //通过服务名称得到注册的实例
            Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
            EurekaMonitors.REGISTER.increment(isReplication);
            //因为之前没有实例,肯定为 null
            if (gMap == null) {

                //新建一个集合来存放实例
                ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
                gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {

                    gMap = gNewMap;
                }
            }
            //gMap 就是该服务的实例
            Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
            if (existingLease != null && existingLease.getHolder() != null) {

                Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {

                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = (InstanceInfo)existingLease.getHolder();
                }
            } else {

                synchronized(this.lock) {

                    if (this.expectedNumberOfClientsSendingRenews > 0) {

                        ++this.expectedNumberOfClientsSendingRenews;
                        this.updateRenewsPerMinThreshold();
                    }
                }

                logger.debug("No previous lease information found; it is new registration");
            }
			//新建一个服务的实例节点
            Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
            if (existingLease != null) {

                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
			//放到注册 map 的列表里
            ((Map)gMap).put(registrant.getId(), lease);
            this.recentRegisteredQueue.add(new Pair(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() + ")"));
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {

                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {

                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }

            InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {

                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);
            if (InstanceStatus.UP.equals(registrant.getStatus())) {

                lease.serviceUp();
            }
			//设置心跳时间等参数
            registrant.setActionType(ActionType.ADDED);
            this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})", new Object[]{
   registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication});
        } finally {

            this.read.unlock();
        }

    }

5.2.3、服务注册源码总结

重要的类:

  • DiscoveryClient 里面的 register()方法是注册的总体构造
  • AbstractJerseyEurekaHttpClient 里面的 register()方法具体发送注册请求(post 方式)
  • InstanceRegistry 里面的 register()方法接受客户端 client 的注册请求
  • PeerAwareInstanceRegistryImpl 里面调用父类的 register()方法实现注册
  • AbstractInstanceRegistry 里面的 register()方法完成具体的注册,保留数据到 map 集合 保存服务实例数据的集合

保存服务实例数据的集合:ConcurrentHashMap

  • 第一个 key 是应用名称(全大写) spring.application.name
  • Value 中的 key 是应用的实例 id 即 eureka.instance.instance-id
  • Value 中的 value 是 具体的服务节点信息
java
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

5.3、服务剔除的源码分析(被动下线)

AbstractInstanceRegistry 的 evict()方法中筛选剔除的节点

在 internalCancel 方法里面真正实现剔除:其实就是对 map 执行 remove 操作。

在服务剔除中涉及到哪些重要的点

怎么删除一个集合里面过期的数据?

首先,Redis 怎么清除过期的 key LRU(热点 key)?

1 定时(k-thread)

2 惰性 (在再次访问该 key 时有作用)

3 定期 (使用一个线程来完成清除任务)

操作:定期(实时性差) + 惰性

何时进行剔除操作?

通过一个定时任务:

img

img

5.4、服务下线的源码分析

5.4.1 eureka-client 发起下线请求

① 通过 unregister()方法来进行发起下线请求

img

② 真正的发起请求下线:AbstractJerseyEurekaHttpClient

img

5.4.2 eureka-server 处理下线请求

① 接受下线请求

img

② 真正的下线服务

img

5.5、服务发现的源码分析

5.5.1、服务发现流程分析

从 discoveryClient.getInstances(serviceId);方法进去,找到 eureka 的实现

img

从 getInstancesByVipAddress 方法进去看到真正的服务发现

img

在 getInstancesByVirtualHostName 方法里面做真正的服务发现

img

img

5.5.2、在 eureka-client 客户端也有 map 集合存放服务列表?

img

我们发现,当我们还没有做服务发现之前,集合里面已经有值了,说明项目启动的时候就去 server 端拉取服务列表并且缓存了起来

到底何时从 server 拉取服务放进去的呢?

在 eureka 的 DiscoverClient 类的一个构造方法里面,有一个任务调度线程池:

img

img

查看 initScheduledTasks()这个方法

img

在 CacheRefreshThread()中

img

fetchRegistry()方法中判断决定是全量拉取还是增量拉取

img

getAndStoreFullRegistry()全量拉取:

img

getAndUpdateDelta()增量拉取:

img

5.5.3、服务发现总结

重要的类:

  • DiscoveryClient 类里面的构造方法执行线程初始化调用
  • CacheRefreshThread 类里面的 run 方法执行服务列表的拉取(方便后期做服务发现)
  • fetchRegistry()方法去判断全量拉取还是增量拉取

全量拉取发生在:当服务列表为 null 的情况 当项目刚启动就全量拉取

增量拉取发生:当列表不为 null ,只拉取 eureka-server 的修改的数据(注册新的服务, 上线服务)

eureka 客户端会把服务列表缓存到本地 为了提高性能 ,但是有脏读问题,当你启动一个新的应用的时候 不会被老的应用快速发现

6.服务发现

6.1、什么是服务发现?

根据服务名称发现服务的实例过程:即微服务之间寻找发现的过程!

客户端会在本地缓存服务端的列表

拉取列表是有间隔周期的 (导致服务上线 客户端不能第一时间感知到 (可以容忍))

其实每次做服务发现 都是从本地缓存的列表来进行的

img

6.2、简单实现服务发现功能

我们在 eureka-client-a 中编写对应的服务发现控制器:

img
java
package com.changlu.eurekaclienta.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Description: 我的服务注册发现
 * @Author: changlu
 * @Date: 10:04 PM
 */
@RestController
public class MyDiscoveryController {

    @Autowired
    private DiscoveryClient discoveryClient; //这个接口是springcloud提供的顶级接口,然后eureka提供了对应的实现类,我们可以直接使用!

    @GetMapping("/find")
    public String find(String serviceId) { //使用?传参,不用加注解,可以直接接收到

        //调用服务发现:在这个client上面,传入name,通过注册发现中心去发现client
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        instances.forEach(System.out::println);
        return instances.toString();
    }

}

启动一个 server 服务以及两个客户端

访问下:localhost:8761/,查看下当前的服务

img

接着我们来进行测试服务发现:localhost:8082/find?serviceId=eureka-client-a

查到了具体的实例信息:

img

7.构建 eureka-server 的 docker 镜像

7.1 修改配置

注意动态参数的设置

yml
#单机
server:
  port: 8761 # eureka的默认端口  6379  8080  3306   8848
spring:
  application:
    name: eureka-server # 应用名称 不要使用特殊字符
eureka: # eureka的配置分为三类 server  client  instance ,其实eureka-server既是服务端又是客户端
  server:
    eviction-interval-timer-in-ms: 10000 # 服务端间隔多少毫秒做定期删除的操作(10s)
    renewal-percent-threshold: 0.85 # 续约百分比 超过85%的应用没有和你续约 那么eureka会保护服务 不会剔除任何一个
  instance: # 实例的配置
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 主机名称 : 应用名称 : 端口号
    # 使用${}取yml中的其他配置信息,可以解耦!
    hostname: localhost # 应用的主机名称 最好写主机的ip
    prefer-ip-address: true # 允许以ip的形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5 # 服务实例的续约的时间间隔(发送心跳的时间间隔)(5s,必须要小于上面的定期删除周期10s)
  #添加的代码为下面的:
  # docker构建并运行容器的命令:docker run -p 端口  -d 后台运行 --link 指定网络host文件映射的  -e MYSQL_ROOT_PASSWORD=123456 -v 文件挂载
  client:
    service-url: #下面这两个动态的值可以通过docker -e后面的参数传入
      defaultZone: ${EUREKA_SERVER_URL:localhost:8761/eureka} #将这个url设置为动态的,localhost:8761/eureka只是默认值而已
    register-with-eureka: ${REGISTER_WITH_EUREKA:true} # server自己注册自己的开关,true为默认值
    fetch-registry: true

7.2 准备文件

创建一个 docker 的目录

image-20220906224834173

1.jar 包

点击 install 进行打包

点击蓝色闪电可以跳过测试,加快打包速度!

image-20220906224332755

2.dockerfile 文件

Dockerfile

dockerfile
FROM openjdk:8
ENV workdir=/root/wh2106/eureka-server #指定工作目录,即服务器上面的目录
COPY . ${workdir} #将当前目录内的东西拷贝到工作目录
WORKDIR ${workdir} #跳转到workdir工作目录内
EXPOSE 8761 #运行在8761端口
CMD ["java","-jar","eureka-server-1.0.jar"] #运行命令

3.运行脚本

run.sh

sh
cd .. && docker build ./eureka-server -t eureka-server:1.0

先退出工作目录到上一级,然后执行 docker build 将 eureka-server 整个文件夹构建成一个镜像文件

打标签为 eureka-server:1.0(重命名)

7.3 在服务器构建镜像

7.3.1 将文件拷贝到服务器

将三个文件拷贝到/root/wh2106/eureka-server 目录下即可

7.3.2 运行脚本

进入/root/wh2106/eureka-server 目录,先给 sh 文件开放权限

bash
chomd 777 run.sh

运行脚本文件

bash
./run.sh

查看镜像:

bash
docker images

发现多了一个 eureka-server:1.0 镜像

7.3.3 构建容器

1.运行镜像构建容器:

bash
docker run --name eureka-server -p 8761:8761 -e REGISTER_WITH_EUREKA=false -d eureka-server:1.0

注意我们指定了动态参数 REGISTER_WITH_EUREKA 的值为 false,覆盖了默认值

2.查看运行的容器:

bash
docker ps

3.查看日志:

docker logs eureka-server

发现 springboot 项目正在运行!

4.在 web 访问服务器上面的 eureka-server:

124.220.15.95:8761 发现访问成功