关于服务注册中心

服务注册中心本质上是为了解耦服务提供者和服务消费者。
对于任何一个微服务,原则上都应存在或者支持多个提供者(比如一个微服务部署多个实例),这是由微服务的分布式属性决定的。
更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB(负载均衡)机制(就是在配置文件中写死服务实例的列表信息,负载均衡策略从这个配置文件中读取)就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。

服务注册中心一般原理

图片.png
分布式微服务架构中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。消费者只需要知道当前系统发布了那些服务,而不需要知道服务具体存在于什么位置,这就
是透明化路由。

  1. 服务提供者启动
  2. 服务提供者将相关服务信息主动注册到注册中心
  3. 服务消费者获取服务注册信息:
    poll模式:服务消费者可以主动拉取可用的服务提供者清单
    push模式:服务消费者订阅服务(当服务提供者有变化时,注册中心也会主动推送更新后的服务清单给消费者
  4. 服务消费者直接调用服务提供者

另外,注册中心也需要完成服务提供者的健康监控,当发现服务提供者失效时需要及时剔除。

主流服务中心对比

  • Zookeeper
    Zookeeper它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
    简单来说zookeeper本质=存储+监听通知。
    znode
    Zookeeper用来做服务注册中心,主要是因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能及时的通知到监听客户端,调用方只要使用Zookeeper的客户端就能实现服务节点的订阅和变更通知功能了,非常方便。另外,Zookeeper的可用性也可以,因为只要半数以上(最少3台)的选举节点存活,整个集群就是可用的。
  • Eureka
    由Netflix开源,并被Pivatal集成到SpringCloud体系中,它是基于Restful API⻛格开发的服务注册与发现组件。
    Eureka有一个自我保护机制,在服务提供者没有发送心跳时,不剔除中断的服务,保证高可用。
  • Consul
    Consul是由HashiCorp基于Go语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件,采用Raft算法保证服务的一致性,且支持健康检查。
  • Nacos
    Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说Nacos就是注册中心+配置中心的组合,帮助我们解决微服务开发必会涉及到的服务注册与发现,服务配置,服务管理等问题。Nacos是Spring Cloud Alibaba 核心组件之一,负责服务注册、发现和配置。
组件名语言CAP对外暴露接口
EurekaJavaAPHTTP
ConsulGoCPHTTP/DNS
ZookeeperJavaCPZookeeper客户端
NacosJava支持AP/CP切换HTTP

P:分区容错性(一定的要满足的)
C:数据一致性
A:高可用
CAP不可能同时满足三个,要么是AP,要么是CP

服务注册中心组件 Eureka

  • Eureka 基础架构
    图片.png
  • Eureka 交互流程及原理
    下图是官网描述的一个架构图
    图片.png
    Eureka包含两个组件:Eureka Server和Eureka Client,Eureka Client是一个Java客户端,用于简化与Eureka Server的交互;Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server 进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息;
  • 图中us-east-1c、us-east-1d、us-east-1e代表不同的区也就是不同的机房。
  • 图中每一个Eureka Server都是一个集群。
  • 图中Application Service作为服务提供者向Eureka Server中注册服务,Eureka Server接受到注册事件会在集群和分区中进行数据同步,Application Client作为消费端(服务消费者)可以从Eureka Server中获取到服务注册信息,进行服务调用。
  • 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。
  • Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒)。
  • 每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的方式完成服务注册列表的同步。
  • Eureka Client会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。

Eureka通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和可用性。

Eureka应用及高可用集群

搭建单例Eureka Server服务注册中心

