Hystrix Command执行以及熔断机制整理

这几天在看Hystrix的一些实现,里面大量运用了rxjava的原理,将代码简化到了极致,对于有rxjava基础的同学,相信看懂Hystrix代码并不是一件难事。我这篇文章主要是针对Hystrix Command执行之后的一个数据流向以及熔断机制做了一个梳理和总结,后续还会出对于Hystrix组装command、超时机制、隔离机制等源代码实现进行一个梳理和总结。这篇文章权当做hystrix梳理的第一步,虽然感觉从代码执行顺序上这篇文章不应该是第一个,但是从这一块开始梳理我感觉可以更好的理解Hystrix中的消息流的一个概念(不得不感叹,有的人写代码就是再创造,而我写代码可能就是为了工作吧。。)

首先我们先了解一下Hystrix熔断的一个基本原理:

Hystrix熔断机制流程图(转自Spring MicroServices in Action)

当出现问题时,Hystrix会检查一个一定时长(图中为10s)的一个时间窗(window),在这个时间窗内是否有足够多的请求,如果有足够多的请求,是否错误率已经达到阈值,如果达到则启动断路器熔断机制,这时再有请求过来就会直接到fallback路径。在断路器打开之后,会有一个sleep window(图中为5s),每经过一个sleep window,当有请求过来的时候,断路器会放掉一个请求给remote 服务,让它去试探下游服务是否已经恢复,如果成功,断路器会恢复到正常状态,让后续请求重新请求到remote 服务,否则,保持熔断状态。

这里我们就会考虑,我们应该怎么去实现这样一个window机制呢?通过ScheduledExecutorService和并发List以及累加器?我确实没想到比较好的方法,当我看了Hystrix的文档和实现之后,恍然大悟。它通过一个叫 metrics.rollingStats.numBuckets的属性,标明我们的window需要被拆分到多少个bucket(桶)中,对于我们上图的例子10s的window,我们设置5个桶的话,每个桶就是2s的时长。我们针对每个桶统计桶内的请求的一个情况(成功or失败),然后对于10s的一个时长window,我们只要组合连续的5个bucket就能得到一个window内的统计数据,就能做一个判断,当有新的bucket来的时候,我们在我们的短路器中只需要抛弃最老的bucket,把最新的bucket加进来,形成一个LinkedList,就能重新做统计了,这样也形成了一个滚动统计的计算模式。这样的样例很适合通过基于流和响应式的reactiveX框架来做,因此Hystrix也采用了RxJava来实现。

在了解了Hystrix熔断的原理之后,我们上一个流程图(跟上面这图简直没法比,丑的想哭):

command执行完成后数据流向

这是我自己整理的一个流程图,途中黄色箭头方向为数据流向方向,其中椭圆框有包含关系是因为这几个类存在继承关系,即HealthCountsStream继承自BucketRollingCountersStream,而BucketRollingCounterStream继承自BucketCountersStream。这个图比较清晰的展现了在一个command执行完成之后,整个数据的流向。下面我将针对每一步的源代码,做下说明。

1. 首先AbstractCommand(HystrixCommand的父类)会发起一个handleCommandEnd方法调用:

在方法执行完成之后调用terminateCommandCleanUp
调用handleCommandEnd

2. 在调用handleCommandEnd方法中,首先会取消掉timoutTimer,避免不必要的timout操作,接着形成一个executionResult,这个result可以看做包含了整个command运行周期所有信息,接着调用HystrixCommandMetrics的markCommandDone操作


handleCommand内容

3. HystrixCommandMetrics这个类本身的用途是作为所有Command的指标衡量的一个工具,在markCommandDone方法中直接调用了HystrixThreadEventStream的executionDone方法


markCommandDone方法

4.在HystrixThreadEventStream中我们需要注意几个东西:首先这个getInstance是从threadLocal中获取一个HystrixThreadEventStream实例,因此我们知道对于一个线程都会有自己独立的HystrixThreadEventStream实例以及它自己的成员变量。

threadLocal声明
getInstance方法

然后在executionDone方法中,会根据executionResult生成一个HystrixCommandCompletion事件,然后将它传递给一个叫wirteOnlyCommandCompletionSubject的实例成员变量。

在这个subject中通过doOnNext的callback最终把消息传递给了HystrixCommandCompletionStream

5. 这个HystrixCommandCompletionStream最终在哪里用到找起来比较麻烦,但是我通过find usage还是找到了他的用处:

在HealthCountsStream中使用了它作为构造函数参数传入到super中,一路跟踪,我们找到了HealthCountsStream的源头:

在BucketedCounterStream(HealthCountersStream终极父类)中,对这个inputStream做了一些列变换,首先通过window操作将HystrixCommandCompletionStream中所得到的事件以bucketSizeInMs的时长分割成了多个子块,然后通过flatMap操作把他们reduce成了一个bucket的summary信息,并通过startWith操作将一段empty的summary列表作为了初始的消息流。

接着我们在BucketCounterStream的子类,也是HealthCounterStream的父类:BucketedRollingCounterStream中,我们看到了进一步的操作:

首先通过window操作将上一步处理好的以bucket为单位的消息流分割成以numBuckets为window长度,1个bucket为步长间隔的消息流(这里可能说的比较拗口,后面会通过图表进行解释),再通过flatMap将每个消息转换成以一个numBuckets长度的window内的summary信息。而flapMap中的reduceWindowToSummary函数,就是HealthCountsStream在构造的时候传入给父类的,因此,在这一步flatMap之后,得到的消息流就已经是HealthCountStream所指定的HealthCounts数据结构了。最终再通过observe方法把这个sourceStream暴露出去(中间有几步不涉及数据变化,所有就不展开了)

