SpringCloud之Eureka注册中心原理及其搭建

Eureka简介

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。

Eureka组件

Eureka包含两个组件:Eureka Server和Eureka Client。

 

Eureka Server

Eureka Server提供服务注册和发现,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。多个Eureka Server之间会同步数据,做到状态一致
Eureka Server本身也是一个服务,默认情况下会自动注册到Eureka注册中心。
如果搭建单机版的Eureka Server注册中心,则需要配置取消Eureka Server的自动注册逻辑。毕竟当前服务注册到当前服务代表的注册中心中是一个说不通的逻辑。
Eureka Server通过Register、Get、Renew等接口提供服务的注册、发现和心跳检测等服务。

Eureka Client

Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)
Eureka Client分为两个角色,分别是:Application Service(Service Provider)和Application Client(Service Consumer)

Application Service(Service Provider)

服务提供方,是注册到Eureka Server中的服务。服务提供方将自身服务注册到Eureka中,从而使服务消费方能够找到。

Application Client(Service Consumer)

服务消费方,通过Eureka Server发现服务,并消费。服务消费方从Eureka中获取注册服务列表,从而能够消费服务。

在这里,Application Service和Application Client不是绝对上的定义,因为Provider在提供服务的同时,也可以消费其他Provider提供的服务;Consumer在消费服务的同时,也可以提供对外服务。

Eureka Server架构原理简介

 

Register(服务注册):把自己的IP和端口注册给Eureka。
Renew(服务续约):发送心跳包,每30秒发送一次。告诉Eureka自己还活着。
Cancel(服务下线):当provider关闭时会向Eureka发送消息,把自己从服务列表中删除。防止consumer调用到不存在的服务。
Get Registry(获取服务注册列表):获取其他服务列表。
Replicate(集群中数据同步):eureka集群中的数据复制与同步。
Make Remote Call(远程调用):完成服务的远程调用。

服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址(即:服务应用名,spring.application.name参数配置),然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。

当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。

服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。

Eureka服务治理体系

服务治理

服务治理是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册和发现。

SpringCloud Eureka主要负责完成微服务架构中的服务治理功能。

Eureka服务治理体系如下:

服务注册

在服务治理框架中,通常都会注册一个注册中心,每个服务单元向注册中登记自己提供的服务,包括服务的主机和端口号、 服务版本号、通讯协议等一些附加信息。注册中心按照服务名分类组织服务清单,同时还需要以心跳检测的方式去监测清单中的服务是否可用,若不可用需要从服务清单中剔除,以达到排除故障服务的效果。

服务发现

在服务治理框架下,服务间的调用不再通过具体的实例地址来实现,而是通过服务名发起请求调用实现。服务调用方通过服务名从服务注册中心的服务清单中获取服务实例的列表清单,通过指定的负载均衡策略取出一个服务实例位置来进行服务调用。

Eureka的自我保护机制

自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障),Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现过大量过期而面临被剔除的风险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的,自我保护机制提高了eureka的服务可用性。

自我保护机制的启动参数如下:

eureka.server.enable-self-preservation=true

 它的原理是,当Eureka Server节点在短时间内丢失过多的客户端时(可能发送了网络故障),那么这个节点将进入自我保护模式,不在注销任何微服务,当网络故障回复后,该节点会自动退出自我保护模式。当自我保护机制触发时,Eureka不再从注册列表中移除因为长时间没有收到心跳而应该过期的服务,仍能查询服务信息并且接受新服务注册请求,也就是其他功能是正常的。如果eureka节点A触发自我保护机制过程中,有了新服务注册了然后网络恢复后,其他peer节点能收到A节点的新服务信息,数据同步到peer过程中是有网络异常重试的,也就是说,是能够保证最终一致性的

Eureka的运行机制

Eureka的整个运行机制大致如下:

  • 注册:Eureka Client一次次反复连接Eureka,直到注册成功为止
  • 拉取或订阅:消费者会把注册中心的整个注册表全都拉取过来缓存到本地,会每隔30s拉取一次注册表,更新注册信息,最终服务消费者会基于服务列表做负载均衡,选中一个微服务后发起的远程调用服务。
  • 心跳:消费提供者每30S发送一次心跳,Eureka在每次收不到心跳后就会记一个数,如果3次没有收到心跳,eureka就会删除这个服务(将地址从注册表中删除)
  • 自我保护模式:特殊情况,由于网络不稳定15S内85%服务器出现心跳异常(一次收不到就算心跳异常),会保护所有的注册信息不删除,就算3次没有收到心跳的情况也不会删除,网络恢复后,可以自动退出保护模式,在开发测试期间,可以关闭保护模式

理解Eureka服务器实现原理

EurekaServer

EurekaServer提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。

注册中心服务端主要对外提供三个功能:

