如何实现高可用系统之“限流、熔断”

176 阅读7分钟

本系列是来源于一个场景的分析:“怎么设计高可用的系统,你会从什么方面考虑?”,本文着眼于高可用架构下的兜底策略,包含限流和熔断。

在前面已经分析了限流的算法限流方案及使用场景分析,接下来进一步说明在项目如何选择对应的限流策略和熔断分析以及对现有框架Sential的探讨。

限流

如何限流策略(略)

参考限流方案及使用场景分析中的几种方案分析,可以使用工具类或者自定义限流算法

Sentinel

流量控制(限流)

包括QPS和并发线程数;其中并发线程数指同时请求的线程数量。
流量控制(flow control),其原理是监控应用流量的 QPS并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性

流控效果

包括快速失败、warm up(预热/冷启动)、排队等待;对应元素controlBehavior

流控模式

统计类型

熔断策略

平均响应时间、异常比例、异常数

如何对异常进行降级处理

自适应限流

单台机器的 loadCPU 使用率平均 RT入口 QPS并发线程数

集群流控

集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

熔断

怎么实现自定义熔断器,参考到sentinel中的几种策略:平均响应时间、异常比例、异常数;如果调用的服务长时间没有响应,有可能是网络波动或者服务响应慢,上述策略变得不适用,接下来介绍基于失败数进行自定义熔断策略。

1、参考Sentinel的给出具体的例子

Sentinel 中,熔断降级策略(DegradeRule)主要分为以下三种类型:

  1. 基于响应时间(RT)
  2. 基于异常比例(Exception Ratio)
  3. 基于异常数(Exception Count)

每种策略都有不同的触发条件和适用场景。以下是具体策略和代码示例:


1. 基于响应时间(RT)

当请求的平均响应时间超过设定阈值,并且在一定时间窗口内的请求数量超过最小请求数(minRequestAmount),触发熔断。

配置示例:
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;

import java.util.Collections;

