Skip to content

Spring Cloud Gateway Reactive

之前网关在遭遇流量洪峰时会重启

经过AI分析得出原因是因为网关的过滤器逻辑中使用了同步逻辑导致。因为Spring Cloud Gateway 底层是基于 NettyReactor 异步非阻塞模型 构建的,如果在过滤器中使用了同步调用,会阻塞 Reactor 的工作线程(通常称为 Event Loop 线程)。一旦Reactor的工作线程被阻塞会导致以下问题:

  • 整个事件循环被挂起,不能继续处理其他请求
  • 吞吐量下降,响应变慢,网关处理能力大幅降低
  • JVM 的 GC 压力加大。系统负载升高

因为不能处理其他请求,k8s的健康检查探针探测到服务异常,就会杀死网关服务。

借助开源项目 BlockHound 来检查阻塞代码。 BlockHound文档 

使用BlockHound检测出了项目中主要是同步Redis阻塞了工作线程

  1. 主要: 同步代码修改为响应式,使用ReactiveRedis来替代同步Redis
  2. 次要: 移除无效代码,减少无效的网络请求
  3. 代码整理。移除无效代码,减少过滤器中的无效逻辑调用。例如: 从请求中解析相关用户信息,最后却什么也不做、请求耗时统计等
响应式Redis核心依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

因为需要压测网关本身优化效果,需要消除下游服务对网关的测试结果的影响,所以让请求在网关内部闭环,不涉及其他服务。

package com.moatkon;
import java.nio.charset.StandardCharsets;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MoatkonEndpointFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if ("/dummy".equals(path)) {
byte[] data = "OK".getBytes(StandardCharsets.UTF_8);
exchange.getResponse().setStatusCode(HttpStatus.OK);
exchange.getResponse().getHeaders().setContentLength(data.length);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory().wrap(data)));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 保证早期执行
}
}
package com.moatkon;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class MoatkonRouteLocator {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("dummy_test", r -> r
.path("/dummy")
.filters(f -> f
.addResponseHeader("X-Test", "gateway")
)
.uri("no://backend")) // 注意:使用一个不会被解析的uri
.build();
}
}
  • 测试环境 、 2个网关节点
  • 压测时间选择在晚上,测试环境流量少。(公司没有压测环境…有压测环境就不用这么麻烦了)

为了方便测试不同并发下的网关表现,写了一个脚本,方便数据收集与处理。

  • 压测工具使用ab
  • 固定请求总数
