《SRE Google运维解密》阅读笔记
前言
《SRE: Google 运维解密》(Site Reliability Engineering)是 Google 首次公开其运维方法论的经典著作。本书揭示了 Google 如何通过工程化手段保障海量服务的可靠性,涵盖监控、容量规划、故障处理、过载保护等核心主题。本文记录了我阅读本书时的关键要点与实践思考。
结论先行
SRE 的核心理念是:通过工程化手段和自动化工具来保障系统可靠性,而不是依赖人工运维。 其中最重要的实践包括:用监控黄金指标量化系统健康度、通过流量抛弃和优雅降级避免过载、合理设计重试和超时机制防止雪崩。
一、监控体系
监控的 4 个黄金指标
Google 认为,衡量一个系统的健康状况,应该关注以下 4 个核心指标:
- 延迟(Latency):请求的响应时间
- 流量(Traffic):系统的 QPS(每秒查询数)
- 错误(Errors):请求失败率
- 饱和度(Saturation):系统的瓶颈指标(如 CPU 利用率、内存利用率),也可以理解为”水位”
这 4 个指标简洁而全面,能够快速反映系统的运行状态。
保持监控系统简单
核心原则:系统越简单,可以发生故障的地方就越少,就越可靠。
监控作为基础设施,即使在发生事故时也必须保持可用,此时失去监控无异于蒙眼狂奔。因此,监控系统本身必须足够简单、可靠。
Prometheus 监控体系的设计哲学:
Prometheus + Grafana + Alertmanager 的开源监控组合充分体现了 Google 对于监控系统的设计理念:
- 指标暴露方式简单:监控指标通过被监控实例上的一个 HTTP API 暴露出来,由 Prometheus 定时收集并存储
- 数据格式松散易解析:响应格式比 JSON 更加松散,容易解析:
1 | # HELP <metric_name> <help_text> |
- 模块职责清晰:控制台(Grafana)、告警系统(Alertmanager)拆分出来,每个模块都很独立、清晰、简单
二、系统设计原则
最小 API 原则
核心观点:
我们向 API 消费者提供的方法和参数越少,这些 API 就越容易理解,我们就能用更多的精力去尽可能地完善这些方法。
设计哲学:
有意识地不解决某些问题可以让我们能够更专注核心问题,使得我们已有的解决方案更好。在软件工程上,少就是多! 一个很小的、很简单的 API 通常也是一个对问题深刻理解的标志。
故障处理最佳实践
处理原则:先止损,再找原因。
发生故障时,应该:
- 先止损:回滚代码、切换流量、降级功能等,优先恢复服务
- 再排查:在服务恢复后,冷静分析根本原因
排查技巧:
对可疑点进行逐个排查时,应该把测试手段和测试结果记录下来:
- 避免绕圈圈:人思绪混乱的时候很容易犯重复排查的错误
- 便于协作:让他人及时了解修复问题的进度,有助于提供帮助
三、容量规划
如何度量容量
不推荐使用 QPS:
Google 不推荐使用 QPS 来度量容量,因为随着业务迭代,同样的 QPS 带来的负载可能是差异巨大的。例如,一个接口增加了复杂的计算逻辑后,1000 QPS 的负载可能比原来 2000 QPS 还要大。
推荐使用 CPU 和内存:
Google 推荐用**直接消耗的资源(CPU 和内存)**来度量容量,因为 CPU 占用率到达什么水平会导致性能恶化是比较稳定的。
甚至可以只用 CPU:
对于有 GC 的语言(如 Java)来说,内存压力会转化为 CPU 占用率(频繁 GC 会消耗大量 CPU)。因此,CPU 可以作为容量度量的主要指标。
其他资源的处理:
对于磁盘、网络等其他资源,Google 建议”给足,让它们几乎不会在 CPU 到达瓶颈之前到达瓶颈”。
QPS 的价值:
尽管 CPU 是度量容量的更好指标,但 QPS 仍然是度量流量大小最重要的指标,因为:
- QPS 可以被直观地”切割”
- 在某一时刻静态地来看,对于同一个负载均衡器后面的所有机器,QPS 大小约等于负载大小
| 指标 | 用途 | 优势 | 局限 |
|---|---|---|---|
| CPU/内存 | 度量容量、判断扩缩容 | 稳定、准确反映负载 | 不直观 |
| QPS | 度量流量、流量调度 | 直观、便于切割 | 随业务变化不稳定 |
四、过载保护
过载的恶性循环
过载发生过程:
- 流量超出单机的处理能力
- CPU 负载升高 → 处理请求速度变慢
- 积压的请求占用更多内存 → 内存不足导致频繁 GC
- 频繁 GC 进一步消耗 CPU → 机器性能开始断崖式下跌
- 此时即使把流量降低到正常水平,机器可能也难以恢复正常状态
集群雪崩:
当单机发生过载时,健康检查可能会失败,负载均衡器会把流量引向其他机器,进而导致其他机器也进入过载状态。这是一个裂变式的过程,最终整个集群都无法提供服务。
雪崩时的困境:
当集群开始雪崩式过载时,人为降低集群的 QPS 并不一定能解决问题,因为此时集群中健康的机器数量可能远少于平时。健康的机器数取决于以下 3 个因素:
- 系统重启机器的速度
- 机器达到性能巅峰的速度
- 机器在被打垮之前能坚持多久
避免过载的手段
Google 提出了以下几种避免过载的策略:
| 策略 | 说明 | 效果 |
|---|---|---|
| 压测 | 知晓服务负载上限 | 预防 |
| 优雅降级 | 返回消耗资源更少的响应 | 缓解 |
| 流量抛弃 | 让请求快速失败 | 止损 |
| 容量规划 + 弹性伸缩 | 扩展资源 | 根治 |
前三种是”在一个空间有限的屋子里尽可能多的塞东西”,最后一种是”扩建房子”。
线程池队列管理
常被忽略的问题:
线程池线程数这个面试题你可能背的滚瓜烂熟,但是你可能没想过队列长度该设多大。
默认配置的陷阱:
Java 中常见的 Executors.newFixedThreadPool() 给队列长度的默认大小是 Integer.MAX_VALUE,几乎不会出现拒绝任务的情况,大不了就是排很久的队。这非常适合不关心延时的离线任务。
在线服务的问题:
但是,对于 Web 或者 RPC 服务器这种关心请求延时的场景来说,请求排队比拒绝请求更可怕:
- 一波流量高峰过后很久服务都无法正常处理新的请求
- 线程池还在一个一个地处理前面排队的请求
- 即使发起那些请求的用户已经放弃了,服务器还在浪费资源处理
Google 的建议:
对于在线服务,应该把线程池队列长度设为线程数量的一半甚至更少。如果一台机器已经无法及时处理新请求了,那就应该快速失败,好把负载转移到其他机器上。
灵活配置:
当然,对于经常有突发流量的服务来说,应该结合以下因素合理地设置线程池队列长度:
- 处理请求的速度
- 超时时间
- 突发流量大小
流量抛弃策略
限制线程池队列长度是避免服务器过载的方式之一,Google 还提出了以下几种流量抛弃策略:
1. 基于 CPU 利用率决定是否抛弃请求
当 CPU 利用率超过阈值(如 80%)时,直接拒绝新请求,避免进入过载状态。
2. 优先抛弃低优先级的请求
Google 给内部 RPC 设置了 4 个优先级,以实现精准流量抛弃。在流量高峰时,优先保障核心业务的请求。
3. 优化排队策略
默认策略的问题:
默认的先进先出(FIFO)策略可能会导致大量已超时的请求还在队列中等待。
改进方案:
- 先进后出(LIFO):优先处理最新的请求
- 可控延迟算法(CoDel):主动丢弃等待时间过长的请求
这样可以针对性地抛弃那些因为等待过久而被用户放弃的请求。
可控延迟算法(CoDel)详解:
CoDel 是由 Van Jacobson 和 Kathleen Nichols 提出的一种主动队列管理(AQM)算法,旨在解决互联网中的”bufferbloat”(缓冲膨胀)问题。
核心机制:
目标停留时间(Target Sojourn Time):
- CoDel 使用 5ms 作为目标停留时间,即数据包在队列中的等待时间应尽量保持在 5ms 以下
滑动测量窗口(Sliding Measurement Window):
- CoDel 使用 100ms 作为滑动测量窗口,用于检测队列中数据包的停留时间是否持续超过目标值
- 100ms 是互联网流量的典型往返时间(RTT)
丢包机制:
- 当队列中数据包的停留时间超过目标值(5ms)并且持续时间超过 100ms 时,CoDel 开始通过丢包来减少队列
- CoDel 会逐渐增加丢包率,与丢包次数的平方根成正比,从而导致受影响的 TCP 连接的吞吐量线性下降
优雅降级
基本概念:
优雅降级通过给客户端返回一个所需资源更少的响应,以降低负载。例如搜索服务可以搜索内存中的不完整数据,而非磁盘上的所有数据。
使用时需要考虑的问题:
- 触发条件:什么条件下采用优雅降级?CPU 利用率到多少时?延时到多少时?
- 监控告警:优雅降级不应该经常触发,触发频率上升时应该发出告警
- 定期演练:由于不经常触发,降级逻辑的维护质量很可能不如正常逻辑,需要有定期演练保证优雅降级能正常工作
五、重试和超时机制
重试的双刃剑
客户端视角:
对于客户端来说,在 RPC 失败时重试是一种提升健壮性的简单有效的方式。
服务端视角:
但是对于服务端来说,在负载接近上限时:
- 失败率上升
- 随之重试请求大幅上涨
- 加重了系统的负载
- 进入恶性循环
重试的最佳实践
为了避免重试风暴,Google 提出了以下限制策略:
1. 指数退避 + 随机抖动
使用指数型增长的有随机抖动的重试周期,而不是固定的重试周期。
实现代码:
1 | int expBackoff = (int) Math.pow(2, retryCount); |
设计思想:
这借鉴了计算机网络中 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:
- A 为这个高层请求设置一个总的超时时间 10s
- B 这个环节花了 8s,留给 C 和 D 的超时时间就只剩 2s 了
- 如果 C 花了 3s,D 在收到这个请求时就应该立即放弃处理该请求
好处:
- 避免浪费资源处理已经被放弃的请求
- 在系统濒临过载时,减少这种浪费尤其重要
六、总结
核心实践总结
| 主题 | 核心要点 | 实践建议 |
|---|---|---|
| 监控 | 4 个黄金指标:延迟、流量、错误、饱和度 | 使用 Prometheus + Grafana 构建简单可靠的监控体系 |
| 容量度量 | 用 CPU/内存度量容量,用 QPS 度量流量 | 根据 CPU 利用率判断扩缩容,不要依赖 QPS |
| 过载保护 | 压测、优雅降级、流量抛弃、弹性伸缩 | 线程池队列长度设为线程数的一半或更少 |
| 流量抛弃 | 基于 CPU、优先级、队列算法 | 使用 CoDel 算法主动丢弃等待过久的请求 |
| 重试机制 | 指数退避 + 随机抖动,最多 3 次 | 避免重试风暴,区分可重试和不可重试错误 |
| 超时机制 | 超时时间在链路上传递 | 避免浪费资源处理已被放弃的请求 |
| 故障处理 | 先止损,再排查 | 记录排查过程,便于协作和避免重复 |
核心理念
SRE 的核心理念可以总结为:
- 简单即可靠:系统越简单,可以发生故障的地方就越少
- 工程化运维:通过自动化工具和工程化手段保障系统可靠性,而不是依赖人工
- 面向故障设计:假设故障必然发生,设计健壮的保护机制
- 量化驱动:用指标量化系统健康度,用数据驱动决策
实践价值
本书的实践价值在于:
- 提供了一套经过 Google 海量服务验证的运维方法论
- 帮助理解分布式系统中过载、雪崩等常见问题的本质和解决方案
- 给出了具体的、可落地的工程实践建议
- 适合所有需要保障系统可靠性的工程师阅读
无论你是运维工程师、后端开发,还是架构师,都能从这本书中找到提升系统可靠性的实用方法。




