服务熔断与降级机制

候选人小张在面试字节订单服务团队时,面试官问:"什么是熔断?Hystrix 的熔断器有几种状态?"

小张说:"熔断就是防止雪崩..." 面试官追问:"那 Hystrix 的熔断器状态转换是怎样的?从 CLOSED 到 OPEN 是什么条件触发的?"

小张说:"错误率超过阈值..." 面试官继续追问:"从 OPEN 到 HALF_OPEN 呢?HALF_OPEN 状态下,如果请求成功会怎样?"

小张支支吾吾答不上来。

面试官又问:"Sentinel 的滑动窗口和 Hystrix 有什么区别?Sentinel 的五种流量整形策略是什么?"

小赵彻底卡住。

【面试官心理】

这道题我用来测试候选人对熔断器核心原理的理解。熔断是微服务架构中最核心的容错机制之一,Hystrix 和 Sentinel 是两个主流实现。能说出状态转换的占 50%,能解释滑动窗口和流量整形的只有 15%。这道题能筛选出真正理解熔断器原理的候选人。

一、为什么需要熔断 🔴

1.1 雪崩效应

graph TB
    A[服务 A] --> B[服务 B]
    A --> C[服务 C]
    B --> D[服务 D]
    C --> D
    D -->|数据库超时| E[D 不可用]
    E -.->|请求堆积| B
    E -.->|请求堆积| C
    B -.->|资源耗尽| A
    C -.->|资源耗尽| A

    style E fill:#ff6b6b
    style B fill:#ff6b6b
    style C fill:#ff6b6b

一个服务的故障,引发连锁反应,最终导致整个系统不可用。

1.2 熔断的核心思想

熔断器:当检测到下游服务不可用时,立即"熔断",不再调用下游,直接返回降级结果,防止请求堆积和资源耗尽。

正常状态:
  A → B → C → D → 正常返回

熔断触发后:
  A → B → [熔断器打开] → 直接降级返回

二、Hystrix 熔断器原理 🔴

2.1 熔断器的三种状态

graph TB
    A[CLOSED<br/>关闭状态] -->|错误率/超时 > 阈值| B[OPEN<br/>打开状态]
    B -->|等待时间<br/>超过 sleepWindow| C[HALF_OPEN<br/>半开状态]
    C -->|请求成功| A
    C -->|请求失败| B
    C -->|请求超时| B

    A -->|手动触发| D[FORCE_OPEN<br/>强制打开]
    D -->|手动触发| A
状态说明行为
CLOSED熔断器关闭,正常调用统计成功/失败/超时,错误率超标则打开
OPEN熔断器打开,快速失败所有请求直接降级,不调用实际服务
HALF_OPEN熔断器半开,试探恢复允许一个请求通过,成功则关闭,失败则打开

2.2 HystrixCircuitBreaker 源码解析

// HystrixCircuitBreaker.java - 熔断器核心接口
public interface HystrixCircuitBreaker {
    // 是否允许请求通过
    boolean allowRequest();

    // 是否熔断器打开
    boolean isOpen();

    // 标记熔断器打开
    void open();

    // 标记请求成功
    void markSuccess();

    // 标记请求失败
    void markFailure();
}

// HystrixCircuitBreakerImpl.java - 熔断器实现
public class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
    // 配置参数
    private final HystrixCommandProperties properties;

    // 熔断器状态(默认 CLOSED)
    private final AtomicReference<HystrixCircuitBreaker.State> state;

    public enum State {
        CLOSED,    // 关闭
        OPEN,      // 打开
        HALF_OPEN  // 半开
    }

    @Override
    public boolean allowRequest() {
        // 1. 如果是 FORCE_OPEN,直接拒绝
        if (properties.circuitBreakerForceOpen().get()) {
            return false;
        }

        // 2. 如果是 FORCE_CLOSED,允许所有请求
        if (properties.circuitBreakerForceClosed().get()) {
            return true;
        }

        // 3. 检查状态
        if (state.get() == CLOSED) {
            return true;  // 关闭状态,允许请求
        }

        if (state.get() == OPEN) {
            // 检查是否过了熔断时间
            if (durationInOpenState() >= properties.circuitBreakerSleepWindow().get()) {
                // 进入 HALF_OPEN 状态
                if (state.compareAndSet(OPEN, HALF_OPEN)) {
                    circuitOpened.set(System.currentTimeMillis());
                }
                return true;  // 半开状态,允许一个请求通过
            }
            return false;  // 还在熔断窗口内,拒绝请求
        }

        return false;
    }

    @Override
    public void markSuccess() {
        if (state.get() == HALF_OPEN) {
            // HALF_OPEN 状态下,请求成功,关闭熔断器
            state.set(CLOSED);
            // 重置计数器
            metrics.reset();
        }
    }

    @Override
    public void markFailure() {
        if (state.get() == CLOSED) {
            // 在 CLOSED 状态下,标记失败
            metrics.recordFailure();
        }

        // 在 HALF_OPEN 状态下,任何失败都重新打开熔断器
        if (state.get() == HALF_OPEN) {
            circuitOpened.set(System.currentTimeMillis());
            state.set(OPEN);
        }
    }
}

