java框架之SpringCloud(3)-Eureka服务注册与发现

时间:2023-12-13 16:48:44

上一章节完成了一个简单的微服务案例,下面就通过在这个案例的基础上集成 Eureka 来学习 Eureka。

介绍

概述

Eureka 是 Netflix 的一个子模块,也是核心模块之一。Eureka 是一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。服务注册与发现对于微服务架构来说是非常重要的,有了服务发现与注册,只需要使用服务的标识符,就可以访问到服务。功能类似于 dubbo的注册中心,比如 Zookeeper。

SpringCloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册与发现,Eureka 采用了 C/S 的设计架构。

Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其它微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中的各个微服务是否正常运行。SpringCloud 的一些其它模块(比如 Zuul)就可以通过 Eureka Server 来发现系统中的其它微服务,并执行相关逻辑。

基本架构

Eureka Server 包含两个组件:

  • Eureka Server :提供服务注册的服务,各个节点启动后,会在 Eureka Server 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
  • Eureka Client:是一个 Java 客户端,用于简化 Eureka Server 的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向 Eureka Server 发送心跳(默认周期为 30 秒)。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,Eureka Server 将会从服务注册表中把这个服务节点一处(默认 90 秒)。

三大角色

  • Eureka Server:提供服务注册与发现。
  • Service Provider:服务提供方将自身服务注册到 Eureka,从而是服务消费方能够找到。
  • Service Consumer:服务消费方从 Eureka 获取到注册服务列表,从而能够获取并消费服务。

架构图

java框架之SpringCloud(3)-Eureka服务注册与发现

图 1:Eureka 架构图

java框架之SpringCloud(3)-Eureka服务注册与发现

图 2:Dubbo 架构图

使用

EurekaServer端

1、新建名为 "microservicecloud-eureka-7001" 的子工程作为 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>microservicecloud</artifactId>
        <groupId>zze.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>microservicecloud-eureka-7001</artifactId>

    <dependencies>
        <!--eureka-server服务端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
        <!-- 修改后立即生效,热部署 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies>
</project>

pom.xml

2、配置 Eureka :

server:
  port: 7001

eureka:
  instance:
    hostname: localhost # eureka 服务端实例名称

  client:
    register-with-eureka: false # 表示不向注册中心注册自己
    fetch-registry: false # false 表示自己就是注册中心,不需要检索服务
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与 Eureka Server 交互的地址,可用来查询注册的服务

application.yml

3、编写主启动类,并使用注解开启 Eureka Server 功能:

package zze.springcloud;

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

@SpringBootApplication
@EnableEurekaServer // 开启 Eureka Server 功能,标识当前程序就是一个 Eureka Server
public class Application_7001 {

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

zze.springcloud.Application_7001

4、测试:

运行主启动类,浏览器访问 http://localhost:7001/ 进入 Eureka Server 图形化页面:

java框架之SpringCloud(3)-Eureka服务注册与发现

test

@EnableDiscoveryClient 和 @EnableEurekaClient:
  • 共同点:都是能够让注册中心能够发现,扫描到该服务。
  • 不同点:@EnableEurekaClient 只适用于 Eureka 作为注册中心,@EnableDiscoveryClient 可以是其他注册中心。 从Spring Cloud Edgware开始,@EnableDiscoveryClient 或@EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。

客户端Provider注册

1、修改名为 "microservicecloud-provider-dept-8001" 的工程的 pom 文件,添加如下 Eureka 客户端依赖:

<!--Eureka 客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

pom.xml

2、将当前工程作为 Eureka 客户端注册到 Eureka 服务,在配置文件添加如下配置:

eureka:
  client: # 将当前工程作为 Eureka 客户端
    service-url:
      defaultZone: http://localhost:7001/eureka # Eureka 服务端地址

application.yml

3、在主启动类添加注解标识当前工程为 Eureka 客户端:

package zze.springcloud;

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

@SpringBootApplication
@EnableEurekaClient // 标注当前工程为 Eureka 客户端
public class Application_8001 {
    public static void main(String[] args) {
        SpringApplication.run(Application_8001.class, args);
    }
}

zze.springcloud.Application_8001

4、测试:

先启动 7001 Eureka 服务端工程,再启动 8001 Eureka 客户端工程,浏览器访问 http://localhost:7001/,可以看到 8001 客户端实例已经被注册到 Eureka 服务端:

java框架之SpringCloud(3)-Eureka服务注册与发现

test

服务发现

服务发现实际上就是让 EurekaServer 端能够扫描到我们注册的服务,默认我们可以通过 Web UI 的方式查看哪些服务注册到了 EurekaServer,还可以通过 Eureka 客户端依赖提供的服务发现客户端获取注册到 EurekaServer 的服务信息。

1、修改名为 "microservicecloud-consumer-dept-80" 的服务工程添加如下 Eureka 客户端依赖:

<!--Eureka 客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

2、在其主启动类上添加注解启用 Eureka 客户端功能:

package zze.springcloud;

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

@SpringBootApplication
@EnableEurekaClient
public class Application_80 {

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

zze.springcloud.Application_80

3、修改 Controller:

package zze.springcloud.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.*;
import org.springframework.web.client.RestTemplate;
import zze.springcloud.entities.Dept;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/consumer/dept")
public class DeptController {
    @Autowired
    private RestTemplate restTemplate;

