对于互联网应用以及大型企业应用来说,大多都要求尽可能实现7*24小时不间断运行,但实现完全不间断运行可以说是“极其困难”的。
为此,应用可用性的一般衡量标准是三个9到五个9。
对于一个功能和数据量不断增长的应用来说,维持高可用性绝非易事。为了实现高可用性,富千拉在避免单点故障、保证应用本身的高可用性、解决交易量增长等问题上做了大量的探索和实践。
在不考虑外部依赖系统突发故障,如网络问题、第三方支付、银行大面积不可用等情况下,福千拉的服务能力可以达到99.999%。
本文主要讨论如何提高应用程序本身的可用性。
为了提高应用的可用性,首先要做的就是尽可能的避免应用故障,但完全避免故障是不可能的,互联网是一个容易发生“蝴蝶效应”的地方,任何看似概率为零的小事故都有可能发生,然后被无限放大。
大家都知道它非常稳定、可靠,我们从一开始就是采用单点接触,从来没有出现过运行故障,所以大家心理上都认为这个东西不太可能出现问题。
直到有一天,这个节点所在的物理主机的硬件因为年久失修而发生故障,这时候这台主机已经无法再提供服务,导致系统服务瞬间不可用。
故障没有什么大不了,最重要的是及时发现、解决,福千拉要求自身系统能够秒级发现故障,快速诊断、快速解决,降低故障带来的负面影响。
首先我们简单回顾一下遇到的一些问题:
从历史中学习
一位新开发者在处理新接入的三方通道时,由于经验不足而忽略了设置超时的重要性,这个小细节导致三方队列中的交易全部被阻塞,影响了其他通道的交易。
系统采用分布式部署,支持灰度发布,环境、部署模块众多且复杂,一旦增加新模块,由于存在多个环境,且每个环境都是双节点,导致新模块上线后数据库连接数不足,从而影响其他模块的功能。
这也是一个超时问题。三方超时会导致当前配置的所有事务耗尽,没有可用的线程来处理其他事务。
第三方提供了认证、支付等接口,其中一个接口因为我们的交易量突然暴增,触发了网络运营商侧第三方的DDoS限制,通常机房的出口IP是固定的,所以网络运营商误认为从这个出口IP发出的交易是流量攻击,最终导致第三方的认证和支付接口同时不可用。
再说一个数据库的问题,也是因为我们的交易量突然增加导致的。创建序列的同事给某个序列设置了上限,999,999,999,但是数据库中这个字段的长度是 32 位。当交易量较小的时候,系统生成的值跟 32 位字段匹配,序列就不会变大。但是随着交易量的增加,序列不知不觉就变大了,导致 32 位不够存。
此类问题在互联网系统中非常常见且隐蔽,因此避免这些问题非常重要。
让我们从三个角度来看看我们所做的改变。
尽可能避免故障并设计容错系统
比如重路由,对于用户支付来说,用户不关心自己的钱是从哪个渠道支付的,只关心支付成功与否。如果支付接入了30个以上的渠道,就有可能A渠道支付失败,这时候就需要动态重路由到B渠道或者C渠道,这样系统重路由就可以避免用户支付失败,实现支付容错。
对于 OOM 也有容错性,比如系统内存总是会用完,如果你一开始就给应用本身预留一些内存,当系统发生 OOM 时,就可以处理这个异常,避免这次 OOM。
某些环节中的快速失败原则
快速失败原则是指,当主要流程中的任何一步出现问题时,应该快速合理地结束整个流程,而不是等到产生负面影响时才结束。
以下是一些示例:
设计自我保护系统
系统一般都会有第三方的依赖,比如数据库,第三方接口等等,在开发系统的时候,需要对第三方保持怀疑态度,避免第三方出现问题时引发连锁反应,导致系统宕机。
(1)拆分消息队列
我们给商户提供多种支付接口,包括速汇通、个人网银、企业网银、退款、撤单、批量支付、批量扣款、单笔支付、单笔扣款、语音支付、余额查询、身份证认证、银行卡认证、卡密码认证等。对应的支付渠道包括微信支付、支付宝等三十多个支付渠道,对接上百家商户。在这三个维度下,如何保证不同的业务、第三方、商户、支付类型之间互相不影响,我们做的就是对消息队列进行拆分。下图是部分业务消息队列的拆分图:
(2)限制资源的使用
资源使用限制的设计是高可用系统中最重要的一点,但也容易被忽视。资源是相对有限的,过度使用自然会导致应用宕机。为此,我们做了以下功课:
分布式水平扩展需要考虑数据库连接数,而不是无止境地最大化,数据库连接数是有限的,需要全局考虑所有模块,特别是水平扩展带来的增量。
内存占用过高会导致频繁GC、OOM。内存占用主要来自以下两个方面:
收集容量太大;
不再被引用的对象不会被释放。例如,放入的对象将被回收,直到线程退出。
无限制的创建线程最终导致线程不可控,尤其是隐藏在代码中的线程创建方法。
当系统的SY值过高时,意味着线程切换需要花费更多的时间。Java中出现这种现象的主要原因是创建了很多线程,而这些线程不断的处于阻塞状态(锁等待、IO等待)和执行状态的改变中,从而产生了大量的上下文切换。
另外Java应用程序在创建线程时会操作JVM堆外的物理内存,过多的线程也会占用过多的物理内存。对于线程的创建最好通过线程池来实现,避免线程过多导致的上下文切换。
做过支付系统的应该都知道,有些第三方支付公司对商户的并发是有要求的,第三方支付公司允许的并发数是根据实际交易量来评估的,所以如果不控制并发,把所有交易都发给第三方支付公司,第三方支付公司只会回复“请降低提交频率”。
因此在系统设计和编码阶段都要特别注意将并发度限制在三方允许的范围内。
及时发现故障
故障犹如日寇入寨,来得猝不及防。当第一道防线被突破后,如何及时设置第二道防线,发现故障,保证可用性,是报警监控系统的关键。没有仪表盘的汽车,无法知道车速、油量,转向灯是否打开,即使是经验丰富的司机,也非常危险。同样,系统也需要监控,最好在危险发生时提前发出警报,这样在故障真正造成风险之前,就能解决问题。
实时报警系统
如果没有实时报警,系统运行状态的不确定性将造成无法量化的灾难。我们的监控系统指标如下:
报警主要分为单机报警和集群报警,福千拉属于集群部署。实时预警主要通过对各个业务系统的实时数据点进行统计分析来实现,因此难点主要在数据点和分析系统上。
隐藏的数据
为了达到实时分析,又不影响交易系统响应时间,我们在系统各个模块进行实时数据嵌入,然后将嵌入的数据汇总到分析系统中,分析系统按照规则进行分析、报警。
分析系统
分析体系最难的就是业务报警点,比如哪些报警一发出来就要立即响应,哪些报警只要一发出来就只需要关注。下面我们对分析体系进行详细的介绍:
1.系统运行架构
2.系统运行流程
3.系统业务监控点
我们的业务监控点都是在日常运营过程中一点一点总结出来的,分为报警和注意两大类。
警务调度类别: