Spring Cloud Gateway 的主动健康检查策略
如今,应用程序被构建为小型独立上游服务的集合。这加快了开发速度,并使模块专注于特定职责,提高了质量。这是使用微服务方法的主要优势之一。然而,从一个服务跳转到另一个服务会增加额外的延迟,当服务没有响应时,这种延迟会显著增加。
如果你运行的是微服务,你需要防止上游服务在工作不正常时被调用。即使使用断路器(circuit breaker)模式,也会对响应时间造成影响。因此,有时最好主动检查上游服务,以验证它们是否在需要之前就已准备就绪。
健康检查是确定服务是否能够根据其状态作出正确响应、防止超时和错误的一种方法。
被动健康检查 在请求处理过程中进行。如果服务最终处于不健康状态,应用程序将返回失败,并标记端点不健康。这会增加额外的延迟。
主动健康检查 将在接收请求之前在后台检查并放弃不健康的服务。它不会增加额外的延迟。
最后但并非最不重要的一点是,这些功能可与断路器库结合使用,以便立即 fall back 到另一个 endpoint ,而不会受到首次失误的惩罚。
目标是通过使用负载均策略,将路由请求转发到健康的上游服务:
本文章分为两部分:
- “你需要的 Spring 功能” - 描述你需要哪些 Spring 功能来获得主动的健康检查。
- “为你的服务注册端点”- 参考一些将一个或多个端点添加到路由中的方法。
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>.path
和 spring.cloud.loadbalancer.health-check.interval
。
默认的健康检查配置通过使用
/actuator/health
端点来检查上游服务端点,这需要在上游服务中激活 Spring Actuator。
有关更多选项,请查看 LoadBalancerClientsProperties 和 LoadBalancerProperties 类。
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 SERVER_PORT=8090 ./gradlew :service:bootRun
# 运行服务器 2 SERVER_PORT=8091 ./gradlew :service:bootRun
-
检查
http://localhost:8090/actuator/health
是否为: “UP”curl http://localhost:8090/actuator/health {"status":"UP"}
-
测试
http://localhost:8080/hello
响应是否为 200 OKcurl localhost:8090/hello { "message": "hello world!"}%
-
运行 Spring Cloud Gateway
./gradlew :1-service-disc-by-properties:bootRun
-
测试 Spring Cloud Gateway 负载均衡
curl localhost:8881/hello { "message": "hello world from port 8090!"}%
curl localhost:8881/hello { "message": "hello world from port 8091!"}%
你可能需要多次运行前面的命令才能从不同的服务器得到响应。
-
将 server 1 标记为不健康,向
http://localhost:8090/status/false
发送PUT
请求curl localhost:8090/status/false -X PUT
-
检查
http://localhost:8090/actuator/status
是否为 “DOWN”curl http://localhost:8090/actuator/health {"status":"DOWN"}
-
多次向
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!"}%
-
将服 server 2 标记为不健康,向
http://localhost:8091/status/false
发送PUT
请求curl localhost:8091/status/false -X PUT
-
向
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"}%
-
停止前面步骤中启动的所有服务器
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
尝试
-
运行 Eureka 服务器
./gradlew :eureka-server:bootRun
直到 Eureka 服务器启动。
2023-06-26 12:51:46.901 INFO 88601 --- [ Thread-9] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
-
运行服务器,激活
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)
-
进入
http://localhost:8761/
,检查服务器是否包含在应用程序hello-service
的实例中。 -
运行 Spring Cloud Gateway
SERVER_PORT=8883 ./gradlew :3-eureka-service-disc:bootRun
-
测试 Spring Cloud Gateway 负载均衡
curl localhost:8883/hello { "message": "hello world from port 8090!"}%
curl localhost:8883/hello { "message": "hello world from port 8091!"}%
-
将 server 1 标记为不健康,向
http://localhost:8090/status/false
发送PUT
请求curl localhost:8090/status/false -X PUT
你应该在 Eureka 仪表板中看到只有一个实例可用,并且你会看到一些日志信息,提示
8090
端口上的服务不可用。健康检查不是即时的,因此你可能需要等待几秒钟才能看到实例被标记为 “DOWN”。 -
停止前面步骤中启动的所有服务器
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 列表中优先考虑它。
尝试
-
运行服务器
# Run server 1 SERVER_PORT=8090 ./gradlew :service:bootRun
# Run server 2 SERVER_PORT=8091 ./gradlew :service:bootRun
-
检查
http://localhost:8090/actuator/health
是否为 “UP”curl http://localhost:8090/actuator/health {"status":"UP"}
-
测试
http://localhost:8080/hello
响应是否为 200 OKcurl localhost:8090/hello { "message": "hello world!"}%
-
运行 Spring Cloud Gateway
SERVER_PORT=8882 ./gradlew :2-custom-service-disc:bootRun
-
测试 Spring Cloud Gateway 负载均衡
curl localhost:8882/hello { "message": "hello world from port 8090!"}%
curl localhost:8882/hello { "message": "hello world from port 8091!"}%
你可能需要多次运行前面的命令才能从不同的服务器得到响应。
-
将 server 1 标记为不健康,向
http://localhost:8090/status/false
发送PUT
请求curl localhost:8090/status/false -X PUT
-
检查
http://localhost:8090/actuator/status
是否为 “DOWN”curl http://localhost:8090/actuator/health {"status":"DOWN"}
-
多次向
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!"}%
-
将 server 2 标记为不健康,向
http://localhost:8091/status/false
发送PUT
请求curl localhost:8091/status/false -X PUT
-
向
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"}%
-
停止前面步骤中启动的所有服务器
5、总结
在这篇文章中,你已经看到了在项目中进行负载均衡和主动健康检查的多种方法。
- 静态方法适用于上游服务数量不变的基本项目或概念验证。
- 使用 Eureka 或 Spring Cloud Gateway Filter,是一种更动态的方法,
总之,如果你不需要在架构中添加额外的组件,Spring Cloud Gateway 也是一个不错的选择。