Spring Cloud Netflix 教程 - Eureka

1、概览

本文将带你了解如何通过 Spring Cloud Netflix Eureka 来实现客户端服务发现。

客户端服务发现允许服务相互查找和通信,而无需硬编码主机名和端口。在这种架构中,唯一的 “固定点” 是服务注册中心(service registry,),每个服务都必须在注册中心中注册。

一个缺点是所有客户端必须实现特定的逻辑与这个注册中心进行交互。这就需要在实际请求之前进行一次额外的网络请求。

有了 Netflix Eureka,每个客户端都可以同时充当服务器,将自己的状态复制给已连接的对等服务。换句话说,客户端在服务注册中心中检索所有已连接对等服务的列表,并通过负载均衡算法向其他服务发出所有进一步请求。

要获知客户端的存在,它们必须向注册中心发送心跳信号。

为了实现本文的目标,需要实现三个微服务:

  • 注册中心(Eureka Server
  • REST 服务,在注册中心中注册(Eureka Client
  • Web 应用,作为客户端(Spring Cloud Netflix Feign Client)来消费 REST 服务(从注册中心获取到服务)

2、Eureka 服务器

使用 Eureka Server 实现一个注册中心,很简单:

  1. 在依赖中添加 spring-cloud-starter-netflix-eureka-server
  2. @EnableEurekaServer@SpringBootApplication 进行注解,从而启用 Eureka 服务器
  3. 配置一些配置属性

一步步来:

首先,创建一个新的 Spring Boot 项目,并添加相应的依赖。

通过 spring-cloud-starter-parent Bom 管理组件的依赖版本。

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

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-parent</artifactId>
            <version>2021.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

你可以在 Spring 的官方文档中查看 最新的 Spring Cloud 版本

然后,创建 Application 类:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

最后,在 application.yml 中配置属性:

server:
  port: 8761
eureka:
  client:
    registerWithEureka: false # 不注册自己
    fetchRegistry: false

如上,配置了应用端口,Eureka 服务器的默认端口是 8761。并且告诉内置的 Eureka 客户端不要注册自己,因为此应用应该充当服务器。

现在,打开浏览器访问 http://localhost:8761,就可以看到 Eureka 面板,稍后会通过这里检查已注册的实例。

目前,可以看到一些基本指标,如状态(status)和健康(health)指标:

Eureka 控制台

3、Eureka 客户端

要让 @SpringBootApplication 具备服务发现能力,必须在 classpath 中包含 Spring Discovery Client(例如 spring-cloud-starter-netflix-eureka-client)。

然后,需要在 @Configuration 中注解 @EnableDiscoveryClient@EnableEurekaClient。注意,如果 classpath 上有 spring-cloud-starter-netflix-eureka-client 依赖,则此注解是可选的。

后者告诉 Spring Boot 明确使用 Spring Netflix Eureka 来发现服务。为了给客户端应用添加一些实际示例,还在 pom.xml 中添加了 spring-boot-starter-web Stater,并实现一个 REST Controller。

首先,添加必要的依赖。同样,使用 spring-cloud-starter-parent Bom 来管理组件版本:

<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-web</artifactId>
</dependency>

实现 Application 类:

@SpringBootApplication
@RestController
public class EurekaClientApplication implements GreetingController {

    @Autowired
    @Lazy
    private EurekaClient eurekaClient;

    @Value("${spring.application.name}")
    private String appName;

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

    @Override
    public String greeting() {
        return String.format(
          "Hello from '%s'!", eurekaClient.getApplication(appName).getName());
    }
}

以及 GreetingController 接口:

public interface GreetingController {
    @RequestMapping("/greeting")
    String greeting();
}

也可以简单地在 EurekaClientApplication 类中声明请求映射,而不是使用接口。不过,如果想在服务器和客户端之间共享接口,这还是很有必要的。

接下来,需要在 application.yml 中配置一个 Spring 应用名称,以在注册的应用列表中唯一标识客户端。

可以让 Spring Boot 随机选择一个端口,因为稍后会使用其名称访问该服务。

最后,必须告诉客户端注册中心的位置:

spring:
  application:
    name: spring-cloud-eureka-client # 应用名称
server:
  port: 0 # 随机端口
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
  instance:
    preferIpAddress: true

如此设置的 Eureka 客户端服务,以后很容易扩展。

现在,运行客户端,并再次通过浏览器访问 http://localhost:8761,以查看其在 Eureka 控制面板上的注册状态。通过控制面板,可以做进一步的配置,比如出于管理目的将已注册客户端的主页与控制面板链接起来。不过,这些配置选项超出了本文的讨论范围:

Eureka 控制面板中注册的服务

4、Feign 客户端

最后,使用 Spring Netflix Feign Client 实现一个依赖于三个微服务的 REST Web 应用。

Feign 视为一种具备服务发现功能的 Spring RestTemplate,它使用接口与端点进行通信。这些接口将在运行时自动实现,它使用的是服务名而不是服务URL。

如果没有 Feign,就必须要在 Controller 中自动装配一个 EurekaClient 实例。该实例可以通过服务名称获取服务信息,返回一个 Application 对象。

通过 Application 对象获取该服务所有实例的列表,从中挑选一个合适的实例,然后使用 InstanceInfo 获取主机名和端口。这样,就可以使用任何 HTTP 客户端执行标准请求了:

@Autowired
private EurekaClient eurekaClient;

@RequestMapping("/get-greeting-no-feign")
public String greeting(Model model) {

    InstanceInfo service = eurekaClient
      .getApplication("spring-cloud-eureka-client")
      .getInstances()
      .get(0);

    String hostName = service.getHostName();
    int port = service.getPort();

    // ...
}

RestTemplate 也可用于通过名称访问 Eureka 客户端服务,但这一主题不在本文讨论范围之内。

创建 Feign 客户端项目,在 pom.xml 中添加以下 4 个依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</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-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Feign 客户端位于 spring-cloud-starter-feign Starter 中。要启用它,必须在 @Configuration 中注解 @EnableFeignClients。要使用它,只需用 @FeignClient("service-name") 注解一个接口,并将其自装配接到 Controller 中即可。

创建此类 Feign 客户端的好方法是创建带有 @RequestMapping 注解方法的接口,并将其放入一个单独的模块中。这样,它们就可以在服务器和客户端之间共享。在服务器端,可以将它们作为 @Controller 来实现,而在客户端,则可以继承它们并注解为 @FeignClient

此外,还需要在项目中包含 spring-cloud-starter-eureka 依赖,并在 Application 类上添加 @EnableEurekaClient 注解以启用客户端服务发现。

spring-boot-starter-webspring-boot-starter-thymeleaf Starter 依赖用于渲染从 REST 服务获取的数据的视图。

Feign 客户端接口如下:

@FeignClient("spring-cloud-eureka-client")
public interface GreetingClient {
    @RequestMapping("/greeting")
    String greeting();
}

实现 Application 类,同时充当 Controller:

@SpringBootApplication
@EnableFeignClients
@Controller
public class FeignClientApplication {
    @Autowired
    private GreetingClient greetingClient;

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

    @RequestMapping("/get-greeting")
    public String greeting(Model model) {
        model.addAttribute("greeting", greetingClient.greeting());
        return "greeting-view";
    }
}

视图的 HTML 模板如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Greeting Page</title>
    </head>
    <body>
        <h2 th:text="${greeting}"/>
    </body>
</html>

application.yml 配置文件与上一步几乎相同:

spring:
  application:
    name: spring-cloud-eureka-feign-client
server:
  port: 8080
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}

现在,构建并运行该服务。然后,通过浏览器访问 http://localhost:8080/get-greeting,返回的内容如下:

Hello from SPRING-CLOUD-EUREKA-CLIENT!

5、TransportException: Cannot Execute Request on Any Known Server

在运行 Eureka 服务器时,经常会遇到类似如下的异常情况:

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

基本上,出现这种情况是因为在 application.propertiesapplication.yml 中进行了错误的配置。

Eureka 为客户端提供了两个可配置的属性:

  • registerWithEureka:如果将此属性设置为 true,那么当服务器启动时,内置客户端将尝试向 Eureka 服务器注册。
  • fetchRegistry:如果将此属性配置为 true,内置客户端将尝试获取 Eureka 注册表。

如果将上述属性设置为 true(或者干脆不配置,因为它们默认为 true),那么在启动服务器时,内置客户端会尝试向 Eureka 服务器注册,并尝试获取注册表,但注册中心尚未可用。结果就是抛出了传输异常(TransportException)。

因此,绝不应在 Eureka 服务器应用中将这些属性配置为 true。在 application.yml 中进行的正确设置是:

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

6、总结

本文介绍了如何使用 Eureka Server 实现服务注册中心,以及如何使用 Eureka Client 把服务到注册中心,最后使用 Feign Client 从注册中心发现服务并发起 REST 调用。

这基本上就是一个完整微服务系统的全貌了。由于步骤 3 中的 Eureka 客户端监听的是随机选择的端口,因此如果没有注册中心中的信息,就无法知道它的位置。有了 Feign 客户端和注册中心,即使位置发生变化,也能找到并使用 REST 服务。


Ref:https://www.baeldung.com/spring-cloud-netflix-eureka