[享学Netflix] 二十七、Hystrix何为断路器的半开状态?HystrixCircuitBreaker详解

看一个人的身价,要看他的对手。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

断路器的思想来源于生活:家中的保险丝、保险盒。而Java程序中的断路器模式的思想貌似首次来源于Netflix这家公司。需要纠正一点:因为很多Java程序员首次听说断路器是源自Spring Cloud,并且恰好Spring Cloud也“不要脸的”有个接口:org.springframework.cloud.client.circuitbreaker.CircuitBreaker,且还有个实现类就恰好也叫HystrixCircuitBreaker(和本文所讲同名),所以不少小伙伴误以为断路器概念是Spring Cloud提出来和实现的。另外,从出生年龄上来看,Spring Cloud晚于Netflix,所以它只是追随者。当然堂堂Spring Cloud也来了一拨趁热度,也恰好证明了Netflix在断路器方面才是先行者。

断路器可以说是Hystrix内部最重要的状态机,是它决定着每个Command的执行过程。


正文

首先我们需要搞清楚的一个问题就是,断路器断的是什么?断路器断的其实就是我们对依赖服务的调用,而我们对依赖服务的调用其实被包装在HystrixCommand里面,断路器断的就是HystrixCommand是否需要对依赖服务发起请求,更直白的一点说,就是断HystrixCommand


概念普及

关于什么是断路器这个问题,应该不用再做更多解释了。本处对它的几个核心概念状态做一个解释。


Retry重试模式 vs 断路器模式

熔断器模式和重试模式有何区别呢?其实两者区别非常明显:

Retry重试模式:不断重试去调用目标方法/远程服务,直到最后成功(或者达到超时或者最大重试次数)为止。
HystrixCircuitBreaker断路器模式:保护每个commandKey对应的程序,防止被拖垮、避免一些无意义的请求尝试。


断路器的三种状态

  • 关闭状态(Closed):断路器关闭,流量可以正常进入
  • 打开/熔断状态(Open):断路器打开,即circuit-breaker熔断状态,拒绝所有流量,走降级逻辑
  • 半开状态(Half-Open):断路器半开状态,Open状态过一段时间(默认5s)转为此状态来尝试恢复。此状态时:允许有且仅一个请求进入,一旦请求成功就关闭断路器。请求失败就到Open状态(这样再过5秒才能转到半开状态)

熔断器的状态管理,可以用状态机来实现:
在这里插入图片描述
关于状态如何流转,各自的触发条件又是什么,建议参考下面的源码解释一起理解。


限流

限流:即限制流量的最大值,是流控的一种方式。

如果做一个简单的限流功能,那是比较容易的,使用常见的限流方案即可比较轻松的实现(当然和算法强相关)。
但如果想做更精准的控制、处理后的细分和快速恢复,还有大量的工作需要做。很多RPC框架也自带流控和熔断功能,比如Dubbo,但功能不够强大,大多需要人工手动操作,离自动还有段距离,这也是为啥需要将其作为一套单独的解决方案的原因,因为它非常重要且对自动化(自愈)要求较高。


降级

降级:即我们常说的服务降级,其实来自于服务等级(或服务分级),根据服务的质量、功能或其他指标,人为的将服务分成多个等级,便于我们分析和定位服务级别,而服务降级指的是当达到某个条件或特殊场景时,需要下调服务等级。比如常见的降级可分为如下几步(仅供参考):

  • 可配置的降级策略:策略一定得可配置,因为不同的服务对服务的质量定义不一样,降级的方案也将不一样
  • 可识别的降级边界:降级边界主要用来植入降级逻辑
  • 数据采集:这些数据可以是当前某段时间的数据,也可以是很长一段时间的历史数据(比如超时时间应该根据采集统计出来的分位数如p99来定)
  • 行为干预:进入降级状态后将会对正常的业务流程产生干预,可能是限流、熔断,也可能是同步流程变为异步流程等(比如发送MQ的变成oneway的形式)等
  • 结果干预:是返回null,还是默认值
  • 快速恢复:如何自动从降级状态变回正常状态