2.3 熔断触发条件

// HystrixCommandProperties.java - 熔断器配置
@HystrixProperty(name = "circuitBreaker.enabled", value = "true")           // 启用熔断
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20") // 最小请求数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000") // 熔断持续时间
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50") // 错误率阈值

/*
 * 熔断触发条件:
 *
 * 1. requestVolumeThreshold = 20
 *    必须至少收到 20 个请求,才会判断是否熔断
 *    (避免统计样本太少导致的误判)
 *
 * 2. errorThresholdPercentage = 50
 *    当 20 个请求中,失败率超过 50% 时,触发熔断
 *    即 10 个以上请求失败/超时/异常
 *
 * 3. sleepWindowInMilliseconds = 5000
 *    熔断打开后,持续 5 秒
 *    5 秒后,允许一个请求通过(HALF_OPEN)
 *    如果成功则关闭熔断器
 *    如果失败则继续打开 5 秒
 */

// 统计失败率
// HystrixCircuitBreakerImpl.java
public void markFailure() {
    // 将当前请求标记为失败
    metrics.markFailure();
}

// 定时检查是否需要熔断
// HealthCountsStream.java
public void checkCircuitBreaker() {
    long total = getTotal();
    long error = getErrorCount();

    if (total >= requestVolumeThreshold) {
        double errorPercentage = error * 100.0 / total;

        if (errorPercentage >= errorThresholdPercentage) {
            // 错误率超过阈值,打开熔断器
            circuitBreaker.open();
        }
    }
}

2.4 Hystrix 执行流程

graph TB
    A[HystrixCommand.execute] --> B{熔断器打开?}
    B -->|是| C{半开状态?}
    C -->|半开| D[允许请求通过]
    C -->|非半开| E[直接降级返回]
    B -->|否| F[线程池有空闲?]
    F -->|是| G[执行业务逻辑]
    F -->|否| H[超时降级<br/>或队列超时]
    G --> I{执行成功?}
    I -->|成功| J[标记成功<br/>统计健康]
    I -->|失败/超时| K[标记失败<br/>判断错误率]
    K -->|错误率>阈值| L[打开熔断器]
    K -->|正常| M[返回结果]

三、Sentinel 滑动窗口算法 🟡

3.1 Sentinel vs Hystrix 的核心区别

维度HystrixSentinel
统计方式滚动窗口(每 10 秒一个桶)滑动窗口(精确到毫秒)
熔断策略基于错误率/数量基于错误率/异常数/慢调用比例
限流策略线程数/QPSQPS/并发线程数/冷启动/匀速排队
配置方式代码注解控制台 + SDK
动态配置不支持支持(推模式)

3.2 LeapArray 滑动窗口

// LeapArray.java - Sentinel 的滑动窗口实现
public class LeapArray<T> {
    // 时间窗口参数
    private final int windowLengthInMs;   // 每个桶的时间长度
    private final int sampleCount;        // 桶的数量
    private final int intervalInMs;       // 总时间窗口 = windowLengthInMs * sampleCount

    // 实际存储的桶数组
    private final AtomicReferenceArray<Window<T>> array;

    // LeapArray 示例:
    // windowLengthInMs = 200ms
    // sampleCount = 5
    // intervalInMs = 1000ms (1秒)
    //
    // 滑动窗口示意:
    // |<-- 200ms -->|<-- 200ms -->|<-- 200ms -->|<-- 200ms -->|<-- 200ms -->|
    // |    Bucket0  |    Bucket1  |    Bucket2  |    Bucket3  |    Bucket4  |
    //                                                              ↑
    //                                                        当前时间
    //
    // 计算 QPS = (Bucket0 + Bucket1 + ... + Bucket4) / 1s
}

