1. 概述
因印尼双十二促销,系统中存在个别服务问题,组内领导考虑到在目前的网关中加入限流。虽然在Nginx层加入了引流。但在促销时,个别服务问题会导致服务器级联吃空CPU和内存,导致服务超时响应。加入hystrix,虽然消耗掉一些性能,但是能防止系统雪崩,能提高系统整体服务能力。下面从hystrix的原理和网关如何引入hystrix两个方面进行讲解。
2. 一般系统存在的问题
无论是采用SOA架构的系统,还是微服务架构系统。都会存在一个用户请求在后台由多个服务在支撑,如下图1:一个用户请求由Tomcat的某个线程接收,该线程请求网络上的依赖服务A,H,I和P。当I出现拒绝服务时,就会导致该用户请求一直占用Tomcat的线程,直到Tomcat的线程超时(超时时间由connectionTimeout="20000"设置,默认为20s,maxThreads:最大并发连接数,默认为200,maxSpareThreads:这个参数标识,一旦创建的线程数量超过这个值,Tomcat就会关闭不活动的线程,默认为50)。
图1 一般系统内部依赖关系
一般系统从整个框架来看,如下图的API网关, Web、Android等设备都从API获得数据。
图2 来自腾讯API网关
API网关概念:API 网关(API Gateway)是 API 托管服务。提供 API 的完整生命周期管理,包括创建、维护、发布、运行、下线等。您可使用 API Gateway 封装自身业务,将您的数据、业务逻辑或功能安全可靠的开放出来,用以实现自身系统集成、以及与合作伙伴的业务连接。
从API网关内部来看,如图1。容器用某个线程接收到用户请求(Tomcat为例),容器线程从远程获取各个依赖的服务资源,组装计算好并返回。如果单个网络调用在每秒50次请求以上,用户请求将因延迟而阻塞,所有的请求线程也将在短时内阻塞。这种结构有以下问题:
- 单个rpc延迟、失败重试由jsf控制。但没有统计延迟失败信息,也无法预测失败。
- 单个依赖阻塞,当不能快速熔断时,线程一直占用cpu及内存资源导致级联影响,甚至引起雪崩。因为无法快速失败。
- 监控由ump控制,ump是公司内部统一监控平台,其他公司不一定有监控。
考虑到应用容器的线程数目基本都是固定的(比如tomcat的线程池默认200),当在高并发的情况下,某一外部依赖服务超时阻塞,就有可能使得整个主线程池被占满,这是长请求拥塞反模式。如在秒级打到该容器上存在多个请求,大于Tomcat应用容器的线程数目,且其中某个服务提供者阻塞的情况下,将导致所有请求所在的线程阻塞。如下图所示。全部线程栈占用内存和CPU将导致整个机器拒绝服务,而依赖该服务的其他服务,就又可能会重复产生上述问题。因此整个系统就像雪崩一样逐渐的扩散、坍塌。
3. 什么是hystrix?
hystrix([hɪst'rɪks])是豪猪的意思。豪猪是一种全身是刺的动物,netflix使用hystrix意味着hystrix能够像豪猪的刺一样保护你的应用。Hystrix是Netflix(网飞公司)开源的一款容错系统。该框架为分布式环境提供了弹性、增加了延迟容忍和容错。
3.1 雪崩产生原因?
- 服务提供者不可用,导致服务调用者线程资源耗尽是产生雪崩的原因之一。
- 服务调用者自身流量激增,导致系统负载升高。比如异常流量、用户重试、代码逻辑重复。
- 缓存到期刷新,使得请求都流向数据库,Cache瞬间命中率为0,相当于Cache被击穿。
- 重试机制,比如我们rpc框架的retry次数,每次重试都可能会进一步恶化服务提供者。
- 硬件故障,比如机房断点,电缆被挖等等。
3.2 雪崩实例
主要模拟服务提供者不可用。在Controller层通过当前线程做循环操作来模拟服务提供者的耗时操作导致的服务调用者线程占用大量内存。代码如下:
通过ab命令,一共发起10万个请求,同时并发为100,并激活每个请求的keepAlive属性。(-k:激活HTTP中的“keepAlive”特性)。用jvisualvm来监控java线程的活动状态,如下图左边内容。
通过左图cpu使用率可以看到,一旦开启测试,cpu立即上升,并维持在80%左右。GC活动一直占用很低的cpu利用率。第二图堆大小维持在1000MB左右,而被使用的堆因GC回收成锯齿状上下波动。并且波峰都维持在80%。左下角图中,总的类加载维持在一个相对稳定的状态(6000以上),后台线程与可用线程维持在150以下的一个稳定值。
整个系统资源几乎都为Tomcat容器耗尽。如下图。4核将近有3核在运行Tomcat。8G内存也几乎将全部耗尽。
当kill掉ab测试。资源将成下图变化。cpu和内存使用率急剧下降。请读者分析左四图形成的原因。
通过上面的测试,如果单个网络请求如果被阻塞,在短时间内的系统开销将变得不容乐观。
3.3 常见的解决方案
针对上述雪崩情景,有很多应对方案,但是没有一个万能的模式能够应对所有的情况。
- 针对服务调用者自身流量激增,我们可以采用auto-scaling方式进行自动扩容以应对突发流量,或在负载均衡上安装限流模块。参考:春节日活跃用户超一亿,探秘如何实现服务器分钟级扩容。
- 针对缓存到期刷新,我们也有很多方案,参考Cache应用中的服务过载案例研究。
- 针对重试机制,我们可以减少或关闭重试,直接采用failfast,或采用failsafe进行优雅降级。
- 针对硬件故障,我们可以做多机房容灾,异地多活等。
- 针对服务提供者不可用,可以使用资源隔离,熔断机制等避免在分布式系统中,单个组件的失败导致的级联影响,甚至是雪崩的情况。参考Martin Fowler的熔断器模式。
而Hystrix采用缓存机制减少对后台服务的请求,缓存刷新由自己代码控制。采用熔断器回退fallback机制能够解决针对重试的快速失败。如果存在异地多活,Hystrix可以配置成主备双活机制。针对服务不可用,Hystrix作用在客户端,客户端程序依赖Hystrix相关的第三方包,使得客户端与所依赖的服务形成资源隔离,通过线程隔离或者信号量隔离保护主线程池(Tomcat线程池,调用者线程池),使用熔断器的快速失败,迅速恢复机制,当单个组件服务失败率到一定程度后,再次请求,会直接响应失败,并且之后会有重试机制。另外,Hystrix提供优雅降级及近乎实时的监控。
3.4 hystrix设计原则
- 防止任何单个依赖占满Web容器线程池。
- 直接响应失败,而不是一直等待。-不放到队列排队。
- 提供错误返回接口,而不是让用户线程直接处理依赖服务抛出的异常。
- 使用隔离或熔断技术来降低并限制单个依赖对整个系统造成的影响(耗尽系统资源)。内部通过舱壁隔离(线程隔离和信号量隔离)使请求互不影响,熔断器机制使本次请求Fast Fail。
3.5 hystrix如何解决以上问题?
- 对外部系统的请求,使用HystrixCommand包装,并使用单独的线程执行。
- 可对服务请求设置timeout,超时后,直接返回,可根据平时监控,查看该服务95%的请求的运行时间为多少进行设置。
- 维护一个小的线程池在客户端,专门处里对指定服务的请求,当线程使用量超过线程池容量时,直接返回响应,而不是排队等待处里。
- 触发熔断来停止所有对指定服务在一定时间范围内的请求,针对该服务请求的错误数百分比可手动,可自动熔断。
- 对超时,失败,或者熔断,做特殊错误返回处理fallback。
4. Hystrix工作原理
4.1 加入Hystrix后系统的依赖关系
Hystrix采用舱壁隔离的原则,将外部依赖的每个请求包装成一个个小线程池。每个线程池负责一个请求资源的调用。当某个线程池出现资源拒绝获取或者超时时,将只会导致该线程池被打满,当再次有其他主线程池中线程请求资源时,Hystrix会快速失败,而不会影响整个应用主容器资源被耗尽,如下图。
4.2 Hystrix工作流
- 构建HystrixCommand或者HystrixObservableCommand对象;
- 执行命令execute()、queue()、observe()、toObservable();
- 如果请求结果缓存这个特性被启用,并且缓存命中,则缓存的回应会立即通过一个Observable对象的形式返回;
- 检查熔断器状态,确定请求线路是否是开路,如果请求线路是开路,Hystrix将不会执行这个命令,而是直接使用『失败回退逻辑』,即不会执行run(),直接执行getFallback();
- 如果和当前需要执行的命令相关联的线程池和请求队列(或者信号量,如果不使用线程池)满了,Hystrix 将不会执行这个命令,而是直接使用『失败回退逻辑』,即不会执行run(),直接跳转到8执行getFallback();
- 执行HystrixCommand.run()或HystrixObservableCommand.construct(),如果这两个方法执行超时或者执行失败,则执行getFallback();如果正常结束,Hystrix 在添加一些日志和监控数据采集之后,直接返回回应;
- Hystrix会将请求成功,失败,被拒绝或超时信息报告给熔断器,熔断器维护一些用于统计数据用的计数器。
- 得到 Fallback。
- 返回成功的响应。
4.3 设计模式
4.3.1 命令模式
命令模式类似于司令员发出命令让士兵执行。其中有三个实体,一个是司令员,一个是士兵,一个是命令。如下图,主线程相当于于司令员,发出一个YouCommand命令,而该命令继承HystrixCommand<R>,HystrixCommand<R>继承AbstractCommand<R>,这两者都实现HystrixExecutable接口。最后由依赖线程(士兵)执行。所以,Tomcat线程中某个类中必须有YouCommand对象,最后由依赖线程池执行。
4.3.2 观察者模式
观察者模式是广播机制的核心。此模式中有两个实体:观察者及观察的目标对象。把多个订阅者、客户称为观察者。需要同步给多个订阅者的数据封装到对象中,称为观察者的目标(订阅主题)。如果站在主题的角度考虑:一个主题,往往有多个观察者在观察。Hystrix通过观察者模式对服务进行状态监听。
每个任务都包含一个对应的Metrics,所有Metrics都由一个ConcurrentHashMap来进行维护,Key是CommandKey.name(),在任务的不同阶段会往Metrics中写入不同的信息,Metrics会对统计到的历史信息进行统计汇总,供熔断器使用。如下图,Metrics Storage中保存了每个接口的访问结果数据。
4.3.3 Hystrix命令模式和观察者模式的应用
- 同步执行。调用.execute()方法。将会阻塞当前线程直到获取结果。
- 异步执行。调用.queue()方法。不阻塞当前线程,返回一个Future对象。从图上可以看出,.queue().get()等于同步调用.execute()。
- 热注册观察者执行。调用.observer()方法。返回一个Observable对象,当run方法执行完后,进入观察者订阅的事件中。
- 冷注册观察者执行。调用.toObservable()方法。同样返回一个Observable对象。但在注册时即执行run()方法。
无论你使用哪种方式引发命令,Hystrix Command总是回到Observable对象的形式。
4.4 断路器
下图展示了HystrixCommand或HystrixObservableCommand怎样与HystrixCircuitBreaker交互及它的逻辑和决策流程图,包括在熔断器中如何计数等。
下面描述了断路器打开或者关闭发生的精确方式。
- 假设大量的请求数量超过了HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()的阈值【1】,并且依赖调用失败的百分比超过了HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()的阈值【2】,熔断器将会从关闭状态变成打开状态;
- 在熔断器处于打开状态的期间,所有对这个依赖进行的调用都会短路,即不进行真正的依赖调用,返回失败;
- 在等待(冷却)的时间超过HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()的值后,熔断器将处于半开的状态,将允许单个请求去调用依赖,如果这次的依赖调用还是失败,熔断器状态将再次变成打开,这个打开状态持续时间是HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()配置的值;如果这次的依赖调用成功,熔断器状态将变成关闭,后续依赖调用可正常执行。
下面描述了熔断器中的计数方式。其中涉及到circuitBreakerSleepWindowInMilliseconds、circuitBreakerRequestVolumeThreshold、circuitBreakerErrorThresholdPercentage三个值的理解。
先说下Metrics如何统计?
Metrics在统计各种状态时,采用滑动窗口的思想。在一个滑动窗口时间中又划分为多个Bucket(这里滑动窗口时间与Bucket一定成倍数关系,否则会报错),滑动窗口的移动以Bucket为单位滑动,而每个HealthCounts对象记录一个Buckets的监控状态,Buckets为一个滑动窗口的一小部分,如果一个滑动窗口时间为t,Bucket数量为n,那么每t/n秒将新建一个HealthCounts对象。
三个值的意义?
circuitBreakerSleepWindowInMilliseconds(断路器睡眠窗口时间):此属性用来设置电路跳闸之后拒绝请求之前允许再次尝试的时间大小用来决定电路是否应该再次关闭。
circuitBreakerRequestVolumeThreshold(断路器请求容量阈值):这个属性设置滚动窗口中的最小请求数量来决定是否触发断路器。比如,如果这个值为20,然后如果仅仅只有19个请求在滚动窗口(指的是10秒钟的窗口)能接收到,即时这19个请求都失败,电路也不会触发断路器打开。
circuitBreakerErrorThresholdPercentage(断路器错误阈值百分比):这个属性设置错误百分比之上的电路应该会被触发打开开始短路,请求转到fallback逻辑。