服务熔断降级

服务熔断降级之前给大家讲了很多服务之前的关系 今天主要给大家介绍 当服务出现问题时 我们该如何解决 首先我们先进行一些场景分析 场景一服务提供端提供了 A B C D4 个服务 服务调用方调用服务时 D 服务出现了问题 导致调用服务 D 的请求都出现了超时或者错误 从而进一步影响了整个请求队列的性能 场景二我们根据 client1 和 client2 的请求压力确定了我们 serverA 的负载情况 但是当 client2 的请求从 1500 增加到 15000 时 会远远大于 ServerA 的性能瓶颈 这个时候也会导致 C

之前给大家讲了很多服务之前的关系,今天主要给大家介绍,当服务出现问题时,我们该如何解决。

首先我们先进行一些场景分析:

  • 场景一

在这里插入图片描述

服务提供端提供了A、B、C、D 4个服务,服务调用方调用服务时,D服务出现了问题,导致调用服务D的请求都出现了超时或者错误,从而进一步影响了整个请求队列的性能。

  • 场景二

在这里插入图片描述

我们根据client1和client2 的请求压力确定了我们serverA的负载情况,但是当client2的请求从1500 增加到15000 时,会远远大于ServerA 的性能瓶颈,这个时候也会导致Client1的请求出现超时或者失败的情况。

总结一下,会有哪些情形导致我们服务出现不可以,需要我们做容错呢?

  • 单个节点故障,可能被无限向上放大
  • 多租户相互影响
  • 瞬时流量激增,系统扛不住

针对上面总结的场景,我们有哪些解决方案呢?

  • 资源隔离
  • 熔断
  • 降级

1、资源隔离

资源隔离主要是对线程资源进行隔离。这里有两种方法进行隔离:使用线程池和信号量

(1)线程池隔离

使用线程池进行资源隔离,我们有两种途径,一种是在服务端,根据不同的请求类型划分不同的队列进行处理,比如我们可以根据业务的优先级,创建高中低、默认等优先级队列来处理业务请求。

在这里插入图片描述

还有一种方式是通过在服务调用端进行队列划分,比如我们有多个服务进行调研,每个调用内部都有一个队列,通过连接管理的方式进行服务调用

在这里插入图片描述

(2)信号量

还有一种方式是通过信号量的方式来实现。我们可以通过给每个服务提供方设置可用的信号量来实现资源隔离。

在这里插入图片描述

(3)对比

信号量与线程池对比

对比项 线程切换 异步 超时 熔断 限流 开销
信号量
线程池

下面我们来具体分析下服务熔断和降级的设计应用。

2、服务熔断

什么叫服务熔断?

服务熔断就是临时关闭对某些功能的调用,个别的业务不可用,但是系统整体可用

有哪些服务熔断的具体例子呢?

比如一些大的电商平台在双11那天关闭退款入口,关闭换头像功能等等,这些功能的关闭对于用户来说是可感知的。

在做服务熔断设计中,我们需要关注哪些点呢?

  • 可熔断服务

我们需要判断在我们的整体架构中,哪些服务是可用进行熔断的,哪些是核心业务,不能出现不可用的情况。

  • 熔断触发:触发熔断的方式有哪些
    • 主动熔断:系统管理员根据将要出现的场景,提前关闭某些服务
    • 被动熔断:系统根据服务的状态触发熔断
  • 恢复时机

3、服务降级

什么叫服务降级?

服务降级就是有损的提供服务,保证服务柔性可用。

业务中有哪些服务降级的场景呢?

比如我们在请求高峰期的时候,对于用户的个人推荐,我们返回兜底数据;计数服务返回假数据等。这些服务对于用户来说他是不易感知的。

我们在进行服务降级设计时,需要考虑到哪些方面呢?

  • 可降级服务

首先我们要确认在我们的整体架构中,哪些服务是可以采用降级的方案的,哪些场景不适合降级的方法

  • 降级方法

我们一般的降级方法有哪些,比如常见的默认返回等等

  • 降级触发

降级触发的条件是什么,现在有2种常见的情况:主动和被动

主动:管理员发现某个服务出现异常,通过手动的方式触发某个服务的熔断,这是我们需要有一个降级的方法

