服务熔断和服务降级,是微服务面试里出现频率极高的两个概念,也是线上稳定性治理里绕不开的两个动作。最近我筛简历的时候发现一个现象:候选人能把 Hystrix 的三个状态背得滚瓜烂熟,但问到“你们项目里熔断和降级分别怎么落地、出了故障怎么配合”时,就回答得特别虚。这题问的不是记忆力,是你有没有真的在流量和故障里被锤过。今天借“每日一道面试题 12”这个场景,聊聊我自己的理解,以及我在订单链路里做过的事。
1. 先别背定义:熔断和降级分别保护的是谁
这里先给一个最直白的结论:熔断做的是资源保护,降级做的是业务兜底。一个是机制,一个是策略。
很多人把熔断理解成“超时后的兜底”,把降级理解成“故障时返回默认值”,方向对,但没有触及关键。熔断保护的是调用方自己的资源,或者说保护整个调用链路的稳定性;降级保护的是最终用户体验,是在不可用场景下用一种有损的方式继续提供服务。
1.1 熔断是资源保护机制,降级是业务兜底策略
可以打个比方。你是前台,隔壁部门系统总是卡住。熔断是你给自己设的规则:如果连续 5 次挂掉,接下来 10 分钟内我不再主动找它,所有请求直接说“稍后再办”;降级是你在给访客答复时的策略:对方系统挂了,我就先给访客递一张手工登记表,而不是让访客在柜台前面干等。前者保护你自己的工作节奏,后者保证服务窗口还能运转。
具体到微服务里,熔断器通过状态机控制对下游服务的调用:正常情况下正常调用,异常率达到阈值后直接短路,后续请求快速失败,不再继续消耗连接池和线程池;降级则是当调用失败、超时、熔断触发或者人工主动切换时,执行一段兜底逻辑,比如返回缓存数据、默认值、空列表。
两者的关系用一张表可以看得比较清楚:
| 对比项 | 服务熔断 | 服务降级 |
|---|---|---|
| 核心目的 | 保护调用方进程/线程不被下游故障拖垮 | 故障发生时主流程仍能返回可用结果 |
| 触发方式 | 错误率、慢调用率、并发异常达到阈值后自动触发 | 可由熔断、异常、超时触发,也可以人工主动触发 |
| 典型行为 | 快速失败,拒绝继续调用下游 | 执行 fallback:缓存、默认值、空结果 |
| 粒度 | 通常针对某个下游服务/接口/资源 | 通常针对业务流程里的某个功能点 |
| 状态 | 有状态:闭路-开路-半开 | 无强状态,更像规则/策略 |
1.2 两者常被混在一起,是因为工程上经常配套出现
Hystrix、Resilience4j、Sentinel 这些框架里,熔断之后通常会走到 fallback,所以很多人以为“熔断就是降级”。其实熔断器只负责让请求不要继续打到下游,fallback 才负责接下来怎么回应调用方。
如果去掉 fallback,熔断后接口会直接抛异常;加上 fallback,熔断后接口才变成了降级响应。这是两者最容易被搞混的地方:你在框架里看到的往往是一个组合能力,不是同一个概念。
所以面试时不要只说一句话,要能拆开讲:熔断是“我不能等你了”,降级是“虽然等不到你,但我换个方式把活干完”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 熔断的状态机与阈值计算:为什么不能只靠超时
我最早搭订单详情接口的时候,只做了超时控制,没有加熔断。上线一段时间后发现,超时控制挡不住资源耗尽。这里面的逻辑,很多人没想透。
2.1 Closed / Open / Half-Open 三种状态在回答什么问题
熔断器的三个状态,其实分别回答三个问题:能不能调、要不要停、能不能恢复。
- Closed 闭路:下游正常,请求正常放行,维护滑动窗口里的成功、失败、慢调用数据。
- Open 开路:下游异常达到阈值,后续请求不再调用下游,直接失败或走降级逻辑,持续一段冷却时间。
- Half-Open 半开:冷却时间结束后,放少量探测请求进去。如果探测成功,说明下游恢复了,切回 Closed;如果探测失败,重新切回 Open,继续等待。
设计意图很直白:下游故障不能只靠调用方无限重试。Open 状态下新请求根本不进下游,资源被释放,系统才有机会自愈。Half-Open 状态则是为了避免全量流量冲进来把正在恢复的下游再次打挂。
以 Resilience4j 为例,一个比较典型的配置是这样:
yaml复制resilience4j.circuitbreaker:
configs:
default:
slidingWindowSize: 100
minimumNumberOfCalls: 10
failureRateThreshold: 60
slowCallRateThreshold: 60
slowCallDurationThreshold: 800ms
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 5
这里需要注意,错误率和慢调用率是两套指标。推荐服务如果只是响应慢但没报错,光盯着异常率发现不了问题,必须配合慢调用比例。
2.2 阈值不能拍脑袋,要根据流量和正常基线算
我之前踩过一个典型的坑:把 minimumNumberOfCalls 设成 10,窗口 10 秒,QPS 稍微低一点的服务一天到晚误入 Open 状态。因为正常情况下 10 秒内可能就 10 来个请求,偶发一次超时,失败率直接变成 10% 甚至更高,熔断器当场打开。
后来我调整的思路是这样:
- minimumNumberOfCalls 建议覆盖正常单窗口请求量的 20%-30%。比如 10 秒窗口约 1000 个请求,minimumNumberOfCalls 可以放到 100;如果服务本身 QPS 低,就相应调小,但不要小于 20。
- failureRateThreshold 要在正常错误率基础上留出余量。正常错误率 1% 的服务,阈值可以设在 20%-30%,不要设在 5%,否则一个第三方抖动就误伤。
- waitDurationInOpenState 要大于下游平均恢复时间的一到两轮。如果是数据库连接池被打满,恢复可能需要 1-2 分钟,冷却时间只有 10 秒,半开探测大概率还是会失败。
- permittedNumberOfCallsInHalfOpenState 通常设置为单机 QPS 的 1%-5%,同时不要超过下游连接池或线程池大小。
为什么不能只靠超时?因为超时只是让当前请求不再等待,但如果调用已经占用了线程池和连接池,资源控制权还是在下游手里。打个比方,假设下游每个请求要处理 3 秒,客户端超时设了 2 秒,Tomcat 200 个线程在高并发下仍然会在 2 秒内全部被占满,因为每个请求在超时前都占着一个线程。熔断的意义在于 Open 状态直接短路,新请求根本不会进入下游,连接池和线程池立刻释放。
3. 我项目里的两类降级实践:主动降级和被动降级
降级在很多人印象里是故障时才做的被动操作,其实我在项目里用得更多是主动降级。
3.1 主动降级:流量高峰前先把非核心功能摘掉
之前做秒杀活动时,商品详情页里挂着推荐模块、优惠券模块、品牌故事模块。优惠券计算依赖会员标签服务,平时很稳,但活动期间会员服务压力很大。我们提前在配置中心里写了一个降级开关,活动开始前 30 分钟把优惠券模块摘掉,改成展示固定文案,等会员服务 QPS 回落后再恢复。
这不是故障逃生,是容量规划的一部分。既然已经预判到某个服务会超载,为什么还要让它硬扛?主动降级的核心是先想清楚:某个功能不展示,业务还能不能成立。能成立,就提前降级;不能成立,就要把容量保障和应急预案做好。
很多团队把降级做成“出事了再改配置”,但到了出事的时刻,运维同学可能连配置中心都登录不进去。更好的做法是把降级开关提前埋好,常态化演练,在故障时只需要一秒钟切换。
3.2 被动降级:异常、超时、熔断时走 fallback
被动降级就是常说的 fallback。比如用户打开订单详情页,需要查优惠券;如果优惠券服务超时了,不能让整个页面白屏。
我用 Sentinel 的资源定义做过一个比较典型的降级方法:
java复制@SentinelResource(
value = "queryUserCoupon",
fallback = "queryUserCouponFallback",
blockHandler = "queryUserCouponBlock"
)
public CouponDTO queryUserCoupon(Long userId) {
return couponFeignClient.queryByUserId(userId);
}
public CouponDTO queryUserCouponFallback(Long userId, Throwable ex) {
log.warn("queryUserCoupon degraded, userId={}, error={}", userId, ex.getMessage());
return CouponDTO.empty();
}
public CouponDTO queryUserCouponBlock(Long userId, BlockException ex) {
return CouponDTO.empty();
}
这里要注意一点:fallback 和 blockHandler 在 Sentinel 里分工不同。fallback 主要兜业务异常,blockHandler 处理流控、熔断这类 BlockException。区分开的好处是,熔断触发时你可以知道是规则拦截,业务异常时你可以看到真实报错,不会把限流和系统错误混在一起排查。
降级方法里有一件重要的事:一定要打日志,一定要上报指标。不要静默地返回一个空对象,否则降级时间长了,业务方收到错误数据都发现不了。我们一般在降级方法里记录 warn 日志,同时上报一个 degraded 指标,监控平台能看到某个资源在持续触发降级。
3.3 并非所有功能都能降级:先回答三个问题
不是所有功能被摘掉都无损。商品详情页的推荐模块可以隐藏,用户昵称可以用默认头像替代,但价格、库存、余额这类资金和履约相关数据,绝对不能随随便便降级成“成功”或“无数据”。
判断一个功能能不能降级,我一般会问三个问题:
- 如果这里返回默认值,用户会不会被误导?
- 如果这里直接不做,主流程还能不能继续往前推进?
- 如果后续要恢复,能不能保证数据一致性?
比如支付回调,如果支付结果确认失败,绝不能在业务上降级为“支付成功”;顶多提示用户稍后查询。如果把不可降级的东西降级了,短期看接口不报错,长期看一定会出资损或客诉。
4. 一次订单详情页故障复盘:推荐服务超时拖垮了主接口
概念说再多,不如看一次真实故障。这个案例我复盘过很多次,也是我后来在面试里重点讲的内容。
4.1 故障现象和排查链路
有一天下午,订单详情接口的 P99 突然从 200ms 涨到 3.8 秒,错误率到了 4%。第一反应是看慢调用路径,然后发现是推荐服务里的第三方接口平均响应时间到了 2.4 秒。
当时的依赖链路大概是这样的:
- 订单服务查询基础订单数据,耗时 50ms;
- 并行调用用户服务拿昵称头像,耗时 100ms;
- 并行调用物流服务查轨迹,耗时 80ms;
- 并行调用推荐服务查相关商品,耗时 2.4 秒。
订单详情接口是同步等这三个并行调用全部执行完才返回的。也就是说,推荐服务一个 2.4 秒,就决定了整个接口的 P99。更麻烦的是,推荐服务自己的线程池 50 个线程全部被打满,订单服务到推荐服务的 HTTP 连接池 50 个连接也被占满。新请求进来后连接拿不到,继续排队等待,最终订单服务的 Tomcat 线程也被耗尽,健康检查开始失败,网关超时开始增多。
从本质上看,这不是推荐服务单独故障,而是慢依赖通过同步阻塞传导到了上游。所谓雪崩,就是每个请求都卡在一个依赖上,调用方线程被慢慢吃掉。
4.2 熔断和降级上线后的对比
故障复盘后,我们做了两件事。第一,给推荐服务调用加了熔断规则;第二,通过 fallback 返回一个空的推荐列表,前端拿到空列表后不渲染推荐模块。
| 项 | 改造前 | 改造后 |
|---|---|---|
| 推荐服务调用失败策略 | 超时 3s + 重试 1 次 | 熔断 + fallback 隐藏推荐模块 |
| 订单详情 P99 | 3.8s | 220ms |
| 订单服务活跃线程峰值 | 120 | 45 |
| 第三方下游调用量 | 高峰仍打满 | Open 状态直接拦截,显著下降 |
| 用户可感知 | 页面偶尔白屏 | 推荐区域隐藏,主数据正常 |
熔断规则当时是这样设置的:异常比例和慢调用比例阈值 60%,滑动窗口 10 秒,minimumNumberOfCalls 20,waitDurationInOpenState 15 秒,半开状态下放 5 个请求探测。fallback 里返回空推荐列表的同时,还会在响应里带一个 moduleStatus 字段,前端判断到这个字段是 degraded,就把推荐区域静默隐藏,不展示异常提示。
这个案例能说明一个核心观点:熔断解决的是“别再继续打故障依赖”,降级解决的是“主流程还能不能给用户一个可用的页面”。两者配合,才能真正止住雪崩。
5. 落地时容易踩的五个坑,以及我现在的参数选择
这段内容不是教科书里写的,是我一个个踩出来的。每个坑背后都是一段要写故障报告的回忆。
5.1 坑一:把熔断阈值设得过于敏感
第一个坑是“为了安全”把阈值设得很低,比如异常率 5% 就熔断。结果下游只是偶发抖动了一次,接口就被熔断半天。熔断触发后会影响可用性,所以阈值一定要参考正常的错误基线和慢调用基线。我的经验是:正常错误率 1% 的服务,熔断阈值至少放到 20%-30%;正常慢调用比例比较高的服务,慢调用阈值也要相应放宽。
5.2 坑二:half-open 参数拍脑袋
半开状态是恢复窗口,放多少探测请求进去非常关键。放太少,比如只放 1 个,一次偶发失败就会重新打开,恢复能力很差;放太多,下游刚恢复时瞬间涌入大量请求,又把下游压垮。
我现在一般把 permittedNumberOfCallsInHalfOpenState 设置成单机 QPS 的 1%-5%,同时不会超过下游连接池或者线程池大小。waitDurationInOpenState 不会设得太短,至少覆盖下游一次健康检查加资源回收的周期,比如 15 秒到 30 秒。
5.3 坑三:fallback 里又调同一个下游
这是最隐蔽的坑。有些同学写 fallback 时,为了数据完整,在降级方法里又去查了一遍同一个接口,或者通过另一个入口调了同一个服务。结果熔断器 Open 后,流量照样穿透到下游,熔断形同虚设。
fallback 里要做的是本地缓存、静态配置、默认值,而不是远程调用。如果确实需要下游数据,也应该在调用前就准备好本地副本,比如 Caffeine 缓存或者配置中心的下发结果。
5.4 坑四:降级吞掉异常,失去排查能力
降级方法里只 return 一个默认值,不打日志、不上报指标,是最常见的问题。结果就是线上降级了很久都没有人发现,等发现时用户已经产生了大量投诉。
我现在的做法是:每个降级方法第一行打 warn 日志,记录 resource、userId、异常信息;同时通过 Prometheus 暴露一个 degraded 计数器。监控面板上能看到每个资源在什么时间段触发了多少次降级,配合告警规则,降级时间超过 5 分钟就触发告警。
5.5 坑五:只加熔断,不配链路超时和线程池隔离
熔断是保护手段,但不是唯一手段。如果下游接口没有合理的超时,请求仍然会占着线程等很久,熔断前的资源损耗已经发生了。如果线程池或信号量隔离做得不好,一个慢依赖照样能拖垮整个服务。
所以我的建议是:调用下游时先设合理的超时,再给关键资源做线程池隔离或者信号量隔离,最后再上熔断和降级。四者配合,不是选一个就完事。
6. 面试官想听到的回答链路:从概念到复盘再到细节
面试里遇到这种题,很多人的问题是太急着背框架。Hystrix 三个状态背得再熟,如果没有真实的故障或项目场景支撑,面试官会觉得你只是在背书。
6.1 一个高质量回答的推进顺序
如果让我重新回答这道题,我会按这个顺序来:
先给一句结论:“熔断是资源保护机制,降级是业务兜底策略。熔断解决的是不要再打故障依赖,降级解决的是调用方还能不能给用户一个可用结果。”
再讲区别:“熔断有状态,有阈值触发;降级是动作,可以由熔断触发,也可以主动触发。框架里熔断后自动走 fallback,是因为把两者组合起来了,不代表它们是一件事。”
然后上项目案例:“我在订单详情页碰到过推荐服务慢查询导致接口 P99 从 200ms 涨到 3.8s 的事故,给推荐服务调用加了熔断规则,fallback 返回空推荐列表,前端隐藏模块,P99 回落到 220ms。”
最后讲细节:“阈值怎么设的、半开探测放多少、fallback 不打日志会造成什么后果、哪些业务不能降级。这些才是面试官想听的。”
6.2 几个容易被追问的细节
面试官通常会顺着项目经验继续追问:
- 降级和熔断哪个先发生?正常是先有异常判断或熔断触发,再进入降级;主动降级也可以不需要熔断。
- 熔断打开后,下游恢复了怎么办?依靠 half-open 探测,用少量真实请求确认下游恢复,避免全量流量突然进来。
- fallback 返回 null 行不行?可以,但要明确告诉调用方这是降级后的结果,不能和正常情况下的空数据混在一起。我习惯返回带状态标记的对象。
每次面试提到这道题,我最深的感受是:能把概念讲清楚的人不少,能把自己线上故障的依赖图、阈值、恢复链路讲清楚的人不多。如果你准备这道题,建议把你们项目里最近一次抖动翻出来,画出依赖关系,标出哪些服务可降级、哪些不能,再把参数和告警规则过一遍。这套连贯的实战回答,比背 100 个概念都值钱。