基于Maven构建SpringBoot工程,在SpringBoot工程之上搭建EurekaServer服务(cloud-eureka-server-8761)

  • 父工程parent中引入Spring Cloud依赖
    Spring Cloud 是一个综合的项目,下面有很多子项目,比如eureka子项目(版本号 1.x.x)

    <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
    </dependencyManagement>
  • 当前工程pom.xml中引入依赖

    <dependencies>
    <!--Eureka server依赖-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    </dependencies>

    注意:在父工程的pom文件中手动引入jaxb的jar,因为jdk9之后默认没有加载该模块,EurekaServer使用到,所以需要手动导入,否则EurekaServer服务无法启动。

  • 父工程pom.xml

    <!--引入Jaxb,开始-->
    <dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.2.11</version>
    </dependency>
    <dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.2.11</version>
    </dependency>
    <dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.2.11</version>
    </dependency>
    <dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.2.10-b140310.1920</version>
    </dependency>
    <dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
    </dependency>
    <!--引入Jaxb,结束-->
  • application.yml

    #Eureka server服务端口
    server:
    port: 8761
    spring:
    application:
      # 应用名称,会在Eureka中作为服务的id标识(serviceId)
      name: cloud-eureka-server 
    eureka:
    instance:
      hostname: localhost
    # Eureka服务端本身也可以做客户端,用来连接其他服务端
    client:
      # 客户端与Eureka Server交互的地址,如果是集群,也需要写其它Server的地址
      service-url: 
        defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
      # 自己就是服务不需要注册自己
      register-with-eureka: false 
      # 自己就是服务不需要从Eureka Server获取服务信息,默认为true,置为false
      fetch-registry: false 
  • SpringBoot启动类,使用@EnableEurekaServer声明当前项目为Eureka Server服务。

    package com.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    
    @SpringBootApplication
    // 声明本项目是一个Eureka服务
    @EnableEurekaServer
    public class CloudEurekaServerApplication {
    public static void main(String[] args) {
      SpringApplication.run(CloudEurekaServerApplication.class,args);
    }
    }
  • 执行启动类CloudEurekaServerApplication的main函数
  • 访问http://127.0.0.1:8761,如果看到如下⻚面(Eureka注册中心后台),则表明EurekaServer发布成功
    图片.png
    以下两图作一下解释:
    图片.png
    图片.png

    搭建Eureka Server HA高可用集群

    在互联网应用中,服务实例很少有单个的。
    即使微服务消费者会缓存服务列表,但是如果EurekaServer只有一个实例,该实例挂掉,正好微服务消费者本地缓存列表中的服务实例也不可用,那么这个时候整个系统都受影响。
    在生产环境中,我们会配置Eureka Server集群实现高可用。Eureka Server集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。我们开启两台 Eureka Server以搭建集群。
    图片.png

  • 修改本机host属性
    由于是在个人计算机中进行测试很难模拟多主机的情况,Eureka配置server集群时需要执行host地址。所以需要修改个人电脑中host地址。

    127.0.0.1 CloudEurekaServerA
    127.0.0.1 CloudEurekaServerB
  • 复制一个cloud-eureka-server工程(注意修改工程,不要重复),修改两个cloud-eureka-server工程中的yml配置文件。

    #原工程的配置文件
    spring:
    application:
      name: cloud-eureka-server
    server:
    port: 8761
    eureka:
    instance:
      # 在上一步hosts文件中修改过的host地址
      hostname: CloudEurekaServerA
    client:
      register-with-eureka: true
      fetch-registry: true
      serviceUrl:
        # 如果有其他实例,逗号拼接地址即可
        defaultZone: http://CloudEurekaServerB:8762/eureka
    #复制工程的配置文件
    spring:
    application:
      name: cloud-eureka-server
    server:
    port: 8762
    eureka:
    instance:
      hostname: CloudEurekaServerB
    client:
      register-with-eureka: true
      fetch-registry: true
      serviceUrl:
        defaultZone: http://CloudEurekaServerA:8761/eureka

    由于集群环境,每个Eureka服务端相对于集群中的其他Eureka服务端来说都是一个客户端,所以配置文件中要注意:

  • 在一个实例中,把另外的实例作为了集群中的镜像节点,那么这个http://CloudEurekaServerB:8762/eureka URL中的CloudEurekaServerB就要和其它个profile中的eureka.instance.hostname保持一致。
  • register-with-eureka和fetch-registry在单节点时设置为了false, 因为只有一台EurekaServer,并不需要自己注册自己,而现在有了集群,可以在集群的其他节点中注册本服务。
  • 启动两个SpringBoot项目(EurekaServer)。
  • 访问两个EurekaServer的管理台⻚面 http://cloudeurekaservera:8761/http://cloudeurekaserverb:8762/ 会发现注册中心CLOUD-EUREKA-SERVER已经有两个节点,并且registered-replicas (相邻集群复制节点)中已经包含对方。

    微服务提供者—>注册到Eureka Server集群

    注册微服务(服务部署两个实例,分别占用8080、8081端口)

  • 父工程中引入spring-cloud-commons依赖

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-commons</artifactId>
    </dependency>
  • pom文件引入坐标,添加eureka client的相关坐标

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  • 配置application.yml文件
    在application.yml中添加Eureka Server高可用集群的地址及相关配置

    eureka:
    client:
      serviceUrl: # eureka server的路径
        # 把eureka集群中的所有url都填写了进来,也可以只写一台,因为各个eureka server可以同步注册表
        defaultZone: http://cloudeurekaservera:8761/eureka/,http://cloudeurekaserverb:8762/eureka/ 
    instance:
      # 使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
      prefer-ip-address: true
      # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
      instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  • 启动类添加注解@EnableDiscovertClient或@EnableEurekaClient

