当前位置:   article > 正文

SpringCloud 组件之 Eureka 详解及实战

eureka

Eureka 注册发现中心

什么是 Eureka?

Eureka是Netflix公司开发的服务发现框架,SpringCloud将其集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。Eureka在微服务架构中扮演了关键角色,帮助解决服务之间的通信问题。
具体来说,Eureka通过服务注册与发现的机制,使得每个注册到Eureka的服务都可以在Eureka上找到其他注册的服务。服务提供者可以将自己的服务注册到Eureka服务器中,而服务消费者则可以从Eureka服务器中获取可用的服务实例列表。当服务消费者需要调用某个服务时,它可以根据负载均衡策略从Eureka提供的服务实例列表中选择一个实例进行请求转发。
此外,Eureka还提供了健康检查功能,确保只有健康的服务实例才会被注册和发现。当服务提供者宕机或下线时,Eureka服务器会自动将其从服务实例列表中移除,从而保证了服务的可用性和稳定性。

如何实现服务间的通信问题

可以把Eureka分为客户端、服务端两部分,在非集群模式下,通常部署一个Eureka服务端(Eureka Server)来作为服务注册中心,同时可以有多个Eureka客户端(Eureka Client)来注册和发现服务。
Eureka客户端主要向Eureka服务端注册自己的IP地址和端口等信息,注意:Eureka的服务端也会把自己注册上去,也就是自己注册自己。
那么有了所有客户端的IP地址和端口等信息,自然也就可以实现调用功能了,Eureka主要使用RestFul风格进行服务之间的调用,Restful风格是一种基于HTTP协议的服务调用方式。
先来看看注册的大致流程:

客户端如果主动停掉了服务端还有这个客户端的信息吗?
当客户端主动停掉时,理想情况下,它会向Eureka Server发送一个注销请求,告知自己即将下线。这样,Eureka Server就能及时地从其注册表中移除该客户端的信息。然而,如果客户端因为某种原因异常终止,它可能无法发送这样的请求。因此,Eureka Server还依赖于其他机制来检测和处理这种情况。
如果客户端因为某种原因异常终止,服务端如何知道?
Eureka服务端会对注册表中所有的客户端进行一个健康检查,也可以叫做心跳机制,比如服务端每间隔十秒就会向客户端发起一个请求,客户端收到请求并回应,服务端收到客户端的回应就代表这个客户端还活着,如果接收不到回应那么就会将其注册信息删除。
健康检查的间隔时间是可以通过配置设置的,例如,通过配置eureka.instance.lease-renewal-interval-in-seconds参数,可以设定客户端发送心跳信息的间隔时间。同样,服务端对于心跳请求的响应超时时间也是可配置的,以确保服务端能够及时处理客户端的心跳请求。这种机制确保了Eureka注册表中的信息始终是最新和准确的。
如果同一时间大量客户端都挂掉了,服务端会怎么做?
不会的。Eureka服务端会有一个阈值,比如这个阈值是80%,那么如果有80%的客户端在同一时间都没有响应服务端时Eureka服务端就不会采取删除方法,它会认为可能是网络问题或者其他问题导致自己收不到客户端的响应而采取其他措施(后面会讲到),这也确保了服务的可用性和稳定性。而Eureka服务端的这个阈值是可以通过配置自己控制的。
客户端每次通信都要先经过服务端再去找对应的其他客户端吗?
并不是。Eureka客户端通常会在启动时从Eureka Server拉取服务注册表的信息,并在本地缓存一份。这样,客户端在需要调用其他服务时,可以直接从本地缓存中获取服务信息,提高了通信效率。然而,这种缓存机制也需要注意缓存的更新和同步问题,以确保客户端获取的服务信息始终是最新的。在某些情况下,根据业务需求,客户端也可以选择实时查询Eureka Server来获取最新的服务信息。

Eureka 代码实战

服务端
  1. 首先需要创建一个SpringBoot项目来作为服务端。
  2. 引入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>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>fan.demo</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>eureka-server</description>
<properties>
    <java.version>8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  1. 在启动类上面加上@EnableEurekaServer注解启动服务端。
  2. 编写服务端相关配置
spring:
    application:
        name: eureka-server

server:
    port: 8761 # eureka 服务端默认端口号

