Spring Cloud Gateway 的主动健康检查策略

如今,应用程序被构建为小型独立上游服务的集合。这加快了开发速度,并使模块专注于特定职责,提高了质量。这是使用微服务方法的主要优势之一。然而,从一个服务跳转到另一个服务会增加额外的延迟,当服务没有响应时,这种延迟会显著增加。

如果你运行的是微服务,你需要防止上游服务在工作不正常时被调用。即使使用断路器(circuit breaker)模式,也会对响应时间造成影响。因此,有时最好主动检查上游服务,以验证它们是否在需要之前就已准备就绪。

健康检查是确定服务是否能够根据其状态作出正确响应、防止超时和错误的一种方法。

被动健康检查 在请求处理过程中进行。如果服务最终处于不健康状态,应用程序将返回失败,并标记端点不健康。这会增加额外的延迟。

主动健康检查 将在接收请求之前在后台检查并放弃不健康的服务。它不会增加额外的延迟。

最后但并非最不重要的一点是,这些功能可与断路器库结合使用,以便立即 fall back 到另一个 endpoint ,而不会受到首次失误的惩罚。

目标是通过使用负载均策略,将路由请求转发到健康的上游服务:

Active Health Check Diagram

本文章分为两部分:

  1. “你需要的 Spring 功能” - 描述你需要哪些 Spring 功能来获得主动的健康检查。
  2. “为你的服务注册端点”- 参考一些将一个或多个端点添加到路由中的方法。

1、你需要的 Spring 功能

Spring 的一些功能可以帮助你进行主动的健康检查:

  • Spring Cloud Load Balancer(SLB)是客户端负载均衡器,可在不同上游服务端点之间均衡流量。它是 Spring Cloud 项目 的一部分,包含在 spring-cloud-commons 库中(参见 SLB文档)。
  • 客户端服务发现功能可让客户端查找服务并与之通信,而无需硬编码主机名和端口。spring-cloud-commons 库中也包含该功能(参见 服务发现文档)。 Spring Cloud Gateway 为在 Spring 和 Java 之上构建API网关提供了一个库。它通过 LoadBalancerClientFilter / ReactiveLoadBalancerClientFilter 全局过滤器支持上述功能。在本文章中,你将看到使用这些全局过滤器的不同方法。

首先,让我们来了解其中的一些功能。

Spring Cloud Load Balancer filter

Spring Cloud 中包含用于负载均衡的全局过滤器,可通过使用特殊的 URI 符号激活:lb://your-service-name

