Google工程师如何在实践中避免和处理故障

163 阅读19分钟

前言

鲁迅曾说:决定一个程序员的表现如何,除了他写的代码、完成的需求多少多好,还有他搞出的故障和事故有多少。。。

在当前国内的IT及互联网环境下,我们整体来讲还是处于一个追求进度而非质量的状态,我们的老板和客户总是要求我们这些程序员立下军令状,要求把某月30号作为某个需求或项目的交付日期DDL,没有完成,就等着吧。。。

在这种要求速度的背景下,虽然有时,我们的老板和客户也会把质量和稳定挂在嘴边,但实际上在他们心中,这两者远远落后于对交付速度和时间确定性的诉求。

因此,在这种背景下很常见的一种情况是,我们风风火火地赶在DDL前完成了需求,交付了项目,老板点头,客户展颜。然而,过了一段时间,这个项目就搞出个一个大故障,等待我们的又是劈头盖脸的问候。

这是一种时代浪潮下的无奈,不过,随着IT及互联网产业发展进入相对的平稳期后,这种追求速度而不是质量和稳定的趋势已有所改善,因为大家发现没有多少新项目可做了,似乎也没那么着急了,因此转而更在乎已经运行项目的质量和稳定。

以上是笔者的在若干年工作中产生的个人观察和体会,也因此也开始逐渐考虑怎么把质量和稳定做好的相关问题。从行业内最优秀的公司和团体的经验学习,是笔者选择的一个方向,于是就找到了《SRE:Google运维解密》这本书做了阅读。

当然,站在2024年的今天看,SRE已经不是一个什么新词,但在当前国内SRE还是基本普遍被当做运维的背景下,真正有多少人能够把握SRE对稳定性的思想和原则呢?我想可能并不是很多。同时SRE的思想,也并不是只有运维的人才需要掌握,任何参与、接触以及和IT互联网生产环境产生并关系的人(甚至不仅仅包括技术人员),了解这些思想都会有比较深远的意义,因为它帮你管理和规避风险,降低潜在的损失,甚至这种思想可以超越互联网和IT产业之外,应用到其他需要面对风险的行业。

因此,基于笔者本人的背景、经验和经历,并借这本书中提到的一些共性思想和原则,笔者对如何在实践中避免和处理故障、管理风险,提出一些粗浅的观点,希望能够帮助到对此关注的读者,个人经验,难免错漏,请多包涵。

稳定性原则

首先提纲挈领地讲,对避免和处理故障(稳定性)这件事来说,我们需要先明确一些原则:

  1. 永远没有百分之百的稳定,要把稳定风险视为一种常态化的伴生物,不断从已发生的故障中学习
  2. 如果要改善稳定性,必须先定义一个清晰的目标,然后朝着该目标进行优化迭进(比如SLO)
  3. 要达成稳定,不能仅着眼于威胁稳定的故障本身,而是要系统化地关注稳定目标背后的整个体系(设计、实施、管理、文化、共识、机制)
  4. 速度和稳定(质量)不可兼得,需要取舍,可以在满足稳定性目标的前提下最大化速度,也可以在满足速度的前提下最大化稳定性,这个要结合实际情况看
  5. 不要期望能够靠堆人数解决系统和服务规模扩张带来的稳定性问题
  6. 复杂度是稳定的敌人:越简单、越稳定,如果引入复杂度的收益不能覆盖其带来的稳定性风险,那就不要引入
  7. 对团队来说,达成稳定目标的一半工作量都在团队机制和共识的建设中

对于上述这些原则,从个人的经历和背景出发,笔者是高度认可的。同样的,如果读者想最大程度、最高效地避免和处理故障,从现在开始,就要把上述的原则装到脑中。当然,上述的原则性内容单纯去看,显然是有些干瘪了,我们会在接下来的篇幅逐步展开和具体填充展开这些原则。

如何避免故障:以项目流程的视角看

首先,我们先从项目实施流程的视角下,来考虑如何避免故障,这也是对应了我们上一部分所提到的第3点。

一般来讲,某一个IT&互联网项目或产品,一般会经过如下阶段:

  1. 需求接收&评估
  2. 需求确认(立项)
  3. 方案设计
  4. 方案实现(开发)
  5. 上线
  6. 运营与维护(包含发起和支持重大活动)
  7. 下线