注意:

  1. 从Spring Cloud Edgware版本开始,@EnableDiscoveryClient或 @EnableEurekaClient可省略,即不用添加注解也能正常运行。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。
  2. @EnableDiscoveryClient和@EnableEurekaClient二者的功能是一样的。但是如果选用的是eureka服务器,那么就推荐@EnableEurekaClient,如果是其他的注册中心,那么推荐使用@EnableDiscoveryClient,考虑到通用性就使用@EnableDiscoveryClient
  3. 启动类执行,在Eureka Server后台界面可以看到注册的服务实例

    微服务消费者—>注册到Eureka Server集群

  4. pom文件引入坐标,添加eureka client的相关坐标

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-commons</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  5. 配置application.yml文件

    spring:
      application:
     name: service-consumer
    server:
      port: 8090
    eureka:
      client:
     # eureka server的路径
     serviceUrl: 
       # 把eureka集群中的所有url都填写了进来,也可以只写一台,因为各个eureka server可以同步注册表
       defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ 
      instance:
     # 使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
     prefer-ip-address: true
     # 自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
     instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
  6. 在启动类添加注解@EnableDiscoveryClient,开启服务发现

    服务消费者调用服务提供者(通过Eureka)

    在上一篇RestTemplate的使用中,我们曾用RestTemplate来调用服务端提供的远程方法:

    Integer forObject = restTemplate.getForObject("http://localhost:8080/shop/order/" + orderId,Integer.class);

    通过Eureka对象的方法拼接url改造如下:
    在controller中:

    @Autowired
    private DiscoveryClient discoveryClient;

    在对应的handler方法中:

    // 1、通过Eureka客户端对象获取Eureka中注册的cloud-eureka-server实例列表,参数对应提供者application.yml文件中的spring.application.name的值
    List<ServiceInstance> instanceList = discoveryClient.getInstances("cloud-eureka-server");
    // 2、如果有多个实例,选一个(此处是负载均衡的过程,暂不考虑,下一篇会写到,先拿第一个)
    ServiceInstance serviceInstance = instanceList.get(0);
    // 3、根据实例的元数据信息获取host、port来拼接请求地址
    String host = serviceInstance.getHost();
    String port = serviceInstance.getPort();
    String url = "http://" + host + ":" + port + "/shop/order/" + orderId;
    // 4、消费者直接调用提供者
    Integer forObject = restTemplate.getForObject(url, Integer.class);

    Eureka细节详解

    Eureka元数据详解

    Eureka的元数据有两种:标准元数据和自定义元数据。
    标准元数据: 主机名、IP地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
    自定义元数据: 可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。

    eureka:
      instance:
     prefer-ip-address: true
     # 自定义元数据配置
     metadata-map:
       # 自定义元数据(kv自定义)
       cluster: cl1
       region: rn1

    我们可以在程序中可以使用DiscoveryClient 获取指定微服务的所有元数据信息(SpringBoot测试类举例):

    import com.cloud.CloudEurekaServerApplication;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import java.util.List;
    import java.util.Map;
    
    @SpringBootTest(classes = {CloudEurekaServerApplication.class})
    @RunWith(SpringJUnit4ClassRunner.class)
    public class CloudEurekaServerApplicationTest {
      @Autowired
      private DiscoveryClient discoveryClient;
    
      @Test
      public void test() {
     // 从EurekaServer获取指定微服务实例
     List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("cloud-eureka-server");
     // 循环打印每个微服务实例的元数据信息
     for (int i = 0; i < serviceInstanceList.size(); i++) {
       ServiceInstance serviceInstance = serviceInstanceList.get(i);
       System.out.println(serviceInstance);
     }
      }
    }

    可以使用Debug方式运行,在System.out.println(serviceInstance)打断点查看元数据:
    图片.png

    Eureka客户端详解

    服务提供者(也是Eureka客户端)要向EurekaServer注册服务,并完成服务续约等工作。