    @PostMapping("/add")
    public boolean add(@RequestBody Dept dept) {
        return restTemplate.postForObject(getRestUrlPrefix("MICROSERVICECLOUD-PROVIDER-DEPT") + "/dept/add", dept,Boolean.class);
    }

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id){
        return restTemplate.getForObject(getRestUrlPrefix("MICROSERVICECLOUD-PROVIDER-DEPT") + "/dept/get/" + id, Dept.class);
    }

    @GetMapping("/list")
    public List<Dept> list(){
        return restTemplate.getForObject(getRestUrlPrefix("MICROSERVICECLOUD-PROVIDER-DEPT") + "/dept/list", List.class);
    }

    // 服务发现客户端
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 通过服务名称获取到服务实例对应的 url
     */
    private String getRestUrlPrefix(String serviceName){
        List<String> services = discoveryClient.getServices();
        System.out.println("---------------------------"+services);
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        System.out.println(instances);
        // 比如只注册了一个 MICROSERVICECLOUD-PROVIDER-DEPT 微服务实例
        ServiceInstance serviceInstance = instances.get(0);
        return String.format("http://%s:%s", serviceInstance.getHost(), serviceInstance.getPort());
    }

    /**
     * 获取所有注册到 EurekaServer 的服务信息
     * @return
     */
    @GetMapping("/discovery")
    public Object discovery(){
        Map<String, Object> map = new HashMap<>();
        // 获取所有注册到 EurekaServer 的微服务名称,对应 spring.application.name
        List<String> services = discoveryClient.getServices();
        for (String service : services) {
            // 获取对应服务所有实例
            List<ServiceInstance> instances = discoveryClient.getInstances(service);
            map.put(service, instances);
        }
        return map;
    }

}

zze.springcloud.controller.DeptController

4、测试:

正常访问 http://localhost/consumer/dept/list:

java框架之SpringCloud(3)-Eureka服务注册与发现

访问 http://localhost/consumer/dept/discovery 查看所有服务信息:

java框架之SpringCloud(3)-Eureka服务注册与发现

test

即通过服务发现我们只需要使用约定的服务名称就可以通过注册中心访问到具体服务信息。

EurekaServer集群搭建

1、新建两个子工程作为两个 EurekaServer 端,分别名为 "microservicecloud-eureka-7002"、"microservicecloud-eureka-7003",主启动类分别名为 "Application_7002"、"Application_7003",依赖同 "microservicecloud-eureka-7001"。

2、由于是单机测试,需修改本机 host 映射:

127.0.0.1 www.eurekaserver1.com
127.0.0.1 www.eurekaserver2.com
127.0.0.1 www.eurekaserver3.com

3、分别修改 7001、7002、7003 的配置文件:

server:
  port: 7001

eureka:
  instance:
    hostname: www.eurekaserver1.com # eureka 服务端实例名称

  client:
    register-with-eureka: false # 表示不向注册中心注册自己
    fetch-registry: false # false 表示自己就是注册中心,不需要检索服务
    service-url:
      defaultZone: http://www.eurekaserver2.com:7002/eureka/,http://www.eurekaserver3.com:7003/eureka/

application.yml#7001

server:
  port: 7002

eureka:
  instance:
    hostname: www.eurekaserver2.com # eureka 服务端实例名称

  client:
    register-with-eureka: false # 表示不向注册中心注册自己
    fetch-registry: false # false 表示自己就是注册中心,不需要检索服务
    service-url:
      defaultZone: http://www.eurekaserver1.com:7001/eureka/,http://www.eurekaserver3.com:7003/eureka/

application.yml#7002

server:
  port: 7003

eureka:
  instance:
    hostname: www.eurekaserver3.com # eureka 服务端实例名称

  client:
    register-with-eureka: false # 表示不向注册中心注册自己
    fetch-registry: false # false 表示自己就是注册中心,不需要检索服务
    service-url:
      # 单机版
      # defaultZone: http://${e ureka.instance.hostname}:${server.port}/eureka/ # 设置与 Eureka Server 交互的地址,可用来查询注册的服务
      defaultZone: http://www.eurekaserver1.com:7001/eureka/,http://www.eurekaserver2.com:7002/eureka/

application.yml#7003

4、修改 8001 客户端 Eureka 配置,让客户端注册到多个 Eureka 服务端:

eureka:
  client: # 将当前工程作为 Eureka 客户端
    service-url:
      # 单机版
      # defaultZone: http://localhost:7001/eureka # Eureka 服务端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept
    prefer-ip-address: true # 访问路径显示 IP

application.yml

5、测试:

依次启动 7001、7002、7003 EurekaServer 服务,接着再启动 8001 客户端服务,客户端服务可以正常访问。
访问 http://www.eurekaserver1.com:7001/:

java框架之SpringCloud(3)-Eureka服务注册与发现

访问 http://www.eurekaserver2.com:7002/:

java框架之SpringCloud(3)-Eureka服务注册与发现

访问 http://www.eurekaserver3.com:7003/:

java框架之SpringCloud(3)-Eureka服务注册与发现

test

由测试结果可知,客户端服务已经注册到了多个 Eureka 服务端,每个 Eureka 服务端上又挂载了其它 Eureka 服务端的副本,Eureka 集群搭建成功。

补充

actuator与信息完善

上面我们通过访问 Eureka 的 Web 页看到如下界面:

java框架之SpringCloud(3)-Eureka服务注册与发现

服务名称修改

该界面是描述的是有哪些 Eureka 客户端实例注册到了当前 Eureka 服务端,说明如下:

