前言

《SRE: Google 运维解密》(Site Reliability Engineering)是 Google 首次公开其运维方法论的经典著作。本书揭示了 Google 如何通过工程化手段保障海量服务的可靠性,涵盖监控、容量规划、故障处理、过载保护等核心主题。本文记录了我阅读本书时的关键要点与实践思考。

结论先行

SRE 的核心理念是:通过工程化手段和自动化工具来保障系统可靠性,而不是依赖人工运维。 其中最重要的实践包括:用监控黄金指标量化系统健康度、通过流量抛弃和优雅降级避免过载、合理设计重试和超时机制防止雪崩。

一、监控体系

监控的 4 个黄金指标

Google 认为,衡量一个系统的健康状况,应该关注以下 4 个核心指标:

  1. 延迟(Latency):请求的响应时间
  2. 流量(Traffic):系统的 QPS(每秒查询数)
  3. 错误(Errors):请求失败率
  4. 饱和度(Saturation):系统的瓶颈指标(如 CPU 利用率、内存利用率),也可以理解为”水位”

这 4 个指标简洁而全面,能够快速反映系统的运行状态。

保持监控系统简单

核心原则:系统越简单,可以发生故障的地方就越少,就越可靠。

监控作为基础设施,即使在发生事故时也必须保持可用,此时失去监控无异于蒙眼狂奔。因此,监控系统本身必须足够简单、可靠。

Prometheus 监控体系的设计哲学

Prometheus + Grafana + Alertmanager 的开源监控组合充分体现了 Google 对于监控系统的设计理念:

  1. 指标暴露方式简单:监控指标通过被监控实例上的一个 HTTP API 暴露出来,由 Prometheus 定时收集并存储
  2. 数据格式松散易解析:响应格式比 JSON 更加松散,容易解析:
1
2
3
# HELP <metric_name> <help_text>
# TYPE <metric_name> counter
<metric_name>{<label_name>="<label_value>",...} <value>
  1. 模块职责清晰:控制台(Grafana)、告警系统(Alertmanager)拆分出来,每个模块都很独立、清晰、简单

二、系统设计原则

最小 API 原则

核心观点

我们向 API 消费者提供的方法和参数越少,这些 API 就越容易理解,我们就能用更多的精力去尽可能地完善这些方法。

设计哲学

有意识地不解决某些问题可以让我们能够更专注核心问题,使得我们已有的解决方案更好。在软件工程上,少就是多! 一个很小的、很简单的 API 通常也是一个对问题深刻理解的标志。

故障处理最佳实践

处理原则:先止损,再找原因。

发生故障时,应该:

  1. 先止损:回滚代码、切换流量、降级功能等,优先恢复服务
  2. 再排查:在服务恢复后,冷静分析根本原因

排查技巧

对可疑点进行逐个排查时,应该把测试手段和测试结果记录下来:

  • 避免绕圈圈:人思绪混乱的时候很容易犯重复排查的错误
  • 便于协作:让他人及时了解修复问题的进度,有助于提供帮助

三、容量规划

如何度量容量

不推荐使用 QPS

Google 不推荐使用 QPS 来度量容量,因为随着业务迭代,同样的 QPS 带来的负载可能是差异巨大的。例如,一个接口增加了复杂的计算逻辑后,1000 QPS 的负载可能比原来 2000 QPS 还要大。

推荐使用 CPU 和内存

Google 推荐用**直接消耗的资源(CPU 和内存)**来度量容量,因为 CPU 占用率到达什么水平会导致性能恶化是比较稳定的。

甚至可以只用 CPU

对于有 GC 的语言(如 Java)来说,内存压力会转化为 CPU 占用率(频繁 GC 会消耗大量 CPU)。因此,CPU 可以作为容量度量的主要指标。

其他资源的处理

对于磁盘、网络等其他资源,Google 建议”给足,让它们几乎不会在 CPU 到达瓶颈之前到达瓶颈”。

QPS 的价值

尽管 CPU 是度量容量的更好指标,但 QPS 仍然是度量流量大小最重要的指标,因为:

  1. QPS 可以被直观地”切割”
  2. 在某一时刻静态地来看,对于同一个负载均衡器后面的所有机器,QPS 大小约等于负载大小
指标 用途 优势 局限
CPU/内存 度量容量、判断扩缩容 稳定、准确反映负载 不直观
QPS 度量流量、流量调度 直观、便于切割 随业务变化不稳定

四、过载保护

过载的恶性循环

过载发生过程

  1. 流量超出单机的处理能力
  2. CPU 负载升高 → 处理请求速度变慢
  3. 积压的请求占用更多内存 → 内存不足导致频繁 GC
  4. 频繁 GC 进一步消耗 CPU → 机器性能开始断崖式下跌
  5. 此时即使把流量降低到正常水平,机器可能也难以恢复正常状态