被动:当某个服务调用超时或者异常,采用降级方案

  • 恢复时机

触发降级服务后,什么时候恢复到原来的服务?

4、熔断降级

基于上面的描述,我们可以发现,降级熔断都是系统可用性可靠性的保障手段,为了保障服务的柔性可用。

  • 目标一致:都是从可用性和可靠性出现,为了防止系统崩溃
  • 用户体验:用户感受到某些功能的暂不可用

很多时候,降级和熔断是配置使用的

5、断路器设计

断路器

服务熔断的开关,当对下游服务调用异常量达到设定阈值后,打开断路器,触发熔断

(1)断路器的状态流转

在这里插入图片描述

  • 断路器的状态分为3个状态,closed(关闭),open(打开),half open(半打开),服务正常使用时,断路器的状态为关闭的;
  • 当服务调用在指定时间内达到一定的阈值,就会打开断路器,服务熔断;
  • 当达到关闭时间后,会将断路器的状态更新为halfopen,将少部分流量打到之前熔断的服务;
  • 如何服务调用正常,则将断路器状态更新为closed,否则更新为open

(2)阈值与统计数据

  • 阈值的数据类型
    • 我们一般采用百分比的方式,比如失败率达到多少多少我们出发熔断
  • 颗粒度
    • 我们一般是针对某个节点中的某个实例的某个方法而言,当某个方法的调用失败率达到一定的程度,我们就触发对该方法调用的熔断
  • 统计

在这里插入图片描述

数据结构:如上图,我们需要在规定的时间范围内进行统计,只需要统计最近10个slot范围内的数据,根据场景,我们的数据结构可以是链表,或者循环数组,滑动窗口伪代码实现如下:

// 整个循环数组的结构 public class BucketCircularArray { 
    private volatile int size =10; private static int maxSize = 60; private volatile int dataLength; private Bucket[] data; private int head; private int tail; } // 每个slot的数据结构 public class Bucket { 
    private final long windowStart; private AtomicInteger successNum; private AtomicInteger failNum; private AtomicInteger timeoutNum; } 
public void addBucket(Bucket bucket) { 
    data[tail] = bucket; incrementTail(); } private void incrementTail() { 
    if (dataLength == size) { 
    // the size change head = (head + 1) % size; tail = (tail + 1) % size; } else { 
    tail = (tail + 1) % size; } if (dataLength < size) { 
    dateLength++; } } public Bucket tail() { 
    if (dateLength == 0) { 
    return null; } int index = (head + dataLength -1) % dataLength; return data[index]; } public Bucket[] toArray() { 
    ArrayList<Bucket> list = new ArrayList<Bucket>(); for (int i = 0; i < dataLength; i++) { 
    int index = (head + i) % dataLength; Bucket tmp = data[index]; if (tmp != null) { 
    list.add(tmp); } } return list.toArray(new Bucket[list.size()]); } 
// 断路器CircuitBreaker // open -> half_open public boolean attemptExecution() { 
    CircuitBreakConfigMeta configData = this.srvMgrClient.getCircuitBreakConfig(serviceName, method); ...... ...... } else { 
    if (isAfterSleepWindow(configData)) { 
    if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) { 
    halfOpenPassNum.set(0); return true; } else { 
    if (halfOpenPassNum.incrementAndGet() > maxHalfOpenPassNum) return false; else return true; } } else { 
    return false; } } } private boolean isAfterSleepWindow(CircuitBreakConfigMeta configData) { 
    final long circuitOpenTime = circuitOpened.get(); final long currentTime = System.currentTimeMillis(); final long sleepWindowTime = configData.getSleepwindowInMilliseconds(); return currentTime > circuitOpenTime + sleepWindowTime; } // hald_open -> closed public void markSuccess() { 
    boolean flag = false; if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) { 
    circuitOpened.set(-1L); flag = true; } return flag; } // closed -> open public boolean tryMarkOpen(int errorPercentage, Long requestNum) { 
    circuitBreakConfigMeta configData = this.srvMgrClient.getCircuitBreakConfig(serviceName, method); if (configData != null && !configData.isForceClosed() && requestNum >= configData.getRequestVolumThreshold() && errorPecentage >= configData.getErrorThresholdPercentage()) { 
    if (status.compareAndSet(Status.CLOSED, Status.OPEN)) { 
    circuitOpened.set(System.currentTimeMillis()); return true; } } return false; } // half_open -> open public void markNonSuccess() { 
    if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) { 
    logger.debug("circuitBreakerName {} reset to open", circuitBreakerName); circuitOpened.set(System.currentTimeMillis()); } } 