  • Application 栏对应工程配置中的 spring.application.name 属性,即说明这个客户端是属于哪个工程。
  • 而 Status 栏则表示相应 Eureka 客户端实例的标识,可以通过 eureka.instance.instance-id 属性进行修改。

IP信息提示

Status 栏所显示的实例名称是可以点击的,它所跳转的页面为 随机域名:端口/info ,如下:

java框架之SpringCloud(3)-Eureka服务注册与发现

如果我们希望将这个地址的随机域名改为 IP 地址,则可以在配置文件中修改 eureka.instance.prefer-ip-address 属性值为 true 实现:

java框架之SpringCloud(3)-Eureka服务注册与发现

Info内容构建

为方便我们使用,Eureka 允许我们通过点击客户端实例访问实例的详细信息,对应路径为 /info ,但我们访问时会发现会返回 404 如下:

java框架之SpringCloud(3)-Eureka服务注册与发现

其实 SpringBoot 本身提供的监控功能就可以帮我们解决这个问题,需要在客户端工程中引入监控相关依赖:

<!--监控信息-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

使用 maven 资源插件,让项目属性加载到项目环境变量中:

<build>
    <finalName>microservicecloud</finalName>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <configuration>
                <delimiters>
                    <delimit>$</delimit>
                </delimiters>
            </configuration>
        </plugin>
    </plugins>
</build>

接着我们就可以在客户端配置文件中添加 info 相关配置,例如:

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8001
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}

重启客户端程序,重新访问 /info :

java框架之SpringCloud(3)-Eureka服务注册与发现

自我保护机制

当我们在做上述测试的时候,我们可能会发现 Eureka 页可能会显示如下红字:

java框架之SpringCloud(3)-Eureka服务注册与发现

这个其实就是因为 Eureka 的自我保护机制引起的。默认情况下,如果 EurekaServer 在一定时间内没有接收到某个微服务实例的心跳,EurekaServer 将会注销该实例(默认为 90 秒)。但是当网络分区故障时,微服务与 EurekaServer之间无法正常通信,以上行为可能就变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka 通过“自我保护模式”来解决这个问题:当 EurekaServer 节点在短时间内丢失过多的客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,EurekaServer 就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障回复后,EurekaServer 节点会自动退出自我保护模式。

在自我保护模式中,EurekaServer 会保护服务注册表中的信息,不注销任何服务实例。当它收到的心跳数重新恢复到阈值以上时,该 EurekaServer 就会自动退出自我保护模式。它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例,其目的是遵循 AP 原则。

关于 CAP 原则可参考【CAP 定理的含义

综上,自我保护模式是一种应对网络异常的安全保护措施,它的架构哲学是宁可同时保留所有微服务(无论微服务是健康还是不健康),也不盲目注销任何健康的微服务。使用自我保护模式,可以让 Eureka 集群更加的健壮、稳定。

在 SpringCloud Eureka 服务端工程中,可以通过 eureka.server.enable-self-preservation = false 来禁用自我保护模式。

Eureka VS Zookeeper

作为服务注册中心,Eureka 比 Zookeeper 好在哪里?

著名的 CAP 理论指出,一个分布式系统不可能同时满足 C(一致性)、A(可用性)和 P(分区容错性)。由于分区容错性 P 是在分布式系统中必须保证的,因此我们只能在 A 和 C 之间权衡。

因此 Zookeeper 保证的是 CP,而 Eureka 则是保证 AP。

Zookeeper 保证 CP:
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接 down 掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是 Zookeeper 会出现这样一种情况,当 master 节点因为网络故障与其它节点失去联系时,剩余节点会重新进行 leader 选举。问题在于,选举 leader 的时间太长(30 - 120s),且选举期间整个 Zookeeper 集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得 Zookeeper 失去 master 节点是较大概率会发生的事,虽然服务在最终能够恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
Eureka 保证 AP:
Eureka 看明白了这一点,因此在设计时就优先保证可用性。Eureka 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余节点依然可以提供注册和查询服务。而 Eureka 的客户端在向某个 Eureka 注册师如果发现连接失败,则会自动切换至其它节点,只要有一台 Eureka 服务还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka 还有一种自我保护机制,如果在 15 分钟内超过 85% 的节点都没有正常的心跳,那么 Eureka 就认为客户端与注册中心出现了网络故障,此时就会出现如下几种情况:
  1. Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
  2. Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)。
  3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中。

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