背景
静儿在2017年8月25日怀着“再也不要下班时间收到报警”的美好期待加入美团金融智能支付负责核心交易,结果入职后收到的报警一天紧似一天。核心交易是整个智能支付的核心链路,承担着智能支付百分之百的流量。下面是我们的日单量增长曲线:
从图中可以看到从17年下半年开始,我们的日单量增长迅速,而且压力和流量在午、晚高峰时段非常集中。在这种情况下,交易的稳定性面临着严峻的考验。
为了保证交易的高可用,智能支付技术团队快速整合平台和集团技术资源,成立了专题项目组—“战狼”,聚焦支付技术底层基础,排查系统风险点和系统问题,全力为智能支付商户与客户提供一个良好、安全、顺畅的支付体验。使命必达,保驾护航!
启动排查
核心交易上游承接智能支付业务方,我们的产品POS机、小白盒、小黑盒、二维码和所有通过开放平台接入的商家都通过我们进行收单,下游调用银行等支付渠道。业务逻辑并不复杂。通过系统梳理,我们发现如下图所示,不合理逻辑很多。
发现问题
通过排查,我们了解到了我们的系统的主要问题,从大的方面说就是:“自身不强壮,队友不可靠”。问题分类如下图所示:
分析问题
1>事务中包含外部调用
外部调用包括对外部系统的调用和基础组件的调用。它具有返回时间不确定性,必然会造成大事务。大的数据库事务会造成其他请求数据库连接获取不到,那么和这个数据库相关的所有服务都很可能处于等待状态,造成连接池被打满,多个服务直接宕掉。如果这个没做好,危险指数五颗星。
2>超时时间和重试次数不合理
对外部系统和缓存、MQ等基础组件的依赖,如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,499错误会增多,系统的可用性会降低。如果超时时间设置的短,重试次数设置的多,会增加系统的整体耗时;如果超时时间设置的短,重试次数设置的也少,那么这次请求的返回结果会不准确。
咱们举个具体场景来看这个事情
服务A依赖于两个服务的数据完成此次操作。平时没有问题,一旦发生意外, 服务B在你不知情的情况下,响应时间变长,甚至停止服务,而你的客户端超时时间设置过长,则你完成此次请求的响应时间就会变长。Java的Servlet容器,无论是tomcat还是jetty都是多线程模型,都用worker线程来处理请求。这个可配置有上限,当你的请求打满worker线程的最大值之后,剩余请求会被放到等待队列。等待队列也有上限,一旦等待队列都满了,那这台webserver就会拒绝服务,对应到Nginx上返回就是502。如果你的服务是QPS较高的服务,那基本上这种场景下,你的服务也会跟着被拖垮。如果你的上游也没有合理的设置超时时间,那故障会继续向上扩散.这种故障逐级放大的过程,就是服务雪崩效应。
3>外部依赖的地方没有熔断
在依赖的服务不可用时,服务调用方应该通过一些技术手段,向上提供有损服务,保证业务柔性可用。而系统没有熔断,如果由于代码逻辑问题上线引起故障、网络问题、调用超时、业务促销调用量激增、服务容量不足等原因,服务调用链路上有一个下游服务出现故障,就可能导致接入层其他的业务不可用。
4>对于依赖我们的上游没有限流
在开放式的网络环境下,对外系统往往会收到很多有意无意的恶意攻击,如DDos攻击,用户失败重刷。虽然我们的战友兄弟各个是精英,但是我们必须要做好不被队友搞死的保障,搞不好哪天谁写了一个如果下游返回不符合预期就无限次重试的代码。这些内部和外部的巨量调用,如果不加以保护,往往会扩散到后台服务,最终可能引起后台基础服务宕机。
5>慢查询问题
慢查询会降低应用的响应性能和并发性能。在业务量增加的情况下造成数据库所有的服务器CPU利用率急剧攀升,严重的会导致数据库不响应,只能重启解决。
6>依赖不合理
每多一个依赖方,风险就会累加。特别是强依赖,它本身意味着一荣俱荣、一损俱损。
7>有废弃逻辑和临时代码
过期的代码会对正常逻辑有干扰,让代码不清晰。特别是对新加入的同事,他们对明白干什么用的代码可以处理。但是已经废弃的和临时的,因为不知道干什么用的,所以改起来更忐忑。如果知道是废弃的,其他功能改了这一块没改,也有可能这块不兼容,引发问题。容易造成级联多米诺骨牌效应。
8>没有有效的资源隔离
容易造成级联多米诺骨牌效应。
解决问题
1>事务中不包含外部调用
☆ 排查各个系统的代码,检查在事务中是否存在RPC调用、http调用、MQ操作、缓存、循环查询等耗时的操作,这个操作应该移到事务之外,理想的情况是事务内只处理数据库操作。
☆ 对大事务添加监控报警。大事务发生时,会收到邮件和短信提醒。大事务一般的报警标准是1s。
☆ 建议不要用xml配置事务,而采用注解的方式。原因是xml配置事务第一可读性不强,二是切面通常配置的比较泛滥,容易造成事务过大,三是对于嵌套情况的规则不好处理。
2>超时时间设置合理和重试次数。
☆ 首先要调研被依赖服务自己调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。
☆ 统计这个接口99%的超时时间是多少,设置的超时时间在这个基础上加50%。
☆ 重试次数如果系统服务重要性高,则按照默认,一般是重试三次。否则,可以不重试。
3>外部依赖的地方都要做熔断
☆ 自动熔断:可以使用netflix的hystrix或者美团点评自己研发的Rhino来做快速失败。
☆ 手动熔断:确认下游支付通道抖动或不可用,可以手动关闭通道。
4>对于依赖我们的上游要限流
☆ 通过对服务端的业务性能压测,可以分析出一个相对合理的最大QPS。
☆ 可以使用netflix的hystrix或者美团点评自己研发的Rhino来限流。
5>解决慢查询问题
☆ 将查询分成实时查询、近实时查询和离线查询。实时查询可穿透数据库,其他的不走数据库,可以用ES来实现一个查询中心,处理近实时查询和离线查询。
☆ 读写分离。写走主库,读走从库。
☆ 索引优化。索引过多会影响数据库写性能。索引不够查询会慢。 像核心交易这种数据库读写TPS差不多的,一般建议索引不超过4个。如果这还不能解决问题,那很可能需要调整表结构设计了。
☆ 对慢查询对应监控报警。我们这边设置的慢查询报警阈值是100ms。
6>能去依赖就去依赖,否则尽量同步强依赖改成异步弱依赖
☆ 划清业务边界,只做该做的事情。
☆ 如果依赖一个系统提供的数据,上游可以作为参数传入或者下游可以作为返回值返回,则可以下线专门去取数据的逻辑,尽量让上下游给我们数据。
☆ 我们写入基础组件,数据提供给其他端,如果其他端有兜底策略,则我们可以异步写入,不用保证数据100%不丢失。
7>废弃逻辑和临时代码要删除
☆ 梳理每个接口的调用情况,对于没有调用量的接口,确认不再使用后及时下线
☆ code review保证每段逻辑都明白其含义,弄清楚是否是历史逻辑或者临时逻辑
8>核心路径进行资源隔离
☆ 服务器物理隔离原则:
△ 内外有别:内部系统与对外开放平台区分对待
△ 内部隔离:从上游到下游按通道从物理服务器上进行隔离。低流量服务合并
△ 外部隔离:按渠道隔离,渠道之间互不影响
☆ 线程池资源隔离
△ Hystix通过命令模式,将每个类型的业务请求封装成对应的命令请求。每个命令请求对应一个线程池,创建好的线程池是被放入到ConcurrentHashMap中。注意:尽管线程池提供了线程隔离,客户端底层代码也必须要有超时设置,不能无限制的阻塞以致于线程池一直饱和。
☆ 信号量资源隔离
△ 开发者可以使用Hystix限制系统对某一个依赖的最高并发数。这个基本上就是一个限流策略。每次调用依赖时都会检查一下是否到达信号量的限制值,如达到,则拒绝。
除了上面的措施之外,战狼项目进行很有成效的两地三中心机房互备、组件安全漏洞修复和服务健康验证,限于篇幅,本篇不详述。只是在战狼开始之前,运维和架构师们也强调这些,但是大家忙于对应需求,没有引起重视。沟通可能可以说是项目过程中最重要的环节。没有很好的沟通,就好像是越走越远的两个人,我在等着你回心转意,你却在等着自己死心。你发现心死不了,我却已经放弃了等待。永远在平行线上没有交集,徒劳的苦痛,没有意义。
实施后的效果
经过上面8个步骤,我们同时也新接入一些业务,边界如下:
从图中可以看到,边界更清晰了。我们通过故障演练证实了解决方案实施后的稳定性提升。
持续跟进
我们优化了业务大盘、故障大盘。加强了监控报警机制,持续的监控和保障着系统的稳定性。故障演练也作为了定时的日常工作来做。稳定性需要建立长期规范,维护组内的checklist,定期检查是否达到标准。checklist举例如下:
项目总结
我们家老大是像星星一样散发着智慧的人。他给我们总结系统稳定性的三个要素:第一是别人死我们不死,第二是不自己作死,第三是不被猪队友搞死。
稳定性具体的实施方法总结一下就是:能不依赖就去依赖;尽可能将强依赖转成弱依赖;实在不能降低依赖就保护依赖;做好自保;出了问题只能收敛不能扩大;对危险要能监控。
线上支付平台总结的稳定性“四板斧”:研发规范、自身稳定、容错下游、防御上游。
经过为期4周的战狼项目,多个小组紧密合作,日夜兼程,高效的完成了一个又一个攻坚任务,保证了交易系统的稳定。整个项目收获的不仅是一套稳定的系统,更重要的是通过一次次激烈的探讨,一场场集体推进会,总结出了一套通用的系统稳定提升方法,同时也锻炼出一只充满战斗力的队伍,为整个支付业务快速稳定发展奠定了基础。
春秋战国时期的君主普遍的勤政,满清的康乾盛世繁荣。我总结最根本的原因是忧患。因为连年各国的割据争斗、因为汉族不甘心受外族的统治,君王在压力下反而有作为。战狼项目虽然已经结束,但是战狼精神永存。我们要时时刻刻居安思危,保持稳定第一。
工具介绍
项目中多次提到使用hystrix和Rhino。所以这里对它们做一个简单的介绍。
☆ hystrix
Hystix是一个实现了断路器模式来对故障进行监控,当断路器发现调用接口发生了长时间等待,就使用快速失败策略,向上返回一个错误响应,这样达到防止阻塞的目的。这里重点介绍一下hystrix的线程池资源隔离和信号量资源隔离。
□ 线程池资源隔离
线程隔离优点:
△ 使用线程可以完全隔离第三方代码,请求线程可以快速放回。
△ 当一个失败的依赖再次变成可用时,线程池将清理,并立即恢复可用,而不是一个长时间的恢复。
△ 可以完全模拟异步调用,方便异步编程。
□ 线程隔离缺点:
△ 线程池的主要缺点是它增加了CPU,因为每个命令的执行涉及到排队(默认使用SynchronousQueue避免排队),调度和上下文切换。
△ 对使用ThreadLocal等依赖线程状态的代码增加复杂性,需要手动传递和清理线程状态。(Netflix公司内部认为线程隔离开销足够小,不会造成重大的成本或性能的影响)
☆ 信号量资源隔离
△ 开发者可以使用Hystix限制系统对某一个依赖的最高并发数。这个基本上就是一个限流策略。每次调用依赖时都会检查一下是否到达信号量的限制值,如达到,则拒绝。
信号量隔离优点:
△ 不新起线程执行命令,减少上下文切换。
信号量隔离缺点:
△ 无法配置断路,每次都一定会去尝试获取信号量。
□ 比较一下线程池资源隔离和信号量资源隔离。
△ 线程隔离是和主线程无关的其他线程来运行的;而信号量隔离是和主线程在同一个线程上做的操作。
△ 信号量隔离也可以用于限制并发访问,防止阻塞扩散,与线程隔离的最大不同在于执行依赖代码的线程依然是请求线程。
△ 线程池隔离适用于第三方应用或者接口、并发量大的隔离;信号量隔离适用于内部应用或者中间件;并发需求不是很大的场景。
☆ Rhino
Rhino是美团点评基础架构团队研发并维护的一个稳定性保障组件,提供故障模拟、降级演练、服务熔断、服务限流等功能。和Hystrix对比:
△ Hystrix组件在熔断之后,并在试探线程成功之后,就直接关闭熔断开关,全部流量走正常逻辑。而Rhino会对流量进行恢复灰度。
△ 内部通过Cat(Cat是美团点评开源的一个监控系统)进行了一系列埋点,方便进行服务异常报警。
△ 接入配置中心,能提供动态参数修改,比如强制熔断、修改失败率等。
关于作者
谢晓静,85后程序媛,20岁时毕业于东北大学计算机系。在毕业后的第一家公司由于出众的语言天赋,在1年的时间里从零开始学日语并以超高分通过了国际日语一级考试,担当两年日语翻译的工作。后就职于人人网,转型做互联网开发。中国科学院心理学研究生。有近百个技术发明专利,创业公司合伙人。有日本东京,美国硅谷技术支持经验。目前任美团点评技术专家,负责核心交易。欢迎关注静儿的个人技术公众号:编程一生