最近在系统设计评审里,跟系统可靠性相关的话听得特别多。有人问:服务都上了容器编排,多副本也做了,为什么线上还是经常出问题?也有人问:SLA写了三个九四个九,但故障恢复只停留在重启和回滚层面,是不是等于没有?这些问题绕来绕去,最后都会回到同一个点:可靠性到底是在分析阶段就要想清楚的事,还是上线以后靠运维去补救的事?我做系统设计和稳定性保障这些年,最大的体会是,这恰恰是所有环节里最不该往后放的一章。
今天这篇就不按教材顺序写了,只聊我认为最实操的路径:先把可靠性分析和指标定义做扎实,再看架构层面怎么设计容错,然后讲验证手段,最后聊团队和流程怎么把这些事长期运转起来。面向的读者是做架构、后端研发、SRE、技术负责人的同学。我会尽量把每个环节背后的“为什么”讲清楚,也会把我踩过的坑直接摆出来。可靠性设计这件事,不是把PPT写得漂亮就完了,而是靠一个又一个具体选择堆积出来的。
1. 可靠性分析:先把指标和风险摸清楚
1.1 可用性目标不是拍脑袋定“几个九”
很多团队一提可用性,第一反应就是“我们要做到99.99%”。但问起为什么是99.99%、当前离这个目标还差多少、达不成会有什么后果,基本没有统一答案。目标一旦拍脑袋,后面所有的保障动作也会跟着拍脑袋。
先看一组基础数字:99%的可用性意味着每年有大约3.65天不可用,99.9%是8.76小时,99.99%是52.6分钟,99.999%是5.26分钟。这里的“不可用时间”是全年累计值,包括凌晨、节假日、大促期间所有用户实际无法访问的时间。如果业务完全不允许在白天有故障窗口,那“4个9”对应的一年52分钟额度,分摊到12个月里其实非常紧张,一次核心链路故障就可能用掉一半预算。
这组数字不是用来写PPT的,它直接决定了架构投入和技术选型。我见过一个团队给内部运营后台定了99.99%的SLO,结果运营后台主要使用时间是工作日9点到19点,晚上和周末几乎没有访问,团队却为了这“4个9”投入大量精力做跨机房容灾,属于典型的预算错配。
可用性还可以拆成两个维度来理解:平均故障间隔时间MTBF和平均恢复时间MTTR。可用性约等于MTBF除以MTBF与MTTR之和。这个简化公式告诉我们一个很朴素的道理:故障不可能完全消除,但让故障恢复得更快,往往比单纯追求不发生故障更容易做到。代码写得再小心,物理机也会坏、依赖也会抖、网络也会闪断。所以可靠性工作的重心,在故障发生后如何快速恢复这件事上,投入产出比通常更高。
在实践中,目标口径不要只看一个全年数字。要跟业务方确认清楚:是否包含计划内维护窗口?是否只统计核心链路?是否允许在低峰时段有短时间不可用?把这些边界定清楚之后,每月再看剩余的错误预算。如果本月故障已经消耗了大部分不可用时间,剩余时间就不应该再做高风险的变更。这就是SLO和变更管理之间的联动。
1.2 FMEA:在故障发生前把风险点摆上桌
做可靠性分析,最怕凭印象说“这里应该没问题”。我自己用过最顺手的方法是FMEA,故障模式与影响分析。它不复杂,核心是把系统拆开,逐个环节追问“如果它出问题,会发生什么”,然后按风险大小排序,给出应对动作。
具体操作上,我建议按四步走。第一步,画出核心业务链路,从用户入口到所有依赖项,包括服务、数据库、缓存、消息队列、第三方接口、对象存储等,全部列出来。第二步,针对每个环节问三句话:它挂了会怎样?它变慢了会怎样?它返回错误数据会怎样?第三步,给每个故障模式从三个维度打分:严重度S,业务影响有多大;发生频度O,发生的可能性有多高;可检测度D,故障发生后我们能不能及时发现。需要说明的是,这里D的评分方向通常是反向的:D分越高代表越难发现,D分越低代表监控能立刻感知。第四步,用严重度乘以发生频度再乘以可检测度,得到RPN风险系数,按数值从高到低排序,逐项落实缓解动作。
举个例子,一个订单系统在FMEA分析时,可以列出这样的故障模式:
| 链路环节 | 故障模式 | 严重度S | 发生频度O | 可检测度D | RPN |
|---|---|---|---|---|---|
| 应用实例 | 容器OOM导致服务重启 | 5 | 6 | 4 | 120 |
| 主数据库 | 磁盘写满 | 10 | 2 | 2 | 40 |
| 支付平台 | 异步回调延迟 | 8 | 4 | 7 | 224 |
| 下游库存 | 接口变慢拖垮线程池 | 9 | 4 | 6 | 216 |
| 缓存集群 | 大量key同时失效 | 6 | 5 | 3 | 90 |
支付回调延迟和库存接口变慢的RPN是最高的,因为它们影响大、发生概率不算低,加上可检测性也不乐观。针对这两种情况,至少要设计主动查单补偿机制、消息队列削峰、调用超时配置、熔断策略等;而主库磁盘写满虽然严重度很高,但通常监控能比较直接地发现问题,可检测度得分低,主要动作就偏向于磁盘水位告警和快速扩容或清理预案。
FMEA的最大价值往往不在那张表本身,而是分析过程能把团队里所有依赖的真实行为拉到台面上确认一遍。我做过多次分析之后发现,产出的最常见问题清单不是“哪个组件可能挂”,而是“原来这个老服务根本没人知道它没配超时”“某个第三方接口根本没有SLA承诺”。这类缺口平时安静地躺在代码里,等到故障爆发时,往往就是压死骆驼的最后一根稻草。
1.3 分析输出:把“防不住的事”也做出预案
风险分析做完之后,一定会有一批风险是想通过架构设计彻底规避但成本极高的,甚至有些外部依赖根本无法控制。这时候要区分两类产出:一类是设计项,需要在系统上线前把冗余、超时、熔断、降级等能力落地;另一类是预案项,针对那些“无法彻底防住但可以提前准备”的场景,写出故障发生后的具体操作步骤。
这里最容易被忽略的是预案的可执行性。很多团队写的应急预案是“联系DBA切换主从”“找运维扩容”,看起来有动作,但实际上值班的人第一步该做什么、确认什么条件、操作哪个平台、回滚命令是什么,全部含糊。我建议把预案写成操作卡,像飞机起飞前的检查单一样,附带具体的执行命令或平台入口。预案不是写给别人看的文档,而是故障状态下,一个没有休息好的同事也能照着执行的手册。
另外,预案必须随着系统演进持续维护。服务拓扑变了、中间件版本升了、机房调整了,旧预案可能已经和真实环境对不上。最好的办法是每隔一段时间做一次预案推演,把“纸上谈兵”变成“沙盘演习”,至少确保预案的步骤是通的。这也是为后面第三部分要讲的故障演练打基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 可靠性设计:从架构层面给系统“减震”
2.1 先分清有状态和无状态,设计思路完全不同
可靠性设计最容易犯的错误,是一上来就说“所有服务都加副本、都做双活”。但不同的系统,抗故障的手段和代价差别极大,第一步应该先分清模块是有状态还是无状态。
无状态服务,比如订单查询接口、商品列表接口,请求打到哪台机器都一样,天然适合水平扩展。给它们部署多个副本,前面加负载均衡,节点故障后自动摘除,可用性提升非常明显。这类服务的健康检查要做得真实,不能只是“进程还活着就返回200”。我见过不少案例,服务进程活着,但内部线程池已经耗尽,或者依赖的数据库连接断了,此时负载均衡还在继续派流量,结果所有请求都卡在超时里,故障在用户侧已经爆发,集群却毫无感知。健康检查应该调用一个轻量的业务探测接口,最好能顺带验证依赖的连通性,而不是仅仅返回一个固定字符串。
有状态服务,比如数据库、消息队列、分布式缓存,处理起来要复杂得多。以数据库为例,主库承担写入,从库承担读流量,主从同步天然存在延迟。主库挂了之后,如果强行把还没同步完最新数据的从库提升为主库,就可能丢数据或产生主键冲突。这时候必须先回答一个问题:业务到底能不能接受少量数据丢失?如果线上交易场景不能接受,就得考虑半同步复制等机制,确保主库确认事务提交时,至少有一个从库已经落盘;如果只是报表系统,异步主从也可以接受,切换快就是最大诉求。
还有一个容易让人忽视的点:不要把无状态服务的副本数设计看作是全部。服务能无限扩容,但如果它们都要连同一个有状态存储,存储节点仍然是整个链路的单点。所以可靠性设计里最关键的,往往是识别出那些“所有请求最终都要经过它”的组件,并优先保障它们的可靠性与容量。
2.2 冗余与故障转移:分层设计,别一上来就搞双活
冗余的根本目的是消除单点,但冗余不是让每个组件都复制一份,而是要根据业务的重要程度分成不同层级来做。
在基础设施层,至少要考虑服务器跨机柜或跨可用区部署。在应用层,核心服务多副本并配置优雅上下线和健康检查。数据层是重头戏:数据库做主从复制,消息队列做多副本持久化,缓存做分片集群。每一层的高可用手段都不一样,复杂度也逐层上升。
很多团队对“双活”有执念,认为只有做到两个机房同时提供服务才算高可用。但从我见到的实际案例来看,对于大多数中小规模业务,盲目上多活架构不是提高可靠性,而是给系统增加一种全新的故障模式。跨机房网络抖动、数据双向同步冲突、配置不同步、流量调度错误……每一样都足以让系统比单机房更不稳定。更务实的做法,是先在单机房内做好服务多副本、数据库主从、自动化备份和恢复演练。这一套组合下来,已经能覆盖大多数常见故障。只有业务规模真正大到单机房无法承载,或者机房的整体故障风险已经不可接受时,才值得去考虑同城双活甚至异地多活。
故障转移的自动化程度,也是一个值得思考的问题。很多人以为自动化程度越高越好,但我的经验是数据层的故障切换不能一上来就做成全自动。自动切换的难点在于:系统检测到的“故障”未必是真故障,可能只是节点抖动、网络瞬时分区。如果误判导致频繁切换,不仅会造成正常服务中断,还可能引发脑裂等问题。更稳妥的方案是采用“自动检测加半自动切换”:监控系统检测到主库异常后,立刻告警,并把备库状态、数据延迟等关键信息展示出来,由值班DBA确认后执行切换。等团队对切换流程足够熟练,再把决策逻辑逐步自动化。这一步不是技术问题,而是信心问题,信心只能靠一次次演练积累起来。
另外要强调一点:任何切换方案都必须和数据备份方案放在一起设计。数据库切换不是简单启动一个备用节点就完事,还要确认备库数据和主库一致到什么程度、有没有同步延迟积压。没有同步延迟监控的从库,可能在不知不觉间已经落后主库几个小时,一旦切换,丢的数据量会非常惊人。
2.3 超时、重试、幂等、限流、熔断:容错组合拳
分布式环境下最可怕的故障不是“对方挂了”,而是“对方慢了”。一个上游接口被拖住,调用方的线程池和连接池会被逐渐占满,最后整个调用方也跟着不可用,故障像多米诺骨牌一样向外传播。针对这种场景,第一道防线是超时。
超时配置看似简单,实际坑最多。有些团队只在HTTP客户端配了一个很长的超时,或者干脆不配,理由是不能让业务因为超时而失败。这是典型的错误观念:百分之百不超时,等于把故障蔓延范围从单个接口扩大到整个系统。我的经验是,服务间调用必须同时配置连接超时和读取超时,刚开始可以按连接超时500毫秒、读取超时2到3秒作为默认值,再根据实际业务压测结果单独调整长耗时接口。宁可让个别慢请求失败,也不能让一个慢接口拖垮整个应用。
第二道防线是重试策略。有些团队对重试的理解就是“失败了就再试一次”,结果在下游已经过载时,所有调用方同时发起重试,流量被放大了好几倍,把下游彻底打垮。重试必须有明确约束:第一次,接收方接口要做到幂等,保证重复请求不会产生重复数据。第二次,重试次数要有限制,配合指数退避,比如间隔1秒、2秒、4秒,最多重试三次。第三次,每次重试间隔加入随机抖动,否则多个客户端会同步进行退避,退避结束后又形成一波流量尖峰。
幂等这个话题值得单独多写几句。很多业务故障,本质上都是因为没有认真设计幂等。支付回调重复通知、报表任务重复执行、MQ消息重复消费,这些都是非常常见的情况。最简单可靠的做法,是在数据表上增加业务唯一键,用数据库唯一索引直接挡住重复请求;如果业务有状态流转,还可以配合状态机,例如订单状态从“待支付”到“已支付”之后,再收到“支付成功”回调就直接忽略。不要指望消费端能保证消息只被投递一次,只有自己做好幂等保护,才能对上游的重试无所畏惧。
熔断解决的是下游出问题时的快速失败问题。连续失败次数超过阈值后,熔断器打开,后续请求不再真正调用下游,而是直接返回错误或走缓存兜底;经过一段冷却时间后,放少量请求进入试探,如果成功则逐步关闭熔断器。限流解决的是流量超过系统承载能力的问题,直接在入口拦截一部分请求,让后端在能力边界内继续提供稳定服务。降级则是在高峰期主动放弃非核心功能,比如把商品推荐的个性化结果换成通用列表、关闭评论计数展示,把线程和数据库连接让给下单、支付这类核心链路。
这里最想分享的经验是:这些机制要形成一个组合,而不是孤立存在。流量上来时先限流,防止系统被击穿;依赖抖动时熔断,避免线程耗尽;重试退避要防止雪崩;被降级的非核心功能不能影响主流程的数据一致性。任何一环缺失,系统在真实故障中的表现都会差一个量级。
2.4 数据可靠性和对账,是系统可靠性的兜底
分布式系统里谈论强事务一致性往往不现实,跨服务调用、消息异步化都会让数据在中间状态停留。比较现实的方案是状态机加本地消息表加补偿动作,让数据最终一致。但最终一致的系统必须配一个“警察”,也就是对账。
对账的基本思路非常简单:定期从两个独立的数据来源拉取数据,逐条比对。比如订单系统的支付流水和支付平台的对账单,每天做一次比对,核对订单状态、金额、笔数是否一致。差异超过阈值就告警并通知人工介入。这个过程能发现那些通过常规重试机制始终无法解决的隐蔽问题,比如回调丢失、消息被错误消费、某次异常导致状态没有推进。
我给好几个系统做过对账设计,最深的体会是,对账任务本身也要被监控。如果对账任务因为某种原因没有运行,系统不应该静默地停在那里,而要立刻发出告警。没有跑的对账比结果对不上还要危险,因为大家会误以为系统是安全的。对账不是事后找补,它是发现系统设计缺陷的重要渠道——第一周对账就发现问题,说明重试和补偿逻辑存在缺口,要回过去改代码,而不是简单修一批数据就完事。
数据可靠性还有一块基础工作:备份。很多人以为备份就是数据库定时导出,但备份有没有用,要看能不能恢复。每年至少做一次从备份开始的完整恢复演练,把备份文件拿到一套全新的环境里启动,确认服务能起来、数据能查到、关键流程能跑通。日常也要监控备份任务的执行状态、备份文件大小是否异常,防止备份任务早就失败了几个月,团队却毫无察觉。备份失效的案例远比想象中多,因为真正需要恢复的那一天总是不期而至。
3. 可靠性验证:演练、压测与可观测性
3.1 混沌演练:别让第一次故障成为预案首次执行
可靠性设计做得好不好,不能在PPT上说,要看故障真实来临时系统表现怎么样。但全部依赖“真故障”来验证,代价太大,于是一种主动出击的思路出现了:混沌演练,也叫故障注入。
我第一次带团队做故障演练时,大家非常紧张,总觉得“系统跑得好好的,为什么非要制造故障”。后来的经验是先选择风险最低的场景起步,比如停掉一个非核心下游服务、给某个接口注入网络延迟、模拟磁盘空间不足。选这种场景的原因很明确:即使演练过程中出现问题,也不至于影响核心业务。
演练的过程要关注三件事:第一,监控能不能第一时间发现问题,值班同事是收到报警才知道出事了,还是用户先来投诉;第二,应急预案能不能执行下去,负责人能不能在几分钟内找到操作指南并且步骤是正确的;第三,业务实际受到影响的时间有多长。演练结束后的产出不是“演练通过”这个结论,而是一张问题清单:哪些监控缺失、哪些预案步骤不清晰、哪些代码路径没有做好容错。每一条问题都要有负责人和deadline。
工具方面,Chaos Mesh、ChaosBlade、Chaos Monkey都是业界常用的故障注入工具。但每个团队的场景不同,不必追求工具多完整。在没有成熟工具的情况下,从脚本层面手动注入网络延迟、杀进程、停依赖也能起到类似效果。关键是形成常态化机制,而不是一年只在汇报前做一次“表演式演练”。
还有一个实操建议:每次新服务上线前,把故障演练纳入发布checklist。上线不仅仅验证性能压测,还顺手做一次下游依赖断开演练。有团队担心这会影响发布节奏,但经历过一次大型故障的人都会认同:花10分钟演练发现的问题,和在线上用一夜故障换来的教训,成本完全不同。
3.2 容量压测:系统不是稳定就好,还要接得住
可靠性和性能经常被分开谈,这是不对的。一个系统平时很稳定,但流量一冲上来就超时、报错、重启,本质上还是不可靠。很多线上事故的源头并不是代码bug,而是流量超过了系统设计上限,所以容量规划和压力测试也是可靠性设计的组成部分。
容量规划怎么做?先看业务预期:下一次活动预估流量是平峰的多少倍。再看单机在目标延迟下能支撑多少QPS,这个数字最好通过压测得到,不要靠估计。最后根据“目标峰值除以单机能力再乘以冗余系数”来确定节点数。冗余系数一般取1.5到2,确保单机故障或发布过程中,剩余节点仍然有余量。
压测最大的难点是流量特征要足够贴近真实。只做固定QPS的满负荷压测远远不够,真实的线上流量有读有写、有快有慢、有正常请求也有异常请求。有条件的话可以用线上流量录制回放,没有条件也要构造混合场景。压测过程中重点观察P99延迟,因为平均值会掩盖大量慢请求。比如平均延迟只有100毫秒,但P99已经是3秒,综合体验其实很差。一旦观察到P99随并发上升而急剧恶化,说明系统即将进入过载区间,这个点就是容量水位线。
压测的安全边界也必须管好。我见过不止一次因为压测流量没做好隔离,导致线上真实用户被影响,甚至把测试订单写进生产库的案例。正确的做法是压测入口加独立标记、数据走独立批次或打上特殊标识、关键操作在代码层识别并跳过真实外部调用。还要准备一键停止压测的开关,压测过程中一旦指标异常,能第一时间终止,而不是手忙脚乱地改代码。
3.3 可观测性:系统有没有事,不能靠用户告诉你
有些团队宣称系统高可用,但线上出问题时是用户打电话投诉后才知道。一个故障发现机制依赖于外部反馈的系统,无论设计多精妙,都不能称为可靠。可观测性建设是可靠性的眼睛,通常包含三个维度。
第一个维度是指标。对于业务系统,建议重点监控延迟、流量、错误、饱和度这几类黄金信号。需要特别注意的是错误率定义要清晰:HTTP 5xx算错误,调用下游超时算错误,业务返回码表示失败的响应也要计为错误。只看HTTP状态码会漏掉大量真实故障。第二个维度是日志。日志要结构化,至少要带上traceId、服务名、耗时这些基础字段,方便过滤和关联。同时要对日志量做容量规划,打印过多会把磁盘打满,这在故障场景里屡见不鲜。第三个维度是链路追踪。在分布式环境下,一次请求跨多个服务,只有指标和日志很难定位具体卡点,全链路追踪是最直接的排查手段。
报警规则是另一块实践坑最多的内容。新手容易把报警阈值设置得非常激进,结果一抖动就疯狂告警,值班同事很快就对报警麻木,甚至直接屏蔽。我建议报警分两级:一级是系统已经实质性受影响,需要立即响应;另一级是接近风险水位,例如某核心接口错误率持续上升、线程池使用率超过百分之七十等,提示关注但不必半夜拉人。报警信息里最好直接附上影响范围、关联模块和近期变更记录,减少排查时间。发现一个现象需要翻三四个页面才能确认根因,这个机制的效率显然是不够的。
4. 团队与流程:让可靠性建设长期运转起来
4.1 先定义“红橙黄蓝”,否则没法谈应急响应
可靠性不仅是技术问题,也是管理问题。在故障发生之前,团队如果没有统一的分级认知,真正的故障场景下,每个成员对“严重程度”的判断会完全不同,响应秩序很容易乱成一锅粥。所以第一件事,是定义一套事故分级标准。
下面这份是比较通用的示例,可以按业务形态自行调整:
| 级别 | 定义示例 | 响应要求 |
|---|---|---|
| P0 | 核心业务完全不可用、用户无法下单支付、数据安全事件,或资金对不上 | 立即启动紧急响应,通知全链路负责人与决策层,分秒必争 |
| P1 | 核心功能降级但有临时方案,或非核心业务不可用 | 一般工作时间响应,15分钟内拉群处理 |
| P2 | 局部功能异常、只影响少量用户,有绕行方案 | 正常工作时间内修复即可 |
| P3 | 体验问题或功能瑕疵,不影响核心流程 | 排期处理 |
分级明确之后,还要配套响应机制:谁第一时间拉群,谁是值班长负责统筹,谁负责恢复业务,谁负责同步消息,是否需要升级到更高级别。所有这些都要提前写进值班手册。很多团队平时没有做“纸上推演”,等故障真的发生时,光确认“今天谁值班、找谁拉群、群名叫什么”就已经浪费大量时间。每半年花半小时沿着故障剧本推演一遍流程,能显著提高紧急情况下的团队熟练度。
4.2 事后复盘不对人,改进项要可验证
任何故障处理完之后,复盘都是最重要的工作。但复盘在多数团队里做歪了,变成一场批斗会或表功会,有人拼命证明自己没责任,有人含糊其辞,最后改进项说了一堆却没人跟进。这里有几个原则可以分享。
复盘要基于时间线,而不是基于结论。先把“什么时候报警、谁先接手、中间做了哪些判断、什么时候做了什么操作”这些事实按时间对齐,再去找根因。很多复盘只写“代码空指针导致接口异常”,但这不是根因,只是触发点。真正要追问的是:为什么这个参数会出现空值?为什么代码评审没发现?为什么灰度测试没有覆盖这个场景?为什么错误率大幅上升没有触发报警?
改进项必须具体到可以验证。像“加强代码评审”“增强超时意识”这类话没有任何可执行性。好的改进项是:“为xxx接口增加超时配置,统一调整为3秒,补充单测覆盖超时场景”。有明确的边界、动作和验收方式。每个改进项都要有负责人和截止日期,并且在下一次版本回顾时逐条检查完成情况。没有回收机制的复盘,开完等于没开。
4.3 可靠性与成本的关系:做正确取舍,比追求极限更现实
可靠性不是免费的,它包含资源成本、研发效率成本和运维复杂度成本。全链路都追求5个9,对绝大多数业务既不现实也没有必要。四个九和五个九的差异,可能意味着从“一套高可用集群”提升到“同城双活再到异地多活”的复杂度,成本不是线性增长,而是成倍增长。
所以可靠性建设最关键的一点,是让投入与业务价值对齐。面向用户的核心交易链路,值得最高的SLO和最完整的冗余设计;内部后台的月度报表,延迟几分钟甚至几十分钟都可以接受;数据备份和对账,是资金安全相关的底线,不能因为“发生的概率低”而砍掉。把可靠性预算留到最重要的地方,才是一个技术负责人真正要做的事。
错误预算的理念在这里很有用:如果某条链路定义了SLO,那么这一个月内的不可用时间就是错误预算。故障消耗了预算意味着本月不适宜继续做高风险变更。反过来说,只要预算充裕,团队就可以放心迭代。这种机制把“稳定”和“迭代速度”统一到同一套度量体系里,让团队从“互相甩锅”变成“共同