# eureka配置总共分为三大类:server、client、instance
# eureka服务端配置,注意eureka服务端也属于客户端,它也会把自己注册上去。
eureka:
    server:
        # eureka检查实例是否存活的间隔时间,每隔10秒将认为不存活的实例剔除掉
        eviction-interval-timer-in-ms: 10000
        # 续约百分比,如果在检查实例是佛存活时,有百分之八十五的实例都认为不存活,则不进行剔除。
        renewal-percent-threshold: 0.85
    instance:
        hostname: localhost
        # eureka服务端保存实例的InstanceId展示格式,一般为:localhost:eureka-server:8761
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
        prefer-ip-address: true # 将localhost解析为真是的IP地址,默认为 false
        # 作为实例,设置想服务端续约间隔时间,单位秒
        lease-renewal-interval-in-seconds: 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 启动服务端,然后访问:localhost:8761,出现下面页面则代表成功

image.png
暂时我们只需要关注下面这一行展示的内容分别是什么,其他的可以先不关注:
image.png

  • **Instances currently registered with Eureka:**当前在Eureka注册的实例。
  • **Application:**实例名称,对应spring.application.name: eureka-server这个配置,只不过变成了全大写的。
  • **Amls(不理解也没关系):**这通常与AWS的EC2实例相关,指的是启动实例时所使用的机器镜像。在Eureka的上下文中,这个字段可能不直接显示,除非Eureka与AWS紧密集成,并且你正在查看与AWS相关的实例信息
  • **Availability Zones(不理解也没关系):**这指的是AWS中的可用区。Eureka用于显示实例注册到的AWS可用区。如果你的Eureka部署不依赖于AWS,那么这个字段可能不适用或显示为其他相关信息。
  • **Status:**UP表示上线的,UP(1)代表目前有一台上线的服务,对应的还有DOWN表示下线,不可用的。-[eureka-server:8761](http://192.168.0.108:8761/actuator/info)表示的就是我们前面设置的instance-id配置(截图中没有localhost是因为我去掉了)。当鼠标悬浮到上面还可以在左下角看到对应的IP地址和端口号,这是因为我们将prefer-ip-address:设置为true的结果。
客户端
  1. 首选需要创建一个Spring-boot项目用来充当客户端。
  2. 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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>fan.demo</groupId>
    <artifactId>eureka-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-client</name>
    <description>eureka-client</description>
    <properties>
        <java.version>8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>
        </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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  1. eureka客户端相关配置:
spring:
    application:
        name: eureka-client

server:
    port: 8080

# eureka配置总共分为三大类:server、client、instance
# eureka客户端配置。
eureka:
    client:
        fetch-registry: true # 是否从服务端拉取实例到本地,默认为 true
        registry-fetch-interval-seconds: 10 # eureka每隔10秒从服务端拉取一次实例自己本地,默认是30秒
        register-with-eureka: true # 是否向服务端进行注册,默认为 true
        service-url: # 不写默认 http://localhost:8761/eureka
          defaultZone: http://localhost:8761/eureka
    instance:
        hostname: localhost
        # eureka服务端保存实例的InstanceId展示格式,一般为:localhost:eureka-server:8761
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
        prefer-ip-address: true # 将localhost解析为真是的IP地址,默认为 false
        lease-renewal-interval-in-seconds: 10 # 作为实例,设置想服务端续约间隔时间,单位秒
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 同样的需要在启动类上加上@EnableEurekaClient 注解表示开启eureka客户端。

到此,我们的客户端就已经写好了,然后再来看下eureka服务端页面展示:
image.png
eureka客户端成功注册到了服务端上。

服务的发现与调用

既然现在eureka服务端上面已经注册两个服务了,因为服务端也是一个客户端嘛。那么我们现在就可以实现服务之间的调用了,具体做法呢?看下面代码例子:

@RestController
public class TestController {

    @Autowired
    private DiscoveryClient discoveryClient; // 这是springCloud提供的,用来获取注册到eureka-server上的服务信息。

    /**
     * 测试服务调用
     * @param serviceName 首先需要一个服务名称,来获取该服务下的所有实例信息
     * @param path 要调用的服务接口,因为我们不知道要调用哪个接口,所以需要告诉服务
     * @return
     */
    @GetMapping("test")
    public String test(String serviceName, String path) {
        // 用来进行http调用的对象
        RestTemplate restTemplate = new RestTemplate();
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        // 因为我们目前eureka-server这个服务名称下仅有一个实例,所以直接取第一个即可。
        ServiceInstance instance = instances.get(0);
        System.out.println("serviceName 下的实例信息:" + instance.toString());
        String host = instance.getHost(); // 获取实例的ip地址
        int port = instance.getPort(); // 获取端口号
        // 拼接请求链接
        String url = "http://" + host + ":" + port + "/" + path;
        // 发起get请求
        String forObject = restTemplate.getForObject(url, String.class);
        return "调用" + serviceName + "服务的" + path + "接口,返回结果:" + forObject;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
@RestController
public class TestController {
    @GetMapping("test")
    public String test()
    {
        return "eureka-client 200";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

都写好以后,然后我们如果在eureka-server端需要调用eureka-client端对外提供的接口,就需要首先知道两个东西,一个是eureka-client端的服务名称,一个是需要调用的路径地址。
发起请求:http://localhost:8761/test?serviceName=eureka-client&path=test
请求结果:
image.png
需要注意的是,在实际使用中我们并不是以上面那种方式去进行服务之间的调用的,SpringCloud提供了Ribbon组件和OpenFegin组件,这两个组件任意一个都能实现服务调用以及负载均衡效果。
可以理解为Ribbon集成了Eureka,而OpenFegin集成了Ribbon,后面这两个组件以及源码都会讲到。

Eureka 源码分析

服务的注册
  1. 首先找到 eureka-client这个依赖的位置:image.png
  2. 在这个依赖下找到 discover 包下有一个 DiscoveryClient 的类,这个就是客户端发起注册请求的地方。
    1. 点击左下角的 structure 选项可以快速查找该类的所有方法,我们找到 register 这个方法,顾名思义它就是客服端发起注册的方法:
      image.png
    2. 该方法我们主要关注这段代码:image.png,它再次调用了一个 registrationClient 属性的 register 方法,并传入了一个叫 instanceInfo 的参数。
    3. 那么说明客户端在进行注册的时候需要传一个 instanceInfo 的参数,所以我们需要看一下这个参数是什么时候赋值的,里面又包含哪些东西:image.png定位到最开始声明的位置发现这是一个全局变量。
      通过寻找发现它是在 DiscoveryClient 的构造方法中初始化的值,@Inject 表示在 Spring 创建这个实例时要使用该构造方法创建。
      image.png
      接下来就需要看一下 InstanceInfo 对象包含哪些参数,从名字来看这个是服务的实力对象,这个类中有很多参数,看不懂的不需要关注,只看我们认识的参数,比如下面几个:
    4. 看到上面参数我们就清楚了,原来客户端在发起服务注册请求时会将自己的IP地址、服务名称、端口号、实例id、组名等信息发送过去。为了验证这个猜想,我们通过断点调试发现果然 InstanceInfo 包含上面这个值:
      image.png
    5. 继续跟着断点往下走,最终来到 AbstractJerseyEurekaHttpClient 类下的 register 方法中,该方法发起了一个 http 的 post 请求,这个请求的服务就是 eureka 服务端:
      image.png
  3. 下面就需要找到请求到服务端哪里去了,我们找到 eureka-server 这个依赖:image.png
  4. 该依赖下面有一个叫 InstanceRegistry 的类,该类就是实例注册的地方,同样点击左下角的 structure 选项找一个叫 register 的方法,发现这里有两个 register 方法
    image.png具体请求到的是哪一个,需要我们继续寻找一下。
  5. 通过寻找发现有一个 com.netflix.eureka.resources.ApplicationResource 的类对外提供了接口,该类里面有一个叫 addInstance 的 post 请求的方法,该方法就是接收客户端请求并进行服务注册的地方了:
    image.png
  6. 通过 addInstance 方法定位到服务注册调用的是 InstanceRegistry 类中的第二个 register 方法。
  7. 跟着断点最终找到服务注册最重要的代码逻辑,在 AbstractInstanceRegistry 类中的 register 方法:
    1. 首先是这段代码,registry 是一个ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> 类型的 Map,它的第一个 key 对应的 value 还是一个 Map 对象,第二个 key 对应的 value 就是我们的服务列表。image.png
    2. 继续下一步,发现第一个 key 值就是注册进来的服务名称,因为第一次注册所以获取到的 gMap 肯定是 null,所以就创建了一个不为 null 的 ConcurrentHashMap 的引用:
      image.png
    3. 继续下一步,发现第二个 key 是我们给客户端设置的实例ID,因为是第一次注册所以同样 existingLease 也是 null,然后执行了最关键的一步:gMap.put(registrant.getId(), lease);这一步就完成了服务的注册。其他的代码我们暂时都先不管,只关注我们看的懂得,到了这一步我们的服务注册就已经完成了。
      image.png
    4. 然后通过 gMap 中的值,我们就可以理解服务列表的结构具体是什么样的:
      image.png
服务的续约
  1. 同样的我们需要先找到客户端发起续约的位置,在上面我们找到 eureka-client 依赖下的 DiscoveryClient 类,这个类里面不止包含了服务的注册方法,还有续约、下线都在里面。找到该类中的 renewal 方法:
    image.png
  2. 同样的我们只关注核心的代码,renewal 方法同样发起了一个请求,具体实现在AbstractJerseyEurekaHttpClient 类中,sendHeartBeat 方法发起了一个 put 请求到服务端,参数是实例的状态和最后一次续约的时间戳:
    image.png
  3. 同样的我们找到服务端的 InstanceRegistry 类,里面有一个叫 renew 的方法就是续约方法,方法中再次调用了父类的 renew 方法:
    image.png
  4. 接着往下找,找到 AbstractInstanceRegistry 类中的 renew 方法,这个方法就是续约的核心代码。首先根据从服务列表服务名称和实例ID获取到对应的服务信息,然后再调用实例自己的 renew 方法完成续约:
    image.png
  5. 实例中的 renew 方法就是将最后一次续约时间更新为当前时间加上固定的续约间隔时间:
    image.png
服务的下线
  1. 服务的下线是指客户端主动向服务端发起下线请求,服务端接收到请求后会对客户端进行删除,其实服务下线的本质就是从服务列表中根据对应的实例ID对客户端进行删除操作。
  2. 首先找到客户端发起下线请求的位置,同样是在 DiscoveryClient 类中,找到一个叫 shutdown 的方法,注意 @PreDestory 注解表示在该Bean 销毁时执行该方法,比如客户端服务器关闭时就会执行该方法。
    可以看到 shutdown 方法先将客户端的实例状态设置为了 DOWN,然后调用了 unregister 方法:
    image.png
  3. unregister 方法通过 registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());向服务端发起了一个请求:
    image.png
  4. 继续向下寻找,同样是到了AbstractJerseyEurekaHttpClient 类中,然后发起了一个 delete 的请求,参数是服务名称和实例ID:
    image.png
  5. 然后去服务端寻找是哪个方法接收的,同样是 InstanceRegistry 这个类,下面也有一个 cancel 方法,最终会走到 AbstractInstanceRegistry 类中的 internalCancel 方法里面去:
    image.png
  6. 同样是先从服务列表中获取到对应的服务,然后关键代码在这一句leaseToCancel = gMap.remove(id);从服务列表中根据实例ID直接删除,到此就完成了服务的下线操作。
服务的剔除
  1. 服务的剔除和下线的本质都是将服务从服务列表删除,但是它们的触发时机不同。服务剔除是指服务端在对客户端进行健康检查时,一但发现有客户端服务超过续约时间仍没有续约就会主动的将客户端服务删除。
  2. 找到 AbstractInstanceRegistry 类中的 evict 方法,该方法就是执行服务剔除操作的方法:
    image.png
  3. 点进去 evict 方法,我们来一步一步分析:
    1. 首先是循环服务列表,调用每一个服务的 isExpired 方法判断当前时间是否大于服务最后一次续约的时间 + 约定续约间隔时间,如果大于则放进 expiredLeases 集合里面:
      image.png
    2. 循环 expiredLeases 集合,这个时候该集合里面装的就都是过期的服务,最终调用internalCancel 方法完成服务的删除操作:
      image.png

总结

经历了一番Eureka的探险之旅,我们终于从它的迷雾中走出,手中握着满满的实战经验和源码秘籍。Eureka,这位微服务世界中的“红娘”,不仅帮我们牵线搭桥,让服务们相亲相爱,还通过其独特的机制保证了整个微服务家族的和谐稳定。

实战中,Eureka的易用性让我们不禁感叹:“哇,这也太简单了吧!”就像我们在微信上摇一摇就能找到附近的人一样,Eureka让我们的服务也能轻松找到彼此。而源码分析则让我们看到了Eureka背后的“黑科技”,那些复杂的算法和机制,就像魔术师的秘密道具,让Eureka能够如此高效、稳定地工作。

然而,就像所有伟大的魔术师一样,Eureka也不是万能的。随着微服务数量的增加,Eureka也开始面临压力和挑战。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号