在 Spring Cloud Gateway 中集成 Sentinel 实现限流
在 Spring Cloud Gateway 中集成 Sentinel 实现限流
1、介绍
Sentinel 是由阿里开源的一个流量控制组件,诞生于 2012 年,它的使用可分为两部分:
- 核心库(
Java客户端):不依赖任何框架/库,能够运行于Java 8及以上的版本的运行时环境,同时对Dubbo / Spring Cloud等框架也有较好的支持 - 控制台(
dashboard):主要负责管理推送规则、监控、管理机器信息等。
笔者注:有
issue称,控制台其实只用来演示Sentinel的基本能力和工作流程,并没有依赖生产环境中所必须的组件;其数据只是简单存储到了内存中,重启控制台服务后发现数据已重置,还需重新配置。
2、使用
2.1、添加依赖
在网关项目中添加以下依赖(省略 Gateway、Nacos 服务发现等配置):
<!-- Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel Gateway 适配模块 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- Sentinel 数据源(Nacos) -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>2.2、配置 Sentinel
spring:
cloud:
gateway:
routes:
- id: app-service
uri: lb://app-service
predicates:
- Path=/app/**
sentinel:
# 可选,连接控制台时使用
# transport:
# dashboard: localhost:8080
datasource:
# 数据源名称,可自定义
ds1:
# 使用nacos数据源
nacos:
# 配置文件ID
data-id: gateway-sentinel.json
server-addr: @nacos.server.address@
namespace: @nacos.namespace@
group-id: @nacos.group@
username: @nacos.username@
password: @nacos.password@
# 必须配置为gw-flow,配置为flow不生效
rule-type: gw-flow2.3、定义限流规则
在 Nacos 中的对应命名空间中,新建上面配置指定的 data-id,如:gateway-sentinel.json,内容如下:
[
{
// 资源名称(对应路由ID)
"resource": "app-service",
// 资源模式(0:路由ID,1:代码中自定义API分组)
"resourceMode": 0,
// 限流阈值类型(1: QPS)
"grade": 1,
// 阈值(每秒)
"count": 5,
// 流控效果(0: 直接拒绝)
"controlBehavior": 0,
// 统计时间窗口,单位是秒,默认是 1 秒
"intervalSec": 1,
// 应对突发请求时额外允许的请求数目
"burst": 10,
// 匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效
"maxQueueingTimeoutMs": 0,
// 参数限流配置
"paramItem": {
// 限流策略(0:按IP)
"parseStrategy": 0
}
}
]2.4、自定义限流响应(推荐)
@Configuration
public class RequestRateLimiterConfig {
@PostConstruct
public void initBlockHandler() {
GatewayCallbackManager.setBlockHandler((exchange, e) -> {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
result.put("msg", "操作过于频繁,请稍后再试");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
});
}
}2.5、自定义限流时获取 IP 的实现逻辑(推荐)
2.5.1、问题浮现
在生产环境实践过程中,发现一个奇怪的问题:在测试环境(内网 IP)测试时,无论是手动用浏览器请求还是使用 Jmeter、Apifox 等工具进行压测时,限流设置都能很好的起作用。而一旦发布到预生产及生产环境(均为域名)时,手动用浏览器频繁请求会触发限流,而用工具压测却怎么也不会触发,这让笔者一头雾水。
2.5.2、摸索排查
不禁思考,目前我们设置的是根据客户端 IP 进行限流,而如何获取客户端 IP 我们并没有告诉 Sentinel,这说明它内部肯定有自己的实现逻辑。那它限流过程应该也会有日志吧?好,翻翻官方文档,幸好有说明(见引用[4])。
接下来,观察日志打印,发现它获取到的 IP 竟然是局域网 IP,而且每次请求 IP 地址都会变:
2025-08-15 12:00:40|1|xxx-route-id,ParamFlowException,100.127.129.132,,|6,0
2025-08-15 12:00:41|1|xxx-route-id,ParamFlowException,100.127.129.132,,|4,0
2025-08-15 12:01:14|1|xxx-route-id,ParamFlowException,100.127.128.115,,|3,0
2025-08-15 12:01:15|1|xxx-route-id,ParamFlowException,100.127.128.115,,|4,0忽然想起来线上的域名用到了阿里的 SLB(ALB) 负载均衡,它会将请求分发到不同的客户端服务器,然后再进行请求服务端服务器,笔者 ping 了一下域名,发现确实每次 IP 都会变化:
C:\Users\ZH>ping api.hmkf688.com
正在 Ping alb-uutajopqio7z2gfafm.cn-beijing.alb.aliyuncsslb.com [112.126.71.178] 具有 32 字节的数据:
来自 112.126.71.178 的回复: 字节=32 时间=8ms TTL=92
来自 112.126.71.178 的回复: 字节=32 时间=8ms TTL=92
C:\Users\ZH>ping api.hmkf688.com
正在 Ping alb-uutajopqio7z2gfafm.cn-beijing.alb.aliyuncsslb.com [112.126.73.226] 具有 32 字节的数据:
来自 112.126.73.226 的回复: 字节=32 时间=8ms TTL=90
来自 112.126.73.226 的回复: 字节=32 时间=10ms TTL=90又陷入思考,那大概率是域名解析的问题了,会不会是阿里的负载均衡导致客户端的 IP 地址发生了变更呢(可以排除反向代理,因为我们没用 nginx,域名通过 ALB 解析后直接指向到了网关服务及端口)?但同时在网关的请求日志中发现,从请求头(X-Forwarded-For)获取到的 IP 是正确的,那答案就很明显了,一定是 Sentinel 获取 IP 的方式存在问题,那它是怎么获取的 IP 呢?继续找资料......
发现了两个 ISSUE,详见引用[5]、[6],有前辈也曾遇到了这个问题,然后提了个 PR,查找官方文档,却没有发现对自定义获取 IP 的相关描述,只能看源码了,默认的获取客户端 IP 的逻辑在 ServerWebExchangeItemParser 类中的如下方法:
@Override
public String getRemoteAddress(ServerWebExchange exchange) {
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
if (remoteAddress == null) {
return null;
}
// 就是因为这个默认实现,导致一直获取到的是客户端局域网IP
return remoteAddress.getAddress().getHostAddress();
}在这里,笔者还踩了一个坑,因为之前有一个自定义限流响应的配置,比着葫芦画瓢发现也有一个自定义请求解析的设置,本想沾沾自喜,结果还是摔了一大跤:
@PostConstruct
public void initBlockHandler() {
GatewayCallbackManager.setBlockHandler((exchange, e) -> {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
result.put("msg", "操作过于频繁,请稍后再试");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
});
// 以下操作只会在原Sentinel框架基础上拼接,限流时仍会调用框架默认的获取方式(会获取到内网地址),因此无法满足彻底自定义获取IP的需求
// GatewayCallbackManager.setRequestOriginParser(WebFluxUtils::getClientIp);
}2.5.3、最终方案
自定义一个 SentinelGatewayFilter Bean,并添加获取 IP 的实现逻辑:
@Bean
public SentinelGatewayFilter sentinelGatewayFilter(SentinelGatewayProperties gatewayProperties) {
ConfigurableRequestItemParser<ServerWebExchange> configurableRequestItemParser = new ConfigurableRequestItemParser<>(new ServerWebExchangeItemParser());
configurableRequestItemParser.addRemoteAddressExtractor(WebFluxUtils::getClientIp);
return new SentinelGatewayFilter(gatewayProperties.getOrder(), configurableRequestItemParser);
}工具类:
public class WebFluxUtils {
private static final Logger logger = LoggerFactory.getLogger(WebFluxUtils.class);
private static final String[] IP_HEADERS = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, ApiResponse<?> apiResponse) {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(apiResponse).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
public static String getClientIp(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String finalIp = "";
logger.debug("------------------------start");
request.getHeaders().forEach((header, values) -> logger.debug("{}|{}", header, ArrayUtil.toString(values)));
logger.debug("------------------------end");
for (String headerName : IP_HEADERS) {
Optional<String> optional = Optional.ofNullable(request.getHeaders().getFirst(headerName))
.map(ips -> ips.split(",")[0].trim())
.filter(ip -> !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip));
if (optional.isPresent()) {
finalIp = optional.get();
break;
}
}
if (finalIp.isEmpty()) {
finalIp = Optional.ofNullable(request.getRemoteAddress()).map(addr -> addr.getAddress().getHostAddress()).orElse("");
}
logger.debug("------------------------finalIp: {}\n", finalIp);
return finalIp;
}
}2.6、题外话
Sentinel 针对网关自动配置的类是 SentinelSCGAutoConfiguration,查看类的源码,发现 SentinelGatewayProperties 配置中还有一个 FallbackProperties 配置,这貌似是用来配置当触发限流时的请求响应策略,经过验证,确实是:
spring:
cloud:
sentinel:
scg:
fallback:
# 限流时的响应策略:redirect-重定向,response-响应
mode: redirect
# 重定向时的地址
# redirect: https://www.baidu.com
# 响应HTTP状态码
response-status: 200
# 响应Body
response-body: '{"code":200, "msg":"操作过于频繁"}'
# 响应Content-Type
content-type: application/json以上就是本篇的全部内容,而且这还只是单机的限流,如果有多个网关实例,需要配置集群模式,而集群模式更为复杂,写到这里,笔者不得不吐槽,这 Sentinel!老夫不用也罢!

