将单体应用迁移到分布式框架后,很大可能会遇到这样的问题:系统仅有一个控制单元,它会调用多个运算单元,如果某个运算单元(作为服务提供者)不可用,将导致控制单元(作为服务调用者)被阻塞,最终导致控制单元崩溃,进而导致整个系统都面临着瘫痪的风险。
服务化后面临的挑战:
-
服务管理:敏捷迭代后的微服务可能越来越多,各个业务系统之间的交互也越来越多,如何做高效集群通信方案也是问题。
-
应用管理: 每个业务系统部署后对应着一个进程,进程可以启停。如果机器掉电或者宕机了,如何做无缝切换都需要强大的部署管理机制。
-
负载均衡:为应对大流量场景及提供系统可靠性,同一个业务系统也会做分布式部署即一个业务实例部署在多台机器上。如果某个业务系统挂掉了,如何按需做自动伸缩分布式方案方案也需要考虑。
-
问题定位:单体应用的日志集中在一起,出现问题定位很方便,而分布式环境的问题定界定位,日志分析都较为困难。
-
雪崩问题:分布式系统都存在这样一个问题,由于网络的不稳定性,决定了任何一个服务的可用性都不是 100% 的。当网络不稳定的时候,作为服务的提供者,自身可能会被拖死,导致服务调用者阻塞,最终可能引发雪崩效应。
Michael T. Nygard 在精彩的《Release It!》一书中总结了很多提高系统可用性的模式,其中非常重要的两条是:使用超时策略和使用熔断器机制。
-
超时策略:如果一个服务会被系统中的其它部分频繁调用,一个部分的故障可能会导致级联故障。例如,调用服务的操作可以配置为执行超时,如果服务未能在这个时间内响应,将回复一个失败消息。然而,这种策略可能会导致许多并发请求到同一个操作被阻塞,直到超时期限届满。这些阻塞的请求可能会存储关键的系统资源,如内存、线程、数据库连接等。因此,这些资源可能会枯竭,导致需要使用相同的资源系统的故障。在这种情况下,它将是优选的操作立即失败。设置较短的超时可能有助于解决这个问题,但是一个操作请求从发出到收到成功或者失败的消息需要的时间是不确定的。
-
熔断器模式:熔断器的模式使用断路器来检测故障是否已得到解决,防止请求反复尝试执行一个可能会失败的操作,从而减少等待纠正故障的时间,相对与超时策略更加灵活。
雪崩问题的本质:
我们先来看一个分布式系统中常见的简化的模型。Web服务器中的Servlet Container,容器启动时后台初始化一个调度线程,负责处理Http请求,然后每个请求过来调度线程从线程池中取出一个工作者线程来处理该请求,从而实现并发控制的目的。
Servlet Container是我们的容器,如Tomcat。一个用户请求有可能依赖其它多个外部服务。考虑到应用容器的线程数目基本都是固定的(比如Tomcat的线程池默认200),当在高并发的情况下,如果某一外部依赖的服务(第三方系统或者自研系统出现故障)超时阻塞,就有可能使得整个主线程池被占满,增加内存消耗,这是长请求拥塞反模式(一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式)。
更进一步,如果线程池被占满,那么整个服务将不可用,就又可能会重复产生上述问题。因此整个系统就像雪崩一样,最终崩塌掉。
雪崩效应产生的几种场景
-
流量激增:比如异常流量、用户重试导致系统负载升高;
-
缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
-
程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
-
硬件故障:比如宕机,机房断电,光纤被挖断等。
-
线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;
常见解决方案
针对上述雪崩情景,有很多应对方案,但没有一个万能的模式能够应对所有场景。
-
针对流量激增,采用自动扩缩容以应对突发流量,或在负载均衡器上安装限流模块。
-
针对缓存刷新,参考Cache应用中的服务过载案例研究
-
针对硬件故障,多机房容灾,跨机房路由,异地多活等。
-
针对同步等待,使用Hystrix做故障隔离,熔断器机制等可以解决依赖服务不可用的问题。
通过实践发现,线程同步等待是最常见引发的雪崩效应的场景。