一、故障域总览
我们通过服务的监控和告警,来保障线上业务持续稳定运行。但是,不管我们如何小心谨慎,故障仍然会不可避免地发生。而且不是所有的故障都会影响用户,一些局部的故障如果处理得当,对用户并不会产生影响。
故障大体可以分为如下几类:
- 软硬件升级与各类配置变更,即发布。
- 软硬件环境的故障。
- 终端用户突发的请求。如:秒杀,导致系统的承载能力超限。
1.1、发布故障
发布过程引发的故障实际上有别于另外两种故障类型,它源于我们主动对系统作出变更,属于过程型的故障。变更是故障的第一大问题源头。所以我们在发布的过程中多谨慎都不为过。
大部分情况下,变更导致的故障在短期内就会得以暴露,这也是我们采用灰度发布这样的手段能够达到规避故障风险的原因。但当我们讨论故障域的时候,我们还是应该意识到,灰度并不能发现所有变更风险。有时代码变更引发的故障需要达到特定的条件下才得以触发,例如数据库规模达到某个临界点可能导致数据库操作异常。
怎么才能避免这类问题?严谨的白盒代码审查 和全面的测试覆盖率提升,才有可能消除此类风险,至少理论上是这样。
1.2、软硬件环境的故障
怎么为 "软硬件环境的故障" 做好故障预案?常见做法有两种:
- 通过监控系统发现特定的软硬件故障并进行报警,收到报警后并不是通知到人,而是触发去自动执行故障恢复的脚本。
- 另一种做法是干脆把故障恢复的逻辑实现在业务服务的代码逻辑中,避免因软硬件故障而出现单点问题。
一个经典的 API 请求,如果我们把所有基础架构也考虑在内的话,它的故障点主要有:
- 网络链路,包括用户端网络和服务端网络;
- DNS;
- 机房;
- 机架;
- 交换机;
- 负载均衡;
- 物理服务器;
- 业务服务本身;
- 缓存 / 数据库 / 存储。
1.3、终端用户请求
对于流量超大时,系统承受不住的模块基本都是存储相关。
对于数据库压力太大导致雪崩,数据库再起来就又立刻被打爆,怎么都起不来的情况,最好的做法是在数据库层面就做好过载保护 。在数据库不支持自我保护的情况下,一个替代的做法是:一旦监控系统发现数据库过载了,就选择由负载均衡来扔掉部分用户请求。
存储要保证高可靠(高持久性)和高可用,必然是多实例的。无论是什么架构,对于特定的数据,这些实例有主(Master)有从(Slave),一旦主节点挂掉就会触发选举确定新的主。
二、故障排查与分析
故障排查是一个可以自我学习,也是一个可以传授的技能。
要做到高效排查的门槛比较高,理想情况下我们需要同时具备两个条件:
- 对通用的故障排查过程的理解。
- 对发生故障的系统的足够了解。虽然只依靠通用性的流程和手段也可以处理一些系统中的问题,但这样做通常是很低效的。对系统内部运行的了解,对系统设计方式和构建原理的知识是不可或缺的。
从理论上讲,我们将故障排查过程定义为反复采用假设 - 验证排除手段的过程:针对某系统的一些观察结果和对该系统运行机制的理论认知,我们不断提出一个造成系统问题的假设,进而针对这些假设进行测试和排除。
我们可以用以下两种方式测试假设是否成立:
- 第一种方式,可以将我们的假设与观察到的系统状态进行对比,从中找出支持假设或者不支持假设的证据。
- 另一种方式是,我们可以主动尝试 "治疗" 该系统,也就是对系统进行可控的调整,然后再观察操作的结果。
第二种方式务必要谨慎,以避免带来更大的故障。但它的确可以让我们更好地理解系统目前的状态,排查造成系统问题的可能原因。
真正意义上的 "线上调试" 是很少发生的,毕竟我们遇到故障的时候,首先不是排查故障而是去恢复它,这有可能会破坏掉部分的现场。所以,服务端软件的 "线上调试" 往往在事后发生,我们主要依赖的就是日志。这里的日志是广义的,它包括监控系统背后的各类观测指标的时序数据,以及应用程序的程序日志。
在现实中,要想让业务系统的故障排查更简单,我们可能最基本要做的是:
- 增加系统的可观察性。不要等狼来了羊丢了才想着要补牢。在实现之初就给每个组件增加白盒监控指标和结构化日志。
- 使用成熟的、观察性好的 RPC 框架。保证用户 API 请求信息用一个一致的方法在整个系统内传递。例如,使用 Request ID 这样的唯一标识标记所有组件产生的所有相关 RPC。这有效地降低了需要对应上游某条日志记录与下游组件某条日志记录的要求,加速了故障排查的效率。
三、系统过载保护
所谓过载,最直白的理解,当然就是因为活跃的用户超过了资源的承载能力范围,导致某类资源耗尽,进而体现出系统过载。
本质上,这是一个容量规划的问题。资源不够了,往往有以下这么几个成因:
- 其一,用户增长太快了,资源规划上的预期没有跟上,导致资源储备不足。
- 其二,部分资源因为故障而下线,导致线上活跃的资源不足。
- 其三,系统的关键资源负载能力变低,比如数据库。随着线上服务时间的推移,数据库越来越大,到达了某个临界点,可能就会导致数据库整体的延时变长,响应变慢,同时能够支撑的并发变低,从而导致过载。
- 其四,某类故障导致系统的反应过激,这通常是因为重试导致的。
3.1、过载后果
过载通常是会有连锁反应的。某类资源的耗尽,会导致其他资源出现问题。某个服务的过载,经常会出现一系列的资源过载现象,看起来都很像是根本问题,这会使得定位问题更加困难。
过载现象可能会是一个短时现象,过一段时间就撑过去了。但也有很多时候会由于正反馈循环(positive feedback)导致恶化,短时间内就快速形成雪崩效应,击垮系统。
3.2、过载监控
一种非常常见,也是很多公司都在做的方式,是给服务的 QPS 设置一个阈值,当 QPS > 阈值时,就触发服务已经过载或即将过载的告警。
这个方式看起来不错,但是它的维护成本很高。就算这个指标在某一个时间段内看起来工作还算良好,但它早晚也会发生变化。有些变动是逐渐发生的,有些则是非常突然的。例如某个服务或客户端的新版本发布,突然就使得某些请求消耗的资源大幅减少。
更好的解决方案,是直接基于该服务所依赖的关键资源,如 CPU 和内存等,来衡量服务的可用容量。我们为该服务预留了多少资源,这些资源已经用了多少,预计还能够用多久。
3.3、过载策略
我们怎么才能够提前防范服务的过载,把过载可能造成的损失降到最低,无非两个方向,一个是把过载发生的概率变低。另一个是即使发生了过载,也要杜绝雪崩效应,把因为过载产生的损失降到最低。
3.3.1、服务端策略
- 首先,在过载情况下主动拒绝请求。服务器应该保护自己不进入过载崩溃状态。
- 其次,应该进行容量规划。好的容量规划可以降低连锁反应发生的可能性。容量规划应该伴随着性能测试进行,以确定可能导致服务失败的负载程度。
- 最后,服务优雅降级。如果说前面主动拒绝请求,是一种无脑、粗暴的降级方式的话,根据请求的类型和重要性级别来降级,则是一种更为优雅的降级方式。
3.3.2、客户端策略
- 降低重试导致的过载概率。
- 请求的重要性级别。将发给服务端的请求重要性级别标记为 1~4 之间的数,它们分别代表 "可丢弃的"、"可延后处理的"、"重要的"、"非常重要的"。在服务端发生过载时,它将优先放弃 "可丢弃的" 请求,次之放弃 "可延后处理的" 请求,以此类推,直到系统负荷回归正常。
- 请求延迟和截止时间。一个超长时间的请求,只是会让一个客户慢。但是结构性的超长时间的请求,它可能会导致系统持续恶化并引起雪崩效应。给 API 请求设置一个小但合理的超时时间,是大幅降低雪崩风险的有效手段。
- 客户端侧的节流机制,也就是是否可能在客户端做自适应的过载保护。在抛弃超过配额的请求时,它完全不会浪费服务端的资源。