|
PHP生态 Hystrix 实践(一)
PHP生态 Hystrix 实践(一)
卢阳
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年11月19日 19:32
为了应对服务雪崩,我们在php生态中采用了hystrix的计数器和断路器设计,来实践其熔断机制。文章主要分为两个部分:第一部分介绍熔断背景、hystrix的流程、计数器原理以及断路器原理;第二部分介绍熔断组件的设计与实现、压测结果和配置监控等闭环。1 立项背景1.1 应对服务雪崩在分布式系统中经常会出现某个基础服务不可用导致整个系统不可用的情况,这种现象被称之为服务雪崩。如下图所示,箭头从服务调用者指向服务提供者。整条链路为服务A调用服务B,而服务B调用服务C和D。如果服务D出问题,会导致服务B在请求时不能够“迅速”返回而是等到超时时间结束后才可返回。如此,会积攒大量的链接,导致IO过多从而拖垮服务B的性能。那么对于服务A在这种场景下,请求服务B的耗时也会变大。大量的请求到服务A后,不能“迅速”返回,和服务B一样,服务A被拖垮。于是雪崩就形成了。图:服务雪崩的形成服务雪崩应对策略总结如下:表:服务雪崩的的对应策略依据具体业务,将依赖服务分为:强依赖和弱依赖。强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不应该导致当前业务的中止。不可用服务的调用快速失败一般通过超时机制或熔断机制来实现,熔断机制是通过计数器+断路器+降级来实现。1.2 现有php生态熔断能力提到熔断能力,不得不提鼻祖—hystrix,基本原理就是通过一定的失败计数并触发熔断或满足一定的成功条件恢复熔断。在github上有多个PHP版本的hystrix(以下会详细介绍hystrix),如upwork/hystrix: https://github.com/upwork/phystrix。同样在公司内部已经有团队实现了php生态的hystix。是不是使用以上两者的任何一个就万事大吉了?显然离我们低耗时、高并发、集群熔断场景还有一定差距。第一个来看upwork/hystrix,是基于odesk/hystrix库来实现的,使用了经典的rolling window(滑动窗口)来实现的metric(计数器),需要安装apcu,对应php的版本是7.2。首先,基于APCU(PHP高性能缓存扩展,https://pecl.php.net/package/APCU)只能实现单机熔断版本,对于当前的分布式系统如果LB(Load Balance负载均衡,以下统称LB)的策略如果不是轮询,则这种方式需要对单机做出不同的熔断参数配置非常麻烦。其次,这里使用的是原生的command方式,Hystrix提供了四种方式,command方式是其中一种,后续介绍这四种方式。这种方式是侵入式的,若集成当前这套代码非常的不优雅。最终:这种方式不能被直接使用,需要改造集群版和单机版的优雅接入方式才能使用。第二个来看公司另一个部门实现的hystrix,提供了集群和单机熔断能力。集群能力使用了redis来实现计数存储,单机能力使用了yac来存储计数。这里在改造RPC的时候使用了guzzle中间件的方式,由于我们大多数系统使用的都是封装guzzle的RPC达到请求下游的目的,这种改造是比较优雅的。这种方式是command方式的一种,但在代码编写的时候是非侵入式的。但是不足的地方表现在集群熔断能力基于redis zset来实现,把每个上报的点以时间为排序值记录,好处是在redis的zset中很容易的插入、按范围筛选、按照范围删除。同时也带来了问题,虽然redis对有序集合做了较大的优化,但仍然有耗时操作,其中比较突出的是在淘汰过期的记录时使用了zRemRangeByScore操作,这个操作的时间复杂度是O(logN + M),N是有序集合的大小,M是Range中元素的大小。如此不适合我们的高并发场景。以上两者都不能满足我们的场景需求,那么如何改进才能满足呢,能否结合以上两者扬长避短实现一个满足高并发场景的hystrix?2 整体设计2.1 Hystrix 原理2.1.1 熔断概念释义:hystrix语义为“豪猪”,是Netflix开源的一款容错框架。hystrix的出现即为解决雪崩效应,它通过四个方面的机制来解决这个问题:1)隔离(线程池隔离和信号量隔离):限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。PHP生态没有这个问题,fpm模式本身就已隔离。2)优雅的降级机制:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。3)熔断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。4)缓存:提供了请求缓存、请求合并实现。其它:支持实时监控、报警、控制(修改配置)熔断:正常状态下,断路器(或称为circuit)处于关闭状态(Closed),如果调用持续出错或者超时,电路被打开进入熔断状态(Open),后续一段时间内的所有调用都会被拒绝(Fail Fast),一段时间以后,保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,如果调用仍然失败,则回到熔断状态,如果调用成功,则回到电路闭合状态。图:断路器的状态转换降级:有了熔断机制后,可以迅速失败不再做原有的操作,但是伴随着熔断也可以做一些降级的操作。例如,不去请求RPC直接返回失败,可以给定既有的数据来填补这个空白,也可以请求另一个认为是比较稳定的服务来替代这个RPC。某次活动,API A熔断了原本需要请求的RPC B,B是抽奖模块。此时拿不到抽奖操作内容,无法进行抽奖操作。但是RPC C是比较稳定的,从C中可以获取此次抽奖的说明等。那么API A采用降级策略,获取了RPC C中的信息,改为展示抽奖说明,也是既能实现了熔断机制,又采用了降级很大程度上弥补了用户体验 。当然,降级可以级联,此时若RPC C也被统计为需要快速失败,若有级联的降级方案可以请求RPC D。图:hystrix降级级联缓存:由于已有RPC的缓存模块,暂用不到hystrix的缓存,也不建议使用。2.1.2 hystrix流程说明:图:hystrix 流程说明这里解释以上提到的四种命令模式:execute()以同步堵塞方式执行run(),只支持接收一个值对象。hystrix会从线程池中取一个线程来执行run(),并等待返回值。queue()以异步非阻塞方式执行run(),只支持接收一个值对象。调用queue()就直接返回一个Future对象。可通过 Future.get()拿到run()的返回结果,但Future.get()是阻塞执行的。若执行成功,Future.get()返回单个返回值。当执行失败时,如果没有重写fallback,Future.get()抛出异常。observe()事件注册前执行run()/construct(),支持接收多个值对象,取决于发射源。调用observe()会返回一个hot Observable,也就是说,调用observe()自动触发执行run()/construct(),无论是否存在订阅者。如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run();如果继承的是HystrixObservableCommand,将以调用线程阻塞执行construct()。observe()使用方法:调用observe()会返回一个Observable对象调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果。toObservable()事件注册后执行run()/construct(),支持接收多个值对象,取决于发射源。调用toObservable()会返回一个cold Observable,也就是说,调用toObservable()不会立即触发执行run()/construct(),必须有订阅者订阅Observable时才会执行。如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run(),调用线程不必等待run();如果继承的是HystrixObservableCommand,将以调用线程堵塞执行construct(),调用线程需等待construct()执行完才能继续往下走。toObservable()使用方法:调用observe()会返回一个Observable对象调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果需注意的是,HystrixCommand也支持toObservable()和observe(),但是即使将HystrixCommand转换成Observable,它也只能发射一个值对象。只有HystrixObservableCommand才支持发射多个值对象。2.1.3 计数器与断路器的工作原理:图:计数器与断路器内核熔断器工作的详细过程如下:第一步,调用allowRequest()判断是否允许将请求提交到线程池。如果熔断器强制打开,circuitBreaker.forceOpen为true,不允许放行,返回。如果熔断器强制关闭,circuitBreaker.forceClosed为true,允许放行。此外不必关注熔断器实际状态,也就是说熔断器仍然会维护统计数据和开关状态,只是不生效而已。第二步,调用isOpen()判断熔断器开关是否打开。如果熔断器开关打开,进入第三步,否则继续;如果一个周期内总的请求数小于circuitBreaker.requestVolumeThreshold的值,允许请求放行,否则继续;如果一个周期内错误率小于circuitBreaker.errorThresholdPercentage的值,允许请求放行。否则,打开熔断器开关,进入第三步。第三步,调用allowSingleTest()判断是否允许单个请求通行,检查依赖服务是否恢复。如果熔断器打开,且距离熔断器打开的时间或上一次试探请求放行的时间超过circuitBreaker.sleepWindowInMilliseconds的值时,熔断器器进入半开状态,允许放行一个试探请求;否则,不允许放行。此外,为了提供决策依据,每个熔断器默认维护了10个bucket,每秒一个bucket,当新的bucket被创建时,最旧的bucket会被抛弃。其中每个blucket维护了请求成功、失败、超时、拒绝的计数器,Hystrix负责收集并统计这些计数器。2.1.4 hystrix-ex composer包设计:讲完了原理和command方式,在第一节中我们提到了使用guzzle中间件(https://guzzle-zh-cn.readthedocs.io/zh_CN/latest/handlers-and-middleware.html)的方式是比较优雅的,那么在植入这个中间件后我们希望达到哪些操作呢,如下:图:hystrix-ex组件作用于guzzle中间件的行为这个项目最终以composer包的形式来提供熔断能力,这个包名字我们暂定叫做hystrix-ex。那么在guzzle请求发出之前,去执行isAvailable,是否可以发出请求,是的话就可以发出,否的话就执行fallback(降级方法)。同样,在请求返回后,拿到了request result,结果可能是succ (success)也可能是fail(failure)。成功也要做vaild check,不一定hystrix认为的成功就是我们定义的成功,还是要区分一下。例如,验签没过有的服务返回http code 200,有的服务返回403,那么需要订制这个valid check 方法才能区分请求的成功或失败。Hystrix 默认的失败一定是失败。更近一步,来解释isAvailable如何实现,成功、失败、拒绝如何统计。图:hystrix-ex组件作用于guzzle中间件的行为详细逻辑分支如图所示:isAvailable首先查看circuit status (断路器状态),如果是close则返回true,允许请求发出。如果是open 或 half-open 则去比较latest fail time (最后一次失败的时间)是否已过期,过期了则返回true,否则返回false。最后一次失败时间失效了,那么意味着可以去请求,断路器虽然是open 或 half-open但允许少量的尝试请求发出。record分为三种情况,首先看记录成功后会判断当前circuit status ,如果是open 则判断 close in current window (在当前窗口关闭)逻辑。这个逻辑首先判断失败数是否为0,若为0则返回true;否则判断失败阈值是否大于当前失败总数,如果满足也返回true;否则继续判断失败比例是否超过阈值,没超过返回true;以上都不满足返回false。当断路器是open状态下,需要失败总数或失败比例低于阈值才能将断路器设置为close状态,和重置计数器。然而,当断路器处于half-open状态时,只需要一个成功的请求就可以重置断路器和计数器。其次,记录失败的情况,如果断路器除以close 状态,也去判断close in current window逻辑,如果为false,此时触发了断路器状态变更,由close 变为open。因为,close in current window返回false 时,失败总数和失败比例均达到设定的阈值。最后,记录reject,无其它动作。本节解释了hystrix的命令模式,工作流程,断路器内核,同时也解释了guzzle中间件的工作流程,以及hystrix-ex composer 包的工作流程。在介绍完hystrix的原理及流程后,我们后续即将介绍php生态的hystrix组件的计数器设计,有循环桶和固定桶两种方式,且对计数器存储做了优化。同时也介绍了集群版和单机版的设计以及动态配置和监控的设计。最后对集群版三种计数器实现的hystrix组件做了性能比对,并说明在准确性和性能之间如何做取舍。(未完待续,请参见《PHP生态 Hystrix 实践(二)》)
预览时标签不可点
后端27php5后端 · 目录#后端上一篇响应式编程和协程在Java语言的应用下一篇PHP生态 Hystrix 实践(二)关闭更多小程序广告搜索「undefined」网络结果
|
|