集群雪崩

当单机发生过载时,健康检查可能会失败,负载均衡器会把流量引向其他机器,进而导致其他机器也进入过载状态。这是一个裂变式的过程,最终整个集群都无法提供服务。

雪崩时的困境

当集群开始雪崩式过载时,人为降低集群的 QPS 并不一定能解决问题,因为此时集群中健康的机器数量可能远少于平时。健康的机器数取决于以下 3 个因素:

  1. 系统重启机器的速度
  2. 机器达到性能巅峰的速度
  3. 机器在被打垮之前能坚持多久

避免过载的手段

Google 提出了以下几种避免过载的策略:

策略 说明 效果
压测 知晓服务负载上限 预防
优雅降级 返回消耗资源更少的响应 缓解
流量抛弃 让请求快速失败 止损
容量规划 + 弹性伸缩 扩展资源 根治

前三种是”在一个空间有限的屋子里尽可能多的塞东西”,最后一种是”扩建房子”。

线程池队列管理

常被忽略的问题

线程池线程数这个面试题你可能背的滚瓜烂熟,但是你可能没想过队列长度该设多大

默认配置的陷阱

Java 中常见的 Executors.newFixedThreadPool() 给队列长度的默认大小是 Integer.MAX_VALUE,几乎不会出现拒绝任务的情况,大不了就是排很久的队。这非常适合不关心延时的离线任务。

在线服务的问题

但是,对于 Web 或者 RPC 服务器这种关心请求延时的场景来说,请求排队比拒绝请求更可怕

  • 一波流量高峰过后很久服务都无法正常处理新的请求
  • 线程池还在一个一个地处理前面排队的请求
  • 即使发起那些请求的用户已经放弃了,服务器还在浪费资源处理

Google 的建议

对于在线服务,应该把线程池队列长度设为线程数量的一半甚至更少。如果一台机器已经无法及时处理新请求了,那就应该快速失败,好把负载转移到其他机器上。

灵活配置

当然,对于经常有突发流量的服务来说,应该结合以下因素合理地设置线程池队列长度:

  • 处理请求的速度
  • 超时时间
  • 突发流量大小

流量抛弃策略

限制线程池队列长度是避免服务器过载的方式之一,Google 还提出了以下几种流量抛弃策略:

1. 基于 CPU 利用率决定是否抛弃请求

当 CPU 利用率超过阈值(如 80%)时,直接拒绝新请求,避免进入过载状态。

2. 优先抛弃低优先级的请求

Google 给内部 RPC 设置了 4 个优先级,以实现精准流量抛弃。在流量高峰时,优先保障核心业务的请求。

3. 优化排队策略

默认策略的问题

默认的先进先出(FIFO)策略可能会导致大量已超时的请求还在队列中等待。

改进方案

  1. 先进后出(LIFO):优先处理最新的请求
  2. 可控延迟算法(CoDel):主动丢弃等待时间过长的请求

这样可以针对性地抛弃那些因为等待过久而被用户放弃的请求。

可控延迟算法(CoDel)详解

CoDel 是由 Van Jacobson 和 Kathleen Nichols 提出的一种主动队列管理(AQM)算法,旨在解决互联网中的”bufferbloat”(缓冲膨胀)问题。

核心机制

  1. 目标停留时间(Target Sojourn Time)

    • CoDel 使用 5ms 作为目标停留时间,即数据包在队列中的等待时间应尽量保持在 5ms 以下
  2. 滑动测量窗口(Sliding Measurement Window)

    • CoDel 使用 100ms 作为滑动测量窗口,用于检测队列中数据包的停留时间是否持续超过目标值
    • 100ms 是互联网流量的典型往返时间(RTT)
  3. 丢包机制

    • 当队列中数据包的停留时间超过目标值(5ms)并且持续时间超过 100ms 时,CoDel 开始通过丢包来减少队列
    • CoDel 会逐渐增加丢包率,与丢包次数的平方根成正比,从而导致受影响的 TCP 连接的吞吐量线性下降

优雅降级

基本概念

优雅降级通过给客户端返回一个所需资源更少的响应,以降低负载。例如搜索服务可以搜索内存中的不完整数据,而非磁盘上的所有数据。

使用时需要考虑的问题

  1. 触发条件:什么条件下采用优雅降级?CPU 利用率到多少时?延时到多少时?
  2. 监控告警:优雅降级不应该经常触发,触发频率上升时应该发出告警
  3. 定期演练:由于不经常触发,降级逻辑的维护质量很可能不如正常逻辑,需要有定期演练保证优雅降级能正常工作

