ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Sentinel @SentinelResource 详解:fallback 与 blockHandler 的区别与实战

Sentinel @SentinelResource 详解:fallback 与 blockHandler 的区别与实战 1. 先分清fallback 和 blockHandler 是两套完全不同的兜底机制最开始接触 Sentinel 的时候最容易让人上头的一对概念就是SentinelResource注解里的fallback和blockHandler。网上很多文章把两个词并列着写示例代码里也是你中有我、我中有你导致不少同学误以为它们功能类似随便配一个就行。我自己第一次做压测就吃过这个亏接口明明配置了fallback限流一触发调用方却收到了一个冷冰冰的FlowException当时还以为是规则配置错了排查了半天才发现问题出在兜底方法选错了。先说结论fallback和blockHandler是两套互相独立的兜底机制触发场景完全不一样。fallback管的是业务方法自身抛出来的异常比如参数校验失败、下游调用超时、空指针这些blockHandler管的是 Sentinel 规则被触发时抛出的BlockException比如 QPS 超过阈值、被熔断降级、被系统保护拦截。换句话说业务代码“正常进场”但“现场翻车”时走的是fallback业务代码“根本进不了场”时走的是blockHandler。不理解这个前提后面所有配置都可能白做。为什么这对概念经常被搞混主要原因是它们语法上长得太像都是给一个方法做兜底方法签名都是“原方法参数列表 一个异常参数”返回值都要求与原方法一致。再加上有些项目为了省事会把两个方法写在同一个Handler类里不仔细看根本分不清谁是谁。后面我会把注解的每个属性、两个方法的具体签名、执行优先级、以及一个完整的订单接口示例拆开讲清楚最后还会给出基于 Nacos 的动态限流配置样例方便你直接抄作业。2. SentinelResource 注解全属性拆解每个字段到底管什么2.1 注解的声明式本质SentinelResource做的事情是“把某个方法声明为一个 Sentinel 资源”。它本身不创建任何限流规则也不负责加载规则。真正的规则是通过FlowRuleManager、DegradeRuleManager等静态 API 加载或者通过配置中心动态推送的。注解的作用是让 Sentinel 在方法执行前后插入一段切面逻辑进入方法前尝试申请资源如果规则不允许进入立即抛出对应的BlockException如果允许进入就执行业务方法方法结束后上报本次调用的 QPS、RT、异常数等指标。因此看一个SentinelResource是否生效至少要确认三件事第一注解确实被切面拦截到了Spring 环境下要求方法走代理对象第二规则里配置的resource名称与注解的value完全一致第三兜底方法的签名符合要求。很多人说“我配了注解但没生效”九成是这三件事里至少有一件没做到。2.2 value 与 entryTypevalue是资源名没有默认值必须手动指定。这个名称是后续规则关联的钥匙规则配置里的resource字段和它精确匹配。这里建议用稳定的业务标识比如order:create、pay:callback而不是直接拼方法名。原因很简单规则放在 Nacos 里如果资源名跟着代码重构一起改配置中心的规则很容易失联。entryType用于标注资源的流量类型取值是EntryType.IN或EntryType.OUT默认IN。绝大多数场景都不需要动它但如果你需要对 HttpClient、OpenFeign 这类出站调用做保护可以把对应的SentinelResource的entryType改为OUT这样 Sentinel 在统计系统自适应限流等逻辑时会区分出入口流量避免把内部调用当外部流量误伤。2.3 兜底方法相关的五个属性用表格梳理一下注解里的关键属性对照着看会更清楚属性作用使用要点value资源名规则匹配的唯一标识必填建议使用稳定的业务语义字符串entryType入口/出口流量类型默认IN出站调用可以配OUTblockHandler处理被 Sentinel 拦截时的逻辑对应BlockException及子类blockHandlerClassblockHandler所在类配置后要求方法必须为public staticfallback处理业务方法抛出的异常对应普通Throwable不处理BlockExceptionfallbackClassfallback所在类配置后要求方法必须为public staticdefaultFallback默认兜底通用异常处理优先级低于fallback签名更灵活exceptionsToIgnore忽略指定的异常类型忽略后的异常不会被fallback接管这里有一个容易忽略的细节SentinelResource没有提供defaultBlockHandler之类的属性。如果很多资源都需要被限流后统一提示“系统繁忙”你只能给每个核心资源单独配置blockHandler或者统一在全局异常处理器里捕获BlockException。我自己在项目里的做法是非核心资源不做注解兜底让BlockException直接向上抛由外层ControllerAdvice统一转成友好提示核心资源才单独配blockHandler这样职责清晰也不需要在每个方法上重复写一堆静态方法。2.4 一个容易忽略的 exceptionsToIgnoreexceptionsToIgnore这个属性很多人没用过但场景其实很实用。默认情况下只要方法抛出异常fallback就会接管。但有些异常你是不希望被吞掉的比如参数校验异常你更希望它直接抛给全局异常处理器让接口返回 400 和明确的提示信息而不是走业务兜底返回一个统一错误码。此时把IllegalArgumentException.class配置到exceptionsToIgnore这个异常就会原样继续向上抛不再进入fallback。注意一下优先级exceptionsToIgnore优先于fallback。如果同一个异常既在忽略列表里又写了匹配的fallback最终结果还是忽略直接抛异常。这个优先级意味着你在设计异常兜底时要先想清楚哪些异常是“可恢复的兜底场景”哪些是“必须亮出真实错误给上层处理”的。3. 两者的核心区别触发条件、函数签名和优先级3.1 触发条件对比业务异常 vs 规则拦截一个被SentinelResource保护的方法执行过程大致是切面先检查 Sentinel 规则如果流量通过才真正调用业务方法如果流量被拦截业务方法根本不会执行直接抛BlockException。所以这两个兜底机制的触发点在时间线上是明确分开的blockHandler在业务方法执行之前触发。只要 Sentinel 判定当前请求需要被限流、降级、限热点参数、拒绝授权等就会抛出BlockException的子类此时由blockHandler接管。fallback在业务方法执行过程中触发。业务代码抛出的任何非BlockException异常都会被fallback接管。有一个非常关键的结论fallback不能处理BlockException。哪怕你只配了fallback没配blockHandler限流时也不会进入fallback而是直接把BlockException抛给调用方。很多新手在这里踩坑以为fallback是万能兜底结果线上限流触发后看到一堆FlowException。记住这两个兜底的分工是blockHandler管“进不来”fallback管“进来后挂了”。3.2 函数签名要求对比签名要求是另一个极易踩坑的点。fallback和blockHandler的返回值都必须与原方法一致或至少能兼容转换参数列表也必须和原方法保持“前缀一致”。具体拆解一下fallback方法参数列表 原方法全部参数 最后的Throwable参数。比如原方法Order createOrder(OrderRequest request)fallback 方法必须是Order createOrderFallback(OrderRequest request, Throwable t)。Throwable不能省略这一点和blockHandler不一样。blockHandler方法参数列表有两种写法。第一种是只保留原方法参数不追加参数第二种是原方法参数 最后的BlockException参数。例如同一个订单方法可以写Order createOrderBlockHandler(OrderRequest request)也可以写Order createOrderBlockHandler(OrderRequest request, BlockException ex)。官方推荐带上BlockException因为你需要知道具体是哪种拦截流控、降级、还是热点参数限制。如果原方法有多个参数比如User getUser(String name, int age)那么fallback必须是User getUserFallback(String name, int age, Throwable t)blockHandler必须是User getUserBlockHandler(String name, int age, BlockException ex)顺序不能乱。我在实际项目里统一要求兜底方法的参数顺序必须严格照抄且异常参数放在最后不要自行调整顺序。否则 Sentinel 在反射查找方法时找不到匹配签名会直接报“no such method”或者干脆忽略你配置的兜底方法。3.3 优先级与默认兜底方法当多个兜底属性同时存在时Sentinel 的处理优先级是有一套明确顺序的exceptionsToIgnore配置的异常类型最高优先级直接忽略不进入任何兜底。blockHandler仅处理BlockException优先级高于所有普通异常兜底。fallback处理普通业务异常优先级高于defaultFallback。defaultFallback当没有匹配到具体的fallback或者你希望统一处理某些异常时使用。很多人会问“同时配置了 fallback 和 blockHandler到底走哪个”答案是看触发原因。被限制流时毫不犹豫走blockHandler因为业务方法都没执行根本轮不到fallback业务方法内部抛出普通异常时blockHandler也不会参与因为这不是BlockException。两者不是二选一的关系而是两条平行赛道。defaultFallback的签名更灵活它可以不带任何参数也可以带一个Throwable参数但不能带原方法参数。也就是说如果原方法有一堆入参你可以在兜底方法里不去管它们只接收Throwable。这个特性很适合做统一异常兜底但要注意优先级比具体fallback低别期望它能覆盖具体fallback的逻辑。3.4 核心区别汇总表对比维度fallbackblockHandler触发原因业务方法执行中抛出普通异常Sentinel 规则拦截抛出BlockException业务方法是否执行已经执行了一部分完全没有执行异常参数Throwable必须有可选的BlockException能否处理限流/降级不能能建议位置当前类实例方法或独立类静态方法建议独立类静态方法典型日志ERROR 级别记录业务失败原因WARN 级别记录被限流/降级这张表我建议截图保存。每次配置SentinelResource前先对着表问一句我当前要兜底的是哪种异常答案直接决定你应该写fallback还是blockHandler。4. 实战一个订单接口同时配置 fallback 和 blockHandler4.1 业务场景和规则规划假设你在做一个订单服务核心接口是创建订单资源名定为order:create。业务上有三个需求当请求参数不合法时返回业务失败提示而不是直接抛 500当接口 QPS 超过 5 时返回“系统繁忙请稍后重试”两条兜底逻辑需要分别记录不同级别的日志方便后续排查。对应到 Sentinel 上第一个需求用fallback处理业务异常第二个需求用blockHandler处理FlowException。我把两个兜底方法都放到一个独立的OrderDegradeHandler类里声明为public static理由后面会讲。4.2 完整代码实现先定义一个简单的订单请求对象和统一返回结构public class OrderRequest { private Long userId; private String skuId; // getter/setter 省略 } public class Order { private String orderId; private String status; // getter/setter 省略 } public class RT { private int code; private String msg; private T data; public static T RT success(T data) { RT r new R(); r.code 200; r.msg success; r.data data; return r; } public static T RT error(int code, String msg) { RT r new R(); r.code code; r.msg msg; return r; } }然后是核心业务方法Slf4j Service public class OrderService { SentinelResource( value order:create, blockHandler createOrderBlockHandler, blockHandlerClass OrderDegradeHandler.class, fallback createOrderFallback, fallbackClass OrderDegradeHandler.class ) public ROrder createOrder(OrderRequest request) { if (request null || request.getUserId() null) { throw new IllegalArgumentException(userId不能为空); } if (request.getSkuId() null) { throw new RuntimeException(skuId不能为空); } // 模拟正常业务处理 Order order new Order(); order.setOrderId(ORD System.currentTimeMillis()); order.setStatus(CREATED); return R.success(order); } }再写独立的兜底类Slf4j public class OrderDegradeHandler { public static ROrder createOrderBlockHandler(OrderRequest request, BlockException ex) { log.warn([blockHandler] 订单接口被拦截资源order:create类型{}, ex.getClass().getSimpleName()); return R.error(429, 系统繁忙请稍后重试); } public static ROrder createOrderFallback(OrderRequest request, Throwable t) { log.error([fallback] 订单接口业务异常, t); return R.error(500, 业务处理失败 t.getMessage()); } }这里有几个细节需要强调。第一两个兜底方法都放在OrderDegradeHandler里并配置了blockHandlerClass和fallbackClass所以方法必须是public static否则 Sentinel 在反射调用时会找不到可实例化的目标。第二第一个参数OrderRequest request与原方法完全一致最后分别追加BlockException和Throwable这是硬性要求。第三返回值类型都写成ROrder和原方法保持一致。4.3 验证过程分别制造异常和触发限流代码写完不能直接上生产先做一次本地验证。启动应用后用 Swagger 或者 curl 访问测试第一步验证fallback。发送一个缺少userId的请求POST /order/create Content-Type: application/json { skuId: ABC123 }此时业务方法会在第一个判断处抛出IllegalArgumentExceptionSentinel 的切面捕获到这个异常后不会原样往上抛而是进入createOrderFallback。接口返回{ code: 500, msg: 业务处理失败userId不能为空 }同时本地日志会打印[fallback] 订单接口业务异常说明走的是fallback分支。第二步验证blockHandler。先把 Nacos 里的限流规则调成一个很小的阈值比如 QPS 为 1然后快速并发调用同一个接口。这里要注意限流规则需要提前配置到当前命名空间资源名必须是order:create线程数或者 QPS 阈值按 1 设置。并发两个请求后第二个请求会被 Sentinel 拦截业务方法根本没有执行直接进入createOrderBlockHandler。该请求返回{ code: 429, msg: 系统繁忙请稍后重试 }日志里打印的是[blockHandler] 订单接口被拦截和fallback的日志完全不同。4.4 验证结论通过这个案例可以直观看到同一个资源、同一个注解同时配置两个兜底方法后触发条件互不干扰。异常和限流是两条独立路径谁触发谁接管不存在“fallback 优先级更高所以 blockHandler 白配”的情况。实际业务中你甚至可以在blockHandler里做限流告警统计在fallback里做业务异常日志上报两个方法各司其职。5. 结合 Nacos 实现限流规则的动态配置5.1 为什么需要动态规则上面的验证过程里我用的是“先把规则配好再启动”的方式。但生产环境不可能每次都改代码重启来调整阈值否则上线一个限流配置还要走发布流程效率太低。正常情况下我们会把 Sentinel 规则放到 Nacos 配置中心通过控制台改配置客户端自动监听并刷新规则整个过程不用重启服务。动态规则的好处不仅仅是省一次发布。压测时发现 QPS 阈值需要从 100 调到 50直接在 Nacos 页面改一个数字就能生效线上发生突发流量也可以在几十秒内把阈值降下来。相比本地FlowRuleManager.loadRules()这种方式更适合真实运维。5.2 引入依赖如果你用的是 Spring Cloud Alibaba集成 Nacos 动态规则很简单只需在pom.xml中增加两个依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-sentinel/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-datasource-nacos/artifactId /dependencysentinel-datasource-nacos是 Sentinel 官方提供的 Nacos 数据源扩展包它负责监听 Nacos 配置变更并把配置内容反序列化成对应的规则对象。注意和普通的 Nacos 配置中心依赖不同这里必须引入专门的数据源扩展否则 Sentinel 不知道去哪里拉规则。5.3 通过 Nacos 配置限流规则登录 Nacos 控制台新建一个配置Data IDorder-flow-rulesGroupSENTINEL_GROUP配置格式JSON配置内容如下[ { resource: order:create, grade: 1, count: 5, limitApp: default, strategy: 0, controlBehavior: 0, clusterMode: false }, { resource: order:create, grade: 0, count: 10, limitApp: default, strategy: 0, controlBehavior: 2, clusterMode: false } ]这里解释一下字段含义。grade是规则类型0表示按线程数限制1表示按 QPS 限制count是阈值strategy是流控策略0直接、1关联、2链路controlBehavior是流控效果0快速失败、1预热、2排队等待。上面这个配置同时做了 QPS 为 5 的快速失败限制以及线程数为 10 的排队等待限制生产环境可以根据业务特性选一种即可。如果你还需要降级规则可以在另一个配置文件里配[ { resource: order:create, grade: 0, count: 0.5, timeWindow: 10, minRequestAmount: 5 } ]grade为0表示按异常比例降级count为0.5表示 50% 的请求异常时触发timeWindow是熔断时间窗口单位秒。降级规则的数据源在 Spring 配置里要用rule-typedegrade。5.4 Spring Boot 配置和验证数据源配置写在application.yml里spring: application: name: order-service cloud: sentinel: transport: dashboard: 127.0.0.1:8080 datasource: ds-order-flow: nacos: server-addr: 127.0.0.1:8848 dataId: order-flow-rules groupId: SENTINEL_GROUP rule-type: flow ds-order-degrade: nacos: server-addr: 127.0.0.1:8848 dataId: order-degrade-rules groupId: SENTINEL_GROUP rule-type: degraderule-type是必填项它告诉 Sentinel 这段 Nacos 配置对应哪类规则常见取值有flow、degrade、param-flow、system、authority。配置好之后启动应用在 Nacos 页面修改order-flow-rules里的count值保存后无需重启服务规则很快会生效。验证方法也很简单把阈值改成 1然后连续访问接口第二个请求就会触发blockHandler。如果在非 Spring Cloud 环境也可以手动注册数据源ReadableDataSourceString, ListFlowRule flowRuleDataSource new NacosDataSource(127.0.0.1:8848, SENTINEL_GROUP, order-flow-rules, source - JSON.parseObject(source, new TypeReferenceListFlowRule() {})); FlowRuleManager.register2Property(flowRuleDataSource.getProperty());这种写法和 Spring Cloud 配置等价核心都是把 Nacos 中配置的 JSON 转换成FlowRule列表再注册到FlowRuleManager。我建议直接使用 Spring Cloud 配置方式代码更少也避免自己处理序列化问题。5.5 动态规则推送的坑Nacos 方案用起来挺爽但有几个坑必须提前知道。第一namespace不配置时默认是public如果应用配置了自定义 namespace一定要在数据源配置里同步指定否则客户端监听的是 public 下的配置你改了自定义 namespace 下的规则自然不生效。第二Nacos 推送的规则会覆盖本地通过FlowRuleManager.loadRules()加载的规则如果本地和服务端都配置了会产生“你以为的规则不是实际规则”的问题。第三JSON 格式解析失败时 Sentinel 通常不会直接报错而是规则不加载排查时要去看 Nacos 客户端的日志确认有没有反序列化异常。6. 常见问题排查与我的避坑经验6.1 为什么 blockHandler 方法必须 static 以及怎么绕前面提到只要配置了blockHandlerClass或fallbackClass对应方法必须声明为public static。原因是 Sentinel 的切面在反射调用兜底方法时需要凭空实例化或者调用一个不依赖 Spring 容器的对象静态方法可以直接通过类名调用不需要构造对象这对一个框架级组件来说是最稳妥的实现方式。但这也带来一个问题静态方法里不能直接访问 Spring Bean。如果你在blockHandler里想调用某个AlertService发送告警你会发现Resource注入不进去。我的做法是提供一个静态的SpringContextHolder在兜底方法里手动从 Spring 容器中取 Beanpublic class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; Override public void setApplicationContext(ApplicationContext applicationContext) { context applicationContext; } public static T T getBean(ClassT clazz) { return context.getBean(clazz); } }然后在blockHandler里这样用public static ROrder createOrderBlockHandler(OrderRequest request, BlockException ex) { SpringContextHolder.getBean(AlertService.class).sendAlert(ex.getClass().getSimpleName()); return R.error(429, 系统繁忙请稍后重试); }当然如果你不想用静态方法也可以不配置blockHandlerClass把兜底方法直接写在原类中用普通实例方法访问 Bean。但这种方式在 Sentinel 反射处理时偶尔会遇到方法查找不到的问题不同版本表现还不一样。我个人为了减少不确定性统一走独立静态类方案。6.2 fallback/blockHandler 同时存在时到底走谁直觉上有人会以为两个方法都配置了优先级高的那个会“抢占”另一个。实际情况是二者按触发原因分流互不抢占。可以做一个极端实验在业务方法第一行就抛异常同时把 QPS 阈值设为 0。QPS 为 0 意味着任何请求都会被 Sentinel 拦截业务方法根本不会执行所以你看到的只会是blockHandler的日志而不是fallback。反过来把 QPS 阈值调大让请求正常进入业务方法并抛出普通异常走的一定是fallback。这个实验能很好帮助团队理解两条兜底路径的独立性。6.3 规则不生效的 5 个原因我排查了无数个“规则不生效”的问题总结下来原因基本集中在下面五类资源名不一致。注解value是order:create规则里的resource写成了order_create或者带上了方法前缀根本不匹配。自调用导致切面失效。在同类里使用this.createOrder(request)绕过了 Spring 代理对象SentinelResource完全没机会执行。解决方法是注入自己或者将调用拆分到另一个 Service。私有方法无法被拦截。SentinelResource加在private方法上没有意义AOP 无法拦截私有方法必须改成public且通过代理对象调用。Nacos 配置没推下来。检查rule-type是否正确、dataId是否存在、应用是否引入了sentinel-datasource-nacos。本地规则覆盖了远程规则。如果你在代码里调用了FlowRuleManager.loadRules()后面 Nacos 再推送时可能会出现互相覆盖的问题尽量避免本地和服务端同时配置相同的规则。你可以把这些问题整理成一份排查清单每次遇到“限流不进 blockHandler”时按顺序过一遍基本能定位九成问题。6.4 日志中如何判断是被限流还是业务异常Sentinel 抛出的BlockException有好几个子类常见的包括FlowException流控、DegradeException降级、ParamFlowException热点参数限流、AuthorityException授权规则、SystemBlockException系统保护。如果blockHandler里打印了ex.getClass()通过异常类名就能精确知道是哪种规则拦截的。如果你没有配置blockHandler线上日志里看到FlowException堆栈不要误以为是代码 BUG。它是 Sentinel 在资源入口处主动抛出的代表当前请求被流量规则挡住了。相反如果看到的是NullPointerException、IllegalArgumentException这类业务异常那才是fallback应该处理的场景。建议在日志里把兜底方法名打印出来通过方法名直接区分blockHandler和fallback走了哪条路。6.5 我沉淀下来的最佳实践经历了多次线上故障后我总结了几个使用SentinelResource的固定规则分享给你参考。第一核心接口必须同时配置blockHandler和fallback不要嫌麻烦。blockHandler负责限流提示和告警fallback负责业务异常兜底和日志记录配置完整了才不容易出线上事故。第二兜底方法集中在独立的Handler类中统一命名规范比如xxxBlockHandler、xxxFallback避免散落在业务类里造成维护困难。第三返回值尽量使用统一包装对象这样兜底方法可以直接返回统一的错误码和提示语不需要在业务类里重复定义返回结构。第四所有规则通过 Nacos 下发禁止在代码里硬编码阈值。规则变化是常态动态调整才是正确的运维方式。最后再说一个小技巧在defaultFallback里只处理通用异常具体业务异常还是交给各自的fallback方法。因为defaultFallback没有原方法参数无法针对某个业务字段做精细化处理如果用得太宽泛容易掩盖问题的真实原因。我见过不少项目把defaultFallback当成万能兜底结果线上所有异常都被统一转成了“系统繁忙”排查问题反而变得更困难。兜底方法不是为了吞异常而是为了在异常发生后给调用方一个合理的响应同时把真实原因记录到日志里。理解了这一点你在配置fallback和blockHandler时思路就会清晰很多。
返回列表