服务注册详解(服务提供者)

  1. 当我们导入了eureka-client依赖坐标,配置Eureka服务注册中心地址。
  2. 服务在启动时会向注册中心发起注册请求,携带服务元数据信息(这就是为什么可以不加@EnableDiscoveryClient或@EnableEurekaClient注解的原因)。
  3. Eureka注册中心会把服务的信息保存在Map中。

服务续约详解(服务提供者)
服务每隔30秒会向注册中心续约(心跳)一次(分布式理论也称为报活),如果没有续约,租约在90秒后到期,然后服务会被失效。每隔30秒的续约操作我们称之为心跳检测。

# 向Eureka服务中心集群注册服务
eureka:
  instance:
    # 租约续约间隔时间,默认30秒
    lease-renewal-interval-in-seconds: 30
    # 租约到期,服务时效时间,默认值90秒,服务超过90秒没有发生心跳,EurekaServer会将服务从列表移除
    lease-expiration-duration-in-seconds: 90

往往不需要我调整这两个配置。

获取服务列表详解(服务消费者)
每隔30秒服务会从注册中心中拉取一份服务列表,这个时间可以通过配置修改。往往不需要调整。

# 向Eureka服务中心集群注册服务
eureka:
  client:
    # 每隔多久拉取一次服务列表
    registry-fetch-interval-seconds: 30
  1. 服务消费者启动时,从 EurekaServer服务列表获取只读备份,缓存到本地。
  2. 每隔30秒,会重新获取并更新数据。
  3. 每隔30秒的时间可以通过配置eureka.client.registry-fetch-interval-seconds修改。

    Eureka服务端详解

    服务下线

  4. 当服务正常关闭操作时,会发送服务下线的REST请求给EurekaServer。
  5. 服务中心接受到请求后,将该服务置为下线状态。

