11. 服务发现:Eureka 客户端

服务发现是基于微服务体系结构的关键原则之一。尝试手动配置每个客户端或某种形式的约定可能很难做到,而且可能很脆弱。Eureka 是 Netflix 服务发现服务器和客户端。可以将服务器配置和部署为高可用,每个服务器都将注册服务的状态复制到其他服务器。

11.1 如何包含 Eureka 客户端

要在项目中包含 Eureka 客户端,请使用 group ID 为 org.springframework.cloud 和 artifact ID 为 spring-cloud-starter-netflix-eureka-client 的 starter。了解有关使用当前 Spring Cloud Release Train 构建系统设置的详细信息,请参见 Spring Cloud 项目页面。

11.2 使用 Eureka 注册

当客户端注册到 Eureka 时,它提供有关自身的元数据  —  例如主机、端口、健康指示器 URL、主页和其他详细信息。Eureka 从属于服务的每个实例接收心跳消息。如果心跳在可配置的时间表上失败,则实例通常会从注册表中删除。

以下示例显示了最小的 Eureka 客户端应用程序:

@SpringBootApplication
@RestController
public class Application {

    @RequestMapping("/")
    public String home() {
        return "Hello world";
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

}

注意,前面的示例展示了一个普通的 Spring Boot 应用程序。通过让 spring-cloud-starter-netflix-eureka-client 位于类路径上,你的应用程序将自动注册到 Eureka 服务器。需要配置才能定位 Eureka 服务器,如以下示例所示:

application.yml. 

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

在前面的示例中,"defaultZone" 是一个神奇的字符串回退值,它为任何不表示首选项的客户端提供服务 URL(换句话说,它是一个有用的默认值)。

默认应用程序名(即服务 ID)、虚拟主机和非安全端口(取自 Environment)分别为 ${spring.application.name}、${spring.application.name} 和 ${server.port}。

将 spring-cloud-starter-netflix-eureka-client 放在类路径上,可以使应用程序同时进入 Eureka “instance”(即它自己注册)和 “client”(它可以查询注册表以定位其他服务)。实例行为由 eureka.instance.* 配置键驱动,但如果确保应用程序具有 spring.application.name 的值(这是 eureka 服务 ID 或 VIP 的默认值),则默认值也可以。

配置可选项的更多细节请参阅 EurekaInstanceConfigBean 和 EurekaClientConfigBean。

要禁用 Eureka Discovery 客户端,可以将 eureka.client.enabled 设置为 false。

11.3 使用 Eureka Server 认证

如果其中一个 eureka.client.serviceUrl.defaultZone Url 中嵌入了凭据(curl 样式,如下所示:http://user:password@localhost:8761/eureka),则会自动将 HTTP 基本身份验证添加到你的 eureka 客户端。对于更复杂的需求,你可以创建 DiscoveryClientOptionalArgs 类型的 @Bean,并将 ClientFilter 实例插入其中,所有这些实例都应用于从客户端到服务器的调用。

[Note] Note

由于 Eureka 的限制,不可能支持每台服务器的基本认证凭据,因此只使用找到的第一组。

11.4 状态页和健康指示器

Eureka 实例的状态页和健康指示器分别默认为 /info 和 /health,这是 Spring Boot 执行器应用程序中有用端点的默认位置。如果使用非默认上下文路径或 servlet 路径(如 server.servletPath=/custom),则即使对于执行器应用程序,也需要更改这些内容。以下示例展示了两个设置的默认值:

application.yml. 

eureka:
  instance:
    statusPageUrlPath: ${server.servletPath}/info
    healthCheckUrlPath: ${server.servletPath}/health

这些链接显示在客户端使用的元数据中,并在某些场景中用于决定是否向应用程序发送请求,因此,如果它们是准确的,则很有帮助。

[Note] Note

在 Dalston 中,还需要在更改管理上下文路径时设置状态和健康检查 URL。此要求从 Edgware 开始删除。

11.5 注册安全应用程序

如果你的应用程序希望通过 HTTPS 联系,可以在 EurekaInstanceConfig 中设置两个标志:

  • eureka.instance.[nonSecurePortEnabled]=[false]
  • eureka.instance.[securePortEnabled]=[true]

这样做会使 Eureka 发布实例信息,显示对安全通信的明确偏好。对于以这种方式配置的服务,Spring Cloud DiscoveryClient 始终返回以 https 开头的 URI。同样,当以这种方式配置服务时,Eureka(本机)实例信息具有安全的健康检查 URL。

由于 Eureka 在内部的工作方式,它仍然为状态和主页发布一个不安全的 URL,除非你也明确地覆盖这些 URL。可以使用占位符配置 Eureka 实例 URL,如下例所示:

application.yml. 

eureka:
  instance:
    statusPageUrl: https://${eureka.hostname}/info
    healthCheckUrl: https://${eureka.hostname}/health
    homePageUrl: https://${eureka.hostname}/

(注意,${eureka.hostname} 是本机占位符,仅在 Eureka 的较新版本中可用。例如,你可以使用 ${eureka.instance.hostName} 来实现与 Spring 占位符相同的功能。

[Note] Note

如果你的应用程序运行在代理之后,而 SSL 终止在代理中(例如,如果你作为服务运行在 Cloud Foundry 或其他平台中),则需要确保应用程序拦截和处理代理 “forwarded” 头。如果嵌入在 Spring Boot 应用程序中的 Tomcat 容器具有 'X-Forwarded-\*` 头的显式配置,则会自动发生这种情况。应用程序呈现给自身的链接是错误的(错误的主机、端口或协议),这表明你的配置是错误的。

11.6 Eureka 的健康检查

默认情况下,Eureka 使用客户端心跳来确定客户端是否启动。除非另有规定,否则发现客户端不会根据 Spring Boot 执行器传播应用程序的当前健康检查状态。因此,注册成功后,Eureka 总是宣布应用程序处于 'UP' 状态。通过启用 Eureka 健康检查可以更改此行为,这将导致向 Eureka 传播应用程序状态。因此,每个其他应用程序都不会将流量发送到除 'UP' 之外的其他状态的应用程序。以下示例显示如何为客户端启用健康检查:

application.yml. 

eureka:
  client:
    healthcheck:
      enabled: true

[Warning] Warning

仅应在 application.yml 中设置 eureka.client.healthcheck.enabled=true。在 bootstrap.yml 中设置该值会导致不良的副作用,例如在 eureka 中以 UNKNOWN 状态注册。

如果你需要对健康检查进行更多控制,请考虑实现自己的 com.netflix.appinfo.HealthCheckHandler。

11.7 实例和客户端的 Eureka 元数据

花点时间了解 Eureka 元数据的工作原理是值得的,因此你可以以一种在你的平台中有意义的方式使用它。对于主机名、IP 地址、端口号、状态页和健康检查等信息,有标准的元数据。它们在服务注册中心发布,客户端使用它们直接联系服务。可以将其他元数据添加到 eureka.instance.metadataMap 中的实例注册中,并且可以在远程客户端中访问此元数据。通常,附加的元数据不会改变客户端的行为,除非客户端知道元数据的含义。在本文后面描述的一些特殊情况中,Spring Cloud 已经为元数据映射赋予了意义。

11.7.1 在 Cloud Foundry 上使用 Eureka

Cloud Foundry 具有全局路由器,因此同一应用程序的所有实例都具有相同的主机名(具有类似架构的其他 PaaS 解决方案具有相同的安排)。这不一定是使用 Eureka 的障碍。但是,如果使用路由器(根据平台的设置方式,建议甚至是强制使用),则需要显式设置主机名和端口号(安全或不安全),以便它们使用路由器。你可能还希望使用实例元数据,以便能够区分客户端上的实例(例如,在自定义负载平衡器中)。默认情况下,eureka.instance.instanceId 为 vcap.application.instance_id,如下例所示:

application.yml. 

eureka:
  instance:
    hostname: ${vcap.application.uris[0]}
    nonSecurePort: 80

根据在你的 Cloud Foundry 实例中设置安全规则的方式,你可能能够注册并使用主机 VM 的 IP 地址进行直接服务到服务的调用。此功能在 Pivotal Web Services(PWS)上尚不可用。

11.7.2 在 AWS 上使用 Eureka

如果计划将应用程序部署到 AWS 云,则必须将 Eureka 实例配置为支持 AWS。你可以通过以下方式自定义 EurekaInstanceConfigBean 来完成此操作:

@Bean
@Profile("!default")
public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) {
  EurekaInstanceConfigBean b = new EurekaInstanceConfigBean(inetUtils);
  AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka");
  b.setDataCenterInfo(info);
  return b;
}

11.7.3 更改 Eureka 实例 ID

普通的 Netflix Eureka 实例的注册 ID 等于其主机名(即,每个主机只有一个服务)。Spring Cloud Eureka 提供了一个合理的默认值,其定义如下:

${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}}

一个例子是 myhost:myappname:8080.

通过使用 Spring Cloud,你可以通过在 eureka.instance.instanceId 中提供唯一标识符来覆盖此值,如下面的示例所示:

application.yml. 

eureka:
  instance:
    instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

使用前面示例中展示的元数据和部署在本地主机上的多个服务实例,将在其中插入随机值以使实例唯一。在 Cloud Foundry 中,vcap.application.instance_id 在 Spring Boot 应用程序中自动填充,因此不需要随机值。

11.8 使用 EurekaClient

一旦你有了一个作为发现客户端的应用程序,你就可以使用它从 Eureka Server 发现服务实例。一种方法是使用本地 com.netflix.discovery.EurekaClient(与 Spring Cloud DiscoveryClient 相反),如下面的示例所示:

@Autowired
private EurekaClient discoveryClient;

public String serviceUrl() {
    InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
    return instance.getHomePageUrl();
}
[Tip] Tip

不要在 @PostConstruct 方法或 @Scheduled 方法(或应用程序上下文可能尚未启动的任何地方)中使用 EurekaClient。它是在 SmartLifecycle(phase=0)中初始化的,因此最早可以依赖它的是另一个具有更高阶段的 SmartLifecycle。

11.8.1 不使用 Jersey 的 EurekaClient

默认情况下,EurekaClient 使用 Jersey 进行 HTTP 通信。如果你希望避免依赖 Jersey,可以将其从依赖项中排除。Spring Cloud 基于 Spring RestTemplate 自动配置传输客户端。以下示例展示不包含 Jersey:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-client</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-apache-client4</artifactId>
        </exclusion>
    </exclusions>
</dependency>

11.9 本地 Netflix Eurekaclient 的替代方案

你不需要使用原始的 Netflix EurekaClient。此外,在某种包装物后面使用它通常更方便。Spring Cloud 通过逻辑 Eureka 服务标识符(VIPS)而不是物理 URL 支持 Feign(一个 REST 客户端生成器)和 SpringRestTemplate。要使用固定的物理服务器列表配置功能区,可以将 <client>.ribbon.listOfServers 设置为物理地址(或主机名)的逗号分隔列表,其中 <client> 是客户端的 ID。

你还可以使用 org.springframework.cloud.client.discovery.DiscoveryClient,它为发现客户端提供了一个简单的 API(不特定于 Netflix),如下面的示例所示:

@Autowired
private DiscoveryClient discoveryClient;

public String serviceUrl() {
    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    if (list != null && list.size() > 0 ) {
        return list.get(0).getUri();
    }
    return null;
}

11.10 为什么注册服务这么慢?

作为一个实例,还需要定期向注册表发送心跳(通过客户端的 serviceUrl),默认持续时间为30秒。在实例、服务器和客户机的本地缓存中都具有相同的元数据(因此可能需要 3 次心跳)之前,客户端无法发现服务。你可以通过设置 eureka.instance.leaseRenewalIntervalInSeconds 来更改间隔。将其设置为小于 30 的值可以加快将客户端连接到其他服务的过程。在生产环境中,最好还是坚持使用默认值,因为服务器中的内部计算会对租约续订期进行假设。

11.11 区域

如果你已经将 Eureka 客户端部署到多个区域,那么你可能更希望这些客户端在尝试在另一个区域中的服务之前使用同一区域中的服务。要设置它,你需要正确配置 Eureka 客户端。

首先,你需要确保将 Eureka 服务器部署到每个区域,并且它们彼此是对等的。有关的更多详细信息,请参阅有关区域和区域的部分。

接下来,你需要告诉 Eureka 你的服务在哪个区域。你可以使用 metadataMap 属性来完成此操作。例如,如果 service 1 同时部署到 zone 1 和 zone 2,则需要在 service 1 中设置以下 Eureka 属性:

Service 1 in Zone 1

eureka.instance.metadataMap.zone = zone1
eureka.client.preferSameZoneEureka = true

Service 1 in Zone 2

eureka.instance.metadataMap.zone = zone2
eureka.client.preferSameZoneEureka = true