spring:
 cloud:
   gateway:
     routes:
       - id: myRoute
         uri: lb://your-service-name
         predicates:
         - Path=/service/**

负载均衡器过滤器 ReactiveLoadBalancerClientFilter (用于响应式应用程序)将检测 URI 并将其替换为与 your-service-name 相关的可用端点。

注意,你需要在服务发现注册中心里注册 your-service-name。我们将在下面的章节中介绍不同的方法。

主动健康检查

默认情况下,即使上游服务不健康,流量也会被路由到它们。为防止选择不良服务,你可以启用 Spring Cloud 负载均衡器客户端提供的 health-check 配置:

spring:
  cloud:  
    loadbalancer:  
      configurations: health-check

所有端点都将通过 Spring Boot Actuator health 端点自动进行定期检查。你还可以自定义一些选项,如 spring.cloud.loadbalancer.health-check.<your-service-name>.pathspring.cloud.loadbalancer.health-check.interval

默认的健康检查配置通过使用 /actuator/health 端点来检查上游服务端点,这需要在上游服务中激活 Spring Actuator。

有关更多选项,请查看 LoadBalancerClientsPropertiesLoadBalancerProperties 类。

Spring Cloud Gateway 有一个内置功能,可将所有可用服务部署为路由。本篇文章的描述恰恰相反,因此我们将声明负载均衡的路由,包括主动健康检查。

2、为你的服务注册端点

在上一节中,指定了一个负载均衡URI(lb://your-service-name),但现在需要注册与 URI 的服务名相关的端点。我们将在下面的章节中介绍一些方法。

静态的方式

你可通过配置 spring.cloud.discovery.client.simple.instances 属性静态激活客户端负载均衡。这是一个 map ,其 key 是服务名称(由 lb:// URI 使用),其 value 是指向上游服务的 org.springframework.cloud.client.ServiceInstance 对象数组。

静态负载均衡的一些优点包括:

  • 负载均衡可以在多个实例之间分配流量,分担服务压力,降低崩溃的可能性。
  • 容错性。

问题在于你在配置中静态设置了上游服务。如果需要更改列表,你需要重启应用程序。

示例:

spring:
  cloud:
    gateway:
      routes:
        - uri: lb://hello-service # Load Balancer URI handled by ReactiveLoadBalancerClientFilter
          predicates:
            - Path=/hello
    loadbalancer:
      configurations: health-check # Required for enabling SDC with health checks
    discovery:
      client:
        simple: # SimpleDiscoveryClient to configure statically services
          instances:
            hello-service:
              - secure: false
                port: 8090
                host: localhost
                serviceId: hello-service
                instanceId: hello-service-1
              - secure: false
                port: 8091
                host: localhost
                serviceId: hello-service
                instanceId: hello-service-2

尝试

  1. 运行服务器

    # 运行服务器 1
    SERVER_PORT=8090 ./gradlew :service:bootRun
    
    # 运行服务器 2
    SERVER_PORT=8091 ./gradlew :service:bootRun
    
  2. 检查 http://localhost:8090/actuator/health 是否为: “UP”

    curl http://localhost:8090/actuator/health
    
    {"status":"UP"}
    
  3. 测试 http://localhost:8080/hello 响应是否为 200 OK

    curl localhost:8090/hello
    
    { "message": "hello world!"}%
    
  4. 运行 Spring Cloud Gateway

    ./gradlew :1-service-disc-by-properties:bootRun
    
  5. 测试 Spring Cloud Gateway 负载均衡

    curl localhost:8881/hello
    
    { "message": "hello world from port 8090!"}%
    
    curl localhost:8881/hello
    
    { "message": "hello world from port 8091!"}%
    

    你可能需要多次运行前面的命令才能从不同的服务器得到响应。

  6. 将 server 1 标记为不健康,向 http://localhost:8090/status/false 发送 PUT 请求

    curl localhost:8090/status/false -X PUT
    
  7. 检查 http://localhost:8090/actuator/status 是否为 “DOWN”

    curl http://localhost:8090/actuator/health
    
    {"status":"DOWN"}
    
  8. 多次向 http://localhost:8881/hello 发起 GET 请求,查看是否仅从 8091 端口获得响应。

    当你发送请求时,由于 healthcheck 尚未检查端点,你可能会在 8090 端口收到一个响应。可以在 spring.cloud.loadbalancer.health-check.interval 属性中修改时间间隔。

    此外,你还可以看到一些信息,描述其中一个上游端点不健康,因此不可用。

    2023-05-08 14:59:53.151 DEBUG 9906 --- [ctor-http-nio-3] r.n.http.client.HttpClientOperations     : [12d42e83-77, L:/127.0.0.1:57439 - R:localhost/127.0.0.1:8090] Received response (auto-read:false) : RESPONSE(decodeResult: success, version: HTTP/1.1)
    HTTP/1.1 503 Service Unavailable
    
    curl localhost:8881/hello
    
    { "message": "hello world from port 8091!"}%
    
  9. 将服 server 2 标记为不健康,向 http://localhost:8091/status/false 发送 PUT 请求

    curl localhost:8091/status/false -X PUT
    
  10. http://localhost:8881/hello 发起一些 GET 请求,查看它是否响应 “503 Service Unavailable”

    curl localhost:8881/hello
    
    {"timestamp":"2023-05-08T13:07:48.704+00:00","path":"/hello","status":503,"error":"Service Unavailable","requestId":"6b5d6010-199"}%
    
  11. 停止前面步骤中启动的所有服务器

3、Eureka 整合(复杂,动态)

静态配置不是很灵活,但使用Eureka作为服务发现可以消除这一缺点。

其代价是你需要在你的架构中添加一个新的组件,这可能会增加你的维护负担。对于某些客户端来说,这可能不是一种选择。

下面的示例配置了 Eureka 整合:

spring:
    application:
    name: scg-client-with-eureka
    cloud:
    loadbalancer:
        configurations: health-check # 注:启用带健康检查的 SDC 时需要 - 如果你想重现因在 LB 中未使用健康检查而产生的问题,请删除此行。
        # 注:LoadBalancerCacheProperties.ttl(或 spring.cloud.loadbalancer.cache.ttl)默认为 35 - 你需要在实例健康后等待 35 秒。
    gateway:
        httpclient:
        wiretap: true
        routes:
        - uri: lb://hello-service
            predicates:
            - Path=/headers
            filters:
            - StripPrefix=0

eureka:
    client:
    webclient:
        enabled: true
    serviceUrl:
        defaultZone: http://localhost:8761/eureka
    fetchRegistry: true
    registerWithEureka: false
    instance:
    preferIpAddress: true

尝试

  1. 运行 Eureka 服务器

    ./gradlew :eureka-server:bootRun
    

    直到 Eureka 服务器启动。

    2023-06-26 12:51:46.901  INFO 88601 --- [       Thread-9] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
    
  2. 运行服务器,激活 eureka profile

    # Run server 1
    SPRING_PROFILES_ACTIVE=eureka SERVER_PORT=8090 ./gradlew :service:bootRun
    
    # Run server 2
    SPRING_PROFILES_ACTIVE=eureka SERVER_PORT=8091 ./gradlew :service:bootRun
    

    你应该在步骤1的服务器日志中看到 SERVER 实例已被添加到 Eureka 中。

    2023-06-26 12:52:50.805  INFO 88601 --- [nio-8761-exec-3] c.n.e.registry.AbstractInstanceRegistry  : Registered instance HELLO-SERVICE/192.168.0.14:hello-service:8090 with status UP (replication=true)
    2023-06-26 12:53:29.127  INFO 88601 --- [nio-8761-exec-9] c.n.e.registry.AbstractInstanceRegistry  : Registered instance HELLO-SERVICE/192.168.0.14:hello-service:8091 with status UP (replication=true)
    
  3. 进入 http://localhost:8761/,检查服务器是否包含在应用程序 hello-service 的实例中。

  4. 运行 Spring Cloud Gateway

    SERVER_PORT=8883 ./gradlew :3-eureka-service-disc:bootRun
    
  5. 测试 Spring Cloud Gateway 负载均衡

    curl localhost:8883/hello
    
    { "message": "hello world from port 8090!"}%
    
    curl localhost:8883/hello
    
    { "message": "hello world from port 8091!"}%
    
  6. 将 server 1 标记为不健康,向 http://localhost:8090/status/false 发送 PUT 请求

    curl localhost:8090/status/false -X PUT
    

    你应该在 Eureka 仪表板中看到只有一个实例可用,并且你会看到一些日志信息,提示 8090 端口上的服务不可用。健康检查不是即时的,因此你可能需要等待几秒钟才能看到实例被标记为 “DOWN”。

  7. 停止前面步骤中启动的所有服务器

4、路由级别的自定义 Filter(动态方法)

如你所见,Spring Cloud Gateway 提供了创建自定义 filter 的选项。它还允许你在不重启网关的情况下应用 filter 和更改路由。

在本节中,你将看到一个自定义 filter 实现,该 filter 通过使用 Spring Cloud Gateway 路由配置为你的服务设置负载均衡和健康检查。

如果你的项目中已经有服务发现(service discovery)服务器,这可能不是你的最佳选择。如果没有,这是一个简单而廉价的方法,可以在你的项目中集成两个很棒的功能。

spring:
    application:
    name: custom-service-disc
    cloud:
    loadbalancer:
        configurations: health-check # 注:启用带健康检查的 SDC 时需要 - 如果你想重现因在 LB 中未使用健康检查而产生的问题,请删除此行。
        # 注:LoadBalancerCacheProperties.ttl(或 spring.cloud.loadbalancer.cache.ttl)默认为 35 - 你需要在实例健康后等待 35 秒。
    gateway:
        routes:
        - uri: lb://hello-service
            id: load-balanced
            predicates:
            - Path=/load-balanced/**
            filters:
            - StripPrefix=1
            - LoadBalancer=localhost:8090;localhost:8091;localhost:8092

新的 LoadBalancer 路由 filter 可让你配置与 lb://hello-service 负载均衡器 URI 相关联的上游服务端点:

@Component
public class LoadBalancerGatewayFilterFactory extends AbstractGatewayFilterFactory<LoadBalancerGatewayFilterFactory.MyConfiguration> {

    // ...

    @Override
    public GatewayFilter apply(MyConfiguration config) {
        return (exchange, chain) -> {
            final Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            if (StringUtils.hasText(config.getInstances()) && route.getUri().getScheme().equals("lb")) {
                config.getServiceInstances(route.getUri().getHost()).forEach(discoveryClient::addInstance);
            }

            return chain.filter(exchange);
        };
    }
}

如果路由与 lb://<service-host> 模式匹配,LoadBalancerGatewayFilterFactory 将把来自 filter 配置的所有上游服务端点与 service-host 关联起来。

我们包含了一个新的 ReactiveCustomDiscoveryClient discovery client 实现,以管理我们代码中的上游服务端点。Spring 会检测到这样的 Bean,并在用于确定可用端点的 DiscoveryClient 列表中优先考虑它。

尝试

  1. 运行服务器

    # Run server 1
    SERVER_PORT=8090 ./gradlew :service:bootRun
    
    # Run server 2
    SERVER_PORT=8091 ./gradlew :service:bootRun
    
  2. 检查 http://localhost:8090/actuator/health 是否为 “UP”

    curl http://localhost:8090/actuator/health
    
    {"status":"UP"}
    
  3. 测试 http://localhost:8080/hello 响应是否为 200 OK

    curl localhost:8090/hello
    
    { "message": "hello world!"}%
    
  4. 运行 Spring Cloud Gateway

    SERVER_PORT=8882 ./gradlew :2-custom-service-disc:bootRun
    
  5. 测试 Spring Cloud Gateway 负载均衡

    curl localhost:8882/hello
    
    { "message": "hello world from port 8090!"}%
    
    curl localhost:8882/hello
    
    { "message": "hello world from port 8091!"}%
    

    你可能需要多次运行前面的命令才能从不同的服务器得到响应。

  6. 将 server 1 标记为不健康,向 http://localhost:8090/status/false 发送 PUT 请求

    curl localhost:8090/status/false -X PUT
    
  7. 检查 http://localhost:8090/actuator/status 是否为 “DOWN”

    curl http://localhost:8090/actuator/health
    
    {"status":"DOWN"}
    
  8. 多次向 http://localhost:8881/hello 发送 GET 请求,发现只有 8091 端口响应

    由于健康检查在你发送请求时尚未检查端点,你可能会在端口 8090 上收到一个响应。可以在 spring.cloud.loadbalancer.health-check.interval 属性中修改时间间隔。

    此外,你还可以看到一些信息,描述其中一个上游端点不健康,因此不可用。

    2023-05-08 15:59:53.151 DEBUG 9906 --- [ctor-http-nio-2] r.n.http.client.HttpClientOperations     : [12d42e83-77, L:/127.0.0.1:57439 - R:localhost/127.0.0.1:8090] Received response (auto-read:false) : RESPONSE(decodeResult: success, version: HTTP/1.1)
    HTTP/1.1 503 Service Unavailable
    
    curl localhost:8882/hello
    
    { "message": "hello world from port 8091!"}%
    
  9. 将 server 2 标记为不健康,向 http://localhost:8091/status/false 发送 PUT 请求

    curl localhost:8091/status/false -X PUT
    
  10. http://localhost:8881/hello 发起一些 GET 请求,查看它是否响应 “503 Service Unavailable”

    curl localhost:8882/hello
    
    {"timestamp":"2023-05-08T14:07:48.704+00:00","path":"/hello","status":503,"error":"Service Unavailable","requestId":"6b5d6010-199"}%
    
  11. 停止前面步骤中启动的所有服务器

5、总结

在这篇文章中,你已经看到了在项目中进行负载均衡和主动健康检查的多种方法。

  • 静态方法适用于上游服务数量不变的基本项目或概念验证。
  • 使用 Eureka 或 Spring Cloud Gateway Filter,是一种更动态的方法,

总之,如果你不需要在架构中添加额外的组件,Spring Cloud Gateway 也是一个不错的选择。