作者介绍
淇公 ,蚂蚁金服技术专家。热爱 java 和一些函数式语言,长期关注系统稳定性领域
一、差旅随想
因为 base 在分公司,需要经常去总部出差,所以搭乘飞机成了家常便饭,很多时候坐在飞机上会不由的感叹,设计制造这样精密复杂的机器的那帮人真的是了不起,他们是怎样保证这样一台由无数零件组成的设备能够稳定运转的?
于是去简单查阅了一些资料,了解到无论任何情况下,我们所乘坐的客机从生产到投入运营,任何细节都是将可靠性放在第一位的,简单来说,体现在:
设计阶段:
方案尽量简化,冗余设计,采用成熟经过验证的技术,标准化,健壮性设计等等.
测试阶段:
气动力试验,环境试验, 可靠性与寿命试验,真实试飞,各项极限测试等等
运行阶段:
执飞前检查,运行状态监控,定期维护检修.
机组:
持证上岗,定期培训,考核
一架飞机在云端飞行,无数次面对气流的颠簸,起飞降落的冲击仍然能够保证安全可靠,这背后从设计到制造再到维护,完全是一项系统工程, 需要极高的专业水平和责任心才能胜任, 而工程领域的实践经验通常都有相通性,作为一名软件开发人员,不禁要思考我们所开发,运营,维护的软件系统,同样是一项专业的系统工程, 同样需要满足极高的稳定性要求,面对汹涌而来的流量冲击,软硬件变化的影响仍然需要屹立不倒,我们需要怎么做? 对比苛刻的航空器可靠性要求以及保障策略和手段,我们可以从中学到什么?
二、概述稳定性保障
首先要明确稳定性保障时什么,在我看来,系统稳定性保障的内容包括:
稳定性保障通常来讲是指保障系统在运行, 运维过程中, 即时面对各种极端情况或突发事件仍然能够提供持续的, 可靠的服务能力.
此处所指各种极端情况或突发事件包括且并不局限于机房级故障, 城市级故障,线上故障, 线上业务量瞬时爆发, 持续快速增长, 系统服务器故障, 依赖数据库故障, 环境数据改变, 依赖系统故障等.
此处所指持续的, 可靠的服务能力是指受影响集群依据突发情况严重程度,在业务可接受的时间范围内完成恢复或失败转移,继续提供正确的服务能力以及足够的运算能力.
看上去挺吓人的,总结起来就是系统一旦上线运行,那么无论面对什么情况都尽量不要出问题,如果出了问题也要求可以快速解决和恢复.
何其复杂的要求,良好的设计, 严格的,全面的测试验证,持续的运营维护,迭代,重构,升级,经验丰富的研发维护团队缺一不可,航空业以安全为最高准则,我们则以系统稳定作为底线.
三、怎么做系统设计
民用飞行器的设计原则包括方案简单,冗余设计,技术成熟,标准化,健壮.而软件系统的稳定性同样与设计方案息息相关:
职责清晰,单一的系统,其稳定性保障的难度会小于大杂烩,功能繁多的系统,不同的业务功能,其业务特征,业务形态,业务量,资源使用形式,依赖关系都可能不同,如果混合在一个系统中,将很难避免相互影响.
设计师在设计飞机时,会在可以达到目标的情况下,尽量选择简单的方案,这样无论从成本节省还是维护效率上都有所提升,两点之间直线最短,对于系统设计也是一样的,一定不要过度设计和为了技术而技术,比如一组配置数据,数据量不大,实时性要求也不高,就不要去引入分布式缓存,变更通知,简简单单的 JVM 堆内缓存+定时刷新就可以搞定,维护简单方便,否则技术栈的加深,维护节点的增加都会带来更多的不稳定因素。
冗余设计,对于飞机而言,体现在机组包括机长副机长,引擎至少双发,操作系统至少两套等等,于我们的系统而言,则通常体现在:
数据冗余,数据多份副本,DB 一主多备,出现问题时可以快速切换.
计算能力冗余,服务器的计算能力始终保持一定冗余,在一组服务器出现问题时其他服务器可以接替提供服务,亦或在业务量波动时可以承受冲击。
网络,存储或其他基础设施冗余,出现意外时,比如断电,光纤断裂时均有备用可以实现切换,快速恢复。
技术成熟,标准化,通常来说,在企业级的应用中,技术选型是非常重要的,由于对可靠性有极高的要求,因此一种技术组件或框架,中间件在引入时要充分的考虑:
是否经过充分的验证
是否有广泛的用户基础,有足够有经验的开发人员
是否有完善的文档和技术支持
标准化则比较典型的运用于系统交互协议标准化,数据模型标准化,服务标准化,标准化带来的好处也非常显著:
规范系统之间的交互方式,便于维护,管理
提升效率,避免重复造轮子
降低技术学习曲线,解放人员劳动力
信息理解无偏差,数据流动顺畅
服务约定明确,能力清晰,对接效率高
健壮性对于飞机的安全至关重要,因此飞机所使用的设备,材料都经过各种严格的健壮性测试,确保可靠性,对于软件系统而言,本身是个比较大的命题,一般来讲是指系统的容错能力,无论是一次错误的输出,或是一个依赖服务的崩溃都不应该影响系统自身的服务能力和正确性,这一点本身不容易做到,但也有一些手段可以辅助我们尽可能做好:
良好的编码规范和严格的测试验证,比如对异常的捕获处理,对边界数值的验证避免单点依赖,无论是系统单点还是技术单点,这包括关键依赖服务应该集群化,多单元甚至多地部署,设计灾备方案,比如 DB 的 FO,交互的同步转异步,RPC 转消息等等,保证在依赖的服务出现问题时可以继续提供服务,同时数据可达成一致,信息可达,压力可控。
四、风险分析
商用客机在交付前会进行一系列的严格测试,这些测试来源于设计人员对于飞机将来的使用场合,环境,以及可能面临的一些极端条件所分析出的可能存在的风险而针对性设计的,要求飞机能够承受住考验才可交付,对于应用系统的稳定性而言,道理是相通的,不过按道理来说,系统设计和风险分析应该放在一起讨论,可是很多时候我们很难有机会重头开始设计一个系统,那么当我们接手一个系统时,需要对系统进行全面的风险分析,这是实施稳定性保障的基础前提.
一般来说,风险分析需要包含几个方面:
对外提供什么服务
那些业务依赖本系统
系统的上游有那些
依赖的下游有那些,依赖的程度如何
系统的部署结构是什么样的
依赖什么数据,从哪里获取
依赖那些资源,资源使用模型时怎么样的
日常常见的维护操作是什么,都有谁参与等等
分析这些内容有什么好处呢,首先可以让我们看清系统对业务的影响情况,影响面,其次,结合日常运维动作,链路关系,资源使用模型可以推导各方面可能存在的风险点和带来的后果,比如一个依赖节点耗时上涨了,带来的后果可能是业务处理耗时整体上涨,甚至拖垮调用节点,这就是可能存在的风险点. 又比如运维人员可能会进行一些日常维护动作而缩容,这有可能导致系统出现容量风险等等.
当我们完成了风险分析,很自然的就可以进入到风险防范,治理阶段了。
五、风险防范三板斧
风险预防
简单来说就是质量+容量+灰度+回滚。
良好的质量保障是保证系统稳定运行的重要基础,如同飞机制造过程中的质量控制,精细到每一个部件,完成后还会进行覆盖到每一个细节的测试验证一样,我们的每一个迭代,每一次变更都需要有完善的质量保障体系进行覆盖, 确保变更质量符合要求,这套体系通常包含如下内容:
需求分析
系统分析,设计文档产出,评审
开发自测,交付测试,联调
持续集成覆盖
代码 CR
整体交付报告 review
这里面每一项的目的都是为了减少出错的可能,避免 bug 上线.
此外,系统从建设之初就应该考虑容量模型的数据是否符合预期以及高度可复用的度量手段的建立, 毕竟大家都不愿意乘坐一架机翼载荷不明的飞机,对于不同资源使用模型的系统而言,其容量模型一般是不一样的,对于一般的在线服务应用系统而言基本上都是计算密集型,容量方面主要需要关注的是 QPS-CPU-耗时 三者的关系, 我们需要知道:
目标 QPS 下, CPU 使用是否处于安全水位之下, 响应时间是否可接受
多少 QPS 下,CPU 水位达到危险状态或耗时升高到无法接受的程度
搞清楚这两组数据,我们就知道日常情况下的集群容量以及极限情况下的集群容量,可以用于指导计算资源的配置,不至于被汹涌而来的请求压垮.
这三个因素相互制约,任何一个不达标,则整体容量不符合要求. 一般来说,获取容量模型的手段包括:
获取单系统容量模型所进行的单机压测
获取完整处理链路容量模型所进行的全链路集群压测
压测时,需要关注的是压测样本的仿真度问题以及服务器配置的差异,如果压测样本与真实情况偏离较大,那么获得的容量模型可能与真实情况相去甚远, 如果集群机器配置差异较大,单机压测或者不全面的全链路压测都可能给出错误的指导数据.
然后来讲一讲灰度, 首先要明确的是,无论多么经验丰富的人编写的代码,经过多么严格的测试验证, 都无法百分百避免问题遗漏, 所以变更发布一定要具备灰度和监控的能力,无论采用什么样的灰度策略, 大部分情况下都可以将问题的爆发控制在可控制的范围内.一般可能采取的灰度策略包括:
按用户比例灰度
内部用户灰度->外部部分用户->全量
白名单灰度
按区域/地域灰度等等
灰度阶段发现的问题,可能是用户的一些反馈,意见,可以快速迭代进行优化,解决,某些可能就是真实的线上问题,会对用户体验甚至资金造成影响, 这时就需要紧急回滚,因此快速回滚的能力必不可少, 这要求做到如下几点:
具备良好的代码,数据版本管理能力,可以实现快速的替换
提前分析好回滚的依赖性,避免出现链路某个节点回滚其他节点还保持依赖导致整体出错。
风险预警
通常来说,我们乘坐的航班,驾驶舱里的设备会实时的对机件的运行状态进行监控, 地面雷达也会不断的监控飞机的动向,一旦出现异常则进行快速应对, 我们的系统也是一样的,必须具备对运行状态的监控能力, 合理有效的监控体系能够帮助我们及时发现存在的问题,进而采取进一步的措施来进行处置,这种监控体系通常是多层次的:
基础资源监控
网络
存储
服务器硬件,系统资源
通用中间件
DNS
流量调配
…
应用服务监控
业务量
来源分布
结果
耗时
成功率
依赖调用量
依赖调用成功率
依赖调用耗时
…
应用基础资源监控
服务器数量,分布
服务器内存
服务器 CPU 使用
GC 情况
线程池使用情况
DB 调用量
DB 调用耗时
IO
磁盘占用
…
链路监控
业务整体成功率
业务链路处理整体耗时
报错量监控
问题节点监控
等等内容
在这些监控内容的基础上, 通常又要按照部署的集群,区域,单元进行逻辑化的监控单元,便于进一步定位问题范围.
在监控的基础上,会配置各种各样的报警,关注各种指标的变化情况,每当出现异动都会触发报警,通常这些报警都是基于专家经验人为设置的,这样的典型结果是一方面我们可以知道现在系统上出现了异动,另一方面是这种异动可能因为业务发展带来的量级变化,偶然的抖动,不同时间段的差异等等原因带来海量的噪音,引发报警疲劳,面对这种问题,通常采用下列办法进行优化:
多监控点,多层次关联,低层次监控关联高层次监控,比如监控 DB 调用量的监控点与业务调用量监控点进行关联,当 DB 调用增加时可以知道是由于业务量上涨导致,快速给出定位原因,避免不必要的恐慌, 这种关联的优势还在于为问题紧急程度提供评估, 比如某个依赖系统 down 掉了,对它的监控报警点关联到整体业务成功率报警点上,如果报警信息上明确告知没有影响到整体成功率,那么一般无需采取一些极端的应急手段,反过来,则需要立即着手快速止血。
另一方面,引入一些智能算法,通过对业务规律,历史数据的分析,学习,输出一套自我更新,自我迭代的监控机制,可以自适应的识别那些常态化的变化因子,从而有效降低无效报警数量,达到精细化,高效的监控报警效果.
应急处置
所谓应急处置,则是指系统确实出现了异常情况,通过某些措施手段快速止血,恢复的过程。如同飞行员在飞机出现问题时决策返航,切换控制系统,关闭失效发动机一样,需要快速掌握情况后决策出合理的下一步动作来消除风险。
通常在接收到监控报警后,如果产生了比较严重或者可能诱发严重问题的情况,那么就需要进入应急阶段,应急处置应该是一套明确的机制和规范,指导人员进行快速有效的应急,这包括:
人员如何分工:
谁负责排查,谁负责关注业务运行情况,谁负责信息同步,谁负责决策,谁操作止血等等,并非所有的事情都需要专人跟进,而是明确角色,可以一人担负多个角色.
如何决策:
什么样的情况下,应该采用何种手段进行止血处理,影响是什么,什么样的情况下可以进行恢复操作.
预案准备:
面对什么问题可以使用什么手段进行处置,达到止血的效果,通常而言,预案会包括回滚,降级,切换,扩容,重启等等,每一种预案都会针对特定的问题,也可能多重预案联合使用,视具体情况而定.
回顾复盘:
处置完成后收集那些信息,如何总结和产出优化提升任务.
应急处置机制应该在做在事前, 团队成员充分学习和掌握,并且每次应急后进行总结,对不足的地方进行提升,总之应急就是争分夺秒,高效的协作和准确的处置可以挽大厦于既倒。
六、在此之外
当我们分析完系统的风险,相应的建立了监控体系,应急处置机制,是不是就高枕无忧了呢,答案肯定是否定的,事物总是在变化之中, 风险点可能随着系统演变而变化, 监控可能随之时效或遗漏,预案可能失效,人员经验可能过时,精气神可能懈怠, 如何应对? 我们还需要常态化的演练机制,不断的吸收出现过的问题进行反复的演练,一如飞行员要定期培训,考核,机组要经常进行应急,疏散等演练一样,通过演练,我们可以达到以下效果:
验证监控系统有效性
验证预案有效性
验证团队应急机制执行,掌握情况
为潜在问题摸索应对方案
这一切都是为了保证整套稳定性保障的体系可以高速运转, 而演练,应该注重如下几点:
仿真度,应该尽可能的贴近真实问题现象和触发原因
突然性,让团队成员在最真实的状态下进行反应
安全性,演练必须注重安全性,不能对真实业务造成影响
即时复盘,总结做的好的,不好的,不好之处加以提升
反哺,用演练不断的查漏补缺,提升整个体系保障能力的厚度
六、结束
系统稳定运行是我们开发者的底线,我本人也长期奋战在维护系统稳定的战线上,深知其中的艰难,敏锐的洞察力,警觉感,执行力,敬业精神是每一个稳定性保障成员的必要素质,完善的风险防控体系是我们手上的利器,守住底线,不要让自己,让队友失败。
评论