创建请求数据文件
echo '{"hello":"Hello World"}' > post_data.json
压测脚本——AI生成的
#!/bin/bash
# 响应式优势测试策略 - 带错误处理版本
# 目标:找到响应式Redis相比同步Redis的性能拐点
TOKEN="Authorization: 自定义token"
POST_DATA="post_data.json"
# 固定总请求数,测试不同并发级别的性能
TOTAL_REQUESTS=8000
## 压测
URL="http://moatkon-gateway-test.com/dummy"
GATEWAY_HOST="moatkon-gateway-test"
GATEWAY_PORT="80"
# 同步redis
RESULT_FILE="k8s_reactive_performance_test_sync.log"
# 响应式redis
# RESULT_FILE="k8s_reactive_performance_test_reactive.log"
echo "响应式Redis性能测试开始..." | tee $RESULT_FILE
echo "测试时间: $(date)" | tee -a $RESULT_FILE
echo "========================================" | tee -a $RESULT_FILE
# 测试函数 - 增强错误处理
run_test() {
concurrency=$1
requests=$2
test_name=$3
echo "[$test_name] 并发: $concurrency, 请求: $requests" | tee -a $RESULT_FILE
# 运行压测
echo "🚀 开始压测..." | tee -a $RESULT_FILE
result=$(ab -n $requests -c $concurrency -T "application/json" -H "$TOKEN" -p $POST_DATA $URL 2>&1)
ab_exit_code=$?
# 检查ab命令执行结果
if [ $ab_exit_code -ne 0 ]; then
echo "❌ 压测执行失败 (退出码: $ab_exit_code)" | tee -a $RESULT_FILE
echo "错误信息:" | tee -a $RESULT_FILE
echo "$result" | grep -E "(apr_socket_recv|Connection refused|Connection timed out|failed|error)" | tee -a $RESULT_FILE
return 1
fi
# 提取关键指标
qps=$(echo "$result" | grep "Requests per second" | awk '{print $4}')
avg_time=$(echo "$result" | grep "Time per request" | head -1 | awk '{print $4}')
failed=$(echo "$result" | grep "Failed requests" | awk '{print $3}')
p95_time=$(echo "$result" | grep "95%" | awk '{print $2}')
connect_errors=$(echo "$result" | grep "Connect:" | awk '{print $2}')
# 检查是否有严重错误
if [ ! -z "$connect_errors" ] && [ "$connect_errors" -gt 0 ]; then
echo "⚠️ 连接错误: $connect_errors" | tee -a $RESULT_FILE
fi
if [ ! -z "$failed" ] && [ "$failed" -gt 0 ]; then
failure_rate=$(echo "scale=2; $failed * 100 / $requests" | bc 2>/dev/null || echo "N/A")
echo "⚠️ 失败请求: $failed (${failure_rate}%)" | tee -a $RESULT_FILE
fi
# 记录结果
if [ ! -z "$qps" ] && [ ! -z "$avg_time" ]; then
echo " ✅ QPS: $qps" | tee -a $RESULT_FILE
echo " ✅ 平均响应时间: ${avg_time}ms" | tee -a $RESULT_FILE
echo " 📊 95%响应时间: ${p95_time}ms" | tee -a $RESULT_FILE
echo " 📊 失败请求: $failed" | tee -a $RESULT_FILE
else
echo "❌ 无法解析测试结果" | tee -a $RESULT_FILE
echo "完整输出:" | tee -a $RESULT_FILE
echo "$result" | tee -a $RESULT_FILE
return 1
fi
echo " ---" | tee -a $RESULT_FILE
return 0
}
# 阶段1:低并发基准测试
echo "📊 阶段1:低并发基准测试 (总请求数: $TOTAL_REQUESTS)" | tee -a $RESULT_FILE
run_test 10 $TOTAL_REQUESTS "低并发基准" || echo "⚠️ 低并发测试失败"
sleep 3
run_test 20 $TOTAL_REQUESTS "低并发基准" || echo "⚠️ 低并发测试失败"
sleep 3
run_test 50 $TOTAL_REQUESTS "低并发基准" || echo "⚠️ 低并发测试失败"
sleep 5
# 阶段2:中等并发测试(响应式开始显现优势)
echo "📊 阶段2:中等并发测试 (总请求数: $TOTAL_REQUESTS)" | tee -a $RESULT_FILE
# run_test 100 $TOTAL_REQUESTS "中等并发" || echo "⚠️ 中等并发测试失败"
# sleep 5
run_test 200 $TOTAL_REQUESTS "中等并发" || echo "⚠️ 中等并发测试失败"
sleep 5
run_test 300 $TOTAL_REQUESTS "中等并发" || echo "⚠️ 中等并发测试失败"
sleep 8
# 阶段3:高并发测试(响应式优势明显)
echo "📊 阶段3:高并发测试 (总请求数: $TOTAL_REQUESTS)" | tee -a $RESULT_FILE
run_test 500 $TOTAL_REQUESTS "高并发" || echo "⚠️ 高并发测试失败,可能接近服务极限"
sleep 10
run_test 800 $TOTAL_REQUESTS "高并发" || echo "⚠️ 高并发测试失败,可能接近服务极限"
sleep 10
run_test 1000 $TOTAL_REQUESTS "高并发" || echo "⚠️ 高并发测试失败,可能接近服务极限"
sleep 10
# 阶段4:极高并发测试(找到性能瓶颈)
echo "📊 阶段4:极高并发测试 (总请求数: $TOTAL_REQUESTS)" | tee -a $RESULT_FILE
echo "⚠️ 开始极限测试,请监控服务状态..." | tee -a $RESULT_FILE
run_test 2000 $TOTAL_REQUESTS "极高并发" || echo "❌ 极高并发测试失败,已达到服务极限"
sleep 15
run_test 3000 $TOTAL_REQUESTS "极高并发" || echo "❌ 极高并发测试失败,已达到服务极限"
sleep 15
run_test 4000 $TOTAL_REQUESTS "极高并发" || echo "❌ 极高并发测试失败,已达到服务极限"
echo "测试完成!详细结果请查看: $RESULT_FILE" | tee -a $RESULT_FILE
echo "测试结束时间: $(date)" | tee -a $RESULT_FILE
并发数QPS(同步)平均响应时间(同步)95%响应时间(同步)QPS(响应式)平均响应时间(响应式)95%响应时间(响应式)
1071.2140.445485176.9356.519157
2060.18332.329986243.5282.128246
50失败失败失败252.08198.353508
10072.431380.564332240.29416.168814
20074.32691.8577655309.46646.2851112
30071.214212.88310943133.242251.5371149
50068.267324.49817332455.81096.981268
80074.6110722.65524476598.161337.4272115
100073.7313563.10931676682.451465.3091649
2000186.2410739.04229671121.0516522.114578
3000失败(服务宕机)失败(服务宕机)失败(服务宕机)1612.741860.1854137
4000失败(服务宕机)失败(服务宕机)失败(服务宕机)1855.992155.1832489
  • 阻塞式代码在3000,4000并发都失败时网关服务重启了,和之前线上表现一致。
  • 如果要体现响应式Redis的优势必须上大并发,如果并发过低,同步和异步的差异不明显甚至会出现异步性能低于同步的情况(这个是我遇到的实际情况,花了点时间查了资料)

图表说明:

  • X轴表示并发数
  • 实线表示同步实现的性能数据
  • 虚线表示响应式实现的性能数据
  • 值为0的测试点表示测试失败
  • QPS越高表示性能越好,响应时间越低表示性能越好

QPS (每秒查询数) 平均响应时间 (毫秒) 95%响应时间 (毫秒)

响应式的优势在高并发、I/O密集型场景下才明显。如果并发不高,同步模式的线程池(通常几十到几百个线程)完全够用