尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sentinel网关流控完整链路解析:从路由匹配到规则下发避坑指南

Sentinel网关流控完整链路解析:从路由匹配到规则下发避坑指南 网关限流这四个字拆开看很多人第一反应是“给某条路由配个 QPS 上限”。真上手做的时候才发现坑全藏在“网关”和“实现原理”这两个词里——同样是限流走 Sentinel 普通接口限流的 SlotChain和走网关适配的 SlotChain底层是完全独立的两条链路同样是“配置限流规则”推给 FlowRuleManager 和推给 GatewayRuleManagerJSON 字段都不一样一开始没搞清楚后面全是泪。这篇文章把 Sentinel 网关流控从请求进门到规则命中的完整链路拆开讲一遍重点讲清楚五件事网关流控到底在控什么、网关侧的 SlotChain 怎么构建、路由/API 分组/参数维度分别怎么命中、滑动窗口和热点参数统计底层怎么算数、以及用 Nacos/Redis 集群方式下发规则时真正要注意的字段和 SPI 机制。适合已经在用 Sentinel 做接口限流、正准备把限流上移到网关或者正在为网关流控规则接 Nacos/Redis 集群的人看。1. 网关流控到底在控什么先理清需求和边界1.1 网关限流和普通接口限流的本质差异普通接口限流Sentinel 面对的是一个个“资源”这个资源可以是 Dubbo 接口、HTTP URL、方法签名。调用链是完整的有入口、有父子关系所以普通流控支持“关联”和“链路”两种模式能表达“A 接口被 B 接口调用时限制多少 QPS”这种语义。网关流控面对的是完全不同的场景。网关是所有请求的统一入口在这里你能拿到的维度是路由 ID、Host、Path、Header、Cookie、客户端 IP。这个位置做的事情本质上不是保护某个业务方法而是保护“整个路线的总吞吐”——上游调用方再猛网关这一步先卡住不让流量涌进下游。所以网关流控天然就是“按路由”或“按 API 分组”来限不需要调用树不需要关联关系。这也是为什么 Sentinel 没有直接在网关插件里复用普通 SlotChain而是单独搞了一套精简链路。还有一个容易忽略的区别普通流控的规则维度不只是资源名还有 gradeQPS/并发线程数、strategy直接/关联/链路这一整套模型。而网关流控只关心 QPS 和并发线程数两个维度策略固定为“按目标资源限”。你如果直接把普通 FlowRule 的那套思路搬到网关来配置文件怎么写怎么别扭因为本来模型就是两套。1.2 网关流控的核心能力与适用边界网关流控在 Sentinel 里对应的是sentinel-api-gateway-adapter-common这套适配器Spring Cloud Gateway 和 Zuul 都有对应实现。它支持四种力量按路由 ID 限流routeA 这个路由QPS 上限 100到点直接拒绝。按 API 分组限流把一组 URL pattern 归成一个 API 分组比如/order/**和/pay/**都归到“交易核心”统一给一个总阈值。按参数维度限流同一个路由下按 URL 参数、Path 参数、Header、Cookie、客户端 IP 再拆分子维度实现“同一个接口不同用户不同额度”。自定义请求命中规则API 分组可以组合 URL 匹配策略精确、前缀、正则以及 Host 匹配表达力比单纯路由 ID 强不少。但它也有边界。网关层限流是“粗粒度保护”不是“业务级精确限流”。比如你要“每个用户每秒最多 5 次下单”理论上能用参数维度做但参数基数大的时候内存开销很恐怖后面第三章详细说。我踩过这个坑之后现在的习惯是网关层只做“路由级 少量枚举参数级”的限流真正按用户 ID 的精细化限流放到业务服务内部用 Redis 分布式限流或 Sentinel 热点参数限流来做。这样各层各司其职不硬塞。2. 从请求进门到规则命中网关侧完整执行链路2.1 网关适配器如何拦截请求先说最外层。Spring Cloud Gateway 集成 Sentinel 时你引入sentinel-spring-cloud-gateway-adapter然后注册一个SentinelGatewayFilter和SentinelGatewayBlockExceptionHandler。Filter 被 Spring Cloud Gateway 的过滤器链加载后会在每个请求进入路由转发之前执行。这一步做的事情很明确把当前请求的routeId、请求路径、Header、Cookie、参数取出来然后初始化一个 Sentinel 上下文入口。注意这里的入口资源和普通接口限流的资源不一样它走的是网关专用链路不是默认的com.alibaba.csp.sentinel.adapter.gateway那套普通 Web 资源。拦截器本身不判断限流规则它只负责“拉起一个网关上下文并执行 SlotChain”。真正的闸刀在 SlotChain 里。2.2 网关专用 SlotChain 的构建与规则管理器普通资源的 SlotChain 很长NodeSelectorSlot - ClusterBuilderSlot - LogSlot - StatisticSlot - AuthoritySlot - SystemSlot - FlowSlot - DegradeSlot这一串做统计、鉴权、系统保护、流控、降级。网关链路是刻意裁剪过的。默认的DefaultGatewaySlotChainBuilder构建出来大概是GatewayFlowSlot负责网关流控规则匹配路由/API 分组。GatewayParamFlowSlot负责参数维度限流内部委托给热点参数限流组件。GatewayAuthSlot负责网关自定义认证规则基本很少用。为什么裁剪因为网关是入口请求量巨大链路越短执行开销越小。降级、系统保护、调用链关联这些能力在网关层意义不大——你不在网关层做依赖隔离那是下游服务的事。GatewayFlowSlot的核心是GatewayRuleManager。它内部维护一个MapString, SetGatewayFlowRulekey 是路由 ID 或 API 分组名value 是对应的规则集合。启动时框架会从GatewayRuleManager.getRules()拿全量规则然后逐一比对当前请求的routeId和匹配到的ApiDefinition。这里有个特别容易踩的坑网关规则必须通过GatewayRuleManager.register2Property(property)注册而不是你用惯了的FlowRuleManager.register2Property(property)。两个 Manager 各自维护自己的规则私有变量和监听器注册错了直接不生效。我见过不止一次把流控规则的 Nacos 配置推给 FlowRuleManager网关侧纹丝不动查了半天发现是 Manager 用错了。2.3 规则匹配的两种维度路由与 API 分组GatewayFlowRule里有个字段resourceMode0 表示按路由 ID 匹配1 表示按 API 分组匹配。这个字段决定了规则作用在哪个“目标”上。按路由 ID 匹配最简单请求进来时网关适配器拿到当前请求命中的路由 ID直接和规则里的 resource 字段比较对上了就算命中然后进入参数维度二级判断。按 API 分组匹配则要走一层GatewayApiDefinitionManager。ApiDefinition 由apiName和一组predicateItems组成。predicateItems 支持ApiPathPredicateItem按路径匹配matchStrategy支持精确0、前缀1、正则2。ApiHostPredicateItem按请求 Host 匹配。ApiOtherPredicateItem兜底匹配没被其它项命中的都算进来。实际执行时GatewayFlowSlot会遍历所有 ApiDefinition对当前请求的 path 和 host 做匹配命中的 API 名称作为一个“虚资源”再拿这个名称去和规则里的 resource 比对。这个设计让一个 API 分组可以覆盖多个路由、多个路径非常符合网关层“聚合管控”的诉求。匹配串起来之后的执行顺序从上下文拿到当前请求的 routeId、请求路径、参数。先判断有没有按 routeId 命中的网关规则有就进入参数维度检查。再判断有没有 API 分组命中的规则一个请求可能同时命中路由规则和 API 分组规则两条都要检查。每条规则内部再做参数维度检查全部通过才放行任意一条不通过就抛 BlockException触发你配置的BlockRequestHandler返回自定义错误响应。3. 参数级流控的底层统计原理3.1 滑动窗口计数器是怎么算 QPS 的先回到最基础的统计模型。Sentinel 默认的 QPS 统计不是“每秒一个计数器清零”的固定窗口而是滑动窗口rolling counter。具体结构是一个LeapArray可以理解为一个时间轮数组里面放的是MetricBucket计数器。关键参数是sampleCount也就是一个统计周期内切多少个窗口。默认情况下intervalSec为 1 秒sampleCount为 2也就是说每 500ms 一个窗口。当前 QPS 的值不是看当前窗口单独的计数而是把当前窗口和上一个完整窗口的计数加起来除以窗口总时长换算成“每秒的速率”。举个例子你就明白了。现在时间在 0.8 秒当前窗口 0.5s~1.0s 的计数是 30上一个窗口 0.0s~0.5s 的计数是 70那么当前估算 QPS (30 70) / 1.0 100。所以 Sentinel 的“当前 QPS”其实是最近若干个窗口的平均速率不是瞬时的精确值。这样做的好处是平滑不会出现固定窗口那种“前 0.9 秒打满、最后 0.1 秒全部拒绝”的边界毛刺。这里有一个调优维度要留意sampleCount越大窗口越细统计越接近瞬时值对突发流量的识别越敏感但数组占用的内存也越大。默认 2 个窗口对绝大多数网关场景够用不建议无脑调到 4 或 8。3.2 热点参数限流的独立统计体系网关流控最有价值的地方是同一路由下按参数做二级区分。比如/order/query这个路由整体 QPS 上限 500但渠道 A 最多只能 100。这个能力不是路由规则自己算的而是GatewayFlowSlot把paramItem转换成一条热点参数限流检查委托给GatewayParamFlowSlot最终落到参数维度的统计器上。GatewayParamFlowItem支持的解析策略我列一下parseStrategy含义fieldName 用法0URL 参数fieldName 写参数名如 userId1Path 参数fieldName 写路径占位符名称如 orderId2HeaderfieldName 写 Header 名3CookiefieldName 写 Cookie 名4客户端 IPfieldName 不填热点参数统计和普通 QPS 统计最大的区别是普通统计按资源维度“一个资源一份计量表”热点参数统计按“参数索引 参数值”维度和“每一个不同参数值一份独立计量表”。换句话说如果你对userId做热点限流每个出现的 userId 都会在内存里建立自己的滑动窗口数组。这个设计让“同一个用户重复超限”能被精确定位但也埋了一个雷参数值基数一大内存就像漏水一样涨。我实际测过一个热点参数每秒 200 QPS、不同的 userId 持续进入 10 分钟相关的时间窗口和统计对象就能吃掉几百 MB 堆内存。所以生产环境里网关层的参数限流不要用用户 ID 这种高基数维度优先选择渠道、版本号、客户端类型这种有限枚举值。3.3 内存与精度如何权衡既然讲到内存就展开说说权衡思路。网关层限流的统计结构每时每刻都在写如果每个路由都配上多个参数维度又开了较细的窗口内存和 GC 压力会一起上来。我现在的落地标准大概是路由数量少于 50 条的网关开默认 sampleCount2完全没问题。参数限流的参数值峰值基数控制在 1 万以内能枚举就枚举。网关进程堆内存给到 1G 以上因为网关层除了 Sentinel还有路由定义、过滤器链、Netty 缓冲等一堆资源要养。真要按用户维度限流不在网关做下沉到业务服务或者用 Redis 计数器方案。4. 动态规则下发从 Nacos 到 Redis 集群的数据源接入4.1 规则属性与 DataSource 的 SPI 设计Sentinel 的规则数据源机制核心是三个角色ReadableDataSource读数据、Property持有数据并通知变更、RuleManager注册并监听属性。流程是DataSource从外部存储Nacos、Redis、Apollo、ZooKeeper读取到规则 JSON 字符串通过你提供的 converter 转换成规则对象列表然后调用Property.updateValue()触发所有注册的 listener最终调用GatewayRuleManager.loadRules()或FlowRuleManager.loadRules()热更新内存规则。这个设计的好处是规则可以动态变更不用重启网关。坏处是你要清楚“你注册到了哪个 Manager”错了就静默无效。4.2 Nacos 样例配置与字段全解以网关流控规则接入 Nacos 为例典型的初始化代码String remoteAddress 127.0.0.1:8848; String groupId DEFAULT_GROUP; String dataId gw-flow-rules; ReadableDataSourceString, ListGatewayFlowRule ds new NacosDataSource(remoteAddress, groupId, dataId, source - JSON.parseObject(source, new TypeReferenceListGatewayFlowRule() {})); GatewayRuleManager.register2Property(ds.getProperty());这里最容易翻车的地方是 converter 的泛型。你拿到的 JSON 是GatewayFlowRule的数组不是FlowRule的数组。两个类字段差异很大FlowRule的 JSON 推过来解析会直接报错或者全部为空。Nacos 里的数据格式长这样[ { resource: order-service, resourceMode: 0, grade: 1, count: 200, intervalSec: 1, controlBehavior: 0, burst: 0, paramItem: { parseStrategy: 0, fieldName: channel } }, { resource: transaction-core, resourceMode: 1, grade: 1, count: 500, intervalSec: 1, controlBehavior: 0, paramItem: { parseStrategy: 2, fieldName: X-Client-Version } } ]逐字段解释resource资源名。resourceMode 为 0 时是路由 ID为 1 时是 API 分组名。resourceMode0 路由模式1 API 分组模式。grade1 表示 QPS0 表示并发线程数。count阈值也就是“多少 QPS”或“多少个并发”。intervalSec统计窗口秒数默认 1 秒。如果你配 60意思是在一个 60 秒的窗口内统计总量。controlBehavior流量整形方式。0 快速失败1 Warm Up 预热2 匀速排队。网关场景用的最多的是 0 和 2。burst预热的初始阈值参考一般用 0。paramItem二级参数维度。不配就整个路由/API 一个阈值配了就在一级阈值基础上按参数再切分。如果你配置了 API 分组模式还要额外注册 ApiDefinition。否则规则里的 resource 永远匹配不上。ApiDefinition 不下发控制台配了一条“按 API 分组限流”的规则实际流量过来一条都命中不了这是经典的“规则看似配了但无效”的场景。4.3 自定义 Redis 集群数据源方案有团队不想引入 Nacos想直接用 Redis 下发规则。官方提供了一个RedisDataSource的扩展实现但它针对的是 Redis 单机或哨兵模式对 Redis Cluster 没有直接支持。我项目里用的是一套自己封装的方案Redis Cluster Pub/Sub 广播通知。思路是这样在 Redis Cluster 用 String 或 Hash 结构存规则 JSONkey 固定比如sentinel:gw-flow-rules。规则变更时管理端写入新 JSON同时PUBLISH一个消息到固定 channel比如sentinel:gw-flow-rules:channel。网关侧用 Lettuce 或 Redisson 建立 Pub/Sub 订阅收到通知后重新从 Redis 拉取最新规则 JSON。复用 Sentinel 的 converter 机制解析 JSON 成ListGatewayFlowRule再注册到GatewayRuleManager。核心代码抽象出来就是RedisClusterClient client RedisClusterClient.create(redis://127.0.0.1:7000); StatefulRedisClusterConnectionString, String conn client.connect(); RedisPubSubCommandsString, String pubSub conn.sync(); pubSub.getStatefulConnection().addListener(new BaseRedisPubSubListener() { Override public void message(String channel, String message) { // 收到变更通知后拉取最新规则 String ruleJson redisCommand.get(sentinel:gw-flow-rules); ListGatewayFlowRule rules JSON.parseArray(ruleJson, GatewayFlowRule.class); GatewayRuleManager.loadRules(rules); } }); pubSub.subscribe(sentinel:gw-flow-rules:channel);这个方案注意两点。第一Redis Cluster 的 Pub/Sub 消息是全局广播的所有订阅了 channel 的节点都能收到不存在“只有某个 slot 的节点能收到”的问题可以直接用。第二GatewayRuleManager.loadRules(rules)会替换全量规则所以规则来源一定要单一避免多个管理端同时写入互相覆盖。5. 生产环境落地要点与避坑记录5.1 常见问题排查速查表把我实际遇到过的、以及身边同事踩过的问题整理成一个速查表基本覆盖了“配了不生效”的大部分原因。现象常见原因排查方向网关限流完全没反应规则注册到了 FlowRuleManager而不是 GatewayRuleManager检查GatewayRuleManager.register2Property调用规则下发后内存里有但请求不过resourceMode 和资源名对不上确认是路由 ID 还是 API 分组名API 分组确认是否已注册按参数限流不生效paramItem 没配或者 parseStrategy/fieldName 写错确认参数来源是 URL、Header 还是 Cookie字段名严格区分大小写QPS 阈值明明到了还在放行滑动窗口的估算窗口导致短时毛刺检查配置的 count 是不是远大于实际请求量或调小 sampleCountNacos 解析规则报 JSON 转换异常converter 用成了ListFlowRule改成ListGatewayFlowRule控制台配了网关规则重启丢失没有接入数据源规则只存在于内存把控制台写操作改成“写外部存储 通知客户端刷新”网关流量一上来就频繁 Full GC热点参数值基数过大统计对象太多收紧参数维度使用低基数枚举参数或下钻到业务层限流并发线程数限流看起来不准排查时只看了 QPS没看活跃线程数明确 grade0 的语义是线程并发不是 QPS5.2 规则管理端与控制台的适配问题开源 Sentinel Dashboard 本身支持“网关流控”页签能可视化增删改GatewayFlowRule也能维护 API 分组。但有个坑默认的控制台是“内存推送”模式也就是你点的“新增/修改/删除”只推到当前连上控制台的客户端内存里没有持久化到 Nacos 或 Redis。一旦网关或控制台重启规则全没。生产环境要解决持久化通常有两种做法改控制台源码把写操作改为“先写 Nacos/Redis再发布通知客户端刷新”官方文档里有推送模式改造的说明走NacosDataSource和服务端发布接口。不做控制台改造自己封装一个内部管理接口专门负责写规则 JSON 到 Nacos 的 dataId并配合 Nacos 的监听机制自动驱动网关更新。我自己的项目走的是后者。原因是控制台源码改造虽然一劳永逸但升级 Sentinel 版本时要跟着合并代码维护成本很高。内部管理接口很简单一个 POST 接口收 JSON写 Nacos完事。控制台只保留“查看当前实时规则”的用途。5.3 几个值得留意的细节与最终体验最后分享几个零散但很实用的经验。第一网关流控的intervalSec不要轻易调大。默认 1 秒窗口语义最直观也最符合大家“每秒限制多少”的心智。调成 5 秒、10 秒之后你看到的是一个“过去 N 秒内平均速率”突发流量在窗口内可能被平均掉出现“明明瞬时 1000 QPS 打进来但 10 秒窗口一平均没触发阈值”的情况起不到保护作用。第二controlBehavior2匀速排队在网关侧要慎用。匀速排队的语义是让请求以固定速率通过多余的请求进入排队等待。但网关层往往有上游超时时间请求在 Sentinel 里排队等太久等放行的时候客户端早就超时断开等于白等。真要保护下游网关层用快速失败比排队更合适匀速排队更适合在业务接口层控制那时客户端等待语义更明确。第三Gateway 流控规则和普通流控规则的 JSON 字段不能混用。如果你是从零开始接入建议在项目里把两种规则的 dataId 严格分开比如gw-flow-rules和flow-rules互不干扰。Converter 用错导致规则解析失败后Sentinel 默认是静默降级的不会抛异常打断请求所以问题会藏得很深等流量真正打上来你才会发现“阈值形同虚设”。我在验证规则有没有生效时习惯直接调接口把 QPS 打到阈值的 3 倍观察拒绝响应是否出现这比看控制台的数字可靠得多。整体走下来你会发现Sentinel 网关流控的模型并不复杂但坑全在“链路选择”和“规则注册”这层。记清楚网关有自己的 SlotChain、自己的 RuleManager、自己的 JSON 结构再配合一个能持久化的数据源基本就能稳定落地了。
返回列表