熔断器的代码都很短,短到让人以为它只是个 try/catch 加计数器。真正决定效果的是三个藏在参数里的决定:窗口多长、阈值多少、打开后多久试探。Hystrix 与 Sentinel 在这三处的默认值和语义并不一致,照抄参数往往得到"看起来在工作、实际没拦住"的熔断器。
1. 三态与滑动窗口:熔断先是一个统计问题
关闭(CLOSED)→ 打开(OPEN)→ 半开(HALF_OPEN)→ 关闭,是标准状态机。判断"要不要打开"靠滑动窗口里的健康统计,Hystrix 把这套统计做成 10 秒 10 个桶:
| 参数 | 默认 | 含义 |
|---|---|---|
metrics.rollingStats.timeInMilliseconds |
10000 | 统计窗口总长 |
metrics.rollingStats.numBuckets |
10 | 桶数,每桶 1 秒 |
circuitBreaker.requestVolumeThreshold |
20 | 窗口内最少请求数,不足不熔断 |
circuitBreaker.errorThresholdPercentage |
50 | 错误率阈值 |
circuitBreaker.sleepWindowInMilliseconds |
5000 | 打开后多久进半开 |
circuitBreaker.forceOpen |
false | 强制打开,排障用 |
requestVolumeThreshold=20 最常被忽略:窗口里只有 19 个请求时,全部失败也不熔断。低流量接口上这个默认值意味着"熔断器永远不工作",需按实际 QPS 调小。
numBuckets 与窗口长度必须整除,改窗口不改桶数会让桶宽变化,统计粒度变粗。
2. 线程池隔离 vs 信号量隔离
| 维度 | THREAD | SEMAPHORE |
|---|---|---|
| 执行位置 | 独立线程池 | 调用方线程 |
| 能否超时中断 | 能,Future.get(timeout) 后 cancel(true) |
不能,只能计数 |
| 上下文传递 | ThreadLocal 丢失,需 HystrixConcurrencyStrategy |
保留 |
| 开销 | 线程切换 + 队列 + 池维护 | 一个计数器 |
| 适用 | 网络调用、第三方 SDK | 内存计算、本地缓存 |
线程池能"超时中断"是它唯一的硬优势:下游 hang 住时可以把工作线程打断,把调用方线程还回去。代价是两次上下文切换,且线程池本身占内存——coreSize=20 的路由就是 20 个常驻线程。
信号量隔离做不到超时。信号量只保证"同时进入的调用不超过 N 个",一旦拿到许可的调用卡住,调用方线程也跟着卡住,timeoutInMilliseconds 形同虚设。
hystrix:
command:
default:
execution:
isolation:
strategy: THREAD
thread:
timeoutInMilliseconds: 800
circuitBreaker:
requestVolumeThreshold: 20
errorThresholdPercentage: 50
sleepWindowInMilliseconds: 5000
threadpool:
default:
coreSize: 20
maxQueueSize: 10
queueSizeRejectionThreshold: 10
maxQueueSize=-1(默认)时用的是 SynchronousQueue,池满立即 reject;设成正数才变成有界队列,此时 queueSizeRejectionThreshold 才是真正的排队上限。
3. @HystrixCommand 与 fallback 的边界
@HystrixCommand(
fallbackMethod = "queryFallback",
commandKey = "orderQuery",
groupKey = "order",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "800"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
})
public Order query(Long id) {
return orderClient.get(id);
}
private Order queryFallback(Long id, Throwable t) {
log.warn("fallback id={} cause={}", id, t.toString());
return Order.empty(id);
}
三条约束必须记住:
- fallback 方法签名要与原方法一致,末尾可多一个
Throwable参数; - fallback 与原方法必须在同一个类里,代理调用不生效;
- fallback 里再抛异常,异常会直接冒给调用方,
HystrixRuntimeException会包一层。
fallback 返回空对象时注意别让它在业务上变成合法值——降级数据被打上正常标记,比直接报错更难查。
4. Hystrix 停更之后
Hystrix 最后一个版本是 1.5.18(2018 年),Netflix 随后宣布进入维护模式,不再加新功能。Spring Cloud 侧的连锁反应:
| 时间点 | 变化 |
|---|---|
| 2018-11 | Netflix 宣布 Hystrix 进入维护模式 |
| Spring Cloud Greenwich | 官方推荐 Resilience4j,Hystrix 仍可用 |
| Spring Cloud 2020.0 | spring-cloud-starter-netflix-hystrix 移除 |
| 现在 | 新项目用 spring-cloud-starter-circuitbreaker-resilience4j |
Resilience4j 是函数式的,模块拆成 CircuitBreaker / RateLimiter / Bulkhead / TimeLimiter / Retry,可自由组合。缺点是它没有线程池隔离——Bulkhead 只有信号量语义,超时中断得靠 TimeLimiter + CompletableFuture。
5. Sentinel 的流量控制:先限流,再熔断
Sentinel 的定位比 Hystrix 宽:它先做流量整形,再做熔断降级。资源是一等公民,规则通过 FlowRule 描述:
FlowRule rule = new FlowRule("queryOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 1=QPS, 0=线程数
rule.setCount(100);
rule.setStrategy(RuleConstant.STRATEGY_DIRECT); // 直接 / 关联 / 链路
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(Collections.singletonList(rule));
| 概念 | 取值 | 语义 |
|---|---|---|
grade |
QPS / 线程数 | 按每秒通过量还是按并发占用 |
strategy |
直接 / 关联 / 链路 | 关联=资源 A 忙则限 B;链路=只限某入口 |
controlBehavior |
快速失败 / 预热 / 匀速排队 | 拒绝、冷启动爬坡、漏桶排队 |
count |
数值 | 阈值 |
线程数模式统计的是当前占用线程数而非排队长度,在慢接口上比 QPS 模式更稳。预热模式(WARM_UP)适合刚启动、缓存还是空的场景:count 被当作稳态值,前若干秒按比例放行。
6. 熔断、热点与系统自适应
Sentinel 的熔断有三种策略,语义比 Hystrix 的"错误率"细:
grade |
策略 | count 含义 |
额外参数 |
|---|---|---|---|
| 0 | 慢调用比例 | 慢调用占比阈值(0–1) | slowRatioThreshold=最大 RT(ms) |
| 1 | 异常比例 | 异常占比阈值(0–1) | minRequestAmount |
| 2 | 异常数 | 窗口内异常个数 | statIntervalMs |
DegradeRule slow = new DegradeRule("queryOrder")
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
.setCount(0.5) // 一半以上是慢调用就熔断
.setSlowRatioThreshold(200) // 超过 200ms 算慢
.setMinRequestAmount(10)
.setStatIntervalMs(10000)
.setTimeWindow(10); // 打开 10 秒
DegradeRuleManager.loadRules(List.of(slow));
慢调用比例是最实用的策略:它不依赖"异常",专治下游不报错但变慢。Hystrix 没有等价能力,只能用超时把慢变成异常,副作用是超时值必须设得比正常 RT 高很多。
热点参数限流(ParamFlowRule)按参数值单独计数,比如"同一个 userId 每秒最多 5 次",配合 @SentinelResource 的 blockHandler 返回友好错误。系统自适应保护(SystemRule)则用 load1、整体 RT、线程数、入口 QPS、CPU 使用率五个维度做兜底,阈值通常设成容量的 80% 左右。
7. 规则持久化:别把规则写在代码里
Sentinel 默认把规则放在内存,重启即丢。生产要用数据源:
@PostConstruct
public void init() {
Properties p = new Properties();
p.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
p.put(PropertyKeyConst.NAMESPACE, "prod");
ReadableDataSource<String, List<FlowRule>> ds = new NacosDataSource<>(
p, "sentinel-group", "order-flow-rules",
src -> JSON.parseObject(src, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(ds.getProperty());
}
| 数据源 | 类 | 说明 |
|---|---|---|
| Nacos | NacosDataSource |
最常用,支持 namespace/group |
| Apollo | ApolloDataSource |
键值监听,适合已有 Apollo 的团队 |
| ZooKeeper | ZkDataSource |
强一致场景 |
| 本地文件 | FileRefreshableDataSource |
单机兜底 |
规则推送到客户端是秒级延迟,改完阈值立刻观察,会看到"还有一小段时间按旧值走"。
8. 能力对照
| 能力 | Hystrix 1.5.18 | Sentinel | Resilience4j |
|---|---|---|---|
| 线程池隔离 | 有 | 无(只有并发数限制) | Bulkhead(信号量) |
| 超时中断 | 有 | 无 | TimeLimiter |
| 熔断策略 | 错误率 | 慢调用/异常比例/异常数 | 失败率/慢调用 |
| 限流 | 无(靠线程池) | QPS/线程数/热点/系统 | RateLimiter |
| 规则动态化 | Archaius | Nacos/Apollo/ZK | 配置中心 + 自定义 |
| 控制台 | Dashboard(已停更) | 独立控制台 | 无官方控制台 |
| 维护状态 | 停止 | 活跃 | 活跃 |
9. 排错:五种"配了但没生效"
| 现象 | 原因 | 处理 |
|---|---|---|
| 熔断从不触发 | 窗口内请求数 < requestVolumeThreshold |
按实际 QPS 调小到 5–10 |
| 熔断触发后立刻恢复 | sleepWindow 太短,半开放行又全失败 |
按下游恢复时间设 5–30 秒 |
| fallback 不执行 | fallback 与原方法不同类,或签名不匹配 | 同类别私有方法,参数一致 |
| Sentinel 规则重启丢失 | 只调了 loadRules,没接数据源 |
接 NacosDataSource 并 register2Property |
@SentinelResource 不生效 |
方法被内部调用,绕过代理 | 走 Bean 调用,或改用 SphU.entry |
| 限流后错误码是 500 | 未捕获 BlockException |
blockHandler 或全局异常处理返回 429 |
10. 边界:这些场景不该靠熔断解决
| 场景 | 更合适的手段 | 理由 |
|---|---|---|
| 下游持续慢但不出错 | 超时 + 连接池上限 + 慢调用熔断 | 熔断只能减少调用,不能提速 |
| 容量不足导致的排队 | 扩容 / 限流 | 熔断会把正常请求也拒掉 |
| 单机热点 key | 热点参数限流 / 本地缓存 | 全局熔断粒度太粗 |
| 上下游强一致事务 | 不做降级,直接失败回滚 | 降级数据会污染事务 |
| 消息消费重试 | 消费端重试 + 死信队列 | 熔断对拉取模型不适用 |
一句话选型:要线程级隔离与超时中断,Hystrix 那一代最直接但已停更;要限流、热点、系统保护与动态规则,Sentinel 覆盖面最宽;要在 Spring Cloud 2020+ 上零负担,Resilience4j 是默认答案。
11. 自测
- 窗口里只有 19 个请求、全部失败,Hystrix 为什么没熔断?
- 信号量隔离为什么做不到超时中断?
- 线程池隔离下
ThreadLocal为什么丢,怎么补? - Sentinel 的"线程数"模式与 Hystrix 信号量在语义上差在哪?
- 慢调用比例熔断的
count和slowRatioThreshold分别是什么? - 规则只调
loadRules不接数据源,重启后会发生什么?