分布式系统设计的容错机制
目录
1、熔断
1.1、介绍
1.2、原理/状态机
1.3、常用指标 & 参数
1.4、常见实现/库
2、降级
2.1、介绍
2.2、分类
2.3、实现方式
3、限流
3.1、定义
3.2、常见算法
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
- 简单示例(同步调用):
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();
}
简单手写熔断器(伪实现):
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)
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)
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(伪代码):
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 thenredis.call('SET', key, 1, 'PX', window)return 1
end
if tonumber(count) + 1 > limit thenreturn 0
elseredis.call('INCR', key)return 1
end
4、隔离(Bulkhead / 资源隔离)
4.1、概念
将服务或资源按“隔间”划分(线程池、连接池、限额),使得某一隔间发生故障或耗尽时不致影响其他隔间(业务)。灵感来自船舱隔离(bulkhead)。
4.2、实现方式
- 线程池隔离:对不同下游或不同类型请求使用不同线程池(或不同拒绝策略),避免单个慢调用耗尽主线程池。
- 信号量隔离(semaphore):限制并发调用数(轻量,不带线程切换)。
- 连接池/资源配额:每个依赖一个单独连接池,上游耗尽连接不影响其它依赖。
- 容器/微服务资源限制:通过 k8s 限制资源、pod 副本隔离。
Java 示例:线程池隔离(Executor)
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)
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