重点来看,对稳定性影响最高的几个阶段是:方案设计、方案实现、运营与维护

我们最常遇到故障和稳定性问题是在 运营与维护 这个阶段,但这并不意味着我们应该只着眼于此阶段,因为这是整个项目流程中所有累积的错误和疏忽集中暴露出来的阶段,也就是所谓的“还债时刻

对于很多故障问题,我们会发现是前期的设计和实现阶段就有了疏忽和错误,而这样的疏忽和错误,往往有各种各样的形式:

  • 错误的假设:没有预估到实际会应对的流量和性能上限(缺失容量规划和性能设计)、没有考虑到复杂或 奇怪但合理 的使用模式(设计缺陷)、把问题搞得过于复杂(系统复杂度和故障概率急剧上升)、没有预估到外部依赖系统也可能是会出问题的(默认外部依赖完全稳定)
  • 疏忽大意的实现和操作:实现逻辑细节中出现了低级的代码Bug、线上环境配置错误(格式、拼写、错写)、误操作(手动误执行了错误命令)
  • 团队和组织协同问题:A认为B做了某件事但实际上没有(沟通Gap)、成员压力过大导致判断和反应能力下降出错、未经测试和质量把控后上线

事实上,上述这些疏忽和错误,都是可以通过一些机制和方法,将其发生的概率不断降低的:

设计不仅面向功能,还要考虑性能、容量和稳定

这是个人和小型团队最容易忽视的一点,他们的着眼重点总是放在功能上,而这个功能是否会有性能和容量问题(支撑多少用户),是否会有稳定性问题(处理超限后的限流和熔断),是否过于复杂(增加故障发生的概率)等等问题,却欠缺了思考

最简单的性能容量和稳定性设计一般是需要准备一个check list,最好在 群体 中(个人思考总有局限性)逐条对照列表中的每一个可能的问题,并对已有的功能设计进行完善和补充。这个check list,Google的书中就提供了一个,具体可参考书本中附录E:发布协调检查列表

P.S. 这里设计检查也是一个可扩展的非常大的话题,可以用另一篇文章的篇幅来讲,如果有读者感兴趣,可以和我留言反馈

建立和实施规范化的实现和操作流程

规范化的实现和操作流程能够增加确定性,确定性意味着意外因素的消除,意味着故障概率的降低

比如建立code review机制,关键的代码让实现者之外的人或机器AI都核查一遍,虽然不期望能发现和避免所有潜在bug,但至少能够避免80%的 低级错误

比如将经常进行的操作固定化、傻瓜化、流程化、自动化,形成固定的SOP(Standard operating procedure);能够让机器自动固定化执行的,就让机器去做而不是人;无法完全自动化的,也要人机结合操作,比如机器在手动操作前后去做检查和确认;机器完全无法介入的,要让经验充足的人把流程文档化,所有的操作均遵照文档流程操作,而不是依靠印象凭空进行,实际操作时由双人实施并互相检查,这也是来自传统高危行业(电力、航空等)的经验

这里自动化带来的潜在收益非常高,自动化手段往往能够避免人工本身的很多问题,带来一致性、可复用、快速、节省时间等收益,因为机器天生适合做重复、枯燥和确定性高的事情,而这样的事情又在软件工程项目或运维中大量存在,因此现在也出现了K8S这样自治化运维的系统

另外,运维发布操作和前期设计一样,也需要check list,尤其是潜在影响比较大的、系统复杂度较高的,需要从多个方面评估发布和操作的潜在影响,这也往往需要在群体内进行评估

建立面向稳定目标的团队规范

一个项目或产品往往是以一个团队来推动和实施,因此团队的协同也成了影响项目或产品稳定目标的重要因素,因此团队的协作也应当面对稳定目标建立一些规范和原则:

比如重要操作的对齐和通知,要做或已做了任何的重要操作,都要把信息及时同步给团队内部的所有人

比如集中评审,所有的设计结论、行为实施前,团队可以进行群体性评估,避免个人判断的局限性

比如压力的及时分担和转移,不能让个别成员承接所有压力,而是要把压力尽可能均匀分担到整个团队范围内,以避免单个成员因为过高的压力出现问题和疏漏

如何处理故障:把损失降到最低