1、服务注册:服务提供者启动时,会通过Eureka Client像Eureka Server注册信息,Eureka Server会存储该服务的信息,Eureka Server内部有二层缓存机制在维护整个注册表Eureka Server端服务注册接口逻辑图如下:

public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();
}

2、提供注册表:服务消费者在调用服务时,如果Eureka Client没有缓存注册表的话,会从Eureka Server获取最新的注册表

3、同步状态:Eureka Client通过注册、心跳机制和Eureka Server同步当前客户端的状态 

Eureka服务器端作为服务提供者具有服务注册、服务续约、服务下线的功能

LeaseManager接口

 

package com.netflix.eureka.lease;

public interface LeaseManager<T> {
    void register(T var1, int var2, boolean var3);

    boolean cancel(String var1, String var2, boolean var3);

    boolean renew(String var1, String var2, boolean var3);

    void evict();
}

 

LeaseManager接口主要实现Eureka服务注册中心的服务注册、服务续约、服务取消、服务驱逐的工作,关注于对服务注册中过程的管理

 

Eureka服务器的启动流程

首先需要说明的是eureka Server是在eureka client的基础之上进一步封装的一个东西,也就是说客户端有的东西服务端也有。服务端额外多的东西就是对注册表的处理部分。它的启动流程如下:

  • 初始化环境配置:这个我们在日常开发中几乎都是使用的默认的
  • 读取服务端的配置信息,也就是读取eureka-server.properties配置文件;由于eureka中对于这种配置类采用的是面向接口的方式,因此非常好扩展,在Spring中是重新实现了这些配置类接口的
  • 构建应用管理器;读取eureka-client.properties配置文件,选择其中的部分配置,基于构造者模式创建服务实例交给应用管理器
  • 读取eureka-client.properties配置文件构建客户端信息;这里的操作和客户端的启动流程几乎就是一样的。
  • 创建注册表感知器registry,这个也可以称为注册表管理器。这里会维护注册表信息。
  • 创建服务端集群节点信息管理器;也就是我们配置的集群地址信息,默认10分钟检查一次
  • 基于上面的信息创建服务端的上下文信息;这里在进行初始化的时候会对相关资源进行初始化;启动相关的定时。
  • 从任意一个其他的服务节点上拉取注册表信息;注意这里只有将fetchRegistry配置为true才是有效的(因为这里最终是从服务端中的客户端中的注册信息里面拿到的,这也是为什么网上很多说从任意一个其他节点去拉取,因为客户端就是选择的一个去拉去的),同时将registry-sync-retries配置的值大于0(spring中默认值是0,也就是在spring-cloud中是不会执行这一步的)。
  • 启动注册感知器registry的定时;这个定时主要就是检查注册表中是否有过期的注册信息。

 EurekademoApplication.java

package com.zk.eurekademo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekademoApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekademoApplication.class, args);
    }

}

 application.yml

spring:
  application:  # 应用名字,eureka 会根据它作为服务id
    name: eureka-server
server:  # 服务端口
  port: 8761
eureka:
  instance:
    hostname: eureka-server
  client:
    register-with-eureka: false  # 不向eureka server 注册自己
    fetch-registry: false  # 不向eureka server 获取服务列表
    service-url:
      defaultZone: http://localhost:8762/eureka/

 pom.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>
    <groupId>com.zk</groupId>
    <artifactId>eurekademo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eurekademo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR9</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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </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>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.zk.eurekademo.EurekademoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project> 

Eureka Client客户端的启动流程

EurekaClient客户端作为服务消费者具有提供服务信息的功能

Eureka客户端的启动主要就是几个定时和之后进行注册表维护的网络请求资源初始化。

  • 构建应用管理器;读取eureka-client.properties配置文件,选择其中的部分配置,基于构造者模式创建服务实力交给应用管理器
  • 读取eureka-client.properties配置信息和应用管理器构建客户端
  • 创建心跳、缓存等需要的线程池;
  • 创建网络通信组件;后面发送注册信息、心跳信息这些请求都是它来处理的;
  • 判断是否需要拉取注册表信息
  • 启动相关的定时任务:注册表更新任务(默认30s执行一次),心跳定时任务(默认30s执行一次)、创建服务状态更新定时任务(默认30s执行一次,这个就是留给我们自定义服务上下线状态的逻辑判断的);
  • 最后进行监视器的绑定

相关流程图如下:

EurekaClient的项目构建

 pom.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>
    <groupId>com.zk</groupId>
    <artifactId>eurekademo2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eurekademo2</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.5.RELEASE</version>
        </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>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.zk.eurekademo.EurekademoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

 EurekademoApplication.java

package com.zk.eurekademo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class EurekademoApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekademoApplication.class, args);
    }
}

 application.yml

spring:
  application:  # 应用名字,eureka 会根据它作为服务id
    name: eureka-client
server:  # 服务端口
  port: 8762
