ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Sentinel熔断恢复与雪崩重启:原理、参数与配置实战

Sentinel熔断恢复与雪崩重启:原理、参数与配置实战 做微服务的同学基本都跟“服务雪崩”打过照面。一个服务慢了调用它的服务跟着排队线程池被打满然后连锁反应整条链路全被拖死。Sentinel 这种熔断组件就是干这个的——它能在依赖服务异常时快速“断臂求生”把失败的调用拦下来给下游留出喘息时间。但这些年我见过不少线上事故比“熔断不生效”更常见的是“熔断生效了恢复时反而把系统搞崩了”。我管这个叫“雪崩重启”熔断窗口一过四面八方积压的请求像开闸放水一样同时涌回来或者一批实例在同一时间点集体重启流量瞬间打满刚恢复的下游又被冲垮于是再次熔断再次恢复陷入一个“坏了-恢复-又坏-又恢复”的恶性循环。这篇文章我就围绕 Sentinel 的熔断自动恢复机制把底层行为、关键参数、以及实战中怎么配置才能避免“雪崩重启”讲透。内容偏 Spring Cloud Alibaba 的落地场景但原理是通用的。适合正在用或者准备用 Sentinel 做服务治理的同学尤其适合被“恢复瞬间流量冲击”坑过的人。1. 熔断与自动恢复先搞清楚问题在哪1.1 熔断到底在干什么熔断这个概念最早是从电力系统借来的。电路过载时断路器跳闸切断电流保护设备。微服务里的熔断逻辑一模一样当被调用的服务出现大量异常、超时或者慢调用时调用方主动切断这条路后续请求不再真正打到下游而是快速失败或者走降级逻辑。Sentinel 在 Spring Cloud Alibaba 生态里通常负责流量控制、熔断降级、系统保护三件事。熔断DegradeRule是其中最依赖“状态机”的一个CLOSED关闭正常→ OPEN打开熔断生效→ HALF_OPEN半开试探恢复。熔断后能不能自动恢复核心就看 OPEN 到 HALF_OPEN再到 CLOSED 这一段怎么设计的。很多刚接触的同学会把熔断理解成“错了就断开过一会儿自动好”这个理解太粗了。断开多久、断开后放多少试探流量、试探时判断成功与否的标准是什么这些都会直接影响恢复得稳不稳。把这些参数搞明白才算真的会用熔断。1.2 自动恢复不是“时间到了就放行”这么简单Sentinel 的熔断自动恢复默认是这么走的熔断器打开后持续一个时间窗口timeWindow窗口结束就进入半开状态HALF_OPEN。半开状态下Sentinel 会放行一定数量的探测请求如果这些请求成功才真正关闭熔断器恢复正常流量如果请求失败熔断器再次打开重新计时。这个设计比“时间到了就全放”安全很多但它只保证了“有试探过程”并不保证试探通过后下游能扛住全量流量。举个例子一个订单服务被打挂了熔断 30 秒后进入半开放行的几个探测请求都成功了于是熔断关闭。但此时积压的调用方可能有上百个线程在同时重试这上百个请求立刻全部打到订单服务上订单服务刚缓过来的线程池瞬间被打满又挂了。你看熔断器本身没错它甚至正确地识别出了“服务能处理请求”——但恢复后的流量从几个跳到上百个中间没有渐变过程。这就是“雪崩重启”的第一种典型形态。1.3 雪崩重启是怎么来的我梳理一下我遇到过和听说过的主要场景基本分三类第一类是开闸放水。熔断恢复的瞬间所有被拦截的调用同时重试。很多调用方默认的重试策略是不带退避的失败就立刻重试恢复点一到大家就一起冲锋。第二类是实例集体重启。有些团队用健康检查失败作为重启标准服务一挂容器平台就把实例 kill 掉重启。如果整个服务有十几个实例都在同一分钟被判定不健康就会同时重启恢复后同时对外提供服务。注册中心里一下子上来一大批新实例客户端感知到之后立刻把流量切过去此时这些实例可能还在冷启动阶段线程池和缓存都没热直接被打死。第三类是“熔断 重启”叠加。依赖服务重启后调用方这边的旧熔断状态还没过期新实例注册后调用方立刻切流量老熔断器可能还在 OPEN 状态于是所有请求被快速失败服务注册了却没人能调通接着运维看到成功率低又去重启形成死循环。这三种情况本质上都不是 Sentinel 的 bug而是恢复策略没设计到位。后面我给出对应的配置和架构思路。2. Sentinel 熔断恢复的底层原理与关键参数2.1 三种熔断策略注意Sentinel 1.8 之后的版本熔断降级规则DegradeRule支持三种维度慢调用比例RT统计窗口内响应时间超过阈值的请求占比达到一定比例触发熔断。异常比例统计窗口内异常请求占比达到阈值触发熔断。异常数统计窗口内异常数量达到阈值触发熔断。实操中慢调用比例用得最多因为很多下游故障的早期表现不是报错而是“变慢”。比如数据库连接池打满请求不会立刻报错而是排队等待RT 从 50ms 涨到 5s。这时候用异常比例可能触发不了但慢调用比例很容易命中。触发之后的状态流转如下状态含义行为CLOSED熔断关闭正常放行统计指标。达到阈值后进入 OPENOPEN熔断打开拒绝请求快速失败或走降级。持续timeWindow后进入 HALF_OPENHALF_OPEN半开状态放行maxProbeRequests个探测请求。成功则 CLOSED失败则重新 OPEN注意这里有两个参数很多文档不太强调maxProbeRequests最大探测请求数和statIntervalMs统计窗口长度。这两个参数和timeWindow配合决定了熔断恢复时的节奏。2.2 HALF_OPEN 半开状态与试探放行先说半开。为什么需要这个状态因为熔断器不知道下游到底恢复了没有。时间到了就全量放风险太大一个都不放又无法探测。所以半开状态就是“放少量请求进去看看情况”。Sentinel 的半开逻辑中进入 HALF_OPEN 后只会放行maxProbeRequests个探测请求。这些请求如果成功熔断器转为 CLOSED如果有失败转回 OPEN并且重新计时。这里有一个容易踩坑的细节探测请求的成功/失败判定和触发熔断的策略是挂钩的。你用的是慢调用比例策略探测请求虽然没抛异常但如果响应时间超时了也算失败熔断器会再次打开。还有一个细节半开放行的探测请求数量默认是 1。对你没看错Sentinel 的默认maxProbeRequests是 1。这意味着恢复瞬间只有 1 个请求去试水如果这个请求恰好被一个还在慢事务中占用的线程处理了它超时了熔断器就会再等一个timeWindow再试。所以线上会有一种“钝”的感觉明明下游已经好了但要等好几个窗口才能恢复。这时候就要考虑把maxProbeRequests调大比如 5 或 10并配合统计窗口去判断。2.3 几个必须背下来的参数关键参数直接影响自动恢复的“手感”我直接列出来参数默认值作用我的建议timeWindow必填熔断打开后保持 OPEN 的时长单位秒不要设置太短至少 10s 起maxProbeRequests1HALF_OPEN 状态放行的探测请求数默认太保守建议 5~10statIntervalMs1000ms统计窗口长度用于计算指标一般保持默认高并发场景可调大minRequestAmount5触发熔断所需的最小请求数防止少量请求误判默认即可极端场景可调大这张表看懂了再配置就顺了。核心一句话timeWindow控制“多久进入试探”maxProbeRequests控制“试探的强度”两个参数决定恢复时是涓涓细流还是大坝开闸。3. 配置实战Spring Cloud Alibaba Sentinel 的自动恢复配置3.1 基础依赖与接入我用 Spring Cloud Alibaba 这套来演示因为它是国内最主流的落地姿势。首先在服务里引入依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-sentinel/artifactId version2021.0.5.0/version /dependency同时把 Sentinel 的 transport 模块带上这样才能连上控制台dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-transport-simple-http/artifactId version1.8.6/version /dependency接着在application.yml里配置控制台地址spring: cloud: sentinel: transport: dashboard: localhost:8080启动时加 JVM 参数指定应用名-Dproject.nameorder-service这样 Sentinel 控制台就能看到order-service的实时监控。基础接入很快真正的坑在后面配置规则。3.2 熔断规则配置时间和探测量要一起调我的习惯是先用代码初始化规则把调参逻辑搞清楚再迁移到动态配置。下面这段代码是常用的 Demo用DegradeRule配置慢调用比例熔断ListDegradeRule rules new ArrayList(); DegradeRule rule new DegradeRule(); rule.setResource(GET:http://user-service/user/info); rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); rule.setCount(500); // RT 超过 500ms 算慢调用 rule.setTimeWindow(30); // 熔断后 30 秒进入半开 rule.setMinRequestAmount(5); // 统计窗口内至少 5 个请求才触发 rule.setStatIntervalMs(1000); // 统计窗口长度 1 秒 rule.setMaxProbeRequests(5); // 半开放行 5 个探测请求 rules.add(rule); DegradeRuleManager.loadRules(rules);resource写的是被保护资源的名称。用 OpenFeign 的场景下除了在方法上显式标注SentinelResource也可以在配置里开启 Feign 的 Sentinel 支持feign: sentinel: enabled: true开启后 Feign 调用会被 Sentinel 自动包装资源名默认是方法签名那一串。我强烈建议显式地用SentinelResource标注关键方法因为默认资源名可读性太差排查问题时根本分不清是哪个接口。可以单独写一个方法包裹外部调用例如SentinelResource(value user-info-remote, fallback userInfoFallback) public UserInfo getUserInfo(Long userId) { return userClient.getUserInfo(userId); }这样规则里的 resource 就写user-info-remote日志和告警对起来也舒服。3.3 动态规则Redis 集群作为数据源如果只用DegradeRuleManager.loadRules()加载规则规则只存在于本地内存控制台改完重启就丢了。生产环境一定要用动态数据源把规则存到外部存储。用 Redis 集群作为 Sentinel 规则的 datasource就是推送模式的常见做法。配置大致如下spring: cloud: sentinel: datasource: ds1: redis: host: redis-cluster-1 port: 6379 database: 0 cluster: nodes: redis-node-1:7000,redis-node-2:7001,redis-node-3:7002然后初始化RedisDataSource把规则从 Redis 拉下来注册到DegradeRuleManager这里我用伪代码表示核心逻辑因为每个项目的序列化方式不太一样。String ruleKey sentinel:rules:order-service; RedisDataSourceListDegradeRule redisDataSource new RedisDataSource( redisConnectionFactory, new DegradeRuleManager(), ruleKey, source - JSON.parseArray(source, DegradeRule.class) ); DegradeRuleManager.register2StatClients(redisDataSource);生产环境我强烈建议把规则配置成“启动时先读 Redis本地规则兜底”。启动时如果 Redis 不可用先用上一次的规则文件初始化等 Redis 恢复后再同步。否则服务一启动规则为空等于裸奔。提示规则数据虽然不大但它是治理配置不能丢。Redis 集群能保证高可用至少主从切换时不会出现规则读不到的情况。我自己踩过一次坑单节点 Redis 在哨兵切换期间服务刷新规则失败某条熔断规则失效下游抖动时没有保护线上出现大量超时。规则存储的可用性要跟服务可用性一样重视。4. 避免雪崩重启的落地经验4.1 恢复窗口的合理设置不要迷信“越快越好”很多人会把熔断的timeWindow设得很短比如 3 秒、5 秒理由是“希望服务快点恢复”。但如果你设 5 秒意味着下游故障可能还没处理完服务就已经开始试探放流量了。探测失败熔断器再次打开又等 5 秒再试再失败。日志里看起来就是频繁的 OPEN/CLOSED 切换底层服务被反复骚扰根本没机会安心恢复。我一般的建议是下游是缓存、配置中心这类轻依赖可以设置 10~30 秒。下游是数据库、消息队列等重依赖建议 30~60 秒甚至更久。恢复窗口长不代表服务不可用时间就长它只是“试探的间隔”。真正的恢复速度取决于下游的恢复能力而不是熔断器的参数。想明白这一点你就不会瞎调快。另外timeWindow和maxProbeRequests要联动调整。如果你把探测量提高到 10但timeWindow还是 60 秒那一次试探失败后又要干等 60 秒体感就是“修好了谁都不知道”。一个比较合理的组合是timeWindow20、maxProbeRequests10、statIntervalMs100020 秒一个试探周期10 个请求验证下游状态成功了立即恢复正常失败了最多再空转 20 秒。4.2 客户端重试的退避策略别让所有调用方一起冲锋熔断控制的是“服务端视角”但雪崩重启还有一个“客户端视角”。当 Feign 调用被熔断拦截后调用方如果立即重试而且多个线程同时重试恢复的一瞬间就是流量洪峰。我的建议是给重试加上退避backoff和抖动jitter。最简单的实现是使用 Spring RetryRetryTemplate retryTemplate RetryTemplate.builder() .maxAttempts(3) .exponentialBackoff(300, 2, 2500) // 初始 300ms倍数递增最大 2500ms .build();再配合随机抖动可以把重试时间分散开long delay 300 ThreadLocalRandom.current().nextInt(500); try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }这样做的好处是熔断恢复后不同调用方的重试请求会分散在一个随机时间窗口内而不是同一毫秒一拥而上。这个跟熔断器参数配合起来基本能杜绝“开闸放水”式的雪崩重启。注意重试要小心幂等性问题。写操作必须谨慎建议只对 GET 请求或者幂等写入开启自动重试否则一个重试可能造成多笔重复订单。4.3 部署层面的滚动恢复别让实例同时复活雪崩重启的第二类场景是实例集体重启。这里有一个很实用的经验给实例启动加“随机延迟”。在 K8s 中可以通过postStart生命周期钩子实现。比如每个 Pod 启动后不是立刻对外服务而是先 sleep 一个随机时间再上报存活lifecycle: postStart: exec: command: [/bin/sh, -c, sleep $((RANDOM % 30))]注意 sleep 太久会影响滚动发布速度一般控制在 0~30 秒内。这个随机偏移能把同时重启的 N 个实例变成间隔到达的 N 个实例注册中心不会一下子冒出全部节点客户端切流量也不会那么集中。另一个关键点是实例重启后要等 JVM 和中间件预热完成再接入流量。Sentinel 有 warm-up 限流策略可以限制启动早期的流量。但跨服务层的话最好的办法是让负载均衡器配合让新实例的权重从低慢慢调高也就是“预热权重”。K8s 里可以用 readinessProbe 控制确认应用真的 ready 了才给流量。别省略 readinessProbe它是应对“启动即崩”最有效的防线。5. 常见问题与排查技巧实录5.1 反复熔断很多时候不是参数问题我见过最典型的反复熔断场景熔断恢复后服务看起来好了但过几秒又熔断。很多人第一反应是调大timeWindow结果只是把节奏放慢了问题还在。这时候要去看下游实际发生了什么。我的排查顺序是先看下游服务的 RT 和异常指标确认是不是真的没恢复。再看调用方的线程池和连接池是不是已经堆满。下游好了调用方自己也起不来一样会超时。最后看数据库连接、缓存连接这些基础设施是不是连接数耗尽。多数情况下反复熔断的根因是连接池或线程池被打满后无法自动恢复。比如数据库连接池的释放超时设置太长连接一直被占用服务端 RT 一直高熔断自然反复触发。这时候调 Sentinel 是止痛药真正要治的是下游和基础设施的恢复能力。5.2 恢复后立刻又被冲垮探测量和重试没配合另一种情况是熔断恢复成功了但立刻又被流量打挂。这大概率就是我前面说的开闸放水。检查方法很简单把恢复那一刻的 QPS 曲线拉出来看如果瞬间 QPS 是平时的几倍以上那就是重试风暴。解决方案整理如下Sentinel 的maxProbeRequests不要设太大。探测的意义是“确认下游能处理请求”不是“让下游承受压力测试”。调用方的重试必须加退避加抖动。有条件的话在网关层做并发限制防止恢复瞬间流量过大。这几条都做了基本就不会出现“刚恢复就被冲垮”的尴尬。5.3 日志与指标排查技巧Sentinel 的实时监控数据默认只保留一段时间。要追溯历史问题最好把指标接入 Prometheus 或日志平台。推荐至少采集这几个指标sentinel_degrade_pass_qps熔断器放行的 QPSsentinel_degrade_block_qps被熔断拦截的 QPSsentinel_degrade_success_qps调用成功 QPSsentinel_degrade_exception_qps异常 QPS这几个指标组合起来能非常清晰地还原一次熔断事件的完整过程。我喜欢看block_qps和exception_qps的关系block_qps突然上涨说明熔断器打开了exception_qps也高说明下游还在报错block_qps降下来但success_qps没恢复说明熔断器关了但流量没真正流过去这时候要查客户端或路由。还有一个容易被忽略的点Sentinel 默认会把日志写到~/logs/csp/目录里面有sentinel-block.log和sentinel-degrade.log。排查问题时别只看控制台直接翻日志更快。sentinel-degrade.log记录了每一次熔断规则的变更和触发对定位“规则谁改的、什么时候生效”非常有用。6. 最后分享一点个人体会这套东西我前前后后在好几个项目里落地过最大的感受是Sentinel 的自动恢复机制本身已经做得很安全了真正出问题的几乎都不是组件设计缺陷而是使用者把“恢复”理解得太简单。我个人现在的默认组合是慢调用比例熔断 timeWindow20maxProbeRequests10 客户端RetryTemplate随机退避 K8s 启动随机延迟。这套组合不完美但在我的系统里确实把“雪崩重启”的故障率降到了一个很低的水平。最后再说一个小技巧熔断规则的变更最好走发布审批并且有版本回滚能力。规则是动态的线上改错了影响比改错代码还快。我曾经就因为把某条规则的timeWindow从 30 秒改成 3 秒导致一个下游抖动时反复试探、反复熔断半个小时内告警刷屏。从那以后我对动态规则的敬畏感就特别强。希望看到这里的朋友能少踩一些我踩过的坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进