6. 最终我们可以看到在HystrixCircuitBreak的默认实现中:

我们可以看到断路器subscribe到了我们刚刚看到的HealthCountsStream,并且在onNext中针对每次发出的消息,通过判断window中的请求数量和错误比例来控制了断路器是否断开的逻辑。整个熔断机制分析也就到了数据的终点。

下面我们通过图表对之前的两个消息流的变换做下说明:

BucketCounterStream原理图



BucketRollingCountStream原理图

还有短路器断开情况下的恢复机制,这里可以稍微说明下:

在之前的代码中我们可以看到:在断路器判定错误过多需要短路的时候会做以下操作:

短路操作

首先会将状态设置到OPEN, 然后在circuitOpened中设置当前的时间戳,这个变量在后面sleep window试探操作时会用到。

在一个command执行之前,我们会通过下列代码进行执行前操作(command的组装预处理执行过程我会在后面的文章中讲解):

command执行前判断

我们进入这个方法看:

attemptExecution实现

我们可以看到,在这个方法中,我们经过一些列的判断,最终查看现在距离上次断路器断开时间是否已经过去了一个sleepWindow,如果是,则将status改为HALF_OPEN,即试探状态,并且通过cas操作保证了只有第一个request能够通过,后续request继续回到fallback执行路径下。

随着command的执行,我们在它complete或者出错的时候会有下列操作:

命令执行后逻辑

可以看到,当命令正常完成,会调用断路器的markSuccess操作,而异常到了fallback中则会执行markNonSuccess

标记操作

这里面的代码就比较简单了,当成功并且状态为HALF_OPEN的时候,重置stream,并且恢复断路器状态和circuitOpened到-1,如果失败则设置当前时间到circuitOpened,方便下一次sleepWindow操作。

下面是为了补充整个执行顺序、数据以及线程相关情况,我在一些关键节点上加了打印当前线程和数据的操作,并执行Hystrix core中本身的一些test例子,打出来的结果如下:

执行结果记录

从上图我们可以看到,本身Command的发起和HealthCounterStream的注册以及Timeout的发起都在main线程中进行,而command的run,直到BucketedCounterStream的一系列消息传递都发生在Hystrix为这个Command开辟的线程池中进行。而由于window操作符的关系,BucketedRollingCounterStream、HealthCountsStream和HystrixCircuitBreaker都在RxJava本身的computation线程池中进行。

一点点自己的总结:

Hystrix乃至于netflix团队对于RxJava这种响应式编程范式可以说已经运用的出神入化,对于rxjava与目前传统编程范式所涉及到中间的migrate问题,Hystrix中都有比较好的实践,像操作符的组合,Subject的应用等等,我也不禁觉得想学好rxjava,还是动手最重要。不过rxjava由于自身操作符lift的关系,在调试的时候可能会比较麻烦,特别是在单步的时候会发现有很长的call stack出现,这也是对于以后开发和运维是一个比较大的挑战。我看下来的感觉是尽量用rxjava自己的api做到一个闭环,把问题域集中在rxjava内部,不要引入外部的机制,是比较好的实践方式。另外,对于我现在的工作,在一些场景中引入响应式编程可能也会让较复杂的场景简单化。因为响应式编程的目的是要将问题和逻辑都集中在信息流中,通过订阅与转换甚至是切换执行线程来完成业务逻辑和状态迁移,在逻辑上保持一个比较简单清晰的模型,并且很容易将业务进行分stage处理。

回到Hystrix本身,其实熔断的整个机制通过stream处理已经划分的比较清晰了,每个阶段会有自己的处理逻辑和转换方式,通过window来实现滚动统计也是非常巧妙。有一点我比较关心是最终写入是通过一个全局的SerializableStream做的write操作。而熟悉rxjava的同学都知道,这种序列化的stream是通过emitter-loop来保证的串行访问,大致原理就是当有多个线程进入时,如果都没有开始emit,那先到的那个会获得权力emit,但这并不算完,后到的thread就把自己的event放置到同一个queue里面,就可以撤了,而先到的那个在emit完自己的event之后,需要检查当前queue是否有多的,如果有那你就把多的都emit了吧,谁让你先来的?直到发现现在queue也空了,你就可以走了。在这个期间如果也有thread进来一样也是放到queue里面。这样可以保证尽量不阻塞thread的同时保证串行,但是我一直都在思索,不会出现一个线程一直发而无法退出执行自己后面任务的情况么?不过我后来也在想,就算出现也没什么,因为在这个command自己的thread中,总需要有个thread来做这个事情。只要自己保证执行了onNext,处理了自己的请求,后面的就无所谓了。不知道这样的想法是否正确,可能需要做一定的测试来进行佐证。

第一篇关于Hystrix的大致就是这样。欢迎有兴趣的同学一起讨论,中间有什么问题也希望大家能够指正,多多交流,共同进步!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容

  • 原文:https://my.oschina.net/7001/blog/1619842 摘要: Hystrix是N...
    laosijikaichele阅读 4,317评论 0 25
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,701评论 18 139
  • 一、认识Hystrix Hystrix是Netflix开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔...
    新栋BOOK阅读 4,052评论 0 19
  • 一、认识Hystrix Hystrix是Netflix开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔...
    新栋BOOK阅读 26,485评论 1 37
  • 专业考题类型管理运行工作负责人一般作业考题内容选项A选项B选项C选项D选项E选项F正确答案 变电单选GYSZ本规程...
    小白兔去钓鱼阅读 9,009评论 0 13