服务容错设计:熔断/降级/限流/重试/超时,Hystrix 到 Sentinel 的演进
问题
微服务中服务容错包含哪些核心手段?Hystrix 和 Sentinel 的核心区别是什么?为什么 2025 年大家都在用 Sentinel 而不是 Hystrix?
一、容错的五大手段
微服务架构下,一个请求可能跨越 5-10 个服务,任何一个环节出问题都会导致整条链路失败。容错的关键是防止单点故障扩散成雪崩。
1. 熔断(Circuit Breaker)
原理:对下游调用设置一个失败率阈值(如 50%),当一定时间窗口内失败率超过阈值,熔断器从"关闭"切换到"打开"状态,后续请求直接快速失败(不发起实际调用)。经过休眠时间(如 5 秒)后进入"半开"状态,尝试放行一个请求,成功则关闭熔断器,失败则重新打开。
三种状态:CLOSED → OPEN → HALF_OPEN → CLOSED
// 使用 Resilience4j 实现熔断
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% 失败率触发熔断
.slidingWindowSize(10) // 滑动窗口统计 10 个请求
.minimumNumberOfCalls(5) // 最少 5 个请求才开始统计
.waitDurationInOpenState(Duration.ofSeconds(5)) // 5 秒后进入半开
.build();
CircuitBreaker breaker = CircuitBreaker.of("userService", config);2. 降级(Degradation / Fallback)
原理:熔断触发后,不直接抛异常,而是执行 fallback 逻辑——返回缓存数据、默认值、空结果。核心目标是让主流程不受影响,哪怕降级后的结果不是最优的。
// Sentinel 的降级配置
@SentinelResource(
value = "getUserById",
fallback = "getUserByIdFallback",
blockHandler = "getUserByIdBlock"
)
public User getUserById(Long id) {
return userService.getUser(id);
}
// 熔断后的降级逻辑
public User getUserByIdFallback(Long id, Throwable e) {
return new User(id, "unknown", "fallback user");
}
// 限流后的处理逻辑
public User getUserByIdBlock(Long id, BlockException e) {
return new User(id, "limited", "rate limited");
}3. 限流(Rate Limiting)
原理:控制单位时间内进入系统的请求数,防止突增流量打垮服务。常用算法有三种:
- 令牌桶:固定速率往桶里放令牌,请求拿走令牌才能执行,允许短时突发
- 漏桶:请求以固定速率流出,超出桶容量则丢弃,强制平滑流量
- 滑动窗口:把时间切成多个小窗口,统计当前窗口内的请求数,超过阈值则拒绝
// Sentinel 限流规则(QPS 模式)
FlowRule rule = new FlowRule();
rule.setResource("getUserById");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100); // 每秒最多 100 个请求
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 快速失败
FlowRuleManager.loadRules(Collections.singletonList(rule));4. 重试(Retry)
原理:对临时性失败(网络超时、503 Service Unavailable)自动重试。但重试是最容易出问题的容错手段——重试放大流量可能导致雪崩。
关键原则:
- 配合幂等性:同一个请求重试多次,结果必须一致
- 退避策略:指数退避 + 随机抖动,避免重试请求同时打过来
- 只重试网络层超时:connectTimeout 可重试,readTimeout 不重试(慢的是下游处理,不是网络)
// Spring Retry 配置
@Retryable(
retryFor = {TimeoutException.class, ConnectException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 200, multiplier = 2, maxDelay = 1000)
)
public User getUserWithRetry(Long id) {
return restTemplate.getForObject(url, User.class);
}5. 超时(Timeout)
原理:每个请求必须设置明确的超时时间,防止线程长时间挂起等待。分为连接超时(TCP 建立连接的时间)和读取超时(等待响应的时间)。
// 合理的超时配置
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(500); // 500ms 连不上就放弃
factory.setConnectionRequestTimeout(200); // 从连接池获取连接的超时
factory.setReadTimeout(1000); // 1 秒内没返回就放弃
RestTemplate restTemplate = new RestTemplate(factory);二、Hystrix vs Sentinel 深度对比
| 维度 | Hystrix(Netflix) | Sentinel(Alibaba) |
|---|---|---|
| 隔离机制 | 线程池隔离(每个依赖一个独立线程池) | 信号量隔离 + 滑动窗口统计 |
| 性能开销 | 高(线程切换 + 线程池管理) | 低(Hystrix 的 1/10) |
| 实时统计 | 环形缓冲区(固定 10 秒桶) | 滑动窗口(精度可配置,毫秒级) |
| 规则动态变更 | 不支持(需重启) | 支持(Nacos/Etcd 实时推送) |
| 限流算法 | 不支持限流 | 内置令牌桶、漏桶、Warm Up |
| 维护状态 | 2018 年进入维护模式 | 活跃维护,社区活跃 |
Hystrix 的线程池隔离:每个依赖一个独立的线程池(默认 10 个线程),调用通过线程池执行。优点是隔离性强——一个依赖的线程池满了不会影响其他依赖。缺点也很明显:线程切换开销大,每个请求多一次线程上下文切换,CPU 敏感型场景下影响显著;线程池配置复杂——太小导致请求排队,太大失去隔离意义。
Sentinel 的信号量隔离:不创建独立线程池,使用信号量(Semaphore)控制并发数,请求在当前线程执行。配合滑动窗口做实时统计,性能是 Hystrix 的 10 倍以上。
三、熔断与限流的协同配合
熔断是被动防御——下游已经出问题了才触发保护;限流是主动防御——提前限制流量防止下游被打挂。
生产最佳实践是"限流在前 + 熔断在后":
网关层限流 → 服务 A → 熔断器 → 服务 B- 网关层限流:挡住超出集群整体处理能力的流量(如 1000 QPS 上限)
- 服务间熔断:针对特定依赖做容错(如 B 挂了,A 的熔断器打开,快速降级)
四、Sentinel 滑动窗口算法详解
Sentinel 的滑动窗口是高频面试考点。核心原理:
- 把 1 秒的统计周期切成 2 个 500ms 的窗口(可配置,粒度越细精度越高)
- 每个窗口记录:请求数、异常数、总耗时
- 滑动时:丢弃过期窗口,创建新窗口
- 统计时:汇总当前时间所在窗口及之前窗口的数据
// Sentinel 滑动窗口核心逻辑(简化版)
public class SlidingWindow {
private final AtomicReferenceArray<Window> windows;
private final int windowLengthMs; // 单个窗口时长(ms)
private final int intervalMs; // 总统计周期(ms)
public long passQps() {
long count = 0;
for (int i = 0; i < windows.length(); i++) {
Window w = windows.get(i);
if (isWindowInCurrentInterval(w)) {
count += w.passCount();
}
}
return count;
}
public void addPass() {
Window current = getCurrentWindow();
current.addPass();
}
}相比 Hystrix 的环形缓冲区(固定 10 个 1 秒桶,无法调整精度),Sentinel 的滑动窗口更灵活:窗口个数和时长都可配置,精度更高,内存占用更低。
五、重试的坑:一个真实事故
某团队给下游订单服务配置了 3 次重试 + 指数退避(200ms → 400ms → 800ms)。一个慢 SQL 查询原本需要 1 秒,但重试使请求变成 4 次(1 次原始 + 3 次重试),流量放大 4 倍。下游服务被重试流量打满,触发熔断,熔断后又触发其他服务的重试,最终导致雪崩。
正确的重试策略:
- 只在连接超时(connectTimeout)时重试,读取超时(readTimeout)不重试
- 重试次数限制在 1-2 次,不超过 3 次
- 必须配合退避(backoff)和抖动(jitter)
- 下游服务必须幂等
六、生产级配置示例
# 一个完整的服务容错配置
resilience4j:
circuitbreaker:
configs:
default:
failure-rate-threshold: 50
sliding-window-size: 10
minimum-number-of-calls: 5
wait-duration-in-open-state: 5s
permitted-number-of-calls-in-half-open-state: 3
retry:
configs:
default:
max-attempts: 3
wait-duration: 200ms
retry-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
timelimiter:
configs:
default:
timeout-duration: 1s
cancel-running-future: true总结
- 容错五大手段各有分工:超时是底线,熔断是快速失败,降级是保底返回,限流是主动防护,重试是恢复手段
- Hystrix 已被 Sentinel 全面取代,原因就两点:性能(10 倍差距)和动态规则(不需要重启)
- 熔断和限流要配合使用,不是二选一
- 重试是双刃剑,不做好幂等和退避就是雪崩加速器
- Sentinel 滑动窗口比 Hystrix 环形缓冲区更精确,是 2025 年的事实标准