// WindowBucket.java - 单个时间桶
public class Window<T> {
    private long windowStart;  // 桶的开始时间
    private T value;           // 统计数据

    // value 可能是:
    // - MetricBucket: 请求总数、成功数、异常数、阻塞数、响应时间
    // - BucketWrap: 存储上述数据的包装类
}

3.3 Sentinel 熔断策略

// DegradeRule.java - Sentinel 熔断规则
// 支持三种熔断策略:

// 1. 基于异常比例熔断
@HystrixProperty(name = "degrade.max_exception_ratio", value = "0.3")
// 异常比例阈值:0.3 = 30%
// 当 1 秒内请求数 >= 5,且异常比例 >= 30% 时,熔断

// 2. 基于异常数熔断
@HystrixProperty(name = "degrade.max_exception_count", value = "10")
// 异常数阈值:10
// 当 1 秒内异常数 >= 10 时,熔断

// 3. 基于慢调用比例熔断
@HystrixProperty(name = "degrade.max_slow_ratio", value = "0.5")
@HystrixProperty(name = "degrade.max_rt", value = "1000")
// 慢调用比例阈值:0.5 = 50%
// 慢调用阈值:RT = 1000ms
// 当 1 秒内慢调用比例 >= 50% 时,熔断

// 配置示例
@PostConstruct
public void initRules() {
    List<DegradeRule> rules = new ArrayList<>();

    // 异常比例熔断
    DegradeRule rule1 = new DegradeRule("user-service")
        .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
        .setCount(0.3)  // 30% 异常比例
        .setMinRequestAmount(5)  // 最小请求数
        .setStatIntervalMs(1000)  // 统计时间窗口
        .setTimeWindow(10);  // 熔断持续 10 秒

    // 慢调用比例熔断
    DegradeRule rule2 = new DegradeRule("user-service")
        .setGrade(CircuitBreakerStrategy.SLOW_RT_RATIO.getType())
        .setCount(1000)  // 慢调用阈值 1000ms
        .setMinRequestAmount(5)
        .setSlowRatioThreshold(0.5)  // 50% 慢调用比例
        .setTimeWindow(10);

    rules.add(rule1);
    rules.add(rule2);

    DegradeRuleManager.loadRules(rules);
}

3.4 Sentinel 五种流量整形策略

// FlowRule.java - 限流规则
// 五种流量整形策略:

// 1. 直接拒绝(默认)
// 直接拒绝超过阈值的请求
FlowRule rule = new FlowRule("user-service")
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(100)  // QPS 上限 100
    .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    // 超出 100 QPS 的请求直接拒绝

// 2. 冷启动(Warm Up)
// 预热阶段逐步提升阈值
// 适用于秒杀等场景
FlowRule rule = new FlowRule("user-service")
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(100)
    .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)
    .setWarmUpPeriodSec(10);  // 预热时长 10 秒
    // 开始时 QPS = 100 / 10 = 10
    // 10 秒后逐渐升到 100

// 3. 匀速排队
// 超过阈值的请求排队等待
FlowRule rule = new FlowRule("user-service")
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(100)
    .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)
    .setMaxQueueingTimeMs(500);  // 最多排队 500ms
    // 如果 QPS = 1000,超出 900 个请求会排队
    // 每个请求间隔 10ms 释放
    // 排队超过 500ms 的请求会被拒绝

// 4. 预热 + 匀速排队
// 组合策略
FlowRule rule = new FlowRule("user-service")
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(100)
    .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER)
    .setWarmUpPeriodSec(10)
    .setMaxQueueingTimeMs(500);

// 5. 冷启动 + 匀速排队
// 基于漏桶算法的组合

四、线程池隔离 vs 信号量隔离 🟡

4.1 隔离策略对比

