换个角度聊系统稳定性建设
共 5983字,需浏览 12分钟
·
2022-01-05 00:21
写在前面
对于任何系统来说,系统稳定性都是最基本的一个要求,只不过每个项目都有其发展周期,每个周期都有其主要的发展目标,比如业务爆发初期我们要求业务快速迭代,业务发展中期我们可能更多的是要求精细化运营、精细化治理,业务发展后期我们主要围绕于降本增效做事情,但是系统稳定性基本是贯穿整个项目发展周期。而且我们未来是要做SaaS产品的,稳定性更是SaaS的基石。
什么是系统稳定性
关于如何定义系统稳定性是一个很难的问题,因为围绕于系统稳定性可定义的视角太多了,我简单说下我的理解,起到抛砖引玉的目的。
我们不能完全避免不发生故障,BAT/TMD谁也不能说我的服务永远不发生故障,那么最后大家拼的就是谁出现的故障少,谁出现故障之后恢复的快,业务影响降低到最低。
淘宝也会出现故障,只不过淘宝做了足够的精细化划分,每次故障只影响少部分商家,发生故障的时候,如何减少影响的商家数量,行业里通常的做法是给商家分区,区和区之间是相互隔离的,一个区停机只影响自己。所以故障是天然发生的,如何缩短故障影响面是工程技术视角需要解决的核心命题。
系统稳定性关心的是:服务与数据。
稳定性主要解决的是:容错与恢复。
服务的容错
主要说的是应用服务在面对各种未知边界情况发生时所具备的容错能力,这种能力需要在技术选型、架构合理性、上下游治理等多个角度共同实现。
服务的恢复
我们的应用服务在面对异常、故障发生时,是否可以快速的短时间恢复,降低业务影响,同样是稳定性的一个重要考核指标。
数据的容错
我们是否具备对于目标数据写入规则的强校验能力,比如MySql可以很好的实现几个字段的唯一约束,但是如果大量使用KV,这种唯一约束你只能引入分布式锁了,但是如何做好分布式锁就又是一个复杂的话题。有的情况下我们对于数据一致性有较高的要求,如果架构方案设计的不合理,技术选型有问题,恐怕这种问题就很难避免了。而数据层是有状态的,在面对扩容这个操作时,永远没有无状态服务来的迅速,我们就需要做到多级的高可用。
数据的恢复
既然是有状态的服务,在数据存储服务故障时是否可以快速恢复数据,回溯数据源,进一步减少数据服务故障造成的损失也是体现在服务稳定性本身的一个要求。
如何做到系统稳定性
在聊系统稳定性之前,我们先看下我们的需求是如何一步步交付的。
需求交付生命周期
上图简要介绍了一个需求从采集、到评审、到研发、到最终交付、线上治理的全生命周期,在整个生命周期中哪些可能会影响到系统的稳定性呢?
首先看PM参与阶段
这个阶段研发团队最希望拿到的需求是真正有业务价值的需求,而不是一个短期或临时的需求,因为如果是一个临时需求的话,往往对应的解决方案也是临时的,如果太多临时的解决方案遗留在系统中,久而久之将会变成巨大的“技术债”,总有一天会爆发,对系统稳定性及业务迭代速度造成巨大影响。
所以这个阶段能做的有两点:
PM内部进行需求评审:主要目的是找到真正对用户产生价值的高优需求,或是真正服务于部门整体目标的需求,进而交付到研发团队,减少资源浪费,实现真正的价值交付。
做需求质量把控:上一条讲的是依据业务价值盘整需求优先级,而这条是期望PM可基于系统现状将需求拆解并靠近系统已有能力,以能力复用方式实现产品需求又可以尽量复用系统现有能力,帮助系统持续沉淀通用能力。
总结来说:避免引入过多临时解决方案,使得系统技术债越来越多,影响系统稳定性。
需求评审及方案输出阶段
这个阶段一般是研发各方基于目标需求出各自的技术方案,这里需要注意的是各方向RD需要考虑一些PRD之外的场景需求。什么意思呢?
举几个例子:
如果一个ToC的业务方需要使用一个ToB业务方的接口获取元数据,那么ToC的同学需要告知ToB的同学这个接口可能的QPS是多少。因为在ToC业务团队内部支持高QPS可能是一个默认的要求,但是对于ToB系统来说,他们平时处理的可能是业务复杂度高的需求,对于高QPS的标准和理解可能不同;
还比如一些数据一致性要求较高的场景,对接双方RD需要考虑下重试所可能导致的一些问题,比如我重试多次,你是否会产生脏数据?是否有分布式锁保障?如果分布式锁穿透之后,DB是否有唯一约束做兜底?有人说可以做幂等,其实幂等在不同场景下适用双方也有不同的幂等实现要求,比如如何定义幂等key?可以重试多少次?如何定义接口返回值对重试更友好?如何定义一次调用和多次调用一样?
对外提供的批量操作API接口,需要和对方确认并限制最大传入的批量操作数量,比如超过50就提示参数异常。循环操作DB或Redis需要做好流控,如果这两者都没有,对方传个10000批量操作,你的DB早挂了。
以上只是举了简单的例子,类似的问题相信你还可以举出更多,归根结底是我们对于需求的理解不应该只限于需求表面,在深挖业务价值之外,还需要考虑技术侧的一些潜在需求。最好还做到新功能有线上开关,可快速降级。
总结来说:挖掘需求之外的技术需求,方案覆盖边界场景,具有更好的容错能力。
研发阶段
这个阶段最可能出现的问题是技术方案选项或是方案合理性上的问题,比如选用了一个不适合于当前业务需求场景的解决方案,看似解决了当前问题,但往往也同时埋下了一个坑。
比如缓存一致性的问题,如果你是一个高并发场景,用缓存的目的是解决高QPS对于DB的压力,但选择了定时任务去解决缓存一致性问题,那么在这个数据不一致的时间窗口内势必会有大量流量穿透KV到达DB,引起雪崩问题。
还有很多情况用Redis做持久化存储,比如UPM那个case,Redis集群挂了之后,数据降级不到db也就拿不到真实准确的数据,Redis本身就是起到缓存的目的,不要忘了这个“缓”字。
之前还遇到过一个case,一个同学为优化慢SQL,新建了一个索引,这种想法很好,主动性很强。但是他的操作是先删除索引,再加上这个索引。但是高QPS不会给你这个机会,整个DB压垮了,直到网关禁掉进入的增量流量,服务消化掉存量流量之后,DB才起来。好心办了坏事。
方案合适说的是面对不同业务等级的系统设计的高可用方案有所区别,比如交易类的核心链路要在多机房容灾之上做到异地容灾,两地三中心即可。如果是支付金融类场景需要考虑三地五中心,引入TiDB或MGR解决数据一致性问题。如果是普通的运营系统,做到机房的N+1即可。如果是数据报表类系统,数据来自于T+1就可以的话,其实你挂了3、5个小时只要数据可回溯回来就可以了。如果数据实在回溯不回来了,只能和业务方爸爸诚恳的道歉了。
总结来说:确保方案合适、合理,深挖技术本质。
联调与测试阶段
这个阶段对于稳定性的保障在于是否可以在QA环境尽可能的模仿线上场景,做到全链路全环节的充分测试。所以对于线下测试环境搭建是有比较高的要求的,如果测试环境搭建成本太高,就会侵占开发阶段的时间,如果测试环境不能很好的模拟线上流程,就很难完整的覆盖功能测试。
另一个挑战在于QA同学不能简单的测试本次需求涉及到的功能点和变化,最好有一套全链路的自动化核心测试用例,这样可以进一步回归核心流程,确保RD的增量代码不会对核心流程造成影响,而自动化用例可以尽可能的释放QA的人力,减少回归成本。
总结来说:测试环境尽可能复刻线上流程,自动化回归核心流程测试用例,保障核心流程提高人效。
灰度与全量阶段
在完成了开发与测试环境的测试之后,我们需要将功能真正的交付到线上。但是测试环境毕竟不能很好的复刻线上环境,总会有很多问题可能直到线上才发现。比如发现高QPS写MySql过高,线下低流量就很难发现,比如发现有大量慢查询需要加索引,线下低流量就很难发现,比如线下环境没有服务之间访问鉴权控制,而线上有,导致服务之间调用大量鉴权失败。
而且有数据统计75%以上的线上case是由发版引起的,但是我们又不能不发版,那么我们能做的就是灰度,先发一部分机器上线,验证是否有问题,具体灰度的时间多久,可以视需求而定,确保新功能经过了线上流量充分验证之后,再进行全量。
一旦发现问题快速进行回滚,而不要先想着修复问题,回滚是解决发版造成case的最快方式。如果是出现类似于GC等问题,可以留一台线上机器保留现场,记住摘除此机器流量。
总结来说:为控制发版上线可能造成的线上问题影响面,应做好充分灰度,发现问题尽早回滚。
运营与运维阶段
在需求上线之后我们还是需要持续关注功能上线的指标的,做好监控、告警机制,开天眼早于业务同学反馈问题介入,可以尽量减少事故的影响。比如发现异常数增高,慢查询过多,GC频繁,核心链路流转走势有变化等。
总结来说:上线并不等于结束,需要持续关注相关指标,加监控告警开天眼。
业务架构搭建
接下来我们再看下我们的业务系统是如何一层层搭建起来的。
下面是一副普世的业务架构分层图:
从下到上每一个分层都需要做到尽可能的高可用,进而实现整个业务架构的稳定性。
IDC层
这层主要包括电源、网络、空调、消防等设备。为避免自然灾害或挖掘机挖断电缆导致机房不可用,IDC一般选在地震、洪水、台风等自然灾害发生概率低的地区。所在大楼也需要具备一定的抗灾能力。
为实现多机房的可用一般有:同层双机房(冷备、热备),两地三中心,三地五中心等解决方案,应对不同容灾等级的需求。
对于电源或网络等高可用,一般采用冗余方案,配备多条线路,预备发电机。
物理主机层
早期的物理机存储的磁盘主要是机械磁盘,长时间运行易产生磁盘故障,进而产生稳定性风险。现代物理机通常采用存储与计算分离方案部署,可以尽量避免因磁盘故障导致的计算能力下降问题,还可以整体降低存储成本。
比如可以采用两块磁盘组成RAID,其中一块磁盘做数据镜像磁盘,如一块磁盘故障率为0.1%,那么两块整体故障率就是0.0001%,提高了整体可用性。
IaaS层
这一层一般通过虚拟化技术在宿主机上虚拟化出运行环境,常见的解决方案有:主机虚拟化和容器虚拟化。
有了虚拟化技术,我们可以解决物理机时代的资源隔离等问题,而且在VM出现故障时,可以快速负载切换到冷备的VM上,提高了故障影响的处理恢复效率。
而在容器虚拟化时代,借助于K8S+Docker,IaaS层的可用性进一步提升。
PaaS层
从PaaS层开始,RD就开始与其打交道了。PaaS层基本是围绕于对公司的中间件的使用。这层要求我们掌握常用中间件的实现原理与最佳实践,这样才可以尽量少踩坑。
借助于多个中间件组合编排可以实现数据一致性处理、读写分离、分库分表、主备、哨兵、集群等能力,更好的实现业务整体可用性。
SaaS层
这一层基本就是我们最多打交道的系统应用层了,我们的业务系统都是处于这一层。这一层要求我们掌握端到端的解决方案实现能力,针对于不同系统、不同场景进行不同的技术选型。
比如网关层会面对高流量,但应该业务无感知,尽量选用NIO友好的解决方案,比如Netty。比如前后端交互,为提高数据同步一致性减少资源消耗,应采用推拉结合的方案。比如某个库表其使用场景频繁、数据量大应尽早考虑垂直分库或水平分表。如采用了缓存就需要缓存与DB的数据一致性问题。如对外提供写接口需要考虑幂等处理,以及幂等Key的选择。比如需要做到应用部署多副本,多机房部署。比如服务内部需要做好隔离、限流、熔断、降级、预案等处理。比如大促类需求交付需要做到压测、监控/告警、容量三位一体。为模拟一些不易出现的故障可以借助于混沌工程,模拟磁盘、网络、CPU等故障,监测系统对于稳定性的支持。
稳定性原则
有的同学说了,听下来稳定性怎么和高可用很像呢?其实稳定性一致是个副产品,在我们解决了高并发、高可用、高性能、流程标准化之后,很多稳定性问题自然而然就解决了。但是如果我们没有做好这些,稳定性问题会永远不期而遇。
区别于高可用,稳定性还具有以下几个特点:
敬畏心:对于线上环境要有敬畏之心,严格执行CR、上线、观察核心监控标准流程;
预案可靠:我们很多时候会对很多预期之外的场景做预案,但是我们并没有过多的去考虑或验证预案的可靠性,比如开关是否OK,预案执行是否复杂度太高以至于难以执行,降级场景是否自动化且快速;
高效性:高效、迅速解决问题,是否可以实现1分钟发现问题,5分钟之内止损;
所有事情闭环:在出现case之后,事后必有复盘,包括故障发生时间线、问题总结、深挖问题本质、后续Action等。
稳定性治理是一个非常复杂的命题,业界有这种稳定性治理的方式方法,比如蓝绿发布、灰度发布、混沌工程等治理方式,那么RD同学可遵循的原则有哪些呢?以下列举了一些常见原则。
N+1设计原则:系统中每个组件都应做到不会出现单点故障;
可回滚原则:确保系统可以向前兼容,在系统升级有问题时,可以快速回滚版本;
可禁用原则:应提供控制某个功能是否可提供服务的配置,在出现对应故障时,可以快速禁用该功能;
可监控原则:确保核心指标、核心链路可监控,开天眼在问题发生之前发现问题;
数据库多活设计:如果系统有极高的可用性要求,可以考虑多机房、跨地域的容灾保障,保障在自然灾害或是机房断电时,系统仍然可对外提供服务;
采用成熟技术:新框架或新技术往往存在一些未可知的bug或问题,如果没有很好的文档或是商业支持的话,尽量避免过度采用新技术;
水平扩展设计:系统只要做到可水平扩展,就可以有效避免瓶颈问题;
非核心则购买:非核心功能如果需要占用大量的研发资源才能解决,可以考虑购买成熟的产品或解决方案;
小功能快迭代:将系统拆分成多个具有单一职能内聚的模块,减少牵一发动全身的风险,快速开发,尽快验证,早发现问题降低交付风险;
代码Review:每次迭代上线,团队尽量做到Review代码,多一双眼睛便于发现一些潜在问题;
高峰期值班制度:每个业务的流量峰值可能不同,有的业务高峰期如果出现在上下班时间段,很多同学在路上,出现问题之后没人值班,无法操作降级预案,就不能快速止损;
例行压测:如果你的业务是每天面对流量洪峰的,那么例行压测很有必要;
例行演练:有了预案要例行执行,判断预案是否还可用,不然哪天有问题了,预案反而不敢操作了;
总结
今天只是从两个视角简单了说了下在整个研发与交付生命周期中可能涉及到的对于系统稳定性的影响。但是我们可以得到一个结论,就是系统稳定性不是一个直接的结果,而是间接的结果。
如果我们做不好高性能,在高并发场景下出现的BIO问题或慢查询等问题,那么系统稳定性就不可靠了。如果存储层做不好高可用,上层服务就难言稳定性。如果我们的系统中存在大量未经设计的临时实现,大量的技术债堆积,总有一天会反噬系统,造成稳定性风险。