资讯动态

Sentinel网关规则持久化:Nacos配置中心推模式解决重启丢失

发布时间:2026/10/14 19:12:56 来源:尧图企业网站定制
我接手网关流量治理的第一天就被 Sentinel 的 gateway 规则持久化问题上了一课。Dashboard 上随手拖两条流控规则很容易可一旦网关服务重新发布所有配置全部清零限流像是从未存在过。刚开始我以为是部署脚本没执行反复排查后才发现Sentinel 的网关流控规则和 API 分组默认只存在 JVM 内存里这类规则根本没有官方的持久化实现。这篇文章就是这次 gateway 规则持久化改造的完整复盘。我不打算复述官方文档而是把网关规则从每次重启后手动补齐改成配置中心发布、网关实时感知的真实过程讲清楚包括整体思路、核心代码、踩坑顺序和验证方法。如果你已经给 Spring Cloud Gateway 接上了 Sentinel却始终觉得规则不稳定、每次发布都要重来一遍这篇文章应该能帮你省下不少时间。1. 网关限流规则丢失的真实成本不是重启一次次加回去1.1 网关规则不是 FlowRule 的简单包装很多人第一次听说网关规则持久化下意识以为把FlowRule存到配置中心就够了。网关场景里还有第二类规则API 分组ApiDefinition。这两类规则的存储和生效机制是完全独立的两套Manager。普通接口的流控规则走的是FlowRuleManager而网关路由的流控规则走的是GatewayRuleManagerAPI 分组走的是ApiDefinitionManager。它们各自维护自己的内存状态互不干涉。换句话说你就算把FlowRule持久化做得再完整网关流量控制一样不起作用因为网关适配器根本不读FlowRuleManager里的数据。我第一次改造时就踩了这个坑只给FlowRuleManager注册了配置中心数据源流程上完全没报错但网关限流始终不触发。后来翻源码才明白网关适配器是把路由 ID 映射成资源名再交给GatewayRuleManager去判断的。所以持久化改造必须同时覆盖GatewayRuleManager和ApiDefinitionManager一个都不能漏。1.2 默认行为为什么是重启即归零Sentinel 原生提供 Dashboard 控制台你可以在上面配置限流规则Dashboard 会通过每台机器的 HTTP 接口把规则推送到内存里。问题在于这套链路只有推送到内存这一步没有任何机制把规则写回配置中心或本地存储。开发环境里这没什么问题规则随手加随手调重启了再配一次也就几分钟。到了生产环境这个行为就是事故源头某次发布后限流规则全部丢失流量直接打到下游服务压垮了数据库连接池。事后复盘时我们发现规则丢失不是某一个人的操作失误而是整个存储模型决定的——内存里的规则本来就和进程生命周期绑定进程没了规则就没了。生产环境里规则丢失的隐性成本比看起来大得多。一是规则是否生效成了玄学每次发版后都得人工确认二是规则变更不可审计谁在什么时候改了什么阈值完全无据可查三是团队协作困难不同环境共用一套规则配置时根本说不清哪份才是当前有效的。持久化改造解决了丢失的问题也顺带把规则纳入了配置管理流程。1.3 持久化方案选型的总体判断网关规则持久化主要有三条路预置加载、定时拉取、配置中心推模式。我做了一次简单对比方案改造复杂度实时性运维成本适用场景应用启动时预置规则低只启动时生效低规则极少、极少调整的稳定场景文件定时拉取中秒级到分钟级中依赖文件分发系统无配置中心、机器数量少配置中心推模式中高秒级低规则集中管理生产环境、多环境多网关我最终选了配置中心推模式因为网关限流规则本质上是一个需要集中管理、快速生效、可回滚的配置项。预置方案对规则变更太不友好每次调阈值都要重新发版文件定时拉取虽然简单但每台网关都要保证文件同步多机器场景很容易出现各机器规则不一致。推模式的核心思路是规则文本统一存在配置中心网关启动时和运行中都监听配置中心的变更一旦有更新就实时刷新到内存。这样配置修改后一两秒内全网关生效不需要重启也不需要人工分发文件。2. 持久化改造的总设计推模式如何让规则从配置中心流进内存2.1 一条规则从发布到生效要经过哪几步改造前要先理解规则流转的完整链路。我把它拆成了六步开发者在配置中心创建或修改规则 JSON。配置中心版本变更触发长轮询通知。网关内的 Nacos 数据源客户端收到变更拉取最新配置文本。自定义Converter把 JSON 字符串转换成ListGatewayFlowRule或ListApiDefinition。转换后的规则对象通过SentinelProperty推送进对应的 Manager。Manager更新内部数据结构网关适配器在下一个请求上感知新规则并执行判断。这里面最容易被忽略的是第 4 步。Sentinel 的数据源机制设计得很干净数据源只负责拉取字符串Converter负责把字符串变成规则对象register2Property负责把规则对象推给管理器。每一层的职责边界清晰改造时只要把每一层填对整个链路就能跑通。2.2 引入 Nacos 数据源扩展的模块结构我的改造没有改 Sentinel 源码全部通过扩展机制完成。依赖方面只需要额外引入 Sentinel 的 Nacos 数据源扩展和网关适配器模块sentinel-datasource-nacos提供NacosDataSource用于监听配置中心。sentinel-spring-cloud-gateway-adapter提供网关过滤器和网关规则管理器。自研的两个类一个InitFunc入口两个Converter转换器。InitFunc是 Sentinel 预留的 SPI 初始化接口。应用启动时Sentinel 框架会通过java.util.ServiceLoader加载所有InitFunc实现并执行它们的init()方法。这个机制比PostConstruct更适合做规则持久化因为它在 Sentinel 框架自己的初始化阶段就运行天然保证在网关过滤器正式处理请求之前规则已经就位。2.3 两套 JSON 数据的字段设计既然规则要放到配置中心就必须先把 JSON 字段结构定清楚。网关流控规则的核心字段如下字段含义示例值resource资源名通常对应路由 ID 或 API 分组名order-serviceresourceMode资源模式0 表示路由 ID1 表示 API 分组名0grade限流维度0 表示线程数1 表示 QPS1count阈值100intervalSec统计窗口秒数1controlBehavior流量控制效果0 快速失败1 Warm Up2 排队等待0API 分组的字段相对简单但匹配策略必须理解正确字段含义示例值apiName分组名后续网关流控规则的 resource 指向它order-apipredicateItems匹配条件列表见下pattern匹配表达式/order/**matchStrategy匹配策略0 精确匹配1 前缀匹配2 正则匹配1字段设计决定了下游所有规则配置的写法。我当时定的原则是路由级规则用resourceMode0把 resource 直接写成路由 IDAPI 分组级规则用resourceMode1把 resource 写成 apiName。两类规则混用时很容易搞混统一规范后后续配置排查会省很多事。3. 动手改造InitFunc NacosDataSource 双数据源落地代码3.1 改造入口的选型为什么用 InitFunc 而不是 PostConstruct我有一个习惯能用框架预留的扩展点就不自己发明接口。最初我也考虑过在 Spring 的CommandLineRunner里加载规则后来发现几个问题。第一CommandLineRunner在 Spring Context 刷新完成后才执行这时候网关过滤器已经完成初始化。虽然理论上还能加载规则但存在很短的窗口期启动过程中的流量可能没有规则保护。第二一旦以后要接入多种数据源、多个数据源CommandLineRunner里的代码会越来越臃肿。第三PostConstruct依赖 Spring 容器启动流程如果某些自定义代码在容器初始化之前就想访问规则数据就会拿到空数据。InitFunc没有这些包袱。它是 Sentinel 官方推荐的扩展点加载时机足够早而且同一个 jar 包里的InitFunc可以通过 SPI 自动被发现不需要额外注册到 Spring 容器。后续如果要增加新的数据源只需要在 SPI 文件里加一行。3.2 核心代码两类规则分别注册到对应 Manager我直接用一个入口类同时注册网关流控规则和 API 分组规则避免项目里出现两个互相不知道的初始化器。完整骨架如下package com.example.gateway.sentinel; import com.alibaba.csp.sentinel.datasource.ReadableDataSource; import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource; import com.alibaba.csp.sentinel.init.InitFunc; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionManager; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import java.util.List; public class GatewayRuleNacosInitFunc implements InitFunc { private static final String SERVER_ADDR System.getProperty(nacos.server.addr, 127.0.0.1:8848); private static final String GROUP_ID SENTINEL_GROUP; private static final String GATEWAY_RULES_DATA_ID gateway-flow-rules; private static final String API_DEFINITIONS_DATA_ID api-definitions; Override public void init() throws Exception { loadGatewayFlowRules(); loadApiDefinitions(); } private void loadGatewayFlowRules() { ReadableDataSourceString, ListGatewayFlowRule flowRuleDataSource new NacosDataSource(SERVER_ADDR, GROUP_ID, GATEWAY_RULES_DATA_ID, source - JSON.parseObject(source, new TypeReferenceListGatewayFlowRule() {})); GatewayRuleManager.register2Property(flowRuleDataSource.getProperty()); } private void loadApiDefinitions() { ReadableDataSourceString, ListApiDefinition apiDefinitionDataSource new NacosDataSource(SERVER_ADDR, GROUP_ID, API_DEFINITIONS_DATA_ID, source - JSON.parseObject(source, new TypeReferenceListApiDefinition() {})); ApiDefinitionManager.register2Property(apiDefinitionDataSource.getProperty()); } }这段代码有两个关键点要重点解释。第一个关键点是 lambda 表达式的Converter。NacosDataSource的最后一个参数是ConverterString, T输入是配置中心拉到的字符串输出是希望得到的规则列表。我直接用了 fastjson 的TypeReference保证泛型信息不丢失。这里如果粗心写成List.class运行时会因为类型擦除而反序列化出ListLinkedHashMap推到 Manager 后直接触发类型转换异常。第二个关键点是register2Property。GatewayRuleManager和ApiDefinitionManager都维护了一个SentinelPropertyregister2Property会把数据源的Property注册进去。这一步做完之后配置中心每次推新文本数据源都会自动完成拉取-转换-推送的闭环应用代码里不用再写任何轮询逻辑。3.3 SPI 注册文件与包名注意事项光有类还不够还要让 Sentinel 能找到它。我在resources/META-INF/services/目录下新建了一个文件文件名必须是com.alibaba.csp.sentinel.init.InitFunc内容就一行com.example.gateway.sentinel.GatewayRuleNacosInitFunc这里有个特别容易踩的细节SPI 文件的路径和文件名必须完全正确少一个字符都加载不到。我一开始把文件名写成了InitFunc当然没生效整个改造看起来就像白做了一样。后来检查 classpath 才注意到是文件名少了完整包名。如果你的项目是多模块工程还要确认这个 SPI 文件最终被打进了实际运行的网关模块 jar 包里而不是留在某个不被依赖的公共模块里。很多代码明明写了但没生效的问题最后都是这个原因。3.4 规则操作入口发布、修改、删除动作如何在配置中心完成数据源接好之后日常操作就完全落在配置中心了。发布一条新规则直接向gateway-flow-rules这个 dataId 发布 JSON 数组[ { resource: order-service, resourceMode: 0, grade: 1, count: 100, intervalSec: 1, controlBehavior: 0 } ]API 分组数据类似[ { apiName: order-api, predicateItems: [ { pattern: /order/**, matchStrategy: 1 } ] } ]修改规则就是重新发布同一 dataId删除规则就是发布空数组[]。这里要注意发布空数组和发布 null 是不同的概念空数组会让 Manager 清空全部网关规则而 null 文本可能导致配置中心客户端返回空字符串进而反序列化时报错。所以无论有没有规则我都在配置中心维护一个合法的 JSON 数组文本。4. 上线前必须搞清的六个坑按排查优先级排列4.1 规则确实加载了但流量就是没被限住改造完第一次现场验证我站在生产网关前眼睁睁看着压测流量全数通过限流规则像是没存在过。看日志时规则加载的日志确实打了GatewayRuleManager里也能查到规则但流量就是不受控。最后定位到是资源名对不上。网关适配器在 Sentinel 里生成的资源名是路由 ID而我们配置中心里的 resource 写的是服务名。Sentinel 拿order-service去查规则发现没有自然放行所有流量。排查方法很简单在网关过滤器里打点输出当前请求命中的资源名或者直接看流量链路里 Sentinel 记录的资源。配置规则之前永远先确认资源名和路由 ID 一致。4.2 API 分组匹配不上兜底规则全打到路由配置了 API 分组后更隐蔽的问题来了某些请求明明应该命中分组规则却被按路由规则处理了。问题出在matchStrategy上。前缀匹配策略只匹配前缀不支持**通配符。/order/**这种写法在 Spring 路由里确实是通配路径但 Sentinel API 分组的前缀匹配模式不做通配符展开它只是普通的字符串前缀判断。要让整个/order/前缀都命中pattern 应该写成/order/而不是/order/**。正则匹配策略则要把 pattern 写成真正的正则表达式。如果完全搞不清该用哪种先用精确匹配把少数几个固定路径写上跑通链路后再逐步放开。4.3 配置中心没有这个 dataId 时NacosDataSource 会一直兜底空串刚接入配置中心时我没在配置中心预先创建 dataId想着没配置就是没规则也没关系。结果网关启动后日志里不停出现反序列化异常整个规则刷新链路一直报错。原因是NacosDataSource在 dataId 不存在时会收到空字符串空字符串被Converter拿去做JSON.parseObject自然抛异常。更麻烦的是这个异常会反复触发日志里刷屏真正的问题反而被淹没。解决方法是提前把 dataId 创建好里面放一个空数组[]。这样配置中心不会返回空串数据源拿到的始终是合法 JSON。这个习惯救了我很多次后来所有环境我都先准备一份空规则文本。4.4 SPI 文件没被加载改造做得再好也是白搭如果连init()方法都没执行过后面所有讨论都不成立。判断 SPI 是否加载成功的最快方式是在init()方法第一行打印一行日志。如果日志没出现就按顺序检查三件事META-INF/services/com.alibaba.csp.sentinel.init.InitFunc文件名是否正确文件内容是否指向了你实际的类全限定名打包后这个文件是否真的在网关模块的 classpath 里。我在多模块项目里遇到过打包时把 SPI 文件打进了一个不起作用的依赖模块的情况。用unzip -l gateway.jar | grep InitFunc看一眼最终 jar 包比在 IDE 里折腾半天强得多。4.5 网关适配器与数据源版本对不上Sentinel 各个模块之间版本敏感。sentinel-spring-cloud-gateway-adapter和sentinel-datasource-nacos如果版本不一致可能出现两个问题一是GatewayRuleManager.register2Property方法不存在编译期还好运行期直接 NoSuchMethodError二是规则对象的字段解析不一致配置中心明明配了 count结果内存里读出来是默认值。我踩过一次版本不一致的坑后定了一条规矩所有 Sentinel 相关依赖统一用同一个版本号集中维护在 BOM 或父 POM 里禁止各个模块自行声明版本。4.6 规则反序列化突发的序列化异常用 fastjson 转换规则文本时最烦的是JSON 字段名对不上。Sentinel 规则对象的字段是标准的驼峰命名配置中心里手写 JSON 时一旦写成下划线风格比如interval_sec反序列化就会得到默认值而不是真实配置而且不报错。这类问题最坑人因为规则看起来加载了实际上全是用默认值顶上的。我建议在配置中心侧做一层 schema 校验或者至少让Converter里加一个字段数量的断言配置文本里一个字段都没解析到就直接抛错别让静默的默认值混过去。5. 验证与日常运维规则到底有没有在生效5.1 重启服务后先看日志确认规则数不要直接看控制台持久化改造上线后每次重启我都要确认规则真的回来了。只看 Dashboard 不一定可靠因为 Dashboard 的页面数据来自机器上报一旦上报链路出问题页面显示和真实内存状态会有偏差。我的做法是在应用启动完成后打印一份规则快照。写一个简单的ApplicationRunner把所有数据源加载后的规则数量和内容摘要打印出来比如Gateway flow rules loaded: 15, Api definitions: 6。重启后瞟一眼日志前十秒就知道规则是否就位。5.2 主动改配置后观察是否一两秒内生效规则更新是否实时是推模式改造的核心价值。我在配置中心改了一个阈值然后在压测工具里观察 QPS 曲线正常情况下一两秒内就能看到拦截量变化。如果改了之后迟迟没反应先别急着怀疑数据源检查配置中心发布是否成功、dataId 是否写对、groupId 是否一致。我把这个验证写成了固定的发版 checklist发布前改一条临时规则确认秒级生效后再改回原阈值。这个动作看着简单但能挡住大部分配置链路问题。5.3 压测链路验证真实拦截行为光看日志和控制台还不够要真正压出拦截效果。我通常用并发请求去触发限流比如规则配置是 QPS 50就用 100 并发打接口观察返回是否出现限流响应码。网关场景下还需要区分两类拦截一类是路由级流控响应应该走BlockRequestHandler的定制返回另一类是 API 分组级流控触发后同样走统一拦截逻辑。测试时两类规则都要各压一遍别只验证了其中一类就觉得万事大吉。5.4 回滚与灰度的一种低成本姿势配置中心的另一个好处是规则变更可以回滚。我常用的姿势是给不同环境用不同的 dataId灰度网关监听gateway-flow-rules-gray正式网关监听gateway-flow-rules-prod。先在灰度 dataId 上改规则验证没问题后把同一份文本发布到正式 dataId。这样做的成本很低不用改任何代码只是 dataId 规划不同。回滚则直接发布历史版本配置中心侧保留的版本记录就是天然的规则审计日志。我后来甚至把误删规则后恢复的操作从重新组织一遍规则简化成了选择上一个版本直接发布。6. 另一条路不急着接配置中心时还能做什么6.1 文件定时拉取拉模式适合什么场景如果你的团队暂时没有接入配置中心的计划文件定时拉取也是一个可以接受的过渡方案。Sentinel 的FileDataSource支持设置刷新周期网关进程周期性读取本地规则文件。这个方案的优点是改动极小缺点是规则分发依赖发布系统的文件同步能力。机器少、规则变更频率很低时它足够用机器一多文件分发不一致的问题就会放大那时候还是得回到配置中心。6.2 用 MySQL 或 Redis 自己管理规则再推送到内存的价值还有一种思路规则主数据放在 MySQL 或 Redis网关定时把规则查出来加载到 Sentinel 内存。这个方案看起来持久化了但本质上还是拉模式实时性依赖轮询间隔而且网关实例多了以后每个实例都去数据库里拉规则压力也不小。如果以后要做规则可视化编排、权限审批流把规则主数据落在数据库是合理的。但从纯网关限流角度看配置中心推模式已经覆盖了很大一部分需求初期没必要把规则管理做得太重。等到团队确实需要审批流、历史版本对比时再把配置中心的数据同步到业务库也不迟。6.3 改造完成后我保留的一个习惯这次改造做完之后我保留了一个看起来很小但非常管用的习惯每次发版后第一件事是确认规则快照日志第二件事是观察第一波真实流量的限流拦截情况。规则持久化解决了规则在不在的问题但规则对不对永远要靠真实流量来验证。配置中心里我还会在每条规则后面加上统一的备注字段记录这次阈值调整的原因。这个备注看起来不影响技术链路但半年后再看规则变更历史时它能帮你少做很多无谓的考古。网关流控规则终究只是手段稳定地服务业务才是目的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