Hystrix提供了三种降级策略:并发、耗时(超时)和错误率,而Hystrix的设计本身就能很好的支持动态的调整这些策略(简单的说就是调整并发、耗时和错误率的阈值),当然,如何去动态调整需要用户自己来实现,Hystrix只提供了入口,就是说,Hystrix并没有提供一个服务端界面来动态调整这些策略,这多少有点让人遗憾。至于Hystrix是如何实现配置动态化的,参见之前讲述的HystrixCommandPropertiesHystrixThreadPoolProperties…即可。

说明:HystrixCircuitBreaker断路器它是基于错误率的方式来进行容错降级的


Client端做还是Server端做?

毫无疑问,答案是:必须Client端做。因为降级强调的三方依赖,所以你依赖别人考虑的“别人”挂了怎么办,所以肯定是客户端来做(服务端根本感知不到客户端的存在,如何降级,自己意淫吗?显然不行嘛)。

关于服务降级,有个很好的建议是:给每一个RPC调用方法都提供降级处理(甚至写个fallabck回滚函数),而不是任由其抛出异常或者造成雪崩。但是,但是,但是:需要注意同一个接口若用处不一样,降级处理方式也可能不一样的。
比方说一个getUserInfo接口:

  • 如果只是为列表展示,那么降级为默认值即可提高可用性(因为别人查询列表,即使名字看到,能看到别的有用信息也可以)
  • 如果是拿名字做逻辑判断,比如若名字含有test字样认为是测试用户的话,那么这种case就不能fallabck,选择抛错是一个合理的方式

关于核心功能的熔断、降级工作希望大家可以在自己的核心系统、核心链路里梳理重视起来,因为血的教训告诉我们:一个慢SQL、一个接口超时很有可能就会造成系统雪崩,所有功能不好用了,损失就非常惨重。

当然leader/架构师重视起来才是第一步,实施交给具体落地人即可


HystrixCircuitBreaker

它是链接到HystrixCommand执行的断路器(每个command都会交给一个断路器来控制),用来控制一个HystrixCommand的执行、拒绝等。

HystrixCircuitBreaker只是接口,但如何控制这块关闭、开启逻辑不会要求,比如你可以用滑动窗口、令牌桶等方式均可。

public interface HystrixCircuitBreaker {
	// 每次执行HystrixCommand命令的之前都会调用这个方法
	// true:允许执行   false:禁止执行
	public boolean allowRequest();
	// 断路器是否打开,true:开启(跳闸啦)  false:关闭
	public boolean isOpen();
	// 标记成功:用于关闭断路器
	void markSuccess();
}

它具有两个实现类NoOpCircuitBreakerHystrixCircuitBreakerImpl


NoOpCircuitBreaker

该断路器实现不做任何实现:不具有断路效果。

HystrixCircuitBreaker:

	// 该类为包级别的访问权限
    static class NoOpCircuitBreaker implements HystrixCircuitBreaker {
        @Override
        public boolean allowRequest() {
            return true;
        }
        @Override
        public boolean isOpen() { // 断路器永远不打开
            return false;
        }
        @Override
        public void markSuccess() {

        }

    }

若你想禁用断路器(配置里有禁用断路器的开关哦,默认是打开的)的时候可以使用它来占位。


HystrixCircuitBreakerImpl(重要)

它是HystrixCircuitBreaker接口的默认实现,基于HystrixCommandMetrics指标数据实现控制逻辑。

HystrixCircuitBreaker:

	static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {

		// command的配置:包括断路器的配置。如`circuitBreakerEnabled/circuitBreakerForceOpen`等等
        private final HystrixCommandProperties properties;
        // command指标信息
        private final HystrixCommandMetrics metrics;
        
        // 标记当前断路器是否是打开的(默认是关闭状态)
        private AtomicBoolean circuitOpen = new AtomicBoolean(false);
        // 记录当断路器打开 或者 最后一次try a 'singleTest'时候的时间戳
		private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

		// 唯一构造器
		// 传了4个参数,其实只用到2个参数,是因为Hystrix也希望你自己可以定制断路器的逻辑实现
        protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            this.properties = properties;
            this.metrics = metrics;
        }

	}