eureka:
  instance:
    hostname: eureka-client
    prefer-ip-address: true
    instance-id: ${eureka.instance.ip-address} #表示status
    ip-address: 127.0.0.1
    # 表示eureka client发送心跳给server端的频率,续约间隔,默认30秒(只要服务端没有接收到,并不会直接剔除,会先把Status变为Down状态)
    lease-renewal-interval-in-seconds: 5
    # 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance
    lease-expiration-duration-in-seconds: 15
  client:
    register-with-eureka: true  # 向eureka server 注册自己
    fetch-registry: false  # 不向eureka server 获取服务列表

    service-url:
      defaultZone: http://localhost:8761/eureka/   

运行结果如下

Eureka客户端基本原理

LookupService接口

public interface LookupService<T> {
    Application getApplication(String var1);

    Applications getApplications();

    List<InstanceInfo> getInstancesById(String var1);

    InstanceInfo getNextServerFromEureka(String var1, boolean var2);
}

LookupService接口关注于对应用程序和服务实例的管理

Eureka注册表的原理

eureka的注册表中保存中服务的注册信息,下面我们通过如下几个点来对其原理进行简析

注册表的抓取和缓存机制

其基本流程如下:

 

注册表的数据结构和缓存机制

eureka server中对注册表的信息进行多重缓存,分为:

  • 只读缓存(ConcurrentMap):会有定时任务默认每隔30s主动的去和读写缓存里面的信息同步一次;
  • 读写缓存(guava的Loading):在创建LoadingCache的时候默认设置的过期时间是180s;
  • 注册表:这个就是实时的本地注册信息,每次客户端的注册信息更新后,都会实时的保存在这里,同时在更新它的时候会将读写缓存中的值设置为失效状态。

注册表信息读取流程

注册表的拉取分为全量和增量;在初次拉取时使用的是全量,后面使用的都是增量拉取的。

全量拉取流程: 

  • 服务端收到客户端请求后,会直接从只读缓存里面取值,如果有就返回,否则进行下一步;
  • 只读缓存里面没有时,会从读写缓存里面取值,如果有就返回,同时将其设置达到只读缓存里面;否则进行下一步;
  • 读写缓存里面没有时,会触发LoadingCache的load方法,这里面会从本地注册表中取值返回

增量拉取流程:

  • 服务端收到客户端的请求后,会直接从只读缓存里面取增量信息,如果有就返回,否则进行下一步;
  • 只读缓存里面没有时,会从读写缓存里面取增量信息,如果有就返回,同时将其设置达到只读缓存里面,否则进行下一步;
  • 读写缓存里面没有时,会触发Loading的load方法,这里面会增量队列中获取变化的信息然后返回;

服务端集群间的注册信息是如何同步的

首先看一下注册的 

注册的流程说明:

  • 客户端在启动的最后一步启动服务状态更新定时任务时,里面的定时任务就会向服务器端发送注册信息
  • 客户端会选从置的服务服务注册地址中选择第一个进行尝试,如果成功后面都会用这个,直到失败才会切换到下一个;
  • 服务端收到注册请求后,更新本地注册表中注册信息,将读写缓存中的缓存设置为失效状态;同时将注册表的变更信息保存到最近变更队列中;
  • 将注册请求信息转发给Eureka server集群中的其他节点

心跳的请求也是在服务端自己处理完成后,会自动将这个请求转发给集群中的其他节点。心跳的操作就是更新注册信息中的租约时间。注意这种通知集群中其他节点的操作在失败后会不断的重试,同时正式由于有这个操作,因此服务端的fetchRegistry配置为false,集群间的注册信息依然可以正常同步的原因。

客户端的注册信息什么时候会被摘除

客户端的注册信息被摘除主要是这两种情况:1、客户端服务主动下线;2、服务异常。

客户端服务主动下线

客户端服务主动下线:主动取消注册信息,这种服务端直接接收请求后然后删除即可,其流程图如下:

 服务异常

 客户端异常:没有发送取消请求或者是服务端没有正常接收和处理取消请求的情况下,此时就需要服务端自己定制一套注册信息过期机制,这也就是发送心跳的作用。

服务端中注册信息过期检查的定时任务默认每隔60s检查一次,其大致流程如下:

  • 判断的过期的依据是:当前时间戳>(上一次发送的租约的时间戳+过期时间(默认90s)+补充时间(就是距离上一次执行任务的时间超过定时任务配置的60s执行一次的周期时间));但是由于在设置上一次发送租约的时间戳时候额外加上了一个过期时间;因此最终注册表的过期时间就至少是180s。
  • 选择15%的过期注册信息,然后调用取消操作来删除注册信息;同时会通知集群中的其他的节点。

Eureka Client是一个java客户端

参考文档:https://blog.csdn.net/weixin_40400410/article/details/121230462  

posted @ 2021-01-20 22:39  leagueandlegends  阅读(206)  评论(0编辑  收藏  举报