分类: Java
2019-05-28 19:40:25
本文为系列文章内容,主要讲述关于hystrix源码相关内容,从hystrix项目(2012-11-20 github上立项)推出以来一直备受一线研发人员的关注,在很长一段时间内个人一直在从事针对netflix推出的相关系列框架进行研究及源码解读,为了能够更好的验证自身对netflix hystrix框架的了解,决定针对于自己所了解的内容做一次总结。同时也希望通过发布文章的方式来结识一些对该框架有了解的同学,大家一起来探讨、交流经验。如果分享后效果比较好 ,将会继续对netflix其他框架进行下一系列的分享。在编写系列文章过程中,本主尽量在官方wiki的基础上添加一些在进行源码阅读过程中个人的思考,而不是做一个翻译。
本文讲述Hystrix如何实现其熔断功能——Circuit Breaker。本文在讲述过程中将设计到Hystrix内部源码内容,事先说明一下Hystrix版本,本文以Hystrix 1.5.2版本为主要版本进行代码解读。
在上一节中讲解的服务熔断的基本定义,本节将具体讲解Hystrix如何实现服务的熔断功能,为用户的上游服务起到保护作用。要讲解Hystrix的熔断功能,个人认为官网上的一张图更具代表性说明其熔断的核心原理。所以本节内容首先对官网内容进行翻译,最后根据自身的理解进行最后的阐述。
图 Hystrix执行过程(Hystrix官网)
上图中描述了整个Hystrix内部执行过程,其中包括了:
1、Command的调用过程
2、ResponseCache Available判断过程
3、熔断处理过程
4、内部资源隔离
5、异常处理fallback过程
本节主要讲解Response Cache 和熔断处理过程的原理。首先,Response Cache功能,官方解释是:“如果针对某个command的Cache功能启用,任何请求在发起真正请求(调用熔断处理)之前,都将进行缓存命中判断,如果缓存命中,则返回缓存Response结果”。从描述中我们可以得出,该结果的返回需要依赖“Request Cache”功能的开启,至于RC功能将单独用一篇文章进行描述。在进行RC判断之后,如果没有命中缓存,则调用CircuitBreaker进行判断进行Circuit健康检查,如果CircuitBreaker处于断开状态,则直接fast-fail调用fallback进入错误处理程序。如果CircuitBreaker处于关闭状态,则继续向下执行。其实在整个过程中还存在一个中间状态——half open(半开)。当CircuitBreaker处于半开状态时,将允许一个或者部分请求进行“尝试”执行下游调用,如果调用成功则将断路器进行关闭,否则进行断开。整个过程可以归纳成如下图:
图 熔断状态切换过程及每个过程请求是否可以被执行
上图中主要描述了Hystrix断路器运行时的三个状态:打开、关闭、半开。并描述了每个状态下对于Request请求是否可以被执行的相关内容。接下来,向大家描述一下三个状态如何进行切换(即图中1、2、3)的,即什么条件下断路器才会进行状态的切换,以及切换条件的数据如何进行收集。
首先,断路器切换条件—下游服务调用执行状态(Successes(成功数)、Failures(失败数)、Rejections(拒绝数)、timeouts(超时数)),也就是说断路器的切换由上述四个指标共同决定。这里只解释一下Rejections(拒绝数),大家千万不要认为是所有的拒绝数量,切记只包含在CircuitBreaker关闭状态下线程池或者信号量满了的时候的拒绝数,不包含处于打开状态或者半开状态下的计数。
其次,断路器如何进行监控指标的数据收集,以及如何处罚状态变更?在这一节主要向大家进行简单的概述,在实现一节带着大家通过阅读源码来了解Hystrix 断路器内部如何进行数据收集。此处,只需要大家能够理解断路器在不断接收到监控数据后,通过自身Hystrix配置判断条件是否满足,如果满足则进行断开操作。
图 断路器内部逻辑判断及状态变更(Hystrix官网)
执行过程:
1、判断通过断路器的流量是否满足了某个阈值(配置属性)
2、判断该command的执行错误率是否超出了某个阈值(配置属性)
3、如果满足了上述两个条件,则断路器处于打开状态
4、当断路器处于“打开”状态时,所有针对于该command的执行都将被短路
5、在某个固定时间段后(配置属性),断路器处于“半开”状态,并且接下来的一个请求被允许通过断路器,被执行,如果请求失败,断路器重新回到打开状态并且持续一个新的固定时间段(配置属性),如果请求成功,则断路器变成“关闭”状态。
这里简单介绍一下四个指标的统计过程:
1、Hystrix 断路器会按照用户的配置信息(总的时间区间/BucketsNum数量)设置每个Buckets的监控时间区间
2、每个Bucket内部存储四个指标(成功数、失败数、超时数、拒绝数),这四个指标仅为该Bucket所覆盖的时间区间内的统计数据
3、当执行到下一个时间区间时,创建新的bucket,并将该时间区间的数据记录到对应的指标上。
4、当Bucket数量==用户设置的Buckets数量时,将第一个Bucket进行舍弃操作,并创建新的Bucket。即任何一个时间点断路器内部最多存在BucketsNum个Bucket。
在编写该部分内容的时候,看了一下官方文档,感觉官方文档已经讲的非常清楚了,只不过官方文档是用英文进行撰写的,所以本节以官方文档为主,将官方内容进行翻译。
Hystrix关于Circuit Breaker相关配置属性都存放在HystrixCommandProperties文件中,如下:
1、circuitBreaker.enabled
该属性主要用于控制断路器功能是否生效,是否能够在断路器打开状态下路请求的执行。
默认值 |
true |
默认属性 |
hystrix.command.default.circuitBreaker.enabled |
根据command自定义 |
hystrix.command.HystrixCommandKey.circuitBreaker.enabled |
通过Comand实例进行动态设置 |
HystrixCommandProperties.Setter() .withCircuitBreakerEnabled(boolean value) |
该属性主要表示在一个监控窗口内能够触发断路器打开的最小请求量阈值。听着很绕口是吧 ,意思就是如果监控最小窗口为1s,那么在这一秒内只有请求量超过这个阈值时,才进行错误率的判断,如果没有超过这个阈值,那么将永远都不会以触发熔断。
默认值 |
20 |
默认属性 |
hystrix.command.default.circuitBreaker.requestVolumeThreshold |
根据command自定义 |
hystrix.command.HystrixCommandKey.circuitBreaker.requestVolumeThreshold |
通过Comand实例进行动态设置 |
HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(int value) |
3、circuitBreaker.sleepWindowInMilliseconds
该属性表示断路器打开后,直接执行拒绝请求时间段,在此时间区间内,任何请求都将被直接Fast-Fail。只有当过了该时间段后,circuitBreaker就进入了“半开”状态,才允许一个请求通过尝试执行下游调用。如果该请求在执行过程中失败了,则circuitBreaker进入“打开”状态,如果成功了,则直接进入“关闭”。
默认值 |
5000 |
默认属性 |
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds |
根据command自定义 |
hystrix.command.HystrixCommandKey.circuitBreaker.sleepWindowInMilliseconds |
通过Comand实例进行动态设置 |
HystrixCommandProperties.Setter() .withCircuitBreakerSleepWindowInMilliseconds(int value) |
该属性表示熔断触发条件,即触发熔断时错误百分比。一旦打到这个触发比例,断路器就进入“打开”状态,但前提是,需要整体的请求流量能够满足设置的容量——requestVolumeThreshold。
默认值 |
50 |
默认属性 |
hystrix.command.default.circuitBreaker.errorThresholdPercentage |
根据command自定义 |
hystrix.command.HystrixCommandKey.circuitBreaker.errorThresholdPercentage |
通过Comand实例进行动态设置 |
HystrixCommandProperties.Setter() .withCircuitBreakerErrorThresholdPercentage(int value) |
5、circuitBreaker.forceOpen | circuitBreaker.forceClose
上述两个属性是相互斥的两个属性,分别表示:强制处于“打开”状态、强制处于“关闭”状态,当设置成强制“打开状态”时,所有的请求将被直接Fast-Fail,阻断所有请求。当设置成强制“关闭”状态时,表示Hystrix将忽略所有的错误。
默认值 |
false |
默认属性 |
hystrix.command.default.circuitBreaker.forceClosed | forceOpen |
根据command自定义 |
hystrix.command.HystrixCommandKey.circuitBreaker.forceClosed | forceOpen |
6、metrics.rollingStats.timeInMilliseconds
该属性主要用来设置熔断监控的时间区间,整个熔断过程将实时统计每个时间区间内的监控数据。并计算该时间区间内的错误率(errorPercentage),如果错误率达到用户设置的阈值,则断路器将进行熔断处理,以此实现断路器熔断功能。在这里大家有时很容易产生一个疑问,这个时间区间时连续的吗?回答是:不是连续的,整个hystrix对于时间区间控制为rolling(旋转的),如下图所示:
当系统时钟通过一个Time时,就会判断n == numBuckets是否相等,如果相等,则第一个Bucket将被舍弃,同时创建一个新的Bucket,将这个Time时间内的所有维度在该Bucket内进行计数统计。
7、metrics.rollingStats.numBuckets
该属性表示的上文中提到的Bucket数量。
接下来我们从代码层面深入了解Hystrix如何实现熔断功能的。在Hystrix核心代码库中存在这样两个类HystrixCommand.java 和 AbstractCommand.java ,都是对Hystrix中的Command进行的底层抽象类,HystrixCommand继承AbstractCommand类。继承关系如下图:
图 Hystrix Command类图
此处不过多介绍Command的类图结构以及每个接口的用途,这里主要为了介绍Command如何与circuitBreaker进行关联的。(Hystrix通过HystrixCommand类定义一个Command及其执行过程,如果自定义command需要继承HystrixCommand类并实现其run方法。)Hystrix将command与circuitbreaker关联及circuitbreaker创建过程都在AbstractCommand类的构造方法中完成。具体看如下代码:
从代码中可以看出,在AbstractCommand类的构造方法中初始化了包括:groupKey、commandKey、metrics、threadpool在内的所有内容。本文中我们只关注circuitBreaker,是通过调用initCircuitBreaker() 方法实现,方法内部代码内容如下:
首先通过方法入口参数enabled(hystrix.circuitBreaker.enabled属性)判断是否开启熔断功能,如果全局配置没有开启熔断功能,则返回NoOpCircuitBreaker实现(该类是实现了HystrixCircuitBreaker接口的不包含任何实现内容的实现类),如果开启了熔断功能,则查看入口参数fromConstructor是否为空,如果不为空,说明用户有自定义熔断实现机制,直接返回用户自定义实现,否则,返回Hystrix默认实现,该默认实现是通过HystrixCircuitBreaker.Factory静态内部工厂类进行构建的(在Hystrix内部,你会发现很多以这种接口内部工厂来来构建默认实现的例子。)。该工厂类getInstance()方法的内部实现如下:
首先通过commandKey从工厂的本地缓存中获取默认实现,本地缓存通过ConcurrentHashMap数据结构进行存储,如果缓存不为空则直接返回,如果缓存为空则说明是第一次构建该command的circuit breaker。则直接创建HystrixCircuitBreakerImpl类的实例,并将commandkey、groupKey 、properties、metrics作为构造方法入口参数。并将创建完成的实例存放到本地缓存中。整个circuit breaker的创建过程就此结束。流程如图:
图 Hystrix创建Circuit Breaker过程
接下来我们看一下HystrixCircuitBreakerImpl在创建过程中都做了哪些事。首先构造方法:
先是将入口参数赋值给本地参数,之后(也是最重要的)为了能够实时获取到流量方面的监控,执行了Subscription s = subscribeToStream();执行了这样一段代码,subscribeToStream()方法内部执行了如下操作:
其内部其实只有一段Rxjava的订阅代码,是对Hystrix 的metrics中的HealthCountStream进行了订阅,并实现了Subscriber接口的onNext方法,用于实时处理circuitBreaker的状态变更。OnNext方法中逻辑很简单两个if,分别判断熔触发条件,一个else设置circuitbreaker状态(从close到open),并记录断路器“打开”时间(该时间的记录主要为了配合sleepwindow时间使用)。其实circuitbreaker默认实现还有其他方法:具体如下:
本文内容就此结束,关于断路器如何获取监控指标数据相关内容将在metric一文中进行详细讲解。