注意:在老版本的Hystrix中,状态使用一个枚举值来表示的,而新版本中仅有true和false两种状态,至于半开状态新版本实现更加巧妙,请注意区分。

enum Status {
    CLOSED, OPEN, HALF_OPEN;
}
private final AtomicReference<Status> status = new AtomicReference<Status>(Status.CLOSED);
private final AtomicLong circuitOpened = new AtomicLong(-1);

使用Atomic的意义是保证并发时,只有一笔请求能成功改变状态。

它用HystrixCommandMetrics来完成指标收集(成功or失败次数等),依据HystrixCommandProperties的配置策略来执行逻辑。下面它对接口方法逐个介绍:


markSuccess()

标记一下成功了,用来闭合断路器。

HystrixCircuitBreaker.HystrixCircuitBreakerImpl:

        public void markSuccess() {
        	// 断路器必须是已经处于打开状态,mark才有意义嘛
            if (circuitOpen.get()) {
            	// mark一下后,立马关闭断路器~~~~
                if (circuitOpen.compareAndSet(true, false)) {
                    // 重置度量指标
                    metrics.resetStream();
                }
            }
        }

标记成功,对应的就会把断路器给闭合,并且重置度量指标信息,以恢复到正常状态。


isOpen()

判断前断路器是否打开。这个逻辑是最为重要的,负责了断路器的判断和开启工作~

HystrixCircuitBreaker.HystrixCircuitBreakerImpl:

        @Override
        public boolean isOpen() {
        	// 如果已经是打开的,那就没啥好说的喽
        	// 注意:这里并不会尝试去关闭断路器,这个事交给markSuccess或者allowSingleTest去完成即可
            if (circuitOpen.get()) {
                return true;
            }

            // ===到这是关闭状态,所以会执行目标方法。因此需要搜集是否有错误发生====

			// HealthCounts:检索总请求、错误计数和错误百分比的快照。
			// 注意也是获取最新的哦:healthCountsStream.getLatest()
            HealthCounts health = metrics.getHealthCounts();

            // 如果统计这刻的请求数都不达标,那就肯定不开断路器
            // circuitBreakerRequestVolumeThreshold默认值是20,也就是这段时间内要至少20个请求才考虑熔断的逻辑
            // 默认:10秒内必须出现20个请求
            if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
                return false;
            }

			// 请求数超过20了,就继续判断错误率是否达标
			// circuitBreakerErrorThresholdPercentage默认错误率:50
			// 就是说10秒钟内50%的请求都失败了,那才会触发熔断
            if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
                return false;
            } else {
            	// 错误率太高,那就打开熔断器。并且标记打开的时间,
            	// circuitOpenedOrLastTestedTime表示开启一个timer的起始时刻
                if (circuitOpen.compareAndSet(false, true)) {
                   	circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
                    return true;
                } else { // 这一步不要犯错:不管设置成功与否,一定要设置为true
                    return true;
                }
            }
        }
    }

该方法不仅是判断,而且还负责了断路器的打开(不负责关闭,关闭交给markSuccess方法)。这个逻辑本来很复杂,但是当从上几篇文章了解了HystrixCommandMetrics以及HealthCounts的作用后,理解起来异常顺滑,完全0障碍有木有。

该方法处理步骤总结如下:

  • 若断路器已经是打开状态,直接返回true(表示熔断器已打开)。若是关闭状态,继续下一步
  • 根据HealthCounts的指标信息进行判断:
    • 这段时间内(一个时间窗口)内请求数低于circuitBreakerRequestVolumeThreshold(默认值是20),就直接返回false。若高于20就继续下一步判断
    • 若错误率health.getErrorPercentage()小于配置的阈值circuitBreakerErrorThresholdPercentage(默认值是50%),就直接返回false,若符合条件,也就是错误率高于50%就触发熔断(断路器标记为打开),返回true

