记得在三年前公司由于业务发展须要,就曾经将单体应用迁移到分布式框架上来。当时就遇到了这样一个问题:系统仅有一个控制单元,它会调用多个运算单元,若是某个运算单元(做为服务提供者)不可用,将致使控制单元(做为服务调用者)被阻塞,最终致使控制单元崩溃,进而致使整个系统都面临着瘫痪的风险。数据库
那个时候还不知道这其实就是服务的雪崩效应,雪崩效应比如就是蝴蝶效应,说的都是一个小因素的变化,却每每有着无比强大的力量,以致于最后改变总体结构、产生意想不到的结果。雪崩效应也是咱们目前研发的产品直面的一道坎,下面咱们来看有哪些场景会引起雪崩,又如何避免?对于没法避免的雪崩效应,咱们又有哪些应对措施?缓存
近年来,微服务就象一把燎原的大火,窜了出来并在整个技术社区烧了起来,微服务架构被认为是IT软件服务化架构演进的目标。为何微服务这么火,微服务能给企业带来什么价值?安全
咱们以耕种为例来看如何充分利用一块田地的:服务器
先在地里种植了一排排玉米;网络
后来发现玉米脚下空地能够利用,再间隔一段距离再种上豆角,豆角长大后顺着玉米杆往上爬,最后牢牢地缠绕在玉米杆上;架构
再后来发现每排玉米之间的空隙地还能够再种些土豆,土豆蔓藤之后会交织在一块儿,肆虐在玉米脚下吞食养分物质;并发
表面看来一块土地获得了充分利用,实际上各农做物得不到充分的光照和适宜的养分,如此一来加大了后期除草、松土、施肥、灌溉及收割的成本。负载均衡
下面的耕植思路是否是更好点呢? 一整块地根据须要分配为若干大小土地块,每块地之间清晰分界,这样就有了玉米地、土豆地、豆角地,再想种什么划块地再耕做就能够了。框架
这样种植好处不少,好比玉米、豆角和土豆须要的养分物质是不同的,可由专业技术人员施肥;玉米,豆角和土豆分离,避免豆角藤爬上玉米,缠绕玉米不能自由生长。土豆又汲取玉米须要的养分物质等等问题。分布式
软件系统实现与农做物的种植方式其实也很相似,传统的应用在扩展性,可靠性,维护成本上表现都不尽人意。如何充分利用大量系统资源,管理和监控服务生命周期都是头疼的事情,软件系统设计迫切须要上述的“土地分割种植法”。微服务架构应运而生:在微服务系统中,各个业务系统间经过对消息(字符序列)的处理都很是友好的RestAPI进行消息交互。如此一来,各个业务系统根据Restful架构风格统一成一个有机系统。
泰坦尼克号曾经是世界最大的客轮,在当时被称为是”永不沉没“的,但却在北大西洋撞上冰山而沉没。咱们每每只看到它浮出水面的绚丽多彩,水下的基础设施如资源规划、服务注册发现、部署升级,灰度发布等都是须要考虑的因素。
复杂应用分解:复杂的业务场景可被分解为多个业务系统,每一个业务系统的每一个服务都有一个用消息驱动API定义清楚的边界。
契约驱动:每一个业务系统可自由选择技术,组建技术团队利用Mock服务提供者和消费者,并行开发,最终实现依赖解耦。
自由扩展:每一个系统可根据业务须要独自进行扩展。
独立部署:每一个业务系统互相独立,可根据实际须要部署到合适的硬件机器上。
良好隔离:一个业务系统资源泄漏不会致使整个系统宕掉,容错性较好。
服务管理:敏捷迭代后的微服务可能愈来愈多,各个业务系统之间的交互也愈来愈多,如何作高效集群通讯方案也是问题。
应用管理: 每一个业务系统部署后对应着一个进程,进程能够启停。若是机器掉电或者宕机了,如何作无缝切换都须要强大的部署管理机制。
负载均衡:为应对大流量场景及提供系统可靠性,同一个业务系统也会作分布式部署即一个业务实例部署在多台机器上。若是某个业务系统挂掉了,如何按需作自动伸缩分布式方案方案也须要考虑。
问题定位:单体应用的日志集中在一块儿,出现问题定位很方便,而分布式环境的问题定界定位,日志分析都较为困难。
雪崩问题:分布式系统都存在这样一个问题,因为网络的不稳定性,决定了任何一个服务的可用性都不是 100% 的。当网络不稳定的时候,做为服务的提供者,自身可能会被拖死,致使服务调用者阻塞,最终可能引起雪崩效应。
Michael T. Nygard 在精彩的《Release It!》一书中总结了不少提升系统可用性的模式,其中很是重要的两条是:使用超时策略和使用熔断器机制。
超时策略:若是一个服务会被系统中的其它部分频繁调用,一个部分的故障可能会致使级联故障。例如,调用服务的操做能够配置为执行超时,若是服务未能在这个时间内响应,将回复一个失败消息。然而,这种策略可能会致使许多并发请求到同一个操做被阻塞,直到超时期限届满。这些阻塞的请求可能会存储关键的系统资源,如内存、线程、数据库链接等。所以,这些资源可能会枯竭,致使须要使用相同的资源系统的故障。在这种状况下,它将是优选的操做当即失败。设置较短的超时可能有助于解决这个问题,可是一个操做请求从发出到收到成功或者失败的消息须要的时间是不肯定的。
熔断器模式:熔断器的模式使用断路器来检测故障是否已获得解决,防止请求反复尝试执行一个可能会失败的操做,从而减小等待纠正故障的时间,相对与超时策略更加灵活。
一年一度的双十一已经悄然来临,下面将介绍某购物网站一个Tomcat容器在高并发场景下的雪崩效应来探讨Hystrix的线程池隔离技术和熔断器机制。
咱们先来看一个分布式系统中常见的简化的模型。Web服务器中的Servlet Container,容器启动时后台初始化一个调度线程,负责处理Http请求,而后每一个请求过来调度线程从线程池中取出一个工做者线程来处理该请求,从而实现并发控制的目的。
Servlet Container是咱们的容器,如Tomcat。一个用户请求有可能依赖其它多个外部服务。考虑到应用容器的线程数目基本都是固定的(好比Tomcat的线程池默认200),当在高并发的状况下,若是某一外部依赖的服务(第三方系统或者自研系统出现故障)超时阻塞,就有可能使得整个主线程池被占满,增长内存消耗,这是长请求拥塞反模式(一种单次请求时延变长而致使系统性能恶化甚至崩溃的恶化模式)。
更进一步,若是线程池被占满,那么整个服务将不可用,就又可能会重复产生上述问题。所以整个系统就像雪崩同样,最终崩塌掉。
流量激增:好比异常流量、用户重试致使系统负载升高;
缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会形成B系统崩溃;
程序有Bug:代码循环调用的逻辑问题,资源未释放引发的内存泄漏等问题;
硬件故障:好比宕机,机房断电,光纤被挖断等。
线程同步等待:系统间常常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。若是一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统自己出现问题,致使核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引起雪崩;
针对上述雪崩情景,有不少应对方案,但没有一个万能的模式可以应对全部场景。
针对流量激增,采用自动扩缩容以应对突发流量,或在负载均衡器上安装限流模块。
针对缓存刷新,参考Cache应用中的服务过载案例研究
针对硬件故障,多机房容灾,跨机房路由,异地多活等。
针对同步等待,使用Hystrix作故障隔离,熔断器机制等能够解决依赖服务不可用的问题。
经过实践发现,线程同步等待是最多见引起的雪崩效应的场景,本文将重点介绍使用Hystrix技术解决服务的雪崩问题。后续再分享流量激增和缓存刷新等应对方案。
Hystrix 是由Netflix发布,旨在应对复杂分布式系统中的延时和故障容错,基于Apache License 2.0协议的开源的程序库,目前托管在GitHub上。
Hystrix采用了命令模式,客户端须要继承抽象类HystrixCommand并实现其特定方法。为何使用命令模式呢?使用过RPC框架都应该知道一个远程接口所定义的方法可能不止一个,为了更加细粒度的保护单个方法调用,命令模式就很是适合这种场景。
命令模式的本质就是分离方法调用和方法实现,在这里咱们经过将接口方法抽象成HystricCommand的子类,从而得到安全防御能力,并使得的控制力度下沉到方法级别。
Hystrix核心设计理念基于命令模式,命令模式UML以下图:
可见,Command是在Receiver和Invoker之间添加的中间层,Command实现了对Receiver的封装。那么Hystrix的应用场景如何与上图对应呢?
API既能够是Invoker又能够是Reciever,经过继承Hystrix核心类HystrixCommand来封装这些API(例如,远程接口调用,数据库的CRUD操做可能会产生延时),就能够为API提供弹性保护了。
Hystrix之因此可以防止雪崩的本质缘由,是其运用了资源隔离模式,咱们能够用蓄水池作比喻来解释什么是资源隔离。生活中一个大的蓄水池由一个一个小的池子隔离开来,这样若是某一个水池的水被污染,也不会波及到其它蓄水池,若是只有一个蓄水池,水池被污染,整池水都不可用了。软件资源隔离一模一样,若是采用资源隔离模式,将对远程服务的调用隔离到一个单独的线程池后,若服务提供者不可用,那么受到影响的只会是这个独立的线程池。
(1)线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求做处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式须要为每一个依赖的服务申请线程池,有必定的资源消耗,好处是能够应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)。这个你们都比较熟悉,参考Java自带的ThreadPoolExecutor线程池及队列实现。线程池隔离参考下图:
线程隔离的优势:
请求线程与依赖代码的执行线程能够彻底隔离第三方代码;
当一个依赖线程由失败变成可用时,线程池将清理后并当即恢复可用;
线程池可设置大小以控制并发量,线程池饱和后能够拒绝服务,防止依赖问题扩散。
线程隔离的缺点:
增长了处理器的消耗,每一个命令的执行涉及到排队(默认使用SynchronousQueue避免排队)和调度;
增长了使用ThreadLocal等依赖线程状态的代码复杂性,须要手动传递和清理线程状态。
(2)信号量隔离模式:使用一个原子计数器来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操做请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且当即返回模式,没法应对突发流量(流量洪峰来临时,处理的线程超过数量,其余的请求会直接返回,不继续去请求依赖的服务),参考Java的信号量的用法。
Hystrix默认采用线程池隔离机制,固然用户也能够配置 HystrixCommandProperties为隔离策略为ExecutionIsolationStrategy.SEMAPHORE。
信号隔离的特色:
信号隔离与线程隔离最大不一样在于执行依赖代码的线程依然是请求线程,该线程须要经过信号申请;
若是客户端是可信的且能够快速返回,可使用信号隔离替换线程隔离,下降开销。
线程池隔离和信号隔离的区别见下图,使用线程池隔离,用户请求了15条线程,10条线程依赖于A线程池,5条线程依赖于B线程池;若是使用信号量隔离,请求到C客户端的信号量若设置了15,那么图中左侧用户请求的10个信号与右边的5个信号量须要与设置阈值进行比较,小于等于阈值则执行,不然直接返回。
建议使用的场景:根据请求服务级别划分不一样等级业务线程池,甚至能够将核心业务部署在独立的服务器上。
熔断器与家里面的保险丝有些相似,当电流过大时,保险丝自动熔断以保护咱们的电器。假设在没有熔断器机制保护下,咱们可能会无数次的重试,势必持续加大服务端压力,形成恶性循环;若是直接关闭重试功能,当服务端又可用的时候,咱们如何恢复?
熔断器正好适合这种场景:当请求失败比率(失败/总数)达到必定阈值后,熔断器开启,并休眠一段时间,这段休眠期事后熔断器将处与半开状态(half-open),在此状态下将试探性的放过一部分流量(Hystrix只支持single request),若是这部分流量调用成功后,再次将熔断器闭合,不然熔断器继续保持开启并进入下一轮休眠周期。
建议使用场景:Client端直接调用远程的Server端(server端因为某种缘由不可用,从client端发出请求到server端超时响应之间占用了系统资源,如内存,数据库链接等)或共享资源。
不建议的场景以下:
应用程序直接访问如内存中的数据,若使用熔断器模式只会增长系统额外开销。
做为业务逻辑的异常处理替代品。
本文从本身曾经开发的项目应用的分布式架构引出服务的雪崩效应,进而引出Hystrix(固然了,Hystrix还有不少优秀的特性,如缓存,批量处理请求,主从分担等,本文主要介绍了资源隔离和熔断)。主要分三部分进行说明:
第一部分:以耕种田地的思想引出软件领域设计的微服务架构, 简单的介绍了其优势,着重介绍面临的挑战:雪崩问题。
第二部分:以Tomcat Container在高并发下崩溃为例揭示了雪崩产生的过程,进而总结了几种诱发雪崩的场景及各类场景的应对解决方案,针对同步等待引出了Hystrix框架。
第三部分:介绍了Hystrix背景,资源隔离(总结了线程池和信号量特色)和熔断机制工做过程,并总结各自使用场景。
如Martin Fowler 在其文中所说,尽管微服务架构将来须要经历时间的检验,但咱们已经走在了微服务架构转型的道路上,对此咱们能够保持谨慎的乐观,这条路依然值得去探索。