什么叫做熔断,熔断是谁提出的

首页 > 经验 > 作者:YD1662024-03-21 14:26:02

熔断和降级都是系统自我保护的一种机制,但二者又有所不同,它们的区别主要体现在以下几点:

  1. 概念不同
  2. 触发条件不同
  3. 归属关系不同
1.概念不同1.1 熔断概念

“熔断”一词早期来自股票市场。熔断(Circuit Breaker)也叫自动停盘机制,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。比如 2020 年 3 月 9 日,纽约股市开盘出现*,随后跌幅达到 7% 上限,触发熔断机制,停止交易 15 分中,恢复交易后跌幅有所减缓。

熔断在程序中,表示“断开”的意思。如发生了某事件,程序为了整体的稳定性,所以暂时(断开)停止服务一段时间,以保证程序可用时再被使用。

如果没有熔断机制的话,会导致联机故障和服务雪崩等问题,如下图所示:

什么叫做熔断,熔断是谁提出的(1)

1.2 降级概念

降级(Degradation)降低级别的意思,它是指程序在出现问题时,仍能保证有限功能可用的一种机制。

比如电商交易系统在双 11 时,使用的人比较多,此时如果开放所有功能,可能会导致系统不可用,所以此时可以开启降级功能,优先保证支付功能可用,而其他非核心功能,如评论、物流、商品介绍等功能可以暂时关闭。

所以,从上述信息可以看出:降级是一种退而求其次的选择,而熔断却是整体不可用

2.触发条件不同

不同框架的熔断和降级的触发条件是不同的,本文咱们以经典的 Spring Cloud 组件 Hystrix 为例,来说明触发条件的问题。

2.1 Hystrix 熔断触发条件

默认情况 hystrix 如果检测到 10 秒内请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求。

什么叫做熔断,熔断是谁提出的(2)

2.2 Hystrix 降级触发条件

默认情况下,hystrix 在以下 4 种条件下都会触发降级机制:

  1. 方法抛出 HystrixBadRequestException
  2. 方法调用超时
  3. 熔断器开启拦截调用
  4. 线程池或队列或信号量已满

虽然 hystrix 组件的触发机制,不能代表所有的熔断和降级机制,但足矣说明此问题。

3.归属关系不同

**熔断时可能会调用降级机制,而降级时通常不会调用熔断机制。**因为熔断是从全局出发,为了保证系统稳定性而停用服务,而降级是退而求其次,提供一种保底的解决方案,所以它们的归属关系是不同(熔断 > 降级)。

题外话

当然,某些框架如 Sentinel,它早期在 Dashboard 控制台中可能叫“降级”,但在新版中新版本又叫“熔断”,如下图所示:

什么叫做熔断,熔断是谁提出的(3)

但在两个版本中都是通过同一个异常类型 DegradeException 来监听的,如下代码所示:

什么叫做熔断,熔断是谁提出的(4)

所以,在 Sentinel 中,熔断和降级功能指的都是同一件事,也侧面证明了“熔断”和“降级”概念的相似性。但我们要知道它们本质上是不同的,就像两个双胞胎,不能因为他们长得像,就说他们是同一个人。

什么叫做熔断,熔断是谁提出的(5)

当用户请求 A、P、H、I 四个服务获取数据时,在正常流量下系统稳定运行,如果某天系统进来大量流量,其中服务 I 出现 CPU、内存占用过高等问题,结果导致服务 I 出现延迟、响应过慢,随着请求的持续增加,服务 I 承受不住压力导致内部错误或资源耗尽,一直不响应,此时更糟糕的是其他服务对 I 有依赖,那么这些依赖 I 的服务一直等待 I 的响应,也会出现请求堆积、资源占用,慢慢扩散到所有微服务,引发雪崩效应。

基本的容错模式

常见的容错模式主要包含以下几种方式:

1.主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积

2.限流:限制最大并发数

3.熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求去重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)

4.隔离:把每个依赖或调用的服务都隔离开来,防止级联失败引起整体服务不可用