// 控制器对象:MetricTimeWindow public MetricTimeWindow(String serviceName, String method, ScfClientService srvMgrClient, int windowLength, EventBus eventBus) { 
    this.metricName = serviceName + "@" + method; this.serviceName = serviceName; this.method = method; this.bucketArray = new BucketCircularArray(windowLength); this.circuitBreaker = new CircuitBreaker(serviceName, method, srvMgrClient); } // 处理事件,成功、失败、超时 public void addEvent(MetricEventType event) { 
    Bucket bucket = getCurrentBucket(); switch(event) { 
    case success: bucket.getSuccessNum().incrementAndGet(); boolean makeClosed = circuitBreaker.markSuccess(); break; case fail: bucket.getFailNum().incrementAndGet(); this.circuitBreaker.markNonSuccess(); break; case timeout: bucket.getTimeoutNum().incrementAndGet(); this.circuitBreaker.markNonSuccess(); break; } } public boolean markSuccess() { 
    boolean flag = false; if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) { 
    circuitOpened.set(-1L); flag = true; } return flag; } public void markNonSuccess() { 
    if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) { 
    logger.debug("circuitBreakerName {} reset to open", circuitBreakerName); circuitOpened.set(System.currentTimeMillis()); } } // 获取当前的bucket public Bucket getCurrentBucket() { 
    long currentTime = System.currentTimeMillis(); lock.readLock().lock(); Bucket bucket = null; try { 
    bucket = bucketArray.tail(); if (bucket != null && currentTime <= (bucket.getWindowStart() + bucketTimeSpan)) return bucket; } finally { 
    lock.readLock().unlock(); } boolean createNewBucket = false; lock.writeLock().lock(); try { 
    Bucket check = bucketArray.tail(); if (check != null && currentTime <= (check.getWindowStart() + bucketTimeSpan)) { 
    bucket = check; } else { 
    bucket = new Bucket(currentTime); bucketArray.addBucket(bucket); createNewBucket = true; } } finally { 
    lock.writeLock().unlock(); } if (createNewBucket) { 
    dealCreateBucketEvent(); } return bucket; } private void dealCreateBucketEvent() { 
    Bucket[] data = null; lock.readLock().lock(); data = bucketArray.toArray(); if (data == null || data.length == 0) { 
    return; } long successNum = 0L; long failNum = 0L; for (int i = 0; i < data.length; i++) { 
    Bucket tmp = data[i]; successNum += tmp.getSuccessNum().get; failNum += tmp.getFailNum().get; failNum += tmp.getTimeoutNum().get; } Long reqNum = successNum + failNum; int errorPercentage = (int) ((double) failNum / (successNum + failNum) * 100); circuitBreaker.tryMarkOpen(errorPercentage, reqNum); } 

6、Hystrix应用

Hystrix 语义为“豪猪”,是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟、异常,引起资源耗尽导致系统不可用的解决方案。

(1)解决问题

  • 阻止某个有问题调用耗尽系统的所有线程
  • 阻止错误在分布式系统之间的传播
  • 快速识别代替请求排队
  • 错误回退、优雅的服务降级

(2)问题出现原因

  • 服务提供者不可用:某依赖不可用,请求无法正常返回
  • 重试加大流量:代码重试逻辑或客户端重试,导致流量加大
  • 服务调用者不可用:线程资源耗尽,导致服务调用者不可用

解决方案:对依赖做隔离

(3)依赖隔离

  • 包装依赖调用逻辑,每个命令在单独线程中执行
  • 可配置依赖调用超时时间,当调用超时时,执行fallback逻辑
  • 线程池已满调用将被立即拒绝,快速失败判断
  • 依赖调用请求失败(异常、拒绝、超时、断路)时执行fallback逻辑
  • 提供熔断器组件,可以自动运行或手动调用