说完了如何尽可能避免故障,下面我们聊聊如何处理故障

处理故障时,我们的唯一目标显然就是把损失降到最低,而要达成这个目的,稍微分解下,我们需要做到的次级目标如下:

  • 尽可能快地恢复故障
  • 尽可能小地降低影响面
  • 尽可能多地避免发生重复问题

我们逐个看下:

尽可能快地恢复故障

如何尽可能块地恢复故障?首先肯定是要把发现故障的时间不断提前,甚至是提前于故障发生。如果我们每次处理故障都是我们的用户告诉我们后才开始,那么我们就应该反思下了

监控和警报

要做到尽可能早的发现故障,我们显然需要 监控和警报 机制,监控是确认我们的服务日常运行的状态,告警是基于监控之上发出的风险事件通知

大部分故障的发生都是有预兆的,而非突然发生的。一个优秀的监控告警机制,能够在某个故障发生前,就探测到预兆的出现(比如CPU内存利用率升高、网络拥堵、应用频繁报错等),然后明确严重程度,如有必要则立即通知相应人员处理

好的监控体系一般会重点关注四个黄金指标:延时流量错误利用率,基于这四个最基本的黄金指标,我们能够感知到大部分的故障异常的预兆和发生情况

对于负责应用层的人员来说,更应该关注应用层面的行为监控,比如关键操作流程中的报错、超时、阻塞等等,可以通过人和机器都易识别的日志(日志也是一种监控数据源)透出,或是通过Prometheus metrics指标的方式透出

基础设施层的监控,则要么是自行搭建,要么是使用现有的产品或服务(比如云计算提供的各类监控产品服务)

P.S. 监控体系也是一个非常大的话题,可以用另一篇文章的篇幅来讲,如果有读者感兴趣,可以和我留言反馈

警报最核心的重点是提高警报真实度,也就是尽可能降低误报率。过多的警报可能会让相关人员处于警报麻痹状态,也就是“狼来了”状态,因为噪声过多,以至于无法识别出真正的风险。要达成这一点,需要和监控系统紧密结合考虑,选择真正合适的警报条件,并不断优化警报触发模式和逻辑

警报发生时,要有专人接收警报并迅速进行判断并发起处理,当判断出问题严重到一定程度后,需要立即召集相关人员,以团队群体化的模式进行解决。以团队群体化模式处理故障时,没有经验的团队可能会乱成一锅粥,这时候就需要事先准备好的故障处理流程,协调整个群体内的成员有序地处理,这一点则又是跟团队规范紧密相关

快速回滚和预案

大部分时候,故障的出现都是因为我们发布了新版本、做了某种运维操作直接导致的。这个时候,如果我们的操作是可逆的,我们应该快速执行回滚操作,大部分时候能够做到恢复。这也要求我们针对每一个变更或发布,都要思考和准备对应的回滚机制和预案考量,也就是所谓的“留一手”

同时,回滚的操作和预案最好也是经过测试验证的,防止实际回滚和执行预案时出现问题导致故障无法恢复

先恢复,而不是确认原因

大部分开发人员在处理故障时最常见的一个误区就是把处理故障搞成在线debug,他们会花大量的时间和精力分析问题的原因。殊不知在故障出现时,最应该和优先做的是尽可能快地恢复,而不是查明原因(除了某些时候必须查明原因后才能恢复)

时刻铭记,我们的首要目标是在故障发生时,把损失降到最低,而不是搞清楚为什么(这是次要目标)

尽可能小地降低影响面

原子化和灰度变更

大部分时候,故障的出现都是因为我们发布了新版本、做了某种运维操作直接导致的。对于这种情况,我们是能够事先把影响面降低并控制的,我们可以采用原子化灰度的变更方式

原子化是指把我们的整体操作拆分成可以单独实施的原子化步骤,这样的意义在于我们可以清晰的把每一个操作切分出来,当问题出现时,我们可以清楚地知道到底是哪一个步骤导致的。比如把配置变更和版本发布分开,我们就可以知道到底是配置变更引发的问题还是版本发布引发的,不至于在故障分析、回滚时花掉更多时间