5.降级:服务失败或异常后,返回指定的默认信息

什么叫做熔断,熔断是谁提出的(6)

服务降级

由于爆炸性的流量冲击,对一些服务进行有策略的放弃,以此缓解系统压力,保证目前主要业务的正常运行。它主要是针对非正常情况下的应急服务措施:当此时一些业务服务无法执行时,给出一个统一的返回结果。

服务熔断

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

服务熔断与服务降级比较

服务熔断对服务提供了proxy,防止服务不可能时,出现串联故障(cascading failure),导致雪崩效应。

服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑

1.共性:

目的 -> 都是从可用性、可靠性出发,提高系统的容错能力。

最终表现->使某一些应用不可达或不可用,来保证整体系统稳定。

粒度 -> 一般都是服务级别,但也有细粒度的层面:如做到数据持久层、只许查询不许增删改等。

自治 -> 对其自治性要求很高。都要求具有较高的自动处理机制。

2.区别:

触发原因 -> 服务熔断通常是下级服务故障引起;服务降级通常为整体系统而考虑。

管理目标 -> 熔断是每个微服务都需要的,是一个框架级的处理;而服务降级一般是关注业务,对业务进行考虑,抓住业务的层级,从而决定在哪一层上进行处理:比如在IO层,业务逻辑层,还是在外围进行处理。

实现方式 -> 代码实现中的差异。

服务熔断恢复需注意的问题

如果服务是幂等性的,则恢复重试不会有问题;而如果服务是非幂等性的,则重试会导致数据出现问题。

Java 实现限流

1 滑动窗口