// Hystrix 隔离策略配置
@HystrixCommand(
    commandProperties = {
        // 信号量隔离
        @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
        // 信号量最大并发数
        @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10")
    },
    threadPoolProperties = {
        // 线程池隔离
        @HystrixProperty(name = "coreSize", value = "10"),
        @HystrixProperty(name = "maxQueueSize", value = "100"),
        @HystrixProperty(name = "keepAliveTimeMinutes", value = "1")
    }
)
public String callUserService() {
    return userClient.getUser();
}
维度线程池隔离信号量隔离
线程模型新线程执行调用主线程执行调用
开销线程创建/切换开销无额外线程开销
并发控制线程池大小控制信号量计数控制
超时控制支持(可设置超时)不支持
调用延迟额外延迟(线程切换)无额外延迟
适用场景有远程调用(HTTP/DB)本地方法调用/极其快速的方法
资源隔离好(线程池级别)差(仅计数)

4.2 ThreadPoolKey 的隔离

// 每个 HystrixCommandGroup 有一个 ThreadPoolKey
// 相同 ThreadPoolKey 的命令共享线程池

@HystrixCommand(
    groupKey = "UserGroup",
    threadPoolKey = "UserServicePool",  // 指定 ThreadPoolKey
    commandProperties = {
        @HystrixProperty(name = "coreSize", value = "10")
    }
)
public String getUser() {}

// ThreadPoolKey 相同,则共享线程池
@HystrixCommand(groupKey = "UserGroup", threadPoolKey = "UserServicePool")
public String getUserInfo() {}  // 和上面的命令共享线程池

五、生产最佳实践 🟡

5.1 熔断策略配置

# 生产环境熔断器配置
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: THREAD
          thread:
            timeoutInMilliseconds: 3000  # 超时时间略大于接口 RT
      circuitBreaker:
        enabled: true
        requestVolumeThreshold: 20       # 最小请求数
        sleepWindowInMilliseconds: 5000  # 熔断持续 5 秒
        errorThresholdPercentage: 50     # 50% 错误率触发熔断
      fallback:
        enabled: true

# Sentinel 配置
sentinel:
  transport:
    dashboard: localhost:8080
  rules:
    degrade:
      - resource: user-service
        grade: 2  # ERROR_RATIO
        count: 0.5  # 50% 异常比例
        minRequestAmount: 5  # 最小请求数
        timeWindow: 10  # 熔断持续 10 秒

5.2 降级策略设计

// 分级降级策略
public class GradedFallback {
    // 第一级降级:返回缓存数据
    public User fallbackFromCache(Long userId) {
        User cached = redisTemplate.opsForValue().get("user:" + userId);
        if (cached != null) {
            log.info("一级降级:从缓存返回 user={}", userId);
            return cached;
        }
        throw new NeedSecondFallbackException();
    }

    // 第二级降级:返回默认数据
    public User fallbackDefault(Long userId) {
        log.info("二级降级:返回默认 user={}", userId);
        User user = new User();
        user.setId(userId);
        user.setName("默认用户");
        user.setStatus("降级返回");
        return user;
    }
}

六、常见翻车现场 🔴

❌ 翻车点一:熔断阈值设置过低导致频繁熔断

// ❌ 错误:requestVolumeThreshold 设置过低
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "3")
// 只有 3 个请求就判断熔断,统计样本太少,容易误判

// ✅ 正确:设置足够的统计样本
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20")
// 至少 20 个请求后再判断熔断,统计更准确

❌ 翻车点二:Sentinel 熔断持续时间太短

// ❌ 错误:熔断持续时间太短
.setTimeWindow(1)  // 只熔断 1 秒就恢复
// 服务还没完全恢复,又被打挂了

// ✅ 正确:根据服务恢复时间设置
.setTimeWindow(30)  // 熔断 30 秒后再尝试

❌ 翻车点三:Sentinel 和 Hystrix 混用

<!-- ❌ 错误:同时引入两个依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
</dependency>

<!-- ✅ 正确:选择其中一个
     推荐 Sentinel,功能更丰富,配置更灵活
-->
⚠️

Hystrix 已停止维护(2021 年),生产环境强烈推荐使用 Sentinel。Sentinel 的滑动窗口算法比 Hystrix 的滚动窗口更精确,支持的熔断策略更丰富,且有活跃的社区维护。

【面试官心理】

这道题我通常从熔断器的状态转换开始,逐步深入到滑动窗口算法、流量整形策略、生产避坑。能说出三种状态的占 50%,能解释 Sentinel 滑动窗口的占 30%,能说出五种流量整形策略的只有 10%。熔断是微服务容错的核心,能把这些讲清楚的候选人对分布式系统的稳定性设计有较深的理解。