上一周我在搭统一API网关的时候,又跟Sentinel杠上了。技术栈是Spring Cloud Gateway,流量防护准备交给Sentinel,本来想着“官方适配器都出了,接进去不就完事了吗”,结果控制台确实能看到服务上线,点进“API管理”页面却一片空白,“请求链路”里同样啥也没有。更离谱的是,同一套配置放在普通Spring MVC服务上就一切正常,说明问题就出在网关这一层。折腾了两天,翻了不少源码和文档,总算把根因找齐了。这篇文章就把我踩过的坑、排查的思路、以及最终能稳定复现的控制台链路数据是怎么搞出来的,完整记录下来,给正在跟网关接入较劲的朋友一个参考。
1. 先把对接机制看明白,再排查才不会瞎转
1.1 网关模式下Sentinel的埋点原理
Sentinel的埋点思路其实很统一:每一次请求进到服务,都会去创建一个资源入口,也就是SphU.entry(resourceName),然后交给后面的Slot链表去走限流、熔断、黑白名单这些逻辑。问题在于,普通Spring MVC服务里的Controller方法天然可以被拦截器切到,而Spring Cloud Gateway是基于WebFlux的响应式网关,请求根本不会经过Spring MVC那套DispatcherServlet。所以Sentinel针对网关场景单独提供了一个适配模块,通常是sentinel-spring-cloud-gateway-adapter。
这个适配器的核心是一个名为SentinelGatewayFilter的过滤器,它会被注册进Gateway的过滤器链。请求进到这条链路时,它会从当前路由中取出路由ID,把它作为Sentinel的资源名,然后走一次完整的埋点流程。这也就解释了为什么网关模式下,你看到控制台里的资源名不是“某个Controller的方法”,而是你在application.yml里配的路由ID,比如order-service、user-service之类的。
除此之外,适配器还会监听Gateway的路由刷新事件。一旦路由发生变更,它会尝试把最新的路由信息同步给Sentinel控制台。这个机制很关键,因为后面要说的“API管理不显示”,根源大多出在这一层数据交互上。
1.2 控制台的“API管理”和“请求链路”到底展示什么
很多人在这个点上有误解,我也是踩了坑才看明白。Sentinel控制台里的“API管理”页面,展示的并不直接是Spring Cloud Gateway的路由表,而是Sentinel自定义的API分组。API分组由一组匹配规则组成,比如/order/**这个路径可以归到order_service_api这个分组里。分组可以通过代码注册,也可以在控制台里创建,本质上是为了把多个路径或路由聚合到同一个资源上做统一流控。
而“请求链路”页面展示的是每个资源被访问时的调用情况,包括QPS、异常数、耗时分布等等。在网关场景下,入口资源就是路由ID,或者你自定义的API分组ID,再往下走的子链路取决于下游服务有没有接入Sentinel。页面上能看到的资源要满足一个条件:这个资源名确实被当作entry创建过,并且有流量通过。
所以排查的第一步很清晰:如果控制台“请求链路”里什么都没有,先确认请求到底有没有真的经过SentinelGatewayFilter,有没有创建出对应的资源入口。如果“API管理”是空的,先确认API分组有没有注册上去,而不是去看路由表。
2. 依赖引入与版本匹配:八成问题出在这一步
2.1 正确的依赖组合与版本对齐
我这次项目的骨架是用脚手架生成的Spring Boot 2.6.x,配合Spring Cloud 2021.0.x,流量防护层一开始用的是Spring Cloud Alibaba里的Sentinel依赖。这里最大的问题就是版本“乱搭”。
先看我最后定下来的稳定组合:
- Spring Boot 2.6.3
- Spring Cloud 2021.0.1
- Spring Cloud Alibaba 2021.0.1.0
- Sentinel 1.8.0(core、gateway adapter、transport、dashboard全部对齐)
依赖上,我建议直接采用“Spring Cloud Alibaba的Sentinel starter + 官方网关适配器”的组合:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> <version>1.8.0</version> </dependency>这里有个隐藏问题:spring-cloud-starter-alibaba-sentinel本身会带一个sentinel-core版本,这个版本受Spring Cloud Alibaba的BOM控制。如果你单独引入的适配器版本和这个core版本对不上,轻则控制台数据上报异常,重则直接NoClassDefFoundError。解决方式是在dependencyManagement里显式声明统一的Sentinel版本,或者像我一样直接指定适配器版本,并且确保它和starter带进来的core一致。
我的习惯是把版本号写在properties里统一管理:
<properties> <sentinel.version>1.8.0</sentinel.version> </properties>然后在所有Sentinel相关依赖里都用${sentinel.version},从源头杜绝版本打架。
2.2 两种接入方式不要混着玩
网关接入Sentinel有两种常见姿势。一种是纯手动接入,只引入sentinel-core和sentinel-spring-cloud-gateway-adapter,然后自己注册过滤器。另一种就是先引Spring Cloud Alibaba的starter,再叠加网关适配器。我强烈建议用第二种,因为starter会帮你把transport层、控制台通信、配置绑定这些杂活都处理好,手动接入很容易漏掉某个SPI文件或者传输层依赖,后面排查起来很痛苦。
但第二种方式也有个隐患:有些教程会额外让你引入sentinel-transport-simple-http,而starter其实已经带了这个模块。如果你再引一遍,只是重复依赖倒还好,怕的是你引的transport版本和core不一致。所以引入依赖前先执行一次mvn dependency:tree,把Sentinel相关的依赖树打出来,重点看版本号是不是全对齐了。我这次踩的版本坑,就是一开始适配器用了1.8.6,而starter带的是1.8.0,控制台交互时传输层解析异常,导致API管理数据一直推不上去。
3. 控制台不显示API管理的排查清单
3.1 先确认服务心跳是否正常
控制台“API管理”页面是挂在某个具体应用下面的,如果这个应用压根没有在控制台里上线,后面一切都是白搭。所以排查的第一步永远是看控制台“机器列表”里有没有你的网关服务。
Sentinel客户端通过心跳机制向控制台注册实例。核心配置是:
spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8080 port: 8719 eager: truedashboard是控制台地址,port是客户端本地用于接收控制台指令的端口,eager表示应用启动时立即连接到控制台,而不是等第一条请求过来再连。
我当时就是忘记配eager,启动后光看控制台发现没服务,还以为是依赖没生效,其实只是懒加载导致心跳延迟了。另外,8719这个端口如果被占用,客户端会报Port already in use,然后传输层直接罢工。启动时如果不确定端口是否空闲,可以先手动改成8720之类的其他值试试。
提示:如果网关部署在多台机器上,还要注意控制台所在机器与网关机器之间的网络连通性,尤其是防火墙和安全组策略。这个点看着低级,但在生产环境里最容易让人绕远路。
3.2 API分组不是路由,别等它自己冒出来
这是整个排查过程中最核心的认知。我一开始以为网关路由配好后,控制台“API管理”里就会自动出现所有路由项。事实是:控制台展示的是API分组,而API分组需要显式加载。
在网关适配器里,加载API分组的入口是GatewayApiDefinitionManager.loadApiDefinitions。如果你只引入适配器、只注册了SentinelGatewayFilter,但没有向GatewayApiDefinitionManager注册任何API分组,那么控制台的“API管理”页面就是空的,这不代表接入失败,只是确实没有任何自定义API分组。
正确的做法是在网关应用启动时加载API分组定义:
@Configuration public class GatewaySentinelConfig { @PostConstruct public void initApiDefinitions() { Set<ApiDefinition> definitions = new HashSet<>(); ApiDefinition orderApi = new ApiDefinition("order_service_api") .setPredicateItems(Arrays.asList( new ApiPathPredicateItem().setPattern("/order/**") )); ApiDefinition userApi = new ApiDefinition("user_service_api") .setPredicateItems(Arrays.asList( new ApiPathPredicateItem().setPattern("/user/**") )); definitions.add(orderApi); definitions.add(userApi); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } }这里的ApiDefinition跟Spring Cloud Gateway里的Route没有直接关系,它是Sentinel自己的一套路径匹配规则。加载之后,控制台的“API管理”页面就会看到order_service_api和user_service_api两个分组。
3.3 应用类型标记不能少
还有一个容易被忽略的开关:Sentinel客户端上报应用类型时,默认可能不是“API网关”类型。控制台会根据应用类型决定展示哪些菜单和功能。如果上报的应用类型不对,即使API分组加载了,上面说的某些网关专属页面也可能不显示。
解决方式是给网关进程加上一个JVM参数:
-Dcsp.sentinel.app.type=1这个参数的作用是把当前应用标记为API Gateway类型。普通业务服务通常不需要设置,设置为默认值就行。网关服务加上这个参数后,再配合API分组加载,控制台才能完整识别出网关身份,API管理相关数据才会正常展示。
我之前把这个问题忽略了很久,后来是偶然对比了两台服务的控制台展示差异,才发现网关那台的“API管理”菜单状态明显不一样。这个参数不是什么热门资料里会讲的点,但实际排查中很关键。
3.4 适配器SPI配置有没有被打包漏掉
sentinel-spring-cloud-gateway-adapter里有一个很微妙的设计:它依赖Java SPI机制来启动一些初始化和回调逻辑。具体来说,jar包里META-INF/services目录下的配置文件中,注册了Sentinel的InitFunc实现类,适配器靠这个来干“加载路由同步”“注册回调”之类的活。
如果这个SPI文件在打包时被某些插件过滤掉,或者因为依赖冲突导致jar包被替换成旧版本,那么适配器虽然类能加载,但内部的初始化逻辑完全没跑。表现就是:控制台服务在线,你的业务接口也能正常访问,但API管理和请求链路数据统统不出现。
排查方法也不难,进到最终部署的网关服务中,执行:
jar tf sentinel-spring-cloud-gateway-adapter-1.8.0.jar | grep META-INF/services正常情况下应该能看到com.alibaba.csp.sentinel.init.InitFunc这个文件名,再打开文件,里面至少有一个适配器相关的实现类全类名。如果这个文件不存在,说明你的依赖打包有问题,需要检查maven-shade-plugin或者spring-boot-maven-plugin的过滤配置。
4. 请求链路空白的深层原因与修复
4.1 链路数据流经的完整路径
“请求链路”页面展示的数据,比“API管理”更取决于实际流量。一条请求从网关进来到展示在控制台,要经过这么几步:
第一步,请求被Spring Cloud Gateway的路由处理器匹配,当前路由ID被放在exchange属性中;第二步,过滤器链开始执行,SentinelGatewayFilter在合适的时机取出路由ID,调用Sphu.entry创建资源入口;第三步,每次entry被创建时,Sentinel的实时监控模块会记录这个资源的指标数据;第四步,transport模块按固定周期把指标数据上报给控制台;第五步,控制台“请求链路”页实时查询这个指标数据并渲染出来。
这里面任何一步断了,页面都会表现为空白。所以排查时要一层一层验证,而不是只盯着控制台刷新。
4.2 过滤器顺序与请求被拦截的问题
SentinelGatewayFilter的注册顺序会影响它能不能拿到路由,以及能不能覆盖到所有请求。Spring Cloud Gateway内部有它自己的过滤器排序规则。路由匹配结果实际上在进入过滤器链之前就已经放到exchange属性里了,所以SentinelGatewayFilter的order就算比较靠前,也照样能拿到routeId。真正需要担心的是:如果链路里存在一个order比SentinelGatewayFilter更小的全局过滤器,并且直接返回了响应,那这条请求根本走不到后面的埋点环节。
我遇到过一种情况:网关里有个自定义鉴权过滤器,order设成了-200,比SentinelGatewayFilter的默认order小,鉴权失败时直接写response返回。结果就是鉴权失败的请求压根没进Sentinel统计,控制台里看到的QPS和实际调用的QPS对不上。
解决思路是给SentinelGatewayFilter一个明确的order,让它尽可能早地拿到流量,但又要保证路由信息可用:
@Bean public SentinelGatewayFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(-1); }注册时还可以通过@Order注解或Ordered接口来调整。总之,先确认这个过滤器确实在你自定义的过滤器之前执行,否则链路统计就会缺胳膊少腿。
4.3 监控时间窗口与观察方法
还有一类“假空白”特别迷惑人。Sentinel控制台的“请求链路”和“簇点链路”这类实时监控页面,依赖近一分钟的滑动窗口数据。如果你只是偶尔发几条测试请求,然后在页面上看的时候正好过了窗口期,链路数据就会清空,看起来就像是没采集到。
我的验证方法是开一个循环脚本,持续制造低频请求,同时观察控制台:
while true; do curl http://127.0.0.1:8080/order/detail/1; sleep 1; done保持这种状态后,再切到控制台的“请求链路”页面,选择对应的网关服务,正常情况下能看到路由ID或API分组ID对应的资源,以及实时的QPS曲线。如果持续有流量但链路还是空白,那才真正到了要去查源码的环节。
4.4 资源名对不上的排查
资源名对不上也会造成链路空白的错觉。假设你在API分组里定义了/order/**归到order_service_api,但网关路由ID是order-service-route,那么:
- 如果你在控制台选中
order_service_api这个资源名,你会发现链路里QPS是有的; - 如果你选中
order-service-route,那就要看你请求有没有创建这个资源入口。
网关适配器默认会把路由ID作为资源名,同时也会检查请求路径是否命中API分组。命中了API分组的请求,会额外以API分组ID作为资源名再埋一层。所以在控制台看链路时,要清楚自己看的是哪个资源名,以及当前请求实际匹配的是哪个分组/路由。
我在验证时最常用的方式是先看“簇点链路”,这里面会列出所有被埋点的资源名和对应的QPS。如果“簇点链路”里有数据,但“请求链路”页没有,那多半不是采集问题,而是你选错了资源维度,或者控制台版本对资源类型的展示有差异。
5. 实操记录:从零到控制台出现网关链路
5.1 依赖与启动参数(可直接抄的配置)
这里给出一份完整的网关接入配置。首先是pom.xml里需要重点关注的依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> <version>1.8.0</version> </dependency>如果你没有通过Spring Cloud Alibaba引入,那就手动补齐core和transport:
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>1.8.0</version> </dependency>然后是application.yml里的配置:
server: port: 8080 spring: application: name: gateway-server cloud: sentinel: transport: dashboard: 127.0.0.1:8080 port: 8719 eager: true gateway: routes: - id: order-service-route uri: http://127.0.0.1:8081 predicates: - Path=/order/** - id: user-service-route uri: http://127.0.0.1:8082 predicates: - Path=/user/**启动网关时,JVM参数加上:
-Dcsp.sentinel.app.type=1控制台侧,我直接用官方打好的sentinel-dashboardjar包启动:
java -Dserver.port=8080 -jar sentinel-dashboard-1.8.0.jar默认用户名密码是sentinel/sentinel。控制台和网关可以不在同一台机器,但网络必须通。
5.2 网关配置类与API分组初始化
写一个配置类来注册过滤器和加载API分组:
@Configuration public class GatewaySentinelConfig { @Bean public SentinelGatewayFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(-1); } @PostConstruct public void doInit() { initApiDefinitions(); initGatewayRules(); } private void initApiDefinitions() { Set<ApiDefinition> definitions = new HashSet<>(); ApiDefinition orderApi = new ApiDefinition("order_service_api") .setPredicateItems(Collections.singletonList( new ApiPathPredicateItem().setPattern("/order/**") )); ApiDefinition userApi = new ApiDefinition("user_service_api") .setPredicateItems(Collections.singletonList( new ApiPathPredicateItem().setPattern("/user/**") )); definitions.add(orderApi); definitions.add(userApi); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } private void initGatewayRules() { Set<GatewayFlowRule> rules = new HashSet<>(); GatewayFlowRule orderRule = new GatewayFlowRule("order_service_api") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) .setGrade(RuleConstant.FLOW_GRADE_QPS) .setCount(10); GatewayFlowRule routeRule = new GatewayFlowRule("order-service-route") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID) .setGrade(RuleConstant.FLOW_GRADE_QPS) .setCount(20); rules.add(orderRule); rules.add(routeRule); GatewayRuleManager.loadRules(rules); } }这里有两个资源维度:一个是API分组order_service_api,一个是路由IDorder-service-route。两者都可以配置网关流控规则。不过要特别注意,规则最终是基于资源名的,如果你的网关流控规则里写的资源名和请求实际命中的资源名对不上,限流就永远不会触发。
5.3 验证顺序与判定标准
启动顺序建议是:先启动Sentinel控制台,再启动网关,最后启动下游服务。全部起来后按这个顺序验证:
第一步,等大概10到20秒,看控制台“机器列表”里有没有gateway-server这台机器。如果一直没出现,检查eager配置、transport端口、网络连接。
第二步,用上面的循环脚本持续请求网关的/order/detail/1,同时打开控制台“簇点链路”,确认QPS有数字在跳。如果没有,回头检查SentinelGatewayFilter是否注册成功,以及请求是否真的走到了网关路由。
第三步,打开“API管理”页面,确认order_service_api和user_service_api两个分组已经出现。如果分组存在但页面空白,刷新一次再等10秒。如果还是没有,检查-Dcsp.sentinel.app.type=1有没有配上。
第四步,打开“请求链路”页面,选择gateway-server服务,刷新后能看到order-service-route或者order_service_api的资源和QPS曲线。
到这一步,整个对接链路就算通了。
6. 高频问题速查表与独家避坑建议
实际对接过程中,我还整理了一张排查速查表,基本覆盖了看到的所有异常现象,直接照着查就行。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 控制台看不到网关服务 | transport配置缺失、端口占用、网络不通 | 检查spring.cloud.sentinel.transport.dashboard和port,确认eager=true,看启动日志心跳上报 |
| 服务在线但“API管理”空白 | 没有加载API分组定义 | 调用GatewayApiDefinitionManager.loadApiDefinitions注册API分组 |
| “API管理”页面无网关菜单 | 应用类型上报不对 | 启动参数加-Dcsp.sentinel.app.type=1 |
| “请求链路”空白但“簇点链路”有数据 | 选择了错误的资源名或时间窗口过期 | 持续请求,确认当前资源名是路由ID还是API分组ID |
| 网关流控规则不生效 | 资源名匹配不上 | 检查规则里的资源名和GatewayRuleManager加载的名称是否一致 |
| 全部页面正常但限流异常 | 规则模式配错 | RESOURCE_MODE_ROUTE_ID只能用于路由ID,RESOURCE_MODE_CUSTOM_API_NAME用于API分组 |
启动报NoClassDefFoundError | 适配器与core版本冲突 | 统一Sentinel版本号,执行mvn dependency:tree查看依赖树 |
| 打包后功能消失 | SPI文件被过滤 | 检查META-INF/services/com.alibaba.csp.sentinel.init.InitFunc文件是否存在 |
最后再分享几个不起眼但特别实用的习惯:
一是排查之前先看网关启动日志里有没有关于Sentinel传输层的字样。正常启动时会有类似“Sentinel transport”的日志,如果日志里压根没提,那多半是transport模块没生效。二是像这种框架之间对接的问题,不要只在控制台界面上找答案,先到“簇点链路”这个页面确认资源维度,再判断是不是展示层的问题。三是我个人操作中的深刻体会:网关模式的核心是“API分组”这个抽象概念,它连接了路由、路径匹配和流控规则三层逻辑,把这个概念吃透,很多奇怪的现象都能解释得通。
这次排错之后,我最大的感受是:对接开源组件时,版本统一、SPI完整性、应用类型这三个看起来无关紧要的细节,往往就是问题真正的分水岭。希望这篇记录能让你少走几个弯路。