失效剔除
Eureka Server会定时(此间隔值由服务端设置的eureka.server.eviction-interval-timer-in-ms,默认60s)进行检查,如果发现实例在在一定时间(此值由客户端设置的eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到心跳,则会注销此实例。

自我保护
服务提供者会向注册中心定期续约(服务提供者和注册中心通信),如果服务提供者和注册中心之间的网络有问题,那么不代表服务提供者不可用,不代表服务消费者无法访问服务提供者。
如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制。
为什么会有自我保护机制?
默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。
服务中心⻚面会显示如下提示信息:
图片.png
当处于自我保护模式时:

  1. 不会剔除任何服务实例(可能是服务提供者和EurekaServer之间网络问题),保证了大多数服务依然可用。
  2. Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用,当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中。
  3. 在Eureka Server工程中通过eureka.server.enable-self-preservation配置可关停自我保护,默认值是打开。

    eureka:
      server:
     # 关闭自我保护模式(默认为打开)
     enable-self-preservation: false 

    建议生产环境打开自我保护机制

    Eureka核心源码剖析

    Eureka Server启动过程

    入口:SpringCloud充分利用了SpringBoot的自动装配的特点

  4. 观察eureka-server的jar包,发现在META-INF下面有配置文件spring.factories
    image.png
    springboot应用启动时会加载EurekaServerAutoConfiguration自动配置类
  5. EurekaServerAutoConfiguration类
    首先观察类头分析
    image.png
    第二图中的标注1需要有一个marker bean,才能装配Eureka Server,这个marker 其实是由@EnableEurekaServer注解决定的
    image.png
    image.png
    也就是说只有添加了@EnableEurekaServer注解,才会有后续的动作,这是成为一个EurekaServer的前提
    第二图中的标注2,关注EurekaServerAutoConfiguration
    image.png
    image.png
    image.png
    而在com.netflix.eureka.cluster.PeerEurekaNodes#start方法中
    image.png
    回到主配置类EurekaServerAutoConfiguration中
    image.png
    image.png
    回到主配置类EurekaServerAutoConfiguration中
    image.png
    image.png
    第二图中的标注3,关注EurekaServerInitializerConfiguration
    image.png
    image.png
    重点关注,进入org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#contextInitialized
    image.png
    重点关注initEurekaServerContext()
    image.png
    研究一下上图中的syncUp方法
    image.png
    继续研究com.netflix.eureka.registry.AbstractInstanceRegistry#register(提供实例注册功能)
    image.png
    继续研究com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#openForTraffic
    image.png
    进入postInit()方法查看
    image.png
    至此,Eureka Server启动过程分析完成。

    Eureka Server服务接口暴露策略

    在Eureka Server启动过程中主配置类EurekaServerAutoConfiguration中注册了Jersey框架(是一个发布restful⻛格接口的框架,类似于SpringMVC)
    image.png
    注入的Jersey细节
    image.png
    扫描classpath下的packages已经定义好了
    image.png
    对外提供的接口服务,在Jersey中叫做资源Resource
    image.png
    这些就是使用Jersey发布的供Eureka Client调用的Restful⻛格服务接口(完成服务注册、心跳续约等接口)。

    Eureka Server服务注册接口(接受客户端注册服务)

    ApplicationResource类的addInstance()方法中代码:registry.register(info,"true".equals(isReplication));
    image.png
    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register - 注册服务信息并同步到其它Eureka节点
    image.png
    AbstractInstanceRegistry#register():注册,实例信息存储到注册表(是一个ConcurrentHashMap)

  6. Registers a new instance with a given duration.
    *
  7. @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int,boolean)
    */
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
    read.lock(); //读锁
    // registry是保存所有应用实例信息的Map:ConcurrentHashMap<String,Map<String, Lease>>
    // 从registry中获取当前appName的所有实例信息
    Map<String, Lease> gMap = registry.get(registrant.getAppName());
    REGISTER.increment(isReplication); //注册统计+1
    // 如果当前appName实例信息为空,新建Map
    if (gMap == null) {
    final ConcurrentHashMap<String, Lease> gNewMap = new ConcurrentHashMap<String, Lease>();
    gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
    if (gMap == null) {

     gMap = gNewMap;

    }
    }
    // 获取实例的Lease租约信息
    Lease existingLease = gMap.get(registrant.getId());
    // Retain the last dirty timestamp without overwriting it, if there is already a lease
    // 如果已经有租约,则保留最后一个脏时间戳而不覆盖它
    // 比较当前请求实例租约 和 已有租约的LastDirtyTimestamp,选择靠后的
    if (existingLease != null && (existingLease.getHolder() != null)) {
    Long existingLastDirtyTimestamp = 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 = existingLease.getHolder();

    }
    }
    else {
    // The lease does not exist and hence it is a new registration
    // 如果之前不存在实例的租约,说明是新实例注册
    // expectedNumberOfRenewsPerMin期待的每分钟续约数+2(因为30s一个)
    // 并更新numberOfRenewsPerMinThreshold每分钟续约阀值(85%)
    synchronized (lock) {

     if (this.expectedNumberOfRenewsPerMin > 0) {
       // Since the client wants to cancel it, reduce the threshold
       // (1 for 30 seconds, 2 for a minute)
       this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
       this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
     }

    }
    logger.debug("No previous lease information found; it is new registration");
    }
    Lease lease = new Lease(registrant, leaseDuration);
    if (existingLease != null) {
    lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
    }
    //当前实例信息放到维护注册信息的Map
    gMap.put(registrant.getId(), lease);
    // 同步维护最近注册队列
    synchronized (recentRegisteredQueue) {
    recentRegisteredQueue.add(new Pair<Long, String>(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() +")"));
    }
    // This is where the initial state transfer of overridden status happens
    // 如果当前实例已经维护了OverriddenStatus,将其也放到此Eureka Server的overriddenInstanceStatusMap中
    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 (!overriddenInstanceStatusMap.containsKey(registrant.getId())){

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

    }
    }
    InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
    if (overriddenStatusFromMap != null) {
    logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
    registrant.setOverriddenStatus(overriddenStatusFromMap);
    }
    // Set the status based on the overridden status rules
    // 根据overridden status规则,设置状态
    InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
    registrant.setStatusWithoutDirty(overriddenInstanceStatus);
    // If the lease is registered with UP status, set lease service up timestamp
    // 如果租约以UP状态注册,设置租赁服务时间戳
    if (InstanceStatus.UP.equals(registrant.getStatus())) {
    lease.serviceUp();
    }
    registrant.setActionType(ActionType.ADDED); //ActionType为 ADD
    recentlyChangedQueue.add(new RecentlyChangedItem(lease)); //维护recentlyChangedQueue
    registrant.setLastUpdatedTimestamp(); //更新最后更新时间
    // 使当前应用的ResponseCache失效
    invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    logger.info("Registered instance {}/{} with status { (replication={})", registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
    } finally {
    read.unlock(); //读锁
    }
    }

    PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers
    ![image.png](/upload/2020/07/image-bfaf73aa6c0d48efa893dcddc114f61b.png)
    ## Eureka Server服务续约接口(接受客户端续约)
    InstanceResource的renewLease方法中完成客户端的心跳(续约)处理,关键代码:
    registry.renew(app.getName(), id, isFromReplicaNode);
    ![image.png](/upload/2020/07/image-a8c607e7d0a74e76936a041642b087bc.png)
    ![image.png](/upload/2020/07/image-a07e7a012cb742fba24e739910c48296.png)
    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
    ![image.png](/upload/2020/07/image-7053f54099cf478ca87d6e8cc17472ad.png)
    replicateInstanceActionsToPeers() 复制Instance实例操作到其它节点

    private void replicateInstanceActionsToPeers(Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
    try {
    InstanceInfo infoFromRegistry = null;
    CurrentRequestVersion.set(Version.V2);
    switch (action) {
    case Cancel: //取消

     node.cancel(appName, id);
     break;

    case Heartbeat: //心跳

     InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
     infoFromRegistry = getInstanceByAppAndId(appName, id, false);
     node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
     break;

    case Register: //注册

     node.register(info);
     break;

    case StatusUpdate: //状态更新

     infoFromRegistry = getInstanceByAppAndId(appName, id, false);
     node.statusUpdate(appName, id, newStatus, infoFromRegistry);
     break;

    case DeleteStatusOverride: //删除OverrideStatus

     infoFromRegistry = getInstanceByAppAndId(appName, id, false);
     node.deleteStatusOverride(appName, id, infoFromRegistry);
     break;

    }
    } catch (Throwable t) {
    logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
    }
    }

    renew()方法中—>leaseToRenew.renew()—>对最后更新时间戳进行更新
    ![image.png](/upload/2020/07/image-21e67ff8319d4e57adc86abd980954db.png)

