降级与熔断
我们用学过的架构知识尝试对地铁进行分析
高可用:发动机,信控等核心设备有2套以上形成灾备
高性能:发车间隔尽可能的短
高并发:车厢尽可能的大
之前的文章系列,焦点都在高可用、高性能、高并发上,我们把有限的硬件资源类比有限的轨道,在硬件有限的情况下,如果上面做的努力还是不能满足疯狂增长的流量呢,这时候我们会用到以下手段:
降级:学生凭学生证优先、女士优先、领导优先、工资较高的优先......
流量控制:高峰控制,排队分批放进来
熔断:直接拒绝让乘客改乘其它的交通工具
降级和熔断不应该是常态,如是,则应该考虑增加硬件投入,更多的,是作为应对业务高峰期的一种预案,可以说是硬件投入成本与收益之间的平衡。
前面的文章我们提到从原来的单体应用架构,我们做了垂直拆分,将单个系统拆成若干个不同业务领域的子系统,对于子系统,我们还可以进行微服务化,拆成若干服务,这就足够了吗?一般单台机会运行不同的服务,某类服务占用资源过多,可能导致别的服务不可用。另外即使对单个服务而言在经典的冯诺依曼体系中,越是接近CPU的存储访问速度通常越快,而一个服务,可能贯穿其中,它经过CPU,经过内存,再经过硬盘,甚至经过网络访问数据最终才完成了整个业务。如果未做隔离,那可能就是木桶效应,由最短板决定木桶的容量。
那如何解决单个服务的木桶效应呢?硬件上可以进行调优配置,比如CPU密集型则增加CPU,内存密集型则增加内存。软件上从架构设计时进行隔离,一个较完美的方案是把CPU和其它的IO任务(跨网络访问也可以认为是某种IO操作)隔离,以达到物尽其用,即当前需要CPU时将当前的CPU硬件资源投入进来,当进行IO时则出让CPU,IO结束又需要CPU时则再将CPU资源投入进来,就也是真正意义上的异步。然而现实中在JVM层这种完美的异步很难做得到的,对于Java体系来说,单机上的Socket、Thread是宝贵而有限的(就是短板),通常是要进行隔离的重点对象。
流量控制可以在接入层nginx上做,在应用层使用Hystrix的熔断器做为接入层流控不足的补充,原理是记录总调用数,单位时间调用数,成功失败数等等,使用状态机类似机制进行状态切换。
在服务器的并发请求数量比较大的时候,会产生很多的Servlet线程(这些Servlet线程在Servlet容器的线程池中维护),如果每个请求需要耗费的时间比较长(比如,执行了一些IO的处理等),在之前的非异步的Servlet中,这些Servlet线程将会阻塞,严重耗费服务器的资源.而在Servlet3.0中首次出现的异步Servlet,通过一个单独的新的线程来执行这些比较耗时的任务(也可以把这些任务放到一个自己维护的线程池里),Servlet线程立即返回Servlet容器的Servlet池以便响应其他请求,这样,在降低了系统的资源消耗的同时,也会提升系统的吞吐量。
Hystrix组件提供了两种隔离的解决方案:线程池隔离和信号量隔离。两种隔离方式都是限制对共享资源的并发访问量。
线程池隔离较容易理解,某一时刻有100个请求,我的线程池里只有10个线程,有90个需要排队处理或直接拒绝。
信号量隔离其实相当于你有一个固定容量大小的房间,有人进来,有人出去,保证某一时刻(最多)只能有N个人在房间,通常用来限制CPU的使用资源。
一个系统,除了内部闭环调用,也会有一些外部调用,当外部系统不可用时,如果处理不当,可能会造成内部服务的雪崩。实战中,一个使用线程池隔离处理,也要注意超时间隔设置。
而降级是根据具体业务情况返回托底数据,如本地缓存的数据。
总结:限流、熔断、降级都是不得已而为之,与其高峰期系统直接崩溃(全部拒绝服务),不如保证在有限的资源下尽力而为(部分服务),这就要求我们要提前预案。对Java而言,Servlet Thread,业务 Thread、Socket都是有限的资源,需要做好隔离。而异步Servlet将Servlet Thread和业务 Thread隔离能提升系统的吞吐量,不同的业务之间可以用线程池隔离互相隔离影响,而一些外部或者服务化调用也可以用线程池隔离以防服务雪崩,在这些服务调用时,必须重视重试和超时时间的设置。具体到实现层面,Hystrix提供了熔断、隔离、Fallback、cache、监控等功能,能够在一个、或多个依赖同时出现问题时保证系统依然可用。