支付系统高可用架构设计实战,可用性高达99.999!
作者:冯忠旗
来源:juejin.im/post/5cfde01bf265da1bba58f863
一、背景
对于互联网应用和企业大型应用而言,多数都尽可能地要求做到7*24小时不间断运行,而要做到完全不间断运行可以说“难于上青天”。为此,对应用可用性程度的衡量标准一般有3个9到5个9。
可用性指标 | 计算方式 | 不可用时间(分钟) |
对于一个功能和数据量不断增加的应用,要保持比较高的可用性并非易事。为了实现高可用,「付钱拉」从避免单点故障、保证应用自身的高可用、解决交易量增长等方面做了许多探索和实践。
在不考虑外部依赖系统突发故障,如网络问题、三方支付和银行的大面积不可用等情况下,「付钱拉」的服务能力可以达到99.999%。
本文重点讨论如何提高应用自身的可用性,关于如何避免单点故障和解决交易量增长问题会在其他系列讨论。
为了提高应用的可用性,首先要做的就是尽可能避免应用出现故障,但要完全做到不出故障是不可能的。互联网是个容易产生“蝴蝶效应”的地方,任何一个看似很小的、发生概率为0的事故都可能出现,然后被无限放大。
大家都知道RabbitMQ本身是非常稳定可靠的,「付钱拉」最开始也一直在使用单点RabbitMQ,并且从未出现运行故障,所以大家在心理上都认为这个东西不太可能出问题。
直到某天,这台节点所在的物理主机硬件因为年久失修坏掉了,当时这台RabbitMQ就无法提供服务,导致系统服务瞬间不可用。
故障发生了也不可怕,最重要的是及时发现并解决故障。「付钱拉」对自身系统的要求是,秒级发现故障,快速诊断和解决故障,从而降低故障带来的负面影响。
二、问题
三、解决方案
3.1 尽可能避免故障
3.1.1 设计可容错的系统
比如重路由,对于用户支付来说,用户并不关心自己的钱具体是从哪个通道支付出去的,用户只关心成功与否。「付钱拉」连接30多个通道,有可能A通道支付不成功,这个时候就需要动态重路由到B或者C通道,这样就可以通过系统重路由避免用户支付失败,实现支付容错。
还有针对OOM做容错,像Tomcat一样。系统内存总有发生用尽的情况,如果一开始就对应用本身预留一些内存,当系统发生OOM的时候,就可以catch住这个异常,从而避免这次OOM。
3.1.2 某些环节快速失败“fail fast原则”
Fail fast原则是当主流程的任何一步出现问题的时候,应该快速合理地结束整个流程,而不是等到出现负面影响才处理。
举个几个例子:
(1)「付钱拉」启动的时候需要加载一些队列信息和配置信息到缓存,如果加载失败或者队列配置不正确,会造成请求处理过程的失败,对此最佳的处理方式是加载数据失败,JVM直接退出,避免后续启动不可用;
(2)「付钱拉」的实时类交易处理响应时间最长是40s,如果超过40s前置系统就不再等待,释放线程,告知商户正在处理中,后续有处理结果会以通知的方式或者业务线主动查询的方式得到结果;
(3)「付钱拉」使用了redis做缓存数据库,用到的地方有实时报警埋点和验重等功能。如果连接redis超过50ms,那么这笔redis操作会自动放弃,在最坏的情况下这个操作带给支付的影响也就是50ms,控制在系统允许的范围内。
3.1.3 设计具备自我保护能力的系统
系统一般都有第三方依赖,比如数据库,三方接口等。系统开发的时候,需要对第三方保持怀疑,避免第三方出现问题时候的连锁反应,导致宕机。
(1)拆分消息队列
「付钱拉」提供各种各样的支付接口给商户,常用的就有快捷,个人网银,企业网银,退款,撤销,批量代付,批量代扣,单笔代付,单笔代扣,语音支付,余额查询,身份证鉴权,银行卡鉴权,卡密鉴权等。与其对应的支付通道有微信支付,ApplePay,支付宝等30多家支付通道,并且接入了几百家商户。在这三个维度下,如何确保不同业务、三方、商户、以及支付类型互不影响,「付钱拉」所做的就是拆分消息队列。下图是部分业务消息队列拆分图:
(2)限制资源的使用
对于资源使用的限制设计是高可用系统最重要的一点,也是容易被忽略的一点,资源相对有限,用的过多了,自然会导致应用宕机。为此「付钱拉」做了以下功课:
限制连接数
随着分布式的横向扩展,需要考虑数据库连接数,而不是无休止的最大化。数据库的连接数是有限制的,需要全局考量所有的模块,特别是横向扩展带来的增加。
限制内存的使用
内存使用过大,会导致频繁的GC和OOM,内存的使用主要来自以下两个方面:
A:集合容量过大;
B:未释放已经不再引用的对象,比如放入ThreadLocal的对象一直会等到线程退出的时候回收。
限制线程创建
线程的无限制创建,最终导致其不可控,特别是隐藏在代码中的创建线程方法。
当系统的SY值过高时,表示linux需要花费更多的时间进行线程切换。Java造成这种现象的主要原因是创建的线程比较多,且这些线程都处于不断的阻塞(锁等待,IO等待)和执行状态的变化过程中,这就产生了大量的上下文切换。
除此之外,Java应用在创建线程时会操作JVM堆外的物理内存,太多的线程也会使用过多的物理内存。
对于线程的创建,最好通过线程池来实现,避免线程过多产生上下文切换。
限制并发
做过支付系统的应该清楚,部分三方支付公司是对商户的并发有要求的。三方给开放几个并发是根据实际交易量来评估的,所以如果不控制并发,所有的交易都发给三方,那么三方只会回复“请降低提交频率”。
所以在系统设计阶段和代码review阶段都需要特别注意,将并发限制在三方允许的范围内。
我们讲到「付钱拉」为z实现系统的可用性做了三点改变,其一是尽可能避免故障,接下来讲后面两点。
3.2 及时发现故障
故障就像鬼子进村,来的猝不及防。当预防的防线被冲破,如何及时拉起第二道防线,发现故障保证可用性,这时候报警监控系统的开始发挥作用了。一辆没有仪表盘的汽车,是无法知道车速和油量,转向灯是否亮,就算“老司机”水平再高也是相当危险的。同样,系统也是需要监控的,最好是出现危险的时候提前报警,这样可以在故障真正引发风险前解决。
3.2.1 实时报警系统
如果没有实时报警,系统运行状态的不确定性会造成无法量化的灾难。「付钱拉」的监控系统指标如下:
实时性-实现秒级监控;
全面性-覆盖所有系统业务,确保无死角覆盖;
实用性-预警分为多个级别,监控人员可以方便实用地根据预警严重程度做出精确的决策;
多样性-预警方式提供推拉模式,包括短信,邮件,可视化界面,方便监控人员及时发现问题。
报警主要分为单机报警和集群报警,而「付钱拉」属于集群部署。实时预警主要依靠各个业务系统实时埋点数据统计分析实现,因此难度主要在数据埋点和分析系统上。
3.2.3 分析系统
网络异常预警; 单笔订单超时未完成预警; 实时交易成功率预警; 异常状态预警; 未回盘预警; 失败通知预警; 异常失败预警; 响应码频发预警; 核对不一致预警; 特殊状态预警;
交易量异常预警; 交易额超过500W预警; 短信回填超时预警; 非法IP预警;
针对MQ消费队列,通过RabbitMQ脚本探测,实时分析队列深度; 针对数据库部分,通过安装插件xdb,实时监控数据库性能。
单通道网络异常预警:1分钟内A通道网络异常连续发生了12笔,触发了预警阀值; 多通道网络异常预警1: 10分钟内,连续每分钟内网络异常发生了3笔,涉及3个通道,触发了预警阀值; 多通道网络异常预警2:10分钟内,总共发生网络异常25笔,涉及3个通道, 触发了预警阀值.
3.2.5 日志记录和分析系统
日志打印规范如下:
2016-07-22 18:15:00.512||pool-73-thread-4||通道适配器||通道适配器-发三方后||CEX16XXXXXXX5751||16201XXXX337||||||04||9000||【结算平台消息】处理中||0000105||98XX543210||GHT||03||11||2016-07-22 18:15:00.512||张张||||01||tunnelQuery||true||||Pending||||10.100.140.101||8cff785d-0d01-4ed4-b771-cb0b1faa7f95||10.999.140.101||O001||||0.01||||||||http://10.100.444.59:8080/regression/notice||||240||2016-07-20 19:06:13.000xxxxxxx
||2016-07-22 18:15:00.170||2016-07-22 18:15:00.496xxxxxxxxxxxxxxxxxxxx
||2016-07-2019:06:13.000||||||||01||0103||111xxxxxxxxxxxxxxxxxxxxxxxxx
||8fb64154bbea060afec5cd2bb0c36a752be734f3e9424ba7xxxxxxxxxxxxxxxxxxxx
||622xxxxxxxxxxxxxxxx||9bc195a59dd35a47||f2ba5254f9e22914824881c242d211
||||||||||||||||||||6xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx010||||||||||
简要日志可视化轨迹如下:
3.2.6 7*24小时监控室
3.3 及时处理故障
3.3.1 自动修复
3.3.2 服务降级
四、Q&A
Q1: 能讲讲当年那台RabbitMQ宕掉的具体细节和处理方案吗?
A1: RabbitMQ宕机时间引发了对系统可用性的思考,当时我们的RabbitMQ本身并没有宕机(RabbitMQ还是很稳定的),宕机的是RabbitMQ所在的硬件机器,但是问题就出在当时RabbiMQ的部署是单点部署,并且大家惯性思维认为RabbitMQ不会宕机,从而忽略了它所在的容器,所以这个问题的产生对于我们的思考就是所有的业务不可以有单点,包括应用服务器、中间件、网络设备等。单点不仅仅需要从单点本身考虑,比如整个服务做双份,然后AB测试,当然也有双机房的。
Q2: 贵公司的开发运维是在一起的吗?
A2: 我们开发运维是分开的,今天的分享主要是站在整个系统可用性层面来考虑的,开发偏多,有一部分运维的东西。这些付钱拉的走过的路,是我一路见证过的。
Q3: 你们的后台全部使用的Java吗?有没有考虑其他语言?
A3: 我们目前系统多数是java,有少数的python、php、C++,这个取决于业务类型,目前java这个阶段最适合我们,可能随着业务的扩展,会考虑其他语言。
Q4: 对第三方依赖保持怀疑,能否举个具体的例子说明下怎么样做?万一第三方完全不了用了怎么办
A4: 系统一般都有第三方依赖,比如数据库,三方接口等。系统开发的时候,需要对第三方保持怀疑,避免第三方出现问题时候的连锁反应,导致宕机。大家都知道系统一旦发生问题都是滚雪球的,越来越大。比如说我们扫码通道,如果只有一家扫码通道,当这家扫码通道发生问题的时候是没有任何办法的,所以一开始就对它表示怀疑,通过接入多家通道,如果一旦发生异常,实时监控系统触发报警后就自动进行路由通道切换,保证服务的可用性;其二,针对不同的支付类型、商户、交易类型做异步消息拆分,确保如果一旦有一种类型的交易发生不可预估的异常后,从而不会影响到其他通道,这个就好比高速公路多车道一样,快车和慢车道互不影响。其实总体思路就是容错+拆分+隔离,这个具体问题具体对待。
Q5: 支付超时后,会出现网络问题,会不会存在钱已付,订单丢失,如何做容灾及数据一致性,又有没重放日志,修过数据?
A5:做支付最重要的就是安全,所以针对订单状态我们都是保守处理策略,因此对于网络异常的订单我们都是设置处理中状态,然后最终通过主动查询或者被动接受通知来完成和银行或者三方的最终一致性。支付系统中,除了订单状态还有响应码问题,大家都知道银行或者三方都是通过响应码来响应的,响应码和订单状态的翻译也是一定要保守策略,确保不会出现资金多付少付等问题。总之这个点的总体思路是,资金安全第一,所有的策略都是白名单原则。
Q6: 刚才提到过,若某支付通道超时,路由策略会分发至另一通道,根据那个通道图可看出,都是不同的支付方式,比如支付宝或微信支付,那如果我只想通过微信支付,为啥不是重试,而要换到另一通道呢?还是通道本身意思是请求节点?
A6:首先针对超时不可以做重路由,因为socket timeout是不能确定这笔交易是否发送到了三方,是否已经成功或者失败,如果是成功了,再重试一遍如果成功,针对付款就是多付,这种情况的资金损失对公司来说不可以的;其次,针对路由功能,需要分业务类型,如果是单笔代收付交易,用户是不关心钱是哪个通道出去的,是可以路由的,如果是扫码通道,用户如果用微信扫码,肯定最终是走微信,但是我们有好多中间渠道,微信是通过中间渠道出去的,这里我们可以路由不同的中间渠道,这样最终对于用户来说还是微信支付。
Q7: 能否举例说下自动修复的过程?如何发现不稳定到重路由的细节?
A7: 自动修复也就是通过重路由做容错处理,这个问题非常好,如果发现不稳定然后去决策重路由。重路由一定是明确当前被重路由的交易没有成功才可以路由,否则就会造成多付多收的资金问题。我们系统目前重路由主要是通过事后和事中两种方式来决策的,针对事后比如5分钟之内通过实时预警系统发现某个通道不稳定,那么就会把当期之后的交易路由到别的通道;针对事中的,主要是通过分析每笔订单返回的失败响应码,响应码做状态梳理,明确可以重发的才做重路由。这里我指列举这两点,其他的业务点还非常多,鉴于篇幅原因,不做详述,但是总体思路是必须有一个内存实时分析系统,秒级决策,这个系统必须快,然后结合实时分析和离线分析做决策支撑,我们的实时秒级预警系统就做这个事情。
Q8: 商户促销有规律吗?促销时峰值与平时相比会有多少差别?有技术演练么?降级的优先级是怎样的?
A8:商户促销一般我们会事先经常和商户保持沟通,事先了解促销的时间点和促销量,然后针对性做一些事情;促销峰值和平时差距非常大,促销一般都是2个小时之内的比较多,比如有的卖理财产品,促销也就集中在1个小时之内,所以峰值非常高;技术演练是我们在了解商户的促销量,然后预估系统的处理能力,然后提前做演练;降级的优先级主要是针对商户的,由于接入我们的商户支付场景比较多的,有理财,有代收付,有快捷,有扫码等等,所以我们整体原则就是不同的商户之间一定不可以相互影响,因为不能因为你家做促销影响了其他商家。
Q9:rsyslog归集日志怎么存储的?
A9: 这个是好问题,刚开始我们的日志也就是订单轨迹log是记录在数据库表中的,结果发现一笔订单流转需要好多模块,这样一笔订单的日志轨迹就是10笔左右,如果一天400w笔交易的话,这张数据库表就有问题了,就算拆分也是会影响数据库性能的,并且这个属于辅助业务,不应该这样做。然后,我们发现写日志比写数据库好,所以把实时日志打印成表格的形式,打印到硬盘上,这块由于只是实时日志所以日志量不大,就是在日志服务器的一个固定目录下。由于日志都是在分布式机器上,然后通过归集日志到一个集中的地方,这块是通过挂载存储的,然后有专门运维团队写的程序去实时解析这些表格形式的日志,最终通过可视化页面展示到运营操作页面,这样运营人员看到的订单轨迹几乎是实时的,您关心的怎么存储实际上不是啥问题,因为我们分了实时日志和离线日志,然后超过一定时间的离线日志会切割,最终被删除。
Q10: 系统监控和性能监控如何配合的?
A10:我理解的系统监控包括了系统性能监控,系统性能监控是系统整体监控的一部分,不存在配合问题,系统性能监控有多个维度,比如应用层面,中间件,容器等。系统的非业务监控可以查看文章分享。
正文结束
1.不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事
4.人才迁徙潮,2019年互联网各梯队排名重组,最适合程序员去的互联网公司有哪些?
5.37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...
7.日均5亿查询量的京东订单中心,为什么舍MySQL用ElasticSearch?
一个人学习、工作很迷茫?
点击「阅读原文」加入我们的小圈子!