hystrix请求合并原理

Hystrix要解决的问题是什么

在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障, 如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务

举例来说:某个服务有 30 个依赖服务,每个依赖服务的可用性非常高,已经达到了 99.99% 的高可用性

那么该服务的可用性就是 99.99% - (100% - 99.99% * 30 = 0.3%)= 99.69%, 意味着 3% 的请求可能会失败,因为 3% 的时间内系统可能出现了故障不可用了

对于 1 亿次访问来说,3% 的请求失败也就意味着 300万 次请求会失败,也意味着每个月有 2个 小时的时间系统是不可用的, 在真实生产环境中,可能更加糟糕

上面的描述想表达的意思是:即使你每个依赖服务都是 99.99% 高可用性,但是一旦你有几十个依赖服务, 还是会导致你每个月都有几个小时是不可用的

下面画图分析说,当某一个依赖服务出现了调用延迟或者调用失败时,为什么会拖垮当前这个服务? 以及在分布式系统中,故障是如何快速蔓延的?

简而言之:

1.假设只有系统承受并发能力是 100 个线程,

2.C 出问题的时候,耗时增加,将导致当前进入的 40 个线程得不到释放

3.后续大量的请求涌进来,也是先调用 c,然后又在这里了

4.最后 100 个线程都被卡在 c 了,资源耗尽,导致整个服务不能提供服务

5.那么其他依赖的服务也会出现上述问题,导致整个系统全盘崩溃

Hystrix 是如何实现它的目标的?
通过 HystrixCommand 或者 HystrixObservableCommand 来封装对外部依赖的访问请求 d这个访问请求一般会运行在独立的线程中,资源隔离

对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。这个超时时间默认是 99.5% 的访问时间,但是一般我们可以自己设置一下

为每一个依赖服务维护一个独立的线程池,或者是 semaphore(信号量),当线程池已满时,直接拒绝对这个服务的调用

对依赖服务的调用的成功次数、失败次数、拒绝次数、超时次数,进行统计
如果对一个依赖服务的调用失败次数超过了一定的阈值,自动进行熔断

在一定时间内对该服务的调用直接降级,一段时间后再自动尝试恢复

当一个服务调用出现失败、被拒绝、超时、短路(熔断)等异常情况时,自动调用 fallback 降级机制

对属性和配置的修改提供近实时的支持
hystrix请求合并原理

为什么要使用 request collapser?

偶发高并发状态下:我们常用的处 理方式???

1.限流
2.削峰
3.请求合并

hystrix请求合并原理

由上图可见,使用请求合并,能够减少并发执行 HystrixCommand 执行所需的线程数和网络连接数,当我们遇见偶发性高并发场景时,可以使用请求合并来降低服务器和数据库的压力。

如果让我们来实现一个请求合并,思路如何?

我们可以看到上图:
1.当请求进来时,我们将请求放入队列中;
2.设置定时任务去定时处理这些任务。如单个查询id=1,2,3的去请求合并为批量查询。
3.将批量查询返回的结果集,通过请求的唯一标识code分发到每个请求中去。

hystrix的实现:

1.请求发起时,创建HystrixCollapser实力,判断是否存在缓存,如果存在直接返回。否则继续执行。

2.判断当前合并请求设置的作用域,不同的作用域获得不同请求合并器:
作用域如下:

1. global context
tomcat 所有调用线程,对一个依赖服务的任何一个 command 调用都可以被合并在一起,hystrix 就传递一个HystrixRequestContext对于这一级别,没有更多的解释,官方推荐使用下面的那一个

2. user request context
tomcat 内某一个调用线程,将某一个 tomcat 线程对某个依赖服务的多个 command 调用合并在一起 此种方式在前面 request cache 中已经演示过怎么配置请求上下文了 此种级别是 hystrix默认级别

3. object modeling 基于对象的请求合并
如果有几百个对象,遍历后依次调用每个对象的某个方法,可能导致发起几百次网络请求,基于 hystrix
可以自动将对多个对象模型的调用合并到一起此方式也不推荐使用,没有更多的描述 上面的一个能实现的重要点是:使 用请求合并 command 的时候,是需要单独提供一个批量获取数据的接口。 hystrix 负责把某一个时间窗口内的 command 合并成一个 collapser ,然后由你去调用这个批量获取数据接口

3.拿到请求合并器之后,调用submitRequest()方法将提交的批量请求进行合并。这里面使用到了
timerListener-》CollapsedTask类设置了定时任务,间隔10ms执行中断,将10ms内的所有请求执行
createObservableCommand,将返回的结果集,response仍然塞入CollapsedRequest(shardRequests)中,将返回结果设置为Observable,其中Observable就持有请求和响应的所有信息。

4.每一个请求线程,在发起请求之后,会调用this.toObservable().toBlocking().toFuture().get(),通过阻塞的方式从future中拿去响应结果,在合并线程执行完之后,会将结果集放入Observable中,此时future.get()就会拿到结果,接口返回。

请求合并的代价是多少?

使用请求合并技术最大的代价就是:导致延迟大幅度增强,因为需要将一定时间内的多个请求合并

比如:发送 10 个请求,每个请求大概是 5 毫秒可以返回,要把 10 个请求合并在一个 command
内,统一执行,那么就有一个等待时间,假设是 10 毫秒,执行时间就可能变成 15 毫秒,延迟了 10
毫秒;通常情况下请求不会恰好在等待时间到达刚好 10 毫秒的时候才发出去请求,所以延迟还可以降一半,本例子为 5ms。
这个延迟开销是否是有意义的取决于正在执行的
command,并且与并发数量也有关系,因为有两个策略会触发合并请求发出去,一个是请求达到阀值,一个是等待时间达到阀值。请求合并并不适合在单线程中等待执行,这样会加大延迟。
比如 10 个请求单个数据总耗时将花费 50 毫秒,平均 5 毫秒;当并发 10 线程请求的时候,那么只需要 5 毫秒即可拿到数据;
同样,为了尽快达到合并发出请求阀值,在并发下会更快的达到阀值,批量请求数据可能需要耗时 10 毫秒。
总起看起来,不合并至需要 5 毫秒,合并之后可能需要 15 毫秒(10 毫秒请求接口,因为是批量数据的接口,假设会比单个耗时一点 + 5毫秒的等待时间)

那么好处是什么呢?

单个接口获取数据与批量接口获取数据去掉获取数据的时间因素(往往是业务聚合等因素),那么就是网络开销了;
如果特定命令同时大量使用并且可以将数十个甚至数百个调用一起批量处理,那么由于 Hystrix减少了所需的线程数量和网络连接数量,因此实现的吞吐量通常远远超过了这个代价。所以总结:

请求合并适合在高延迟 + 大量并发的情况下使用

每个请求就 2ms,batch 需要 8~10ms,延迟增加了 4~5 倍
每个请求本来就 30ms~50ms,batch 需要35ms~55ms,延迟增加不太明显