至此,续约完成。

Eureka Client注册服务

启动过程:Eureka客户端在启动时也会装载很多配置类,我们通过spring-cloud-netflix-eureka-client-2.1.0.RELEASE.jar下的spring.factories文件可以看到加载的配置类
image.png
引入jar就会被自动装配,分析EurekaClientAutoConfiguration类头
image.png
如果不想作为客户端,可以设置eureka.client.enabled=false
image.png
回到主配置类EurekaClientAutoConfiguration
EurekaClient启动过程:

  1. 读取配置文件
    image.png
  2. 启动时从EurekaServer获取服务实例信息
    image.png
    image.png
    观察父类DiscoveryClient()
    image.png
    在另外一个构造器中
    image.png
    image.png
    image.png
    image.png
  3. 注册自己到EurekaServer
    image.png
    DiscoveryClient#register
    image.png
    底层使用Jersey客户端进行远程请求。
  4. 开启一些定时任务(心跳续约,刷新本地服务缓存列表)
    image.png
    刷新本地缓存
    image.png
    image.png
    心跳续约定时任务
    image.png
    image.png
    image.png
    image.png

    Eureka Client下架服务

    image.png
    com.netflix.discovery.DiscoveryClient#shutdown
    image.png
    image.png

标签: Spring Cloud, Eureka

评论已关闭