0 引言
大型网站及软件系统,其高可用性直接影响客户体验,这是大型网站都需要面对的基础性课题。高可用性涉及到IT 基础设施、软硬件架构、开发测试、运维等各个方面。目前,大型网站通常是领域业务多元化,面临高并发、高流量的挑战。为了获得更好的性能和可扩展性,按照服务组件化设计思想,以领域业务为功能单元做垂直切分,各模块之间提供服务接口关联起来,这样可以提高整个系统的可用性。然后随着应用规模的扩大,服务之间的依赖关系更为复杂,如何在系统出现故障或异常时,避免由点故障到面故障的扩散,避免不同领域业务相互影响,避免非核心影响核心,是开发者在做应用架构设计与物理部署架构设计时必须要考虑的问题。本文结合日常项目中的实践经验,提出在服务组件化的过程中,如何做服务级故障隔离的原则和方法,提升网站可用性这一需求。
1 服务级隔离基本思想
形式上,系统与系统之间,服务与服务之间(无论是两个服务是否为同一业务组件)存在以下两种依赖关系。
⑴ 强依赖。所谓A 系统强依赖于B 系统是指,A 系统必须依赖B 系统的处理结果,才能正常的完成逻辑;简单的来说,如果B 不能提供服务,A 也无法正常工作。从高可用性设计的角度出发,在这种依赖关系下,A 与B 系统需要达到如下几点目标:对于B 系统,A 直接RPC 调用,B 在承诺的SLA 基础上,做好自我保护;B 系统宕机时,A 尽管不能使用,但要保证机器不挂掉;B 系统故障恢复时,A 可以自动快速恢复;B 故障时,A 可以自动检测,自动降低或关闭对B 的访问,防止情况恶化。
⑵ 弱依赖。所谓A系统弱依赖于B 系统,是指B 系统如果发生了故障,A 系统可以继续提供正常的服务。弱依赖通常有这些特点:可以不等待B 结果的返回(比如发送消息、ajax 区域加载);B 是非核心功能,结果不返回不影响A 的关键流程(合理超时时间的控制),A、B 的调用可以是异步(队列、线程的FutureTask、协程akka);对B 的服务调用可通过功能开关实现降级。
针对强依赖与弱依赖的不同特点,在架构和设计时,为避免故障或异常时由点故障到面故障的扩散,我们考虑在区分核心与非核心(服务、组件、产品重要度分级)、按功能组与后台依赖隔离、同一容器内服务间隔离、按客户群体DNS 层隔离、引入异步模式隔离服务调用者与提供者等层面和场景下提供服务故障隔离策略和方法。
2 具体隔离策略与方法的设计
本节根据服务之间的依赖关系以及物理部署结构等特点,总结服务间如何做隔离和解耦的策略和方法。
2.1 服务间依赖隔离与解耦
在服务A 与B 存在强依赖的情况下,描述RPC与基于Queue 两种方式的区别。通常系统TPS(每秒事务处理量)、RT(响应时间以毫秒计算)、CurrentNum(并发数)有如下关系:tps=(1000/responseTime)currentNum) (公式1)根据公式1,按B 系统正常情况与异常情况,分别计算对A系统的影响。正常情况:A 系统对外承诺500 的TPS,系统的平均响应时间为100ms,这时候只需要50 个线程并行即可支撑。故障情况:B 系统变慢,导致A 的平均响应时间变为1000ms,现在A系统的线程数是500。B 系统的异常,在直接RPC 的调用情况会导致A 系统宕机,同时B 系统由于A 系统的并发访问数由50 变为500,B 系统进一步恶化。由此可见,在RPC 的调用方式下,无论是同步调用还是异步调用,对于服务器端都是直接压力传导,在A 与B是强依赖的情况下,如果是同步RPC 调用,超时控制在异常时刻是决定性问题。在强依赖的情况下,除了需要在采用RPC 调用的时候合理的设置超时外,在架构时可用采用基于消息队列的方式,来达到服务间由于容量不匹配导致的强耦合。如果调用者不需要服务方返回结果,则椭圆框的部分是不需要的。基于Queue的依赖关系与基于RPC的方式相比,队列模式有如下特点。
⑴ 为服务调用者与服务提供者解耦,队列模式尤其适合弱依赖情况下的异步单相消息模式。
⑵ 引入队列中间层可以对任务做优先级、丢弃策略、持久化等,同时由于中间节点的存在,也引入了复杂性,从响应时间来看,与RPC方式相比会有所增加。
⑶ 在应对突发尖峰流量时,服务端可以实现压力逐步释放的目标,保持稳定吞吐率,达到稳定消化能够处理的量,快速丢掉不能消化的量,客观上达到了服务组件间解耦的目的。
2.2 同一容器内服务间线程隔离
在系统服务化的切分过程中,通常是以业务领域的切分为依据,属于同一业务领域的服务在部署时基本部署在同一个容器中。由于各个服务对于资源的消耗不同,响应时间与关联组件也不相同,导致在容器线程总数固定情况下,其中一个服务突然变慢会占用大量线程,导致线程耗尽。同时线程数飙升,引起Context Switch[1]加剧,在JAVA 平台下,还会引起对象生命周期变长,Full GC频繁,CPU Load Average和Usage高企,最终容器整体宕机。
为解决容器内的服务线程隔离问题,在实践中首先需要根据历史访问数据及系统容量规划的数据,计算出每个服务的峰值与均值并发数、响应时间、交易吞吐率等,具体的数据采集与分析过程在本文中不作详述。对于服务隔离策略的设计可以采用定额与弹性资源配置两种方式。
⑴ 悲观策略:对于每个服务设定固定的最大资源量,任何一种服务当访问量达到最大时,即使总资源存在富余也不能使用。
⑵ 乐观策略:在保证每种服务预留最低资源的情况下,允许任务依据一个弹性配额去争抢线程资源,达到线程利用率的最大化。
2.3 核心与非核心服务隔离
组件A 与组件B 都强依赖于组件C,同时组件C 强依赖于组件D。其中组件A 属于非核心业务组件,B 属于核心业务组件。由于C 组件总体处理能力是固定有限的(假定平均响应时间100ms,最大TPS 为3000/s),当A 组件由于突发流量的影响,对C 组件访问量变大,或者A 组件的某些请求会耗费C 组件较多的资源时,导致C 组件不能处理B 组件的请求,级联导致B 系统出现故障的情况发生。在这个场景下,虽然A 与B 在物理部署上已经做了隔离,但是复杂的关联组件依赖关系,间接导致因为非核心业务组件影响核心业务组件的事例。为避免上述问题的发生,可以考虑以下两种隔离策略。
⑴ 如果C 组件及其关联组件水平伸缩后,可以支持更高的性能,推荐采用路由隔离方式进行,即A 组件与B 组件分别访问不同C 组件服务群组。
⑵ C 系统不具备进一步水平扩展的能力(比如瓶颈点不在C,而在于C 所依赖的系统D);这个时候可以在C 系统上设置流量控制和功能开关标志位,在异常情况下可以限制或关闭非核心系统对C 的访问。
2.4 服务功能开关与降级的设计
在各种服务隔离策略中,当异常流量发生时,在了解全局服务依赖关系和服务重要度排列的基础上,架构设计中通常使用功能开关和服务降级的策略来分解流量,达到丢卒保车的目的来应对突发状况。在具体的实践中主要考虑三个方面。⑴ 管理全局性服务组件的依赖关系,识别各服务调用链中的关键路径。在大型网站中,通常存在上千级别的组件,服务之间的依赖关系异常复杂,大部分网站都实现了基于Google的Dapper系统的分布式系统监控与依赖分析系统。
⑵ 功能开关的设计。在设计实现时,存在单机与集群的区别。单机实现时,对于JAVA 平台建议使用JMX 标准实现,这样做的好处是可以纳入统一的监控体系,使用JConsole 等通用JMX Client 可以处理;集群实现时,由于需要对大量节点统一管控,建议使用Zookeeper 之类的配置管理系统来实现。
⑶ 自动降级与手动降级的设计与运用。自动功能开关的设计主要是首先确定判断异常情况的阈值,比如单位时间内服务调用超时或失败比例超过70%,或者连续失败或超时达到20。此时系统可以把访问窗口按指数降低,直到降为1;服务恢复类似于TCP 的慢启动,窗口逐渐打开。对于封闭点,应该尽可能提前;对于一些关键系统,如支付类系统,最好避免使用自动开关,在接到监控系统报警后,人工介入决定是否需要降级和关闭。
3 结束语
目前新华网要求整体可用性达到99.99%级别,这意味着全年计划停机时间加上非计划停机时间不到53 分钟。系统的高可用性是一个系统性工程,除了IT 基础设施外,在软件层面要具备快速发现定位的监控系统,要拥有完善的容量规划与评测方法,要有应对峰值流量的策略和手段等,这些会涉及到软件过程与基础软件建设的各个方面。本文提出了在实践中总结出来的方法与策略可以为有相似需求的系统提供参考与借鉴。