public class SentinelDegradeRTExample {
    public static void main(String[] args) {
        // 定义熔断规则
        DegradeRule rule = new DegradeRule();
        rule.setResource("testRT")
            .setGrade(DegradeRule.GRADE_RT) // 基于响应时间
            .setCount(50)                   // 平均响应时间超过 50 ms
            .setTimeWindow(10)              // 熔断时间 10 秒
            .setMinRequestAmount(5);        // 最小请求数

        // 加载规则
        DegradeRuleManager.loadRules(Collections.singletonList(rule));

        for (int i = 0; i < 20; i++) {
            try (Entry entry = SphU.entry("testRT")) {
                // 模拟业务逻辑
                Thread.sleep((i % 3 == 0) ? 100 : 30); // 部分请求延时过高
                System.out.println("Request passed: " + i);
            } catch (DegradeException e) {
                System.out.println("Request degraded: " + i);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2. 基于异常比例(Exception Ratio)

当资源在单位时间窗口内的异常比例(异常数 / 总请求数)超过设定值(count),触发熔断。

配置示例:
public class SentinelDegradeExceptionRatioExample {
    public static void main(String[] args) {
        // 定义熔断规则
        DegradeRule rule = new DegradeRule();
        rule.setResource("testExceptionRatio")
            .setGrade(DegradeRule.GRADE_EXCEPTION_RATIO) // 基于异常比例
            .setCount(0.5)                               // 异常比例超过 50%
            .setTimeWindow(5)                            // 熔断时间 5 秒
            .setMinRequestAmount(10);                    // 最小请求数

        // 加载规则
        DegradeRuleManager.loadRules(Collections.singletonList(rule));

        for (int i = 0; i < 20; i++) {
            try (Entry entry = SphU.entry("testExceptionRatio")) {
                // 模拟业务逻辑
                if (i % 2 == 0) { // 模拟异常请求
                    throw new RuntimeException("Simulated exception");
                }
                System.out.println("Request passed: " + i);
            } catch (DegradeException e) {
                System.out.println("Request degraded: " + i);
            } catch (Exception e) {
                System.out.println("Request failed: " + i);
            }
        }
    }
}

3. 基于异常数(Exception Count)

当资源在单位时间窗口内的异常总数超过设定值(count),触发熔断。

配置示例:
public class SentinelDegradeExceptionCountExample {
    public static void main(String[] args) {
        // 定义熔断规则
        DegradeRule rule = new DegradeRule();
        rule.setResource("testExceptionCount")
            .setGrade(DegradeRule.GRADE_EXCEPTION_COUNT) // 基于异常数
            .setCount(5)                                // 异常数超过 5
            .setTimeWindow(10);                         // 熔断时间 10 秒

        // 加载规则
        DegradeRuleManager.loadRules(Collections.singletonList(rule));

        for (int i = 0; i < 20; i++) {
            try (Entry entry = SphU.entry("testExceptionCount")) {
                // 模拟业务逻辑
                if (i < 10) { // 前 10 次请求抛异常
                    throw new RuntimeException("Simulated exception");
                }
                System.out.println("Request passed: " + i);
            } catch (DegradeException e) {
                System.out.println("Request degraded: " + i);
            } catch (Exception e) {
                System.out.println("Request failed: " + i);
            }
        }
    }
}

注意事项

  1. 规则加载:使用 DegradeRuleManager 加载规则。
  2. 时间窗口:熔断触发后,在设定的时间窗口内,所有请求都会被降级。
  3. 最小请求数(minRequestAmount :在统计期间,只有请求数达到该值时,规则才会生效。
  4. 与限流结合:熔断降级通常与限流规则一起使用,以增强系统稳定性。

根据实际场景,可以灵活配置适合的策略,提升服务的容错能力和稳定性。

2、基于失败数具体策略

调用成功数和失败数来进行熔断降级是一种基于统计的熔断思路。这种方法关注的是接口的成功率,而不是响应时间或异常信息,可以有效规避由于超时或其他非业务异常(如网络波动)导致的误判。

以下是实现该思路的具体方法:


核心思路

  1. 定义统计窗口

    • 在一个固定的时间窗口内(比如 10 秒)统计成功数和失败数。
    • 计算成功率:successRate = successCount / (successCount + failureCount)
    • 如果成功率低于某个阈值(比如 50%),触发熔断。
  2. 熔断降级逻辑

    • 进入熔断状态后,在设定的熔断时间窗口内(比如 5 秒),所有请求直接返回降级结果。
    • 熔断时间结束后,进入“半开”状态,允许少量请求通过,验证是否恢复正常。
  3. 实现细节

    • 使用 AtomicIntegerLongAdder 统计成功和失败数,保证多线程安全。
    • 定时清理统计数据,或者基于滑动窗口动态调整统计范围。

示例代码

以下是基于成功数和失败数实现熔断降级的代码示例:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

public class SuccessFailureCircuitBreaker {
    private final int failureThreshold;  // 失败数阈值
    private final int minRequestAmount;  // 最小请求数
    private final long timeWindow;       // 统计时间窗口(毫秒)
    private final long breakTime;        // 熔断持续时间(毫秒)

    private AtomicInteger successCount = new AtomicInteger(0); // 成功数
    private AtomicInteger failureCount = new AtomicInteger(0); // 失败数
    private volatile long lastResetTime = System.currentTimeMillis(); // 上次重置时间
    private volatile boolean isCircuitOpen = false; // 熔断状态
    private final ReentrantLock lock = new ReentrantLock(); // 半开状态控制

    public SuccessFailureCircuitBreaker(int failureThreshold, int minRequestAmount, long timeWindow, long breakTime) {
        this.failureThreshold = failureThreshold;
        this.minRequestAmount = minRequestAmount;
        this.timeWindow = timeWindow;
        this.breakTime = breakTime;
    }

    public boolean tryPass() {
        if (isCircuitOpen) {
            // 如果在熔断状态,检查是否可以进入半开状态
            if (System.currentTimeMillis() - lastResetTime > breakTime) {
                // 尝试进入半开状态
                lock.lock();
                try {
                    if (System.currentTimeMillis() - lastResetTime > breakTime) {
                        isCircuitOpen = false;
                        resetCounts();
                        return true; // 半开状态允许一个请求通过
                    }
                } finally {
                    lock.unlock();
                }
            }
            return false; // 熔断状态直接拒绝
        }
        return true; // 正常状态允许请求通过
    }

    public void onSuccess() {
        if (!isCircuitOpen) {
            successCount.incrementAndGet();
            checkState();
        }
    }

    public void onFailure() {
        if (!isCircuitOpen) {
            failureCount.incrementAndGet();
            checkState();
        }
    }

    private void checkState() {
        long now = System.currentTimeMillis();
        if (now - lastResetTime > timeWindow) {
            // 时间窗口结束,重置统计数据
            resetCounts();
            return;
        }

        int total = successCount.get() + failureCount.get();
        if (total >= minRequestAmount && failureCount.get() > failureThreshold) {
            // 达到最小请求数且失败数超过阈值,触发熔断
            openCircuit();
        }
    }

    private void resetCounts() {
        successCount.set(0);
        failureCount.set(0);
        lastResetTime = System.currentTimeMillis();
    }

    private void openCircuit() {
        isCircuitOpen = true;
        lastResetTime = System.currentTimeMillis();
        System.out.println("Circuit is open! Entering degrade mode.");
    }

    public boolean isCircuitOpen() {
        return isCircuitOpen;
    }
}

示例使用

public class CircuitBreakerTest {
    public static void main(String[] args) throws InterruptedException {
        SuccessFailureCircuitBreaker circuitBreaker = new SuccessFailureCircuitBreaker(
            5, // 失败数阈值
            10, // 最小请求数
            10000, // 时间窗口 10 秒
            5000 // 熔断时间 5 秒
        );

        for (int i = 0; i < 20; i++) {
            if (circuitBreaker.tryPass()) {
                try {
                    if (i % 4 == 0) { // 模拟失败
                        throw new RuntimeException("Service failure");
                    }
                    circuitBreaker.onSuccess();
                    System.out.println("Request succeeded: " + i);
                } catch (Exception e) {
                    circuitBreaker.onFailure();
                    System.out.println("Request failed: " + i);
                }
            } else {
                System.out.println("Request degraded: " + i);
                Thread.sleep(500); // 模拟降级处理时间
            }
        }
    }
}

运行结果

  1. 如果在 10 秒内,失败数超过设定阈值,触发熔断:

    Request succeeded: 0
    Request failed: 1
    Request failed: 2
    Request failed: 3
    Request failed: 4
    Circuit is open! Entering degrade mode.
    Request degraded: 5
    Request degraded: 6
    ...
    
  2. 熔断结束后,进入“半开”状态,允许请求再次尝试:

    Request succeeded: 15
    Circuit is closed! Back to normal.
    

优势

  1. 基于成功和失败的统计

    • 更贴近业务需求,直接衡量接口的有效性。
    • 无需依赖耗时或异常等外部指标。
  2. 灵活配置

    • 通过调整最小请求数、失败阈值和熔断时间,适配不同场景。
  3. 简化降级逻辑

    • 熔断期间所有请求直接降级,降低系统负载。

改进建议

  1. 滑动窗口统计:避免固定时间窗口导致的统计不准确问题,可以使用滑动窗口或环形缓冲区实现更精细的统计。
  2. 分布式支持:如果是分布式服务,可以使用 Redis 或其他分布式存储统计成功和失败数。
  3. 动态调整阈值:结合系统指标(如 QPS、负载)动态调整熔断参数。

这种策略既可以避免响应时间统计的复杂性,也能更贴合业务场景,是一种非常实用的熔断降级方案。