五、重试和超时机制

重试的双刃剑

客户端视角

对于客户端来说,在 RPC 失败时重试是一种提升健壮性的简单有效的方式。

服务端视角

但是对于服务端来说,在负载接近上限时:

  1. 失败率上升
  2. 随之重试请求大幅上涨
  3. 加重了系统的负载
  4. 进入恶性循环

重试的最佳实践

为了避免重试风暴,Google 提出了以下限制策略:

1. 指数退避 + 随机抖动

使用指数型增长有随机抖动的重试周期,而不是固定的重试周期。

实现代码

1
2
3
int expBackoff = (int) Math.pow(2, retryCount);
int maxJitter = (int) Math.ceil(expBackoff * 0.2);
int finalBackoff = expBackoff + random.nextInt(maxJitter);

设计思想

这借鉴了计算机网络中 TCP/IP 的拥塞避免和控制的指数退避算法:

  • 指数型增长:在请求失败次数上升时减少请求频率,降低服务端负载
  • 随机抖动:避免多个客户端扎堆发起重试请求

工具推荐

你可以借助现成的重试工具实现这一点:https://github.com/rholder/guava-retrying

2. 限制重试次数上限

不要无限重试。Google 的建议是最多重试 3 次

理由:如果一个请求 3 次都打到了异常的服务端实例上,这说明服务端集群总体处于不健康的状态,重试已经无济于事。

3. 区分可重试和不可重试的错误

服务端返回的响应中,应该区分可重试和不可重试的错误。例如:

  • 不可重试:参数错误、鉴权错误
  • 可重试:超时、网络错误、服务端内部错误

4. 避免不同层次的重试叠加

否则高层的一个请求可能会导致底层 RPC 的重试次数以乘积式增长,这会:

  • 加重服务端的负载
  • 延长这个错误请求的处理耗时

示例:A 调 B 调 C,如果 A 和 B 都设置了重试 3 次,那么 C 可能会收到 9 次请求(3 × 3)。

超时时间传递

传统超时机制的问题

常见的超时机制是:A 调 B,A 为这个请求设置了一个时长 t,如果 B 在 t 时间内无法返回,A 就认为这次调用失败了。

这样简单的超时机制在调用栈很深的时候就会暴露出问题:

  • 每一层的超时时间没有一个明确的设置依据
  • 为了避免误伤正常请求,通常超时时间都会设置得偏大
  • 高层请求虽然已经因超时被放弃,但底层仍在花费资源处理

Google 的解决方案:超时时间传递

超时时间应当能够在链路上传递。

示例

一个请求的调用链路是 A → B → C → D:

  1. A 为这个高层请求设置一个总的超时时间 10s
  2. B 这个环节花了 8s,留给 C 和 D 的超时时间就只剩 2s
  3. 如果 C 花了 3s,D 在收到这个请求时就应该立即放弃处理该请求

好处

  • 避免浪费资源处理已经被放弃的请求
  • 在系统濒临过载时,减少这种浪费尤其重要

六、总结

核心实践总结

主题 核心要点 实践建议
监控 4 个黄金指标:延迟、流量、错误、饱和度 使用 Prometheus + Grafana 构建简单可靠的监控体系
容量度量 用 CPU/内存度量容量,用 QPS 度量流量 根据 CPU 利用率判断扩缩容,不要依赖 QPS
过载保护 压测、优雅降级、流量抛弃、弹性伸缩 线程池队列长度设为线程数的一半或更少
流量抛弃 基于 CPU、优先级、队列算法 使用 CoDel 算法主动丢弃等待过久的请求
重试机制 指数退避 + 随机抖动,最多 3 次 避免重试风暴,区分可重试和不可重试错误
超时机制 超时时间在链路上传递 避免浪费资源处理已被放弃的请求
故障处理 先止损,再排查 记录排查过程,便于协作和避免重复

核心理念

SRE 的核心理念可以总结为:

  1. 简单即可靠:系统越简单,可以发生故障的地方就越少
  2. 工程化运维:通过自动化工具和工程化手段保障系统可靠性,而不是依赖人工
  3. 面向故障设计:假设故障必然发生,设计健壮的保护机制
  4. 量化驱动:用指标量化系统健康度,用数据驱动决策

实践价值

本书的实践价值在于:

  • 提供了一套经过 Google 海量服务验证的运维方法论
  • 帮助理解分布式系统中过载、雪崩等常见问题的本质和解决方案
  • 给出了具体的、可落地的工程实践建议
  • 适合所有需要保障系统可靠性的工程师阅读

无论你是运维工程师、后端开发,还是架构师,都能从这本书中找到提升系统可靠性的实用方法。