总而言之,默认情况下,若10s秒内请求数超过20个,并且错误率超过50%就打开熔断器,触发熔断


allowRequest()

每个Hystrix命令的请求都通过它判断是否被执行。

HystrixCircuitBreaker.HystrixCircuitBreakerImpl:

        @Override
        public boolean allowRequest() {
            if (properties.circuitBreakerForceOpen().get()) {
                return false;
            }
            if (properties.circuitBreakerForceClosed().get()) {
                isOpen(); // 模拟正常isOpen的逻辑
                return true;
            }
            return !isOpen() || allowSingleTest();
        }

		// 该方法唯一调用方:allowRequest。所以此处我改为private的吧(源码是public)
        private boolean allowSingleTest() {
        	// 最近一次打开断路器的时间
            long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();

			// circuitBreakerSleepWindowInMilliseconds断路器休眠值:默认5s
            if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
                if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
                    return true;
                }
            }
            // 若断路器关闭的。或者打开了还没超过5秒呢
            return false;
        }
  1. 如果配置强制打开,永远返回false,表示不允许请求
  2. 如果配置强制关闭,永远返回true,表示放行。
    1. 需要注意的是:每次还是会执行isOpen()以执行它的计算,以便模拟正常的行为
  3. 调用isOpen()判断断路器是否已经打开状态:
    1. 断路器是关闭的(isOpen() = false):直接放行
    2. 断路器是打开的(isOpen() = true) :那还得看看allowSingleTest()方法的返回值,若它返回true,那也会允许放行的。(你可以认为这里的allowSingleTest就是模拟一个请求去试一试)
      1. allowSingleTest()逻辑:

正常情况下配置并不会强制打开/关闭。而是有步骤3来动态裁定


Hystrix在运行过程中会向每个commandKey对应的熔断器报告 成功、失败、超时和拒绝的状态,熔断器维护计算统计的数据,根据这些统计的信息来确定熔断器是否打开。如果打开,后续的请求都会被截断。然后会隔一段时间默认是5s,尝试半开,放入一个请求进来,相当于对依赖服务进行一次健康检查,如果恢复,熔断器关闭,随后完全恢复调用。

解读(Half-Open)半开状态

因为Hystrix最新版本1.5.18已经没有枚举值去维护CLOSED, OPEN, HALF_OPEN;这三个状态了,所以有的小伙伴以为它已经没有了这种Half-Open中间态,其实不然,只是现在的实现更加巧妙。

它使用的allowSingleTest()这个方法实现半开状态:若断路器是打开状态并且过了睡眠期,就放1个请求进来试试让其正常执行,此时它并不会打开断路器,但是,但是它做了一个小动作:把timeCircuitOpenedOrWasLastTested的值更新为最新值,以确保“睡眠期”内只能有一个请求进来。

总结一下半开状态的实现原理:断路器开着的时候,每5秒钟(默认值)仅放一个请求进来试试。若请求成功了,会关闭断路器(由AbstractCommand调用markSuccess());若请求失败了,就继续维持断路器的打开状态。


sleep / timer改进方案

每当断路器被打开时,都会进入一个“睡眠期”,你也可以理解为timer倒计时。目前设计的缺陷是这是个固定的倒计时,其实是可以优化一把,增加自愈的能力的。

参考支付宝的回调方式,此处的这个timer可以采用不断增长的策略的:在熔断器开始进入断开状态的时候,可以设置超时时间为5秒钟,然后如果错误没有被解决,然后将该超时时间设置为10s、60s、5分钟…,这样其实留给的自愈时间更多,弹性更大是否会更大一些呢?


总结

熔断器模式使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响。它通过快速的拒绝那些试图有可能调用会导致错误的服务,而不会去等待操作超时或者永远不会不返回结果来提高系统的响应事件,而不至于请求积压太多而拖垮。
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了362 篇原创文章 · 获赞 531 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104557387