Spring Cloud源码分析——Hystrix服务容错保护
在微服务架构中,系统被拆分为多个服务单元,各个服务单元之间通过服务注册和订阅的方式互相依赖。每个服务单元运行在不同进程中,依赖通过远程调用的方式执行。
运行期间,可能因为网络原因或服务自身问题导致调用故障或延迟,而这些问题又会直接导致调用方的对外服务也出现延迟。如果调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,继而最终导致自身服务瘫痪,更严重的后果会是故障蔓延导致整个系统的瘫痪。
为了解决以上的问题,断路器等一系列服务保护机制应运而生。(参考了翟永超[程序猿DD])的《Spring Cloud微服务实战》和 Hystrix工作原理(官方文档翻译))
本节从几个方面对Spring Cloud Hystrix进行讨论。
- 服务降级
- 依赖隔离
- 断路器
服务降级
Hystrix工作流程从官方流程图中,我们来解析下都发生了些什么
-
构建一个HystrixCommand或HystrixObservableCommand对象
该对象表示一个依赖请求,向构造函数中传入请求依赖所需要的参数。根据返回响应来决定构建HystrixCommand还是HystrixObservableCommand -
执行命令
1.execute() :该方法是阻塞的,从依赖请求中接收到单个响应(或者出错抛异常)
2.queue() : 从依赖请求中返回一个包含单个响应的Future对象
3.observe() : 订阅一个从依赖请求中返回的代表响应的Observable对象
4.toObservable() : 返回一个Observable对象,只有当你订阅它时,它才会执行Hystrix命令并发射响应P.S 同步调用execute()实际上就是调用queue().get()方法,queue()方法的调用的是toObservable() .toBlocking().toFuture()。简单来说,最终每一个HystrixCommand都是通过Observable来实现的,就算这些命令只是返回一个简单的单个值。
-
响应是否被缓存
-
断路器(circuit-breaker)是否打开
当命令执行时,Hystrix会检查断路器是否打开。如果已打开(或tripped),那Hystrix就不会再执行命令,而是直接路由到getFallback() or resumeWithFallback(),获取fallback方法,并执行fallback逻辑 -
线程池、队列、信号量是否已满
跟上一步一样,如果与该命令相关的线程池或队列已满,就不再执行命令,直接执行fallback逻辑 -
HystrixObservableCommand.construct()或HystrixCommand.run()
通过自定义的方法逻辑来调用对依赖的请求 -
计算回路指标(calculate circuit health)
Hystrix会报告成功、失败、拒绝和超时的指标给断路器(circuit-breaker),断路器(circuit-breaker)包含了一系列滑动窗口数据,并通过该数据统计。
P.S Hystrix使用这些统计数据来决定断路器(circuit-breaker)是否应该熔断,如果需要熔断,则会在一定时间内断开依赖请求(短路请求),当再次检查请求会重新关闭断路器(circuit-breaker) -
获取Fallback(getFallback)
-
返回成功响应
Hystrix命令执行成功,将以Observable形式返回响应给调用者。具体返回根据当初执行的命令是啥。
断路器(circuit-breaker)
上一节服务降级的关键操作之一是断路器。在分布式架构下,当某个服务单元发生故障之后,服务降级逻辑会因为Hystrix命令调用依赖服务超时时间,产生调用堆积、响应延迟。而通过断路器的故障监控,则可以直接切断主逻辑的调用。当然,Hystrix的断路器不仅仅是切断主逻辑依赖这一操作,还有着更复杂的逻辑。
如图所示,服务降级涉及到断路器三个重要参数:快照时间窗、请求总数下限、错误百分比下限。
1.快照时间窗:HystrixCommandProperties.metricsHealthSnapshotIntervalInMilliseconds()
断路器确定是否打开需要统计一些请求或错误数据,统计的时间范围就是快照时间窗。默认为最近10秒。
2.请求总数下限:HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
在快照时间窗内,必须满足请求总数下限才有资格进行熔断。
3.错误百分比下限:HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
当请求总数在快照时间窗内超过下限,并且又超过错误百分比下限,就会打开断路器。
举个例子,断路器在10秒内发现请求总数超过20,并且错误百分比超过了50%,这时断路器会打开Open。打开之后,再有请求调用,将不会调用主逻辑,而是直接调用降级逻辑,这就不会出现等待5秒后才fallback。
主逻辑被熔断后,Hystrix会启动一个休眠时间窗HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),在这个时间窗内,降级fallback逻辑会临时成为主逻辑。当休眠时间窗到期后,断路器会进入HalfOpen半开状态,释放一次请求到原来的主逻辑上,如果正常返回,则断路器闭合Close,主逻辑恢复,否则断路器继续打开Open,休眠时间窗重新计时。
简单来说,断路器可以实现自动发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
再结合HystrixCircuitBreaker源码实现看下
// String is HystrixCommandKey.name() (we can't use HystrixCommandKey directly as we can't guarantee it implements hashcode/equals correctly)
private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap<String, HystrixCircuitBreaker>();
/**
* Get the {@link HystrixCircuitBreaker} instance for a given {@link HystrixCommandKey}.
* <p>
* This is thread-safe and ensures only 1 {@link HystrixCircuitBreaker} per {@link HystrixCommandKey}.
*
* @param key
* {@link HystrixCommandKey} of {@link HystrixCommand} instance requesting the {@link HystrixCircuitBreaker}
* @param group
* Pass-thru to {@link HystrixCircuitBreaker}
* @param properties
* Pass-thru to {@link HystrixCircuitBreaker}
* @param metrics
* Pass-thru to {@link HystrixCircuitBreaker}
* @return {@link HystrixCircuitBreaker} for {@link HystrixCommandKey}
*/
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
// this should find it for all but the first time
HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
if (previouslyCached != null) {
return previouslyCached;
}
// if we get here this is the first time so we need to initialize
// Create and add to the map ... use putIfAbsent to atomically handle the possible race-condition of
// 2 threads hitting this point at the same time and let ConcurrentHashMap provide us our thread-safety
// If 2 threads hit here only one will get added and the other will get a non-null response instead.
HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
if (cbForCommand == null) {
// this means the putIfAbsent step just created a new one so let's retrieve and return it
return circuitBreakersByCommand.get(key.name());
} else {
// this means a race occurred and while attempting to 'put' another one got there before
// and we instead retrieved it and will now return it
return cbForCommand;
}
}
HystrixCircuitBreaker实例化,首先定义ConcurrentHashMap类型的circuitBreakersByCommand对象,最后circuitBreakersByCommand.get(HystrixCommandKey.name())实例出HystrixCircuitBreaker。整个过程是线程安全的,并且确保一个HystrixCommandKey对应一个HystrixCircuitBreaker。
@Override
public void markSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
//This thread wins the race to close the circuit - it resets the stream to start it over from 0
metrics.resetStream();
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
previousSubscription.unsubscribe();
}
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
circuitOpened.set(-1L);
}
}
markSuccess()判断当前状态是不是HalfOpen或Closed,如果是的话,重置metrics指标(或叫Counter计数器),并关掉断路器。
@Override
public boolean attemptExecution() {
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
if (circuitOpened.get() == -1) {
return true;
} else {
if (isAfterSleepWindow()) {
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
//only the first request after sleep window should execute
return true;
} else {
return false;
}
} else {
return false;
}
}
}
attemptExecution() 这个方法,当休眠时间窗到期后,如果当前断路器状态为Open或HalfOpen,就尝试释放请求到原来的主逻辑,从而实现主逻辑自动恢复。
通过一系列断路器机制,实现了切断故障资源的依赖、降级策略自动切换以及主逻辑自动恢复。相较于通过设置开关来监控运维切换的传统方式,断路器模式使得微服务在依赖外部服务或资源的情况下得到很好的保护,同时还具备一些降级逻辑的业务需求自动化切换和恢复的能力。
依赖隔离
Hystrix依赖隔离Hystrix采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。Hystrix会为每一个HystrixCommand命令创建一个独立的线程池,这样即使某个再Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,不会拖慢其他服务。
通过对依赖服务的线程池隔离实现,有以下好处:
- 应用自身得到完全的保护,不会受不可控的依赖服务影响。即使是在给依赖服务分配的线程池被填满的情况下;
- 有效降低了接入新服务的风险;
- 依赖服务自动恢复正常后,它的线程池会被清理并马上恢复健康的服务。要比容器级别的清理恢复速度快很多;
- 依赖服务出现配置错误的时候,线程池可以快速做出反应(通过失败次数、延迟、超时、拒绝等指标的波动);
- 依赖服务因实现机制调整等原因造成其性能出现很大变化的时候,线程池同样可以通过监控指标信息做出反应;
- 每个线程池都提供了内置的并发实现,可以利用其为同步的依赖服务构建异步的访问。
当然,使用线程池隔离会增加系统的负载和开销,如果很在意,Hystrix还有另外一种解决方案:信号量。(信号量默认值为10)。信号量同样可以控制单个依赖服务的并发度,开销和负载都要远小于线程池,但是它不支持设置超时和实现异步访问。
HystrixCommand和HystrixObservableCommand中有两处支持信号量的使用,分别是隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池;Hystrix尝试降级逻辑的时候,它会在调用线程中使用信号量。
第一节中@HystrixCommand将某个方法包装成Hystrix命令,除了定义服务降级之外,还自动为该方法实现调用隔离。所以使用过程中,依赖隔离和服务降级是一体化实现的。
Hystrix源码分析差不多就这样,Hystrix运用了命令模式,线程安全,并发逻辑,设计巧妙。读者可根据以上三个要点,针对性地阅读源码。后面可能会不定期更新,有兴趣的朋友可以在评论区一起讨论研究。
最后有件很重要的事,那就是麻烦点赞关注赞赏,谢谢(๑•̀ㅂ•́)و✧