故障常见原因归类分析及预防和应对措施
每一次故障都是一次宝贵的学习机会。
引语
故障是开发者头上悬着的一把剑。俗语曰:no zuo no die. 可是开发者很难做到 no zuo. 如何在 zuo 的时候防止 die 呢 ?
知己知彼才能百战不殆。要避免故障,就需要对故障有一个相对深入的理解。
故障,一般是指一段时间内较为密集的问题发生导致了一定的负面影响。业务量小的极少影响面的问题不算故障,否则就会混淆真正的故障,导致受限资源投入分配不合理,影响关键问题的解决进度;零星的非密集的问题可能不是故障,因为那可能是小概率事件触发了潜在BUG,需要解决,但定为故障有点勉强。
要避免故障,首先需要深入了解故障发生的原因。以下内容来自于对多起故障的分析、归类和总结。
分析方法
拿到一个故障,如何分析它 ? 如何从中学到最大的收获 ? 如何给它进行归类呢 ?
首先,要确立分析目标。 我重点关注的是故障发生的主要原因及预防措施,而不是现象及处理过程和时长。因此,可以概览故障现象描述、处理过程及时长、次要因素等,除非其中有重要价值的内容;
其次,软件的根本任务是处理数据。 故障的本质就是数据处理出错了。 或者是 数据处理成非预期的结果,或者是数据处理延迟,或者数据展示有问题,或者兼而有之 。 因此,数据是故障分析的一个重要关注视角。
再次,处理可以抽象为算法。 处理有问题,或者是选用了错误的算法,或者是算法里新增或修改的部分对某些场景不适配,破坏了原有约定 ,或者兼而有之。因此,算法是故障分析的另一个重要关注视角。
最后,如果一个故障的原因有一个明确的判定,就可以为之定名;如果它不属于已有的任何一个定名,就要新建一个定名,将其放入其中。
故障原因
多发源
故障多发源,是指发生故障的最常见原因。谨防这几种情形,可以预防大部分的故障可能性。
核心流程出错
核心流程的某个环节出问题,导致整体流程失败,或者部分业务场景的整体流程失败,都会导致密集问题发生。通常是在主流程中添加了一段代码,而这段代码没有考虑到某个场景或者健壮性不佳,影响了整体;在测试的时候,只验证了改动点部分,没有回归核心流程,或者回归了核心流程,却遗漏了某个场景的回归。
预防措施:
- 评估改动点! 非常重要!哪怕只有一行,只要在主流程中,都要仔细评估其影响范围。在主流程中添加的代码越长,越要警惕。
- 增加必要的 try-catch 。如果增加的代码只有局部影响,可以添加必要的 try-catch,防止未预料的情形的处理异常影响整体流程。
- 最好不要轻易改动影响全局的通用方法和配置(影响面和回归面非常大); 尽可能只新增而不是修改。
- 覆盖全面的核心流程的测试用例,每次发布都需要回归通过。
- 有风险性的改动,增加开关。一旦出错,立即关闭改动。
真实案例:
场景遗漏
业务会逐渐发展成庞然大物,随着人员的流动,很多业务知识和场景会逐渐被淡忘。新进的同学如果没有充分评估到各种场景,就很容易以为遗漏某个场景,导致问题。要解决这种原因,是比较棘手的。
预防措施:
- 沉淀业务文档和业务场景。
- 有全景图意识,不限于眼前的一亩三分地。
- 有熟悉业务的同学进行方案评估和 CodeReview 。
缺乏健壮性
实现服务之后,健壮性是保证服务能够平稳运行、正确应对错误和异常的第一道关卡,也是合格程序员的必备代码素养之一。
健壮性不佳,很容易导致由于未预料的局部细节、脏数据、局部调用失败影响整体的流程和展示。
预防措施:
- 思考错误和异常,多多益善。
- 善用 try-catch 保驾护航。
- 使用空字符串、空列表替代 null 。
- 异常分支的测试覆盖。
真实案例:
- 由于一个 null 值导致整个订单列表加载失败。
- 由于一个次要的依赖出错导致整个详情页加载失败。
- 异常分支的代码有问题,但没有测试;当流程走到异常分支代码,任务直接跪掉,反复重启和跪掉。
瞬时大流量
瞬时大流量是造成故障的一大杀手。 瞬时大流量,会导致机器资源短缺,CPU 飙升或内存爆满或网卡、连接数打满,直接影响整体服务的稳定性。
对于消息处理应用来说,瞬时大流量会导致消息处理延迟,业务状态流转滞后,影响后续环节;对于非消息处理应用来说,则会导致任务处理阻塞,接口响应变慢或不响应。
低性能、低吞吐量在面临持续多个较大业务量的冲击时,很容易出现阻塞、延迟;如果应用无限流,或限流失效,或限流不够精确,都可能难以抵挡大流量的侵袭。
预防措施:
- 集群环境:保证集群各机器或 Region 的负载均衡;
- 单机环境:有针对性地限流、限速和限数,并严格测试和验证。
- 压测演练。容器化后的压测。
- 减少或消除过重的锁逻辑。
- 大流量预警和感知。比如线程池队列阻塞预警,计算出的大数据集处理的预警 等。
- 批量调用替换循环单个调用;O(nlogn) 算法;减少不必要的访问和服务依赖。
真实案例:
极端情况
极端情况是指,一些很罕见的事件的发生挑战了系统的某个局部极限,导致系统出了问题。
比如说,一个订单内的商品种数通常不会超过 10 ,但商家或买家刷单,导致大量含有 50 多个商品的订单,然后密集导出,就会导致应用 FullGC 严重,引起接口响应超时或任务无法进行下去。
预防措施:
- 思考极端情形及影响;
- 提前做好极端情形测试和设计方案。
真实案例:
依赖失败
依赖失败有如下情形:
- 所依赖的服务、配置或变量不存在或处于不合适的版本,导致应用启动失败,或者启动后的服务不能正常运行;
- 所依赖的基础服务不稳定出现大量报错时,会导致依赖它的高频应用也出现大量报错,导致雪崩效应。
- 配置或服务循环依赖,导致死循环。
预防措施:
- 当一个项目发布涉及多个系统或许多细节时,就需要编写发布文档,仔细指定发布配置和顺序,保证应用依赖的正确性。在具体发布时,则要严格执行发布文档里指定的检查点清单和发布顺序。检查依赖项:API 版本、Jar 版本、依赖服务、配置项、DB 字段。
- 自动降级。严格控制超时,隔离或去掉不必要的弱依赖。
- 制定明确的依赖原则,上游依赖基础,避免循环依赖。
- 前后端对接口约定的返回值及格式沟通达成一致。
资损
资产是客户非常敏感的私有产权。发生资损时,通常是最高故障级别。
资损一般发生在:1. 直接资损: 系统处理未考虑幂等,导致重复消息多次处理;2. 业务方根据基础服务方的某些字段进行资金业务处理,而字段返回值有误,导致少算或多算。3. 诱导性资损,由于某些展示信息,诱导用户做出某种难以追回的行为,比如已发货订单展示为待发货;
预防措施:
- 直接处理资金业务,注意幂等处理;
- 有依赖状态的资金业务处理?
- 消除诱导性信息;
- 对资金计算敏感,尤其注意边界值处理,避免 +1 或 -1 导致问题。
新旧迁移出错
多发生于技术重构优化的时候。比如旧的领域模型迁移新模型、旧的技术栈迁移新技术栈、旧的页面迁移新页面。做技术改造,侧重点往往在于新服务的测试,而容易忽略老服务的测试兼容。
新旧迁移存在一个权衡:彻底还是减少出错。更为彻底的迁移,出错和故障概率会更大,但新系统会更加清爽;向老系统作一些妥协,可以减少一些出错和故障概率,但新系统会带着老系统的包袱前行,后续依然会出问题。
预防措施:
- 分流。 分流可以确保新服务上线之后的影响面逐渐扩大,即使有未考虑的点,也会将影响面控制在最小范围。
- 充分测试,事先评估好测试用例并严格执行。
- 旧接口迁移到新接口时,返回值的结构和值约定最好一致,确保 新对新,老对老,避免“新对老”的不兼容导致问题。
- 老的页面和功能要回归全面,避免重要场景遗漏。
- 新的和老的代码改动分开 CR ,分批 CR 。
老代码
不可否认,老代码在企业初创期曾立下汗马功劳。可是,随着时间推移,业务量越来越大,复杂度也在快速增加,很多老代码的简单处理就逐渐变成了“定时炸弹”,冷不防让地震一震,让人抖一抖。
预防措施: 定期梳理和清除。
真实案例:
数据泄露
数据安全性越来越成为企业的重要关注点。对于 SaaS 来说,要保证各个租户的数据和操作互不影响,不能看到和操作未授权的数据。
预防措施: 1. 敏感数据脱敏; 2. 避免覆盖; 3. 权限控制; 4. XSS 安全问题。
真实案例:
设备及网络
设备及网络属于互联网的基础设施,位于最底层,一旦出现问题,影响面也是巨大的。当设备老旧出现硬件故障或宕机,或者网络抖动或突然断开,也是很容易导致大面积失败。
预防措施:
- 定期检查和更换老旧设备。花点钱更换老旧设备,比宕机出现问题花费时间、精力和金钱补偿,要划算得多。
- 备用链路和机房。
- 避免单点故障。
操作不当
操作不当主要有如下情形:
- 两种操作同时进行,发生冲突导致出错;
- 代码合并冲突解决不当;
- 操作不规范,引发系统处理失常或失控。
预防措施:
- 代码合并冲突解决,双方确认。
- 同时更改系统配置,需要沟通协调顺序,避免并发。
- 批量数据修复方案要仔细 CodeReview , 当做正式发布处理;
- 批量数据修复在业务低峰期进行,除非是紧急修复。
- 变更操作的工具自动化,审批流程。
真实案例:
- 在业务量比较大的情形下,对表进行 truncate 操作可能会导致数据库 hang 住。
其他原因
资源未隔离
底层集群未能隔离不同业务的资源, A 业务的大流量导致集群机器资源被打满,间接影响了 B 业务。
预防措施:
- 规划和实现资源隔离策略:重要业务和次要业务的资源进行隔离;不同业务的资源进行隔离。
脏数据
由于脏数据缺乏整体的关联性约束,应用读取到脏数据,容易出错;如果应用有一连串的逻辑处理,可能生成更多的脏数据,引出更大的麻烦。
预防措施:
- 检测和消除脏数据。
- 避免在线上造测试数据。
故障处理
发生故障时,第一反应不是立即排查原因,而是立即止损,将影响面最小化。
- 若能确定是发布导致,立即回滚发布。 回滚发布后,再仔细排查原因。
- 及时同步进度,让关注方知悉;
- 建立快速同步机制,预防小问题演变成大故障。
为了更好地减少故障的可能性,还需要事先做好故障应急预案。
- 梳理底层的强弱依赖,确定强依赖不可用时导致的影响面;
- 当强依赖不可用时,能够快速恢复的方案,将影响面降低到最小。
- 故障演练。模拟大流量、极端情况和故障情形发生,检测应急预案是否生效和快速恢复。
根因探讨
故障层出不穷,现象眼花缭乱,究竟从何处来,去往何处呢 ? 是否有根本规律可循 ?
事实上,绝大多数的软件故障都是具备内在的逻辑关联的。从基础逻辑关联来推理,可以推断出很多本可以预防和避免的问题。与正常流程相比,故障本身也是一种路径,产生出特定的数据集,只是这些数据集及引发的现象是不符合人们的预期的。以下是部分基础逻辑关联分析:
- 依赖问题。 一个功能会依赖某个字段、配置、校验、接口约定等;当字段或配置变更不合理,或语义发生变化时,会导致问题; 两个业务 A 和 B 均依赖同一段代码 c ,根据对 A 的某个需求在代码 c 修改了一些逻辑 ,影响了 B ,结果导致 B 出了问题;在原有流程中多了不必要的校验或少了必要的校验,会导致问题;原来依赖格式 A,迁移新服务后变成了不兼容的格式 B。 绝大部分功能性问题都可以归结为评估影响面不准确。
- 流量问题。通常是大流量或极端情形导致,超过了系统能够承载的阈值。对系统的阈值压测摸底并提前预估好容量,辅以经过严格测试和验证过的限流、限速等。
- 环境问题。依赖的环境假设出现问题,导致依赖链路阻断,从而引发各种问题。需要在运维层面保证环境的高可用高可靠性,避免单点。
小结
故障,是每个开发者乃至企业法人都不愿意经历的事情。可是,每一次故障,都蕴含着不同形式的疏忽、未知、真理,正向思考,其实是一次非常珍贵的学习机会。故障,也会引导人抵达更深入的境地,去理解事情的本质与关联。正视故障,从故障里学习真知,预防和避免故障,乃是更佳的姿势。
要预防故障:
- 第一是细心。多个心眼,准确评估影响面,兼顾考虑老业务老功能的回归, 仔细检查依赖项,保证返回约定的一致性,规范执行;
- 设计和实现要考虑健壮性、大流量和极端情形,避免低性能。
- 有针对性避免安全性和资损问题。
- 设置严密的监控报警,在问题的萌芽期掐灭。