Skip to content

服务容错设计:熔断/降级/限流/重试/超时,Hystrix 到 Sentinel 的演进

问题

微服务中服务容错包含哪些核心手段?Hystrix 和 Sentinel 的核心区别是什么?为什么 2025 年大家都在用 Sentinel 而不是 Hystrix?

一、容错的五大手段

微服务架构下,一个请求可能跨越 5-10 个服务,任何一个环节出问题都会导致整条链路失败。容错的关键是防止单点故障扩散成雪崩

1. 熔断(Circuit Breaker)

原理:对下游调用设置一个失败率阈值(如 50%),当一定时间窗口内失败率超过阈值,熔断器从"关闭"切换到"打开"状态,后续请求直接快速失败(不发起实际调用)。经过休眠时间(如 5 秒)后进入"半开"状态,尝试放行一个请求,成功则关闭熔断器,失败则重新打开。

三种状态CLOSED → OPEN → HALF_OPEN → CLOSED

java
// 使用 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 逻辑——返回缓存数据、默认值、空结果。核心目标是让主流程不受影响,哪怕降级后的结果不是最优的

java
// 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)

原理:控制单位时间内进入系统的请求数,防止突增流量打垮服务。常用算法有三种:

  • 令牌桶:固定速率往桶里放令牌,请求拿走令牌才能执行,允许短时突发
  • 漏桶:请求以固定速率流出,超出桶容量则丢弃,强制平滑流量
  • 滑动窗口:把时间切成多个小窗口,统计当前窗口内的请求数,超过阈值则拒绝
java
// 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 不重试(慢的是下游处理,不是网络)
java
// 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 建立连接的时间)和读取超时(等待响应的时间)。

java
// 合理的超时配置
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. 把 1 秒的统计周期切成 2 个 500ms 的窗口(可配置,粒度越细精度越高)
  2. 每个窗口记录:请求数、异常数、总耗时
  3. 滑动时:丢弃过期窗口,创建新窗口
  4. 统计时:汇总当前时间所在窗口及之前窗口的数据
java
// 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)
  • 下游服务必须幂等

六、生产级配置示例

yaml
# 一个完整的服务容错配置
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 年的事实标准

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。