分布式系统设计的容错机制

目录

1、熔断

1.1、介绍

1.2、原理/状态机

[1.3、常用指标 & 参数](#1.3、常用指标 & 参数)

1.4、常见实现/库

2、降级

2.1、介绍

2.2、分类

2.3、实现方式

3、限流

3.1、定义

3.2、常见算法

[4、隔离(Bulkhead / 资源隔离)](#4、隔离(Bulkhead / 资源隔离))

4.1、概念

4.2、实现方式

5、四者组合与实际策略

6、监控与指标(必须)


背景

分布式系统中任一服务或下游依赖变慢或宕掉,可能产生级联故障(请求积压、线程耗尽、连接尽、资源竞用),导致系统整体不可用或雪崩。

熔断(Circuit Breaker)避免对不健康依赖不断重试/请求;

降级(Fallback/Degrade)在功能不可用或超载时提供替代方案;

限流(Rate Limit)控制进入系统或单个服务的请求速率,保护资源;

隔离(Bulkhead/隔离)将故障影响局限在某个隔间,避免资源争抢扩散。

这几种方式是分布式系统中应对高并发、依赖故障的核心容错机制,‌**共同解决服务雪崩风险,保障系统可用性。**‌


1、熔断

1.1、介绍

断路保护。

比如 A 服务调用 B 服务,由于网络问题或 B 服务宕机了或 B 服务的处理时间长,导致请求的时间超长,如果在一定时间内多次出现这种情况,就可以直接将 B 断路了(A 不再请求B)。

而调用 B 服务的请求直接返回降级数据,不必等待 B 服务的执行。因此 B 服务的问题,不会级联影响到 A 服务。

  • 作用 ‌:‌快速失败止损‌,当服务失败率超过阈值时自动切断调用链路。
  • 实现逻辑 ‌:
    • 监控请求失败率(如10秒内失败率>50%)
    • 触发熔断后,后续请求直接走降级逻辑,不再访问故障服务
    • 定期进入"半开状态"试探服务恢复情况
  • 典型场景‌:支付服务持续超时后,网关层直接熔断,避免请求堆积

1.2、原理/状态机

基本三态:

CLOSED(闭合,正常请求通过并收集成功/失败指标);

OPEN(打开,短路,不再调用下游,直接失败或走降级);

HALF_OPEN(半开,允许少量试探请求以探测依赖是否恢复)。

触发条件通常基于:

在滑动窗口内的失败率、失败次数、响应时延、吞吐量等。达到阈值触发 OPEN,过一段时间后进入 HALF_OPEN,若试探请求成功则回到 CLOSED,否则继续 OPEN。

1.3、常用指标 & 参数

  • failureRateThreshold(失败率阈值,例如 50%)
  • minimumNumberOfCalls(最小采样请求数,避免样本量太小)
  • slidingWindowSize(统计窗口大小,基于时间或计数)
  • waitDurationInOpenState(打开态保持时间,过后转 HALF_OPEN)
  • permittedNumberOfCallsInHalfOpenState(半开允许的试探请求数)

1.4、常见实现/库

  • Netflix Hystrix(已停止维护,思想仍然有价值)
  • Resilience4j(现代、轻量、功能丰富)
  • Sentinel(阿里,支持流控、熔断、降级、热点限流)

1.5、Java 示例:Resilience4j(maven)

  • 依赖:
    • org.resilience4j:resilience4j-circuitbreaker
  • 简单示例(同步调用):
java 复制代码
import io.github.resilience4j.circuitbreaker.*;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowType;

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50) // 50%
    .minimumNumberOfCalls(10)
    .slidingWindowType(SlidingWindowType.TIME_BASED)
    .slidingWindowSize(10) // 10 seconds window (if TIME_BASED)
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .permittedNumberOfCallsInHalfOpenState(5)
    .build();

CircuitBreaker cb = CircuitBreaker.of("myService", config);

// 装饰一个 Supplier 或 Callable
Supplier<String> decorated = CircuitBreaker
    .decorateSupplier(cb, () -> callRemoteService());

try {
    String result = decorated.get();
} catch (CallNotPermittedException ex) {
    // 熔断打开,短路到这里,做降级
    fallback();
}

简单手写熔断器(伪实现):

java 复制代码
class SimpleCircuitBreaker {
    enum State { CLOSED, OPEN, HALF_OPEN }
    private State state = State.CLOSED;
    private long openUntil = 0;
    private int failCount = 0;
    private int successCount = 0;
    private final int failThreshold = 5;
    private final long openMs = 10_000L;

    public synchronized <T> T call(Callable<T> callable) throws Exception {
        long now = System.currentTimeMillis();
        if (state == State.OPEN) {
            if (now < openUntil) throw new RuntimeException("circuit open");
            state = State.HALF_OPEN;
        }
        try {
            T r = callable.call();
            onSuccess();
            return r;
        } catch (Exception e) {
            onFailure();
            throw e;
        }
    }

    private void onSuccess() {
        if (state == State.HALF_OPEN) {
            // 一个成功就关闭,也可以要求连续成功次数
            state = State.CLOSED; failCount = 0;
        }
    }
    private void onFailure() {
        failCount++;
        if (failCount >= failThreshold) {
            state = State.OPEN;
            openUntil = System.currentTimeMillis() + openMs;
        }
    }
}

注意:真实生产要用滑窗统计、并发安全、冷启动保护等。


2、降级

2.1、介绍

返回降级数据。

网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级(停止服务,所有的调用直接返回降级数据)。

以此缓解服务器资源的压力,保证核心业务的正常运行,保持了客户和大部分客户得到正确的响应。

降级数据可以简单理解为快速返回了一个 false,前端页面告诉用户"服务器当前正忙,请稍后再试。"

  • 作用 ‌:‌提供柔性方案‌,在熔断或服务不可用时返回预设结果。
  • 实现方式 ‌:
    • 返回缓存数据(如商品详情页降级展示昨日销量)
    • 返回默认值(如查询失败时显示"服务繁忙")
    • 流程简化(下单跳过风控校验)
  • 关键点‌:需提前设计降级策略,确保用户体验平滑

2.2、分类

功能降级(返回默认/缓存数据/静态页面)、流量降级(拒绝非核心请求)、延迟降级(将请求入队异步处理)、降级到降级服务(更便宜或更稳定的实现)。

2.3、实现方式

  • 在代码中实现 fallback(try/catch 或 使用库注入 fallback,例如 Resilience4j 的 fallback 或 Spring Cloud 的 fallback)
  • 使用缓存作为后备(比如返回缓存数据)
  • 业务级降级:降低功能强度(只返回必要字段、减少并行查询、去掉非核心聚合)
  • 降级开关/灰度:通过配置中心(HOT)控制降级

Java 示例(Resilience4j with fallback)

java 复制代码
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;

try {
    String res = decorated.get();
} catch (CallNotPermittedException ex) {
    // 熔断短路 -> 降级处理
    return fallbackData();
} catch (Exception e) {
    // 依赖超时/异常 -> 降级
    return fallbackData();
}
  • 缓存优先(先返回缓存,再异步刷新缓存)
  • 简化响应(只返回 ID 和关键字段)
  • 限功能(把高级功能关闭,保持核心支付/登录业务)

熔断和降级的相同点?

  • 熔断和限流都是为了保证集群大部分服务的可用性和可靠性。防止核心服务崩溃。
  • 给终端用户的感受就是某个功能不可用。

熔断和降级的不同点?

  • 熔断是被调用方出现了故障,主动触发的操作。
  • 降级是基于全局考虑,停止某些正常服务,释放资源。

3、限流

3.1、定义

对请求的流量进行控制, 只放行部分请求,使服务能够承担不超过自己能力的流量压力。

  • 控制突发流量、保护后端资源、避免请求淹没服务、实现 QoS 策略(优先级/计费)

3.2、常见算法

  • 固定窗口计数(Fixed Window):按时间窗口计数(简单但在窗口边界有突发)。
  • 滑动窗口计数(Sliding Window Log/Counter):记录时间戳(更精确,但可能昂贵)。
  • 令牌桶(Token Bucket):以固定速率产生令牌,请求拿到令牌则放行,可实现平滑突发(允许短时突发)。
  • 漏桶(Leaky Bucket):以固定速率处理请求,过载则丢弃/排队(平滑输出)。
  • 令牌桶 + Redis/Lua:分布式限流实现常用。

本地简单实现:令牌桶(Token Bucket)

java 复制代码
class TokenBucket {
    private final long capacity;
    private final long refillTokens;
    private final long refillIntervalMillis;
    private double tokens;
    private long lastRefillTimestamp;

    public TokenBucket(long capacity, long refillTokens, long refillIntervalMillis) {
        this.capacity = capacity;
        this.refillTokens = refillTokens;
        this.refillIntervalMillis = refillIntervalMillis;
        this.tokens = capacity;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    public synchronized boolean tryConsume(int numTokens) {
        refill();
        if (tokens >= numTokens) {
            tokens -= numTokens;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.currentTimeMillis();
        long intervals = (now - lastRefillTimestamp) / refillIntervalMillis;
        if (intervals > 0) {
            double add = intervals * refillTokens;
            tokens = Math.min(capacity, tokens + add);
            lastRefillTimestamp += intervals * refillIntervalMillis;
        }
    }
}

分布式限流(Redis + Lua)

  • 用 Lua 脚本在 Redis 原子执行计数或令牌桶操作,避免竞争。
  • 示例:滑动窗口计数(用时间戳链表)或令牌桶(使用 Redis key 存 token 值与 timestamp)。

简单的 Redis 计数(固定窗口)Lua(伪代码):

java 复制代码
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

local count = redis.call('GET', key)
if not count then
  redis.call('SET', key, 1, 'PX', window)
  return 1
end
if tonumber(count) + 1 > limit then
  return 0
else
  redis.call('INCR', key)
  return 1
end

4、隔离(Bulkhead / 资源隔离)

4.1、概念

将服务或资源按"隔间"划分(线程池、连接池、限额),使得某一隔间发生故障或耗尽时不致影响其他隔间(业务)。灵感来自船舱隔离(bulkhead)。

4.2、实现方式

  • 线程池隔离:对不同下游或不同类型请求使用不同线程池(或不同拒绝策略),避免单个慢调用耗尽主线程池。
  • 信号量隔离(semaphore):限制并发调用数(轻量,不带线程切换)。
  • 连接池/资源配额:每个依赖一个单独连接池,上游耗尽连接不影响其它依赖。
  • 容器/微服务资源限制:通过 k8s 限制资源、pod 副本隔离。

Java 示例:线程池隔离(Executor)

java 复制代码
ExecutorService pool = new ThreadPoolExecutor(
    10, 10, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(50),
    new ThreadPoolExecutor.AbortPolicy()); // 当队列满时拒绝

Future<String> future = pool.submit(() -> callRemoteService());
try {
    String res = future.get(2, TimeUnit.SECONDS); // 超时控制
} catch (TimeoutException e) {
    // 超时 -> 降级
    future.cancel(true);
    fallback();
}

信号量隔离(Resilience4j 提供 Bulkhead)

java 复制代码
import io.github.resilience4j.bulkhead.*;
BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(20)
    .maxWaitDuration(Duration.ZERO) // 不等待,直接拒绝
    .build();
Bulkhead bulkhead = Bulkhead.of("service", config);
Supplier<String> decorated = Bulkhead.decorateSupplier(bulkhead, () -> callRemoteService());
try { decorated.get(); } catch (BulkheadFullException ex) { fallback(); }

为什么优先选择信号量还是线程池?

  • 信号量:延迟低、轻量(适合在同线程中限流),但当依赖阻塞时会占用调用线程。
  • 线程池:能把阻塞转化为排队,保护调用线程(比如 tomcat 请求处理线程),但可能导致上下文切换和队列积压。

5、四者组合与实际策略

请求进入时做鉴权与快速限流(Token Bucket)------ 保护入口。

常见防御链(建议):

  • 对调用下游前先使用隔离(线程池/信号量)------ 保证主线程不被耗尽。
  • 用熔断器判断下游健康,快速短路失败------ 减少无用等待与重试风暴。
  • 对短路/失败使用降级策略(缓存/默认结果/异步排队)------ 保证核心业务可用。
  • 对内部延迟/超时做度量并上报监控(实时告警)。

配置示例(伪):限流 200 rps;线程池 max 50;熔断:10s 窗口内失败率>50% 且最小20个请求 -> 打开 30s -> 半开 5次试探。


6、监控与指标(必须)

  • 熔断器需上报:成功率、失败率、请求数、state transitions(CLOSED->OPEN->HALF_OPEN)。
  • 限流需上报:命中数、被拒绝数、当前速率。
  • 隔离需上报:线程池活跃数、队列长度、拒绝数、等待时长。
  • 降级需上报:fallback 调用次数、降级率、触发原因(熔断、超时、资源压力)。
  • 告警阈值应基于业务SLA设定,避免噪音。

参考文章:

1、Spring Cloud源码 - Hystrix原理分析_spring hystrix 原理 及底层实现-CSDN博客文章浏览阅读1.1k次,点赞28次,收藏29次。Spring Cloud源码 - Hystrix原理分析_spring hystrix 原理 及底层实现https://blog.csdn.net/qq_43350524/article/details/145883838?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522275af48ced5566253a61655c3e43474d%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=275af48ced5566253a61655c3e43474d&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-145883838-null-null.142^v102^control&utm_term=springcloud%E9%87%8C%E9%9D%A2%E7%9A%84hystrix%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E5%8F%8A%E5%8E%9F%E7%90%86&spm=1018.2226.3001.4187

2、【spring cloud】 Hystrix使用及原理_hystrix: enabled: true-CSDN博客文章浏览阅读211次。文章目录服务降级服务端的服务降级Pom依赖service层主启动类添加@EnableCircuitBreaker消费端服务降级pom依赖yml.application业务类为特定方法指定备选方法:服务降级服务降级就是在一个方法调用失败或超时时,调用一个备用的方法,来给调用方一个友好的提示。服务端的服务降级Pom依赖 org.springframework.cloud https://blog.csdn.net/yao09605/article/details/111640736?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-9-111640736-blog-145883838.235^v43^pc_blog_bottom_relevance_base6&spm=1001.2101.3001.4242.6&utm_relevant_index=11