灰度变更是指变更操作不要非黑即白一把梭地实施,而是要逐步过渡(由白转黑,灰色)。在可切分的前提下,我们可以先操作1%的量,然后是5%、10%、20%、50%等等(策略可以根据实际情况灵活定义),这是按照数量比例的灰度,我们还可以在此基础上加入地理位置、用户属性等属性地差异化区分

除此之外要注意的是,每个灰度批次间需要等待一段时间以待观察,很多时候异常问题并不是变更做完后立即出现的,而是随着时间推进逐步暴露出来的,所以每个批次间留一段时间的观察期,能够帮助我们更高概率地发现问题

最终达到的效果是,当我们选择了一小部分样本来做此次发布的试验,如果这次试验有问题,我们能够以足够低的成本发现并进行针对性的修复

服务降级&隔离:避免连锁反应

过载是触发故障很常见的一个原因,过载出现时,可能会产生连锁反应,这个连锁反应可能是垂直的(影响上下游系统),也可能是水平的(影响同一平面的其他服务)

在这种模式下,除了快速进行扩容操作外,我们还要确保单个服务出现的故障不会因为连锁反应而放大,把故障的影响面限制到一定程度,就需要服务降级或隔离的手段。这包括直接隔离关闭服务(熔断)、仅处理部分比例请求(限流)等方式,这些手段可以保证过载时,服务本身也可以处于正常状态,而不是处于异常状态,进而把风险传递到其他地方产生放大效应

同时,如果服务本身有重试机制,需要注意重试的次数限额和时间间隔,避免重试导致的过载情况恶化(譬如所有客户端在同一时间按照相同频率进行重试,产生请求波峰,非常容易进一步打挂服务),因此,所有请求端的重试不能同一时间触发,可以适当加入随机化时间因子,避免重试在同一时间触发,形成流量洪峰

这个模式,就如家庭用电时,每户都有熔断器,保证一户发生的用电异常不会影响到整个电力网络一样

数据丢失:无法恢复的故障

这是最恐怖的一类故障,不同于最常见的服务中断类型的故障,真正意义上的数据丢失无法恢复

对于这种最恐怖的故障,在很早期的设计阶段,就需要考虑充分的备份恢复、冗余、软删除、复制机制,必要时进行早期的存储预警

尽可能多地避免发生重复问题

如果同一个故障问题反复重复发生,那么显然会给我我们带来多次损失,因此一旦我们遇到了一次故障问题,就需要尽量将其彻底解决,并排除关联风险,避免更多潜在损失

详细记录收集故障信息

故障发生时的各类信息,都要在过程中尽可能地详细记录和保留,这对后续的定位、分析和改进有非常重要的作用,也能帮助我们复现还原模拟故障,去测试下一次面对故障时的坚韧性

具体的形式,可以参考Google书中的附录C:事故状态文档示范

从故障中复盘和学习

这是一个非常直观的举措,故障意味着错误,错误意味着反思和改进

如果我们总是忙于应付问题,单纯地把遇到的问题解决掉,停留于表象,而不去思考问题发生的原因,那么我们显然还是会再次掉入同样的坑洞中

复盘就是反思问题的深层原因和关联问题的潜在可能,学习就是从其中吸取经验,并反作用于后续的行为中,从源头消除问题再次发生的可能

同样可以学习参考Google书中的典型案例,附录D:事后总结示范

把故障作为案例加入测试集

很多故障能够反应非常典型的问题,将其加入到已有的测试集中,下一次我们如果出现相同或类似的问题,便能够通过测试阶段就快速发现并解决,避免重复的问题再次进入生产环境

具体来讲,故障演练是测试整个系统在应对以往发生过或预测到的各类故障时系统稳定度表现的绝佳手段

总结

在本篇文章中,笔者参考Google通过书本文章输出的内容,结合自身的经验介绍了自己对于维持系统稳定性,避免和处理故障一些通用性原则、方法和观点。

需要注意的是,避免和应对故障的能力不是能够从干巴巴的文字中完全学习会的,而是从一次次真实且血淋淋的故障教训中切身感受和领会到的,因为只有真正的风险和故障,才能给予你充分的动机和推力,去思考反思哪些自己曾经漫不经心和忽略的东西。

——“多么痛的领悟”,如果不痛,就不会有领悟。


我是Valiant程,如果你对我的文章内容感兴趣,可以持续关注我并给予反馈,我会尝试编写更多有价值的内容提供给感兴趣的读者