public class WindowLimiterComponent implements LimiterComponent { /** * 队列id和队列的映射关系,队列里面存储的是每一次通过时候的时间戳,这样可以使得程序里有多个限流队列 */ private final Map<String, List<Long>> MAP = new ConcurrentHashMap<>(); /** * 限制次数 */ private final int count; /** * 时间窗口大小 */ private final long timePeriod; public WindowLimiterComponent(int count, long timePeriod) { this.count = count; this.timePeriod = timePeriod; } /** * 滑动时间窗口限流算法 * 在指定时间窗口,指定限制次数内,是否允许通过 * * @param id 队列id * @return 是否被限流 */ @Override public synchronized boolean isLimited(String id) { // 获取当前时间 long nowTime = System.currentTimeMillis(); // 根据队列id,取出对应的限流队列,若没有则创建 List<Long> list = MAP.computeIfAbsent(id, k -> new LinkedList<>()); // 如果队列还没满,则允许通过,并添加当前时间戳到队列开始位置 if (list.size() < count) { list.add(0, nowTime); return false; } // 队列已满(达到限制次数),则获取队列中最早添加的时间戳 Long lastTime = list.get(count - 1); // 用当前时间戳 减去 最早添加的时间戳 if (nowTime - lastTime <= timePeriod) { // 若结果小于等于timePeriod,则说明在timePeriod内,通过的次数大于count // 不允许通过 return true; } else { // 若结果大于timePeriod,则说明在timePeriod内,通过的次数小于等于count // 允许通过,并删除最早添加的时间戳,将当前时间添加到队列开始位置 list.remove(count - 1); list.add(0, nowTime); return false; } } } 复制代码

@Test public void test() throws InterruptedException { // 任意10秒内,只允许2次通过 LimiterComponent component = new WindowLimiterComponent(2, 10000L); while (true) { System.out.println(LocalTime.now().toString() component.isLimited("1")); // 睡眠0-10秒 Thread.sleep(1000 * new Random().nextInt(10)); } } 复制代码

2 Redis zset

public class RedisZSetLimiterComponent implements LimiterComponent{ private final redissonComponent redissonComponent; /** * 限制次数 */ private final int count; /** * 时间窗口大小,单位毫秒 */ private final long timePeriod; public RedisZSetLimiterComponent(RedissonComponent component) { this.redissonComponent = component; this.count = 5; this.timePeriod = 1000; } public RedisZSetLimiterComponent(RedissonComponent component, int count, long timePeriod) { this.redissonComponent = component; this.count = count; this.timePeriod = timePeriod; } /** * 基于 zset 的滑动时间窗口限流算法 * 在指定时间窗口,指定限制次数内,是否允许通过 * * @param key 队列key * @return 是否允许通过 */ @Override public synchronized boolean isLimited(String key) { // 获取当前时间 long nowTime = System.currentTimeMillis(); RScoredSortedSet<String> set = redissonComponent.getRScoredSortedSet(key); // 移除一个时间段以前的 set.removeRangeByScore(0, true, (double) (nowTime - timePeriod), true); // 获取集合内元素总数 int size = set.count((double) (nowTime - timePeriod), true, nowTime, true); // 如果队列没满 if (size < count) { // 当前时间加入集合 set.add((double) nowTime, String.valueOf(nowTime)); return false; } return true; } } 复制代码

@Test public void test() throws InterruptedException { // 任意10秒内,只允许2次通过 LimiterComponent component = new RedisZSetLimiterComponent(redissonComponent, 2, 10000L); while (true) { System.out.println(LocalTime.now().toString() component.isLimited("1")); // 睡眠0-10秒 Thread.sleep(1000 * new Random().nextInt(10)); } } 复制代码

3 guava RateLimiter

@SuppressWarnings("UnstableApiUsage") public class GuavaLimiterComponent implements LimiterComponent { private final int count; private final long timePeriod; private final Map<String, RateLimiter> MAP = new ConcurrentHashMap<>(); public GuavaLimiterComponent(int count, long timePeriod) { this.count = count; this.timePeriod = timePeriod; } /** * 令牌桶算法 * * @param key 键值 * @return 是否被限流 */ @Override public synchronized boolean isLimited(String key) { RateLimiter rateLimiter = MAP.computeIfAbsent(key, k -> RateLimiter.create(count, timePeriod, TimeUnit.MILLISECONDS)); return !rateLimiter.tryAcquire(); } } 复制代码

@Test public void test() throws InterruptedException { // 任意10秒内,只允许2次通过 LimiterComponent component = new GuavaLimiterComponent(2, 10000L); while (true) { System.out.println(LocalTime.now().toString() component.isLimited("1")); // 睡眠0-10秒 Thread.sleep(1000 * new Random().nextInt(10)); } } 复制代码

4 redisson RRateLimiter

public class RedisRateLimiterComponent implements LimiterComponent { private final RedissonComponent redissonComponent; /** * 限制次数 */ private final int count; /** * 时间窗口大小,单位毫秒 */ private final long timePeriod; public RedisRateLimiterComponent(RedissonComponent component) { this.redissonComponent = component; this.count = 5; this.timePeriod = 1000; } public RedisRateLimiterComponent(RedissonComponent component, int count, long timePeriod) { this.redissonComponent = component; this.count = count; this.timePeriod = timePeriod; } /** * 基于 rateLimiter 的滑动时间窗口限流算法 * 在指定时间窗口,指定限制次数内,是否允许通过 * * @param key 队列key * @return 是否允许通过 */ @Override public synchronized boolean isLimited(String key) { RRateLimiter rateLimiter = redissonComponent.getRateLimiter(key); rateLimiter.trySetRate(RateType.PER_CLIENT, count, timePeriod, RateIntervalUnit.MILLISECONDS); return !rateLimiter.tryAcquire(); } } 复制代码

@Test public void test() throws InterruptedException { // 任意10秒内,只允许2次通过 LimiterComponent component = new RedisRateLimiterComponent(redissonComponent, 2, 10000L); while (true) { System.out.println(LocalTime.now().toString() component.isLimited("1")); // 睡眠0-10秒 Thread.sleep(1000 * new Random().nextInt(10)); } }总结

熔断和降级都是程序在我保护的一种机制,但二者在概念、触发条件、归属关系上都是不同的。熔断更偏向于全局视角的自我保护(机制),而降级则偏向于具体模块“退而请其次”的解决方案。



栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.