(4)主要特性

  • 资源隔离:限制调用服务使用的资源,当某一下游服务出现问题时,不会影响整个调用链
  • 熔断机制:当失败率达到阈值自动触发熔断,熔断器触发不在进行调用
  • 降级机制:超时、资源或触发熔断后,调用预设的降级接口返回托底数据

(5)Hystrix执行逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQYIXzA8-1636439807520)(/Users/fly.liu/Library/Application Support/typora-user-images/image-20211109143419855.png)]

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/176273.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午10:19
下一篇 2026年3月26日 下午10:19


相关推荐

  • 如何将本地文件通过终端上传到linux服务器 /服务器/阿里云「建议收藏」

    如何将本地文件通过终端上传到linux服务器 /服务器/阿里云「建议收藏」scp-P端口c://xxxx.txtuser@ip:/home/root注意:-P大写-i公钥(我是将文件上传到阿里云)(1)在本地的终端下,而不是在服务器上。在本地的终端上才能将本地的文件拷入服务器。(2)scp-rlocalfile.txtusername@192.168.0.1:/home/username/其中,1)scp是命令,-r是参…

    2022年4月30日
    405
  • SpringBoot+MyBatis整合中的坑以及Property ‘sqlSessionFactory’ or ‘sqlSessionTemplate’ are required错误详解

    SpringBoot+MyBatis整合中的坑以及Property ‘sqlSessionFactory’ or ‘sqlSessionTemplate’ are required错误详解今天想重新学习一下之前大学学过的Springboot框架。然后参考此篇文章进行学习,https://blog.csdn.net/saytime/article/details/74783296使用SpringToolSuit的开发工具。整个项目架构如下图所示。然后开始遇到各种坑,特此记下来,以免以后犯同样的错误。坑一:在Mapper层,它是需要加@Mapper注解的,但是网上又有人加@…

    2022年4月29日
    85
  • lambda表达式用法_使用lambda表达式定义函数

    lambda表达式用法_使用lambda表达式定义函数(一)输入参数在Lambda表达式中,输入参数是Lambda运算符的左边部分。它包含参数的数量可以为0、1或者多个。只有当输入参数为1时,Lambda表达式左边的一对小括弧才可以省略。输入参数的数量大于或者等于2时,Lambda表达式左边的一对小括弧中的多个参数质检使用逗号(,)分割。示例1下面创建一个Lambda表达式,它的输入参数的数量为0.该表达式将显示“ThisisaLambdae…

    2026年1月26日
    5
  • 个人能不能开发ctp期货交易_什么是程序化交易期货

    个人能不能开发ctp期货交易_什么是程序化交易期货接触CTP也才半年多,一边学习一边摸索,看到各大CTP的QQ群里,也都是在问一些很菜的问题,就简单总结和介绍下,今天主要是基础知识,即CTP程序的基础和开源的Demo版本:CTP交易接口是由::::::上海期货信息技术有限公司::::::开发的,提供C++的接口,网上也有很多C++的Demo版本,可以直接使用。1:上期所的接口为两个.dll、两个.lib和四个.h文件,初学者可以不要C

    2022年10月8日
    6
  • 使用Protostuff实现序列化与反序列化

    使用Protostuff实现序列化与反序列化使用Protostuff实现序列化与反序列化(1)Protobuf介绍GoogleProtocolBuffer(简称Protobuf)是Google公司内部的混合语言数据标准,目前已经正在使用的有超过48,162种报文格式定义和超过12,183个.proto文件。他们用于RPC系统和持续数据存储系统。ProtocolBuffers是一种轻便高效的结构化数…

    2022年5月4日
    82
  • java 创建新文件_Java创建新文件[通俗易懂]

    java 创建新文件_Java创建新文件[通俗易懂]创建文件是一种非常常见的IO操作,在这一小节中我们将学习如何在java中创建文件的几个方法。在java中创建文件有三种流行的方法,下面将一个一个地来学习。方法一:使用File.createNewFile()方法java.io.File类可用于在Java中创建新文件。当初始化File对象时,需要提供一个文件名,然后调用createNewFile()方法来在Java中创建新文件。如果创建新文件成功,则…

    2022年6月18日
    37

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号