Google SRE Book 读书笔记
目录
本文是 Dan Luu 对 Google SRE Book 的读书笔记的中文整理版,保留了原文的笔记式风格和个人评论。
第 1 章:简介
本书以 Margaret Hamilton 的故事开篇。在阿波罗计划时期,Hamilton 带着小女儿去 NASA。在一次模拟任务中,她女儿按下了某些键,导致预发射程序在模拟任务期间运行,任务崩溃。Hamilton 提交了一个变更请求,添加错误检查代码来防止此类问题再次发生,但请求被拒绝了,因为"这种错误不可能发生"。
在下一次任务 Apollo 8 中,完全相同的错误条件发生了,一个本可通过简单检查就能预防的潜在致命问题,NASA 的工程师花了 9 个小时才解决。
这听起来很熟悉——我已经数不清有多少次开发团队的事后分析报告有着相同的基本结构。
传统方法:系统管理员
- 组装现有组件并部署以产生服务
- 响应发生的事件和更新
- 随着服务增长扩大团队以吸收增加的工作量
- 优点:
- 因为是标准做法所以容易实施
- 可以从很大的人才池中招聘
- 有大量可用的软件
- 缺点:
- 手动干预导致团队规模随系统负载增长
- 运维与开发本质上是矛盾的,可能导致对变更的病态抵制,开发团队则会将"发布"重新分类为"增量更新"、"标志翻转"等来绕过
Google 的方法:SRE
- 由软件工程师来做运维
- 候选人应能通过或接近通过普通开发的招聘标准,还可能拥有开发中罕见的额外技能(如 L1-L3 网络或 UNIX 系统内核)
- 职业发展与开发轨道相当
- 结果:
- SRE 会因为手工操作而感到无聊
- 拥有自动化任务所需的技能
- 做与运维团队相同的工作,但用自动化替代手工劳动
- Google 将 SRE 的"运维"工作上限设为 50%
- 这是上限,实际预期运维工作量要低得多
- 优点:
- 扩展成本更低
- 绕过了开发/运维分裂
- 缺点:
- 难以招聘
- 可能需要管理层支持的非常规做法(例如产品团队可能反对因错误预算耗尽而停止本季度发布的决定)
我不太理解这如何算是绕过了开发/运维分裂。从某种意义上说确实如此,但因错误预算耗尽而停止所有发布的例子,与"系统管理员"模式中团队反对发布的例子并无根本不同。看起来 SRE 有更多的政治资本可以花费,在给定的具体例子中 SRE 可能更合理,但没有理由认为系统管理员就不能是合理的。
SRE 的信条
- SRE 团队负责:延迟、性能、效率、变更管理、监控、应急响应和容量规划
确保持久的工程专注
- 50% 运维上限意味着额外的运维工作会溢出转给产品团队
- 为产品团队提供反馈机制,同时保持负载可控
- 目标:每次 8-12 小时的值班轮班中最多 2 个事件
- 对所有严重事件进行事后分析,即使没有触发告警
- 无责事后分析
每次轮班最多 2 个事件,但平均值是多少?每周预期有多少值班事件从 SRE 团队转给开发团队?
如何从有责的事后分析文化转变为无责文化?既然现在每个人都知道应该做无责事后分析,每个人都会声称自己在做。就像拥有良好的测试和部署实践一样。我很幸运地在一个从未被呼叫过的值班轮换中,但当我与最近加入并在值班的人交谈时,他们有一些不太好的故事——指责、垃圾话和推卸责任。每个人都知道应该无责这一事实,似乎反而使指出有责行为变得更加困难,而不是更容易。
快速前进而不破坏 SLO
- 错误预算:100% 对几乎所有东西来说都是错误的可靠性目标
- 从 5 个 9 到 100% 可靠性对大多数用户来说不可感知,且需要巨大努力
- 设定一个承认权衡并留出错误预算的目标
- 错误预算可以用于任何事情:发布功能等
- 错误预算允许讨论分阶段发布和 1% 实验如何维持可容忍的错误水平
- SRE 团队的目标不是"零停机"——SRE 和产品开发的目标一致:花错误预算获得最大功能速度
虽然没有明确说明,但对于需要"快速前进"的团队来说,持续远低于错误预算可能被视为团队在可靠性上花费过多努力的信号。
我非常喜欢这个想法,但当我与 Jessica Kerr 讨论时,她对这一想法提出了质疑——也许你只是运气好所以低于错误预算,而一个真正糟糕的事件就可能消耗掉你未来十年的错误预算。后续问题:你如何对你的风险模型有足够的信心,以至于可以有意消耗错误预算来加快速度,而不用担心后续的坏事件会让你超支?Nat Welch(前 Google SRE)回应说,可以通过模拟灾难和其他测试来建立信心。
监控
- 监控永远不应该要求人类解释告警领域的任何部分
- 三种有效的监控输出:
- 告警:人类需要立即采取行动
- 工单:人类最终需要采取行动
- 日志:无需行动
- 注意,例如图表是日志的一种
应急响应
- 可靠性是 MTTF(平均故障间隔时间)和 MTTR(平均恢复时间)的函数
- 评估响应能力时,我们关心 MTTR
- 人类增加延迟
- 不需要人类响应的系统因 MTTR 更低而具有更高的可用性
- 拥有"操作手册"可产生 3 倍更低的 MTTR
- 拥有万能的英雄人物可以响应所有事情是可行的,但有操作手册效果更好
我个人同意,但我们确实喜欢我们的值班英雄。我想知道我们如何培养文档文化。
变更管理
- 70% 的停机由实时系统中的变更引起。缓解措施:
- 实施渐进式发布
- 监控
- 回滚
- 将人类从流程中移除,避免重复性任务上的标准人类问题
需求预测和容量规划
- 直截了当,但令人惊讶的是很多团队/服务不这样做
资源配置
- 添加容量比负载转移风险更大,因为通常涉及启动新实例/位置,对现有系统进行重大更改(配置文件、负载均衡器等)
- 成本高到应该只在必要时才做;必须快速执行
- 如果你不知道实际需要什么而过度配置,会浪费钱
效率和性能
- 负载使系统变慢
- SRE 配置容量以满足特定响应时间目标的容量目标
- 效率 == 金钱
第 3 章:拥抱风险
- 例如:如果用户的智能手机有 99% 的可靠性,他们无法区分 99.99% 和 99.999% 的可靠性
管理风险
- 可靠性与成本不是线性的。获得一个额外的可靠性增量可能轻松花费 100 倍
- 冗余设备的成本
- 为可靠性构建功能而非"普通"功能的成本
- 目标:让系统足够可靠,但不要过于可靠!
衡量服务风险
- 标准做法:确定一个指标来代表要优化的系统属性
- 可能的指标 = 正常运行时间 /(正常运行时间 + 停机时间)
- 对全球分布式服务有问题。正常运行时间到底意味着什么?
- 聚合可用性 = 成功请求 / 总请求
- 显然,并非所有请求都是平等的,但聚合可用性是一个可以接受的一阶近似
- 通常设置季度目标
服务的风险容忍度
- 通常不是客观上显而易见的
- SRE 与产品负责人合作,将业务目标转化为明确的目标
目标可用性
- 以 Bigtable 为例:
- 一些消费者服务直接从 Bigtable 提供数据——需要低延迟和高可靠性
- 一些团队使用 Bigtable 作为离线分析的后端存储——更关心吞吐量而非可靠性
- 太昂贵了,无法通用满足所有需求
- 例如 Bigtable 实例:
- 低延迟 Bigtable 用户想要低队列深度
- 吞吐量导向的 Bigtable 用户想要中等到高队列深度
- 这两种情况下的成功和失败是完全对立的!
成本
- 分区基础设施并提供不同的服务级别
- 除了明显的好处外,还允许服务将提供不同服务级别的成本外部化(例如,预期延迟导向的服务比吞吐量导向的服务更昂贵)
第 4 章:服务水平目标
Chubby 计划停机
- Google 发现 Chubby 始终超过其 SLO,而全球性的 Chubby 停机会导致 Google 异常严重的停机
- Chubby 如此可靠以至于团队错误地假设它永远不会宕机,未能设计考虑 Chubby 故障的系统
- 解决方案:当 Chubby 在一个季度内远超其 SLO 时,主动将其关闭,向团队"展示" Chubby 也可能宕机
你和你的用户关心什么?
- 太多指标:难以关注
- 太少指标:可能忽略重要行为
- 不同类别的服务应该有不同的指标
- 面向用户:可用性、延迟、吞吐量
- 存储:延迟、可用性、持久性
- 大数据:吞吐量、端到端延迟
- 所有系统都关心正确性
收集指标
- 通常可以从服务器端自然获取,但有时需要客户端指标
聚合
- 使用分布而非平均值
- 用户研究表明,人们通常更喜欢更慢的平均值但更好的尾部延迟
- 标准化通用定义,例如 1 分钟内的平均值、集群中任务的平均值等
- 可以有例外,但合理的默认值使事情更容易
选择目标
- 不要基于当前性能选择目标
- 当前性能可能需要英雄般的努力
- 保持简单
- 避免绝对化
- 讨论"无限"规模或"始终"可用是不合理的
- 最小化 SLO 数量
- 完美可以等待
- 总是可以随时间重新定义 SLO
- SLO 设定预期
- 保留安全余量(内部 SLO 可以比外部 SLO 定义得更宽松)
- 不要超额完成
- 参见上面 Chubby 的例子
- 另一个例子是确保系统在轻负载下不会太快
第 5 章:消除苦力活
Carla Geisser:"如果人类操作员需要在正常操作期间触碰你的系统,那你就有了一个 bug。'正常'的定义会随着系统的增长而改变。"
- 苦力活的定义:
- 不是"我不想做的工作"
- 手动的
- 重复的
- 可自动化的
- 战术性的
- 没有持久价值
- O(n) 随服务增长
- 调查显示平均 33% 是苦力活
- 数字可以低至 0%,高至 80%
- 苦力活 > 50% 是管理者应更均匀分配苦力活负载的信号
- 苦力活总是坏事吗?
- 可预测和重复的任务可以让人放松
- 可以产生成就感,可以是低风险/低压力的活动
第 6 章:监控分布式系统
为什么要监控?
- 分析长期趋势
- 随时间比较或做实验
- 告警
- 构建仪表板
- 调试
正如 Alex Clemmer 常说的,我们的问题不是走得太慢,而是构建了错误的东西。我想知道如何从现在的位置出发,拥有足够的 instrumentation,以便在构建新系统时能做出知情的决策。
设定合理期望
- 监控不是简单的
- 10-12 人的 SRE 团队通常有 1-2 人构建和维护监控
- 这个数字随着工具/库/集中式监控基础设施的改进而减少
- 总体趋势是向更简单/更快的监控系统发展,配合更好的事后分析工具
- 避免"魔法"系统
- 复杂的依赖层次(例如"如果数据库慢,告警数据库,否则告警网站")的成功有限
- 主要(仅?)用于系统非常稳定的部分
- 为人类生成告警的规则应该简单易懂,代表明确的故障
避免"魔法"包括避免机器学习吗?
- 大量白盒监控
- 一些黑盒监控用于关键内容
- 四个黄金信号:
- 延迟
- 流量
- 错误
- 饱和度
第 7 章:Google 的自动化演进
- "自动化是力量倍增器,不是万灵药"
- 自动化的价值:
- 一致性
- 可扩展性
- MTTR(平均恢复时间)
- 更快的非修复操作
- 节省时间
第 8 章:发布工程
- 这是 Google 的一个特定工作职能
发布工程师的角色
- 发布工程师与软件工程师和 SRE 合作,定义软件如何发布
- 允许开发团队专注于开发工作
- 定义最佳实践
- 编译器标志、构建 ID 标签的格式等
- 发布自动化
- 团队之间的模型各不相同
- 可以是"绿色即推送"并部署每个构建
- 可以是每小时构建和部署
- 等等
- 封闭式构建
- 构建相同的修订版本号应始终给出相同的结果
- 自包含——包括对编译器在内的所有版本的版本控制
- 可以针对旧修订版本挑选修复来修复生产软件
- 几乎所有变更都需要代码审查
- 分支
- 所有代码在主分支
- 发布从主分支切出
- 修复可以从 master 到分支
- 分支永远不合并回去
- 测试
- CI
- 发布过程创建审计跟踪,运行测试并显示测试通过
- 配置管理
- 看似简单,可能导致不稳定
- 多种方案(都涉及将配置存储在源代码控制中并进行严格配置审查)
- 主线配置——配置在 head 维护并立即应用
- 最初用于 Borg(以及前 Borg 系统)
- 二进制发布和配置变更解耦!
- 将配置文件和二进制文件包含在同一包中
- 简单
- 紧密耦合二进制和配置——适用于配置文件很少或配置很少变更的项目
- 将配置打包成"配置包"
- 与代码相同的封闭式原则
- 发布工程不应是事后才考虑的!
- 在开发周期开始时预算资源
第 9 章:简单性
- 稳定性与敏捷性
- 可以通过冻结来使事物稳定——需要平衡两者
- 可靠的系统可以增加敏捷性
- 可靠的发布使得更容易将变更与 bug 关联
- 无聊的美德!
- 本质复杂性与偶然复杂性
- SRE 应在引入偶然复杂性时予以抵制
- 代码是负债
- 删除死代码或其他臃肿
- 最小化 API
- 更小的 API 更容易测试,更可靠
- 模块化
- API 版本控制
- 与代码相同,你会避免 misc/util 类
- 发布
- 小发布更容易衡量
- 如果同时发布 100 个变更,无法判断发生了什么
第 10 章:从时间序列数据告警
Borgmon
- 类似 Prometheus
- 通用日志数据格式
- 数据用于仪表板和告警
- 形式化了遗留数据格式 "varz",允许通过 HTTP 查看指标
- 手动查看指标:http://foo/varz
- 添加指标只需在代码中进行一次声明
- 添加新指标的用户成本很低
- Borgmon 定期从每个目标获取 /varz
- 还包括合成数据,如健康检查、名称是否已解析等
- 时间序列竞技场
- 数据存储在内存中,并检查点到磁盘
- 固定大小的分配
- GC 在满时过期最旧的条目
- 概念上是一个二维数组,一个轴是时间,另一个轴是项目
- 每个数据点 24 字节 → 1M 个唯一时间序列,12 小时,1 分钟间隔 = 17 GB
Borgmon 规则
- 代数表达式
- 从其他时间序列计算时间序列
- 规则在线程池上并行评估
计数器 vs 仪表
- 计数器:非递减的
- 仪表:可以取任何值
- 计数器优于仪表,因为仪表可能会根据采样间隔丢失信息
告警
- Borgmon 规则可以触发告警
- 有最小持续时间以防止"抖动"
- 通常设置为两个持续时间周期,以便遗漏的收集不会触发告警
扩展
- Borgmon 可以从其他 Borgmon 获取时间序列数据(使用二进制流协议而非基于文本的 varz 协议)
- 可以有多层过滤器
探测器
- 黑盒监控,监控用户看到的内容
- 可以用 varz 查询或直接发送告警给 Alertmanager
配置
- 规则定义与监控目标之间的分离
第 11 章:值班
典型响应时间
- 面向用户或其他时间关键任务:5 分钟
- 不太敏感的事情:30 分钟
响应时间与 SLO 关联
- 例如:季度 99.99% 是 13 分钟的停机时间;显然响应时间不能超过 13 分钟
- SLO 更宽松的服务可以有 10 分钟级别(或更多?)的响应时间
主要 vs 次要值班
- 工作分配因团队而异
- 在某些团队,次要可以是主要的备份
- 在其他团队,次要处理非紧急/非分页事件,主要处理分页
平衡的值班
- 数量定义:值班时间百分比
- 质量定义:值班期间发生的事件数
这很好。我们应该这样做。人们有时会连续几次值班轮换非常糟糕,考虑到值班轮换的低频率,没有理由期望这会在一两年内随机平衡。
数量上的平衡
- >= 50% 的 SRE 时间用于工程
- 剩余时间中,不超过 25% 用于值班
- 首选多站点团队
- 夜班对健康有害,多站点团队允许消除夜班
质量上的平衡
- 平均而言,处理一个事件(包括根因分析、修复、编写事后分析、修复 bug 等)需要 6 小时
- => 12 小时值班轮班中不应超过 2 个事件
- 要保持在上限内,需要非常平坦的分页分布,中位数为 0
- 补偿:值班的额外薪酬(休假或现金)
第 13 章:应急响应
测试引发的紧急情况
- SRE 故意破坏系统看看会发生什么
- 例如:想要发现分布式 MySQL 数据库上隐藏的依赖关系
- 计划:阻止对 1/100 数据库的访问
- 响应:依赖服务报告无法访问关键系统
- SRE 响应:中止演练,尝试回滚权限变更
- 回滚尝试失败
- 恢复副本访问成功
- 1 小时恢复正常运行
- 做得好的地方:依赖团队立即升级问题,能够恢复访问
- 学到的教训:对系统及其与其他系统的交互了解不足,未能遵循本应通知客户停机的事件响应流程,未在测试环境中测试回滚程序
变更引发的紧急情况
- 变更可能导致故障!
- 例如:周五推送的滥用防护基础设施配置变更触发了崩溃循环 bug
- 几乎所有面向外部的系统都依赖此基础设施,变得不可用
- 许多内部系统也有依赖,变得不可用
- 告警在几秒内开始触发
- 在配置推送后 5 分钟内,推送变更的工程师回滚了变更,服务开始恢复
- 做得好的地方:监控立即触发,事件管理运作良好,带外通信系统让人们保持最新状态即使许多系统已宕机,运气好(推送变更的工程师正在关注实时通信渠道,这并不是发布流程的一部分)
- 学到的教训:推送到金丝雀没有触发相同问题,因为它没有命中特定的配置关键字组合;推送被认为是低风险的,经过了不太严格的金丝雀流程;停机期间告警太嘈杂
第 16 章:跟踪停机
- Escalator:集中式系统,跟踪对告警的确认,必要时通知其他人等
- Outalator:提供多个队列通知的时间交错视图
- 还保存相关电子邮件,允许将某些消息标记为"重要",可以折叠非重要消息等
我们的 Escalator 版本似乎还不错。但我们真的很需要一个像 Outalator 这样的东西。
第 18 章:SRE 中的软件工程
Auxon:容量规划自动化工具
- 传统容量规划周期:
- 收集需求预测(提前数季度到数年)
- 规划分配
- 审查计划
- 部署和配置资源
- 传统方法的缺点:
- 很多事情可能影响计划:效率提升、采用率增加、集群交付日期延迟等
- 即使小变更也需要重新检查分配计划
- 大变更可能需要完全重写计划
- 劳动密集且容易出错
- Google 的解决方案:基于意图的容量规划
- 指定需求,而非实现
- 编码需求并自动生成容量计划
- 除了节省劳动力,求解器可以比人工生成的方案做得更好 → 节省成本
- 意图驱动的层次示例:
- 想在集群 X、Y 和 Z 中有 50 个核心——为什么是那些资源在那些集群?
- 想在区域中任何 3 个集群中有 50 个核心的配置——为什么那么多资源和为什么 3 个?
- 想以 N+2 冗余满足需求——为什么 N+2?
- 想要 5 个 9 的可靠性。可能发现,例如 N+2 不够
- 发现最大收益来自达到第 (3) 层
- 一些复杂的服务可能追求第 (4) 层
- 将约束放入工具中允许权衡在整个机群中保持一致
- 而非做出个别的临时决策
- Auxon 输入:
- 需求(例如"服务必须每个大陆 N+2","前端服务器距离后端服务器不超过 50ms")
- 依赖
- 预算优先级
- 性能数据(服务如何扩展)
- 需求预测数据(注意像 Colossus 这样的服务从依赖服务派生预测)
- 资源供应和定价
- 输入进入求解器(混合整数或线性规划求解器)
第 20 章:数据中心内的负载均衡
流控制
- 需要避免不健康的任务
- 不健康任务的朴素流控制:
- 跟踪到后端的请求数
- 当达到阈值时将后端视为不健康
- 缺点:通常很糟糕
- 基于健康的流控制
- 后端任务可以处于三种状态之一:{健康, 拒绝连接, 跛脚鸭}
- 跛脚鸭状态仍可接受连接,但向所有客户端发送反压请求
- 跛脚鸭状态简化了干净关闭
- 子集化定义:限制客户端任务可交互的后端任务池
- RPC 系统中的客户端维护到后端的连接池
- 使用连接池比按需进行设置/拆除减少延迟
- 不活跃的连接相对便宜,但并非免费
选择正确的子集
- 典型:20-100,根据工作负载选择
- 随机子集选择:利用率差
- 轮转子集选择:顺序是打乱的;每轮有自己的排列
负载均衡
- 子集选择用于连接均衡,但仍需平衡负载
- 轮循负载均衡:
- 实践中,最高负载和最低负载之间有 2 倍差异
- 实践中,最昂贵的请求可以比最便宜的请求贵 1000 倍
- 此外,请求中还有随机不可预测的变化
- 最少已连接轮循:
- 顾名思义:在最少已连接的后端之间轮循
- 负载似乎以连接数衡量;可能不总是最佳指标
- 这是每个客户端的,不是全局的,所以可能向来自其他客户端有很多请求的后端发送请求
- 实践中,对于大型服务,发现最高负载任务使用的 CPU 是最低负载任务的两倍;与普通轮循类似
- 加权轮循:
- 与上述相同,但用其他因素加权
- 实践中,比最少已连接轮循有更好的负载分布
我想知道 Heroku 在回应 Rap Genius 时说"经过广泛的研究和实验,我们尚未找到任何理论模型或实际实现能够超越随机路由到支持多个并发连接的 Web 后端的简单性和鲁棒性"是什么意思。
第 21 章:处理过载
- 即使有"好的"负载均衡,系统也会过载
- 典型策略是提供降级的响应,但在非常高负载下可能做不到
- 将容量建模为 QPS 或请求的函数(例如请求读取多少个键)是容易出错的
- 这些通常缓慢变化,但可能快速变化(例如因为一次代码提交)
- 更好的解决方案:直接测量可用资源
- CPU 利用率通常是一个好的配置信号
- 对于 GC,内存压力转化为 CPU 利用率
- 对于其他系统,可以配置其他资源使 CPU 可能成为限制因素
- 在过度配置 CPU 太昂贵的情况下,考虑其他资源
像这样一般地过度配置 CPU 要花多少钱?
客户端节流
- 当客户达到配额时后端开始拒绝请求
- 即使被拒绝,请求仍然使用资源——没有节流的话,后端可能将大部分资源花在拒绝请求上
关键性
- 看起来像是优先级但换了个名字?
- RPC 系统中的一等概念
- 客户端节流为每个关键性级别保持单独的统计
- 默认情况下,关键性通过后续 RPC 传播
处理过载错误
- 如果 DC 过载,将负载分流到其他 DC
- 如果 DC 正常但一些后端过载,将负载分流到其他后端
- 客户端收到过载响应时重试
- 每个请求的重试预算 (3)
- 每个客户端的重试预算 (10%)
- 客户端的失败重试导致向下游返回"过载;不要重试"响应
有"不要重试"的响应是"显而易见"的,但在实践中相对罕见。很多真实系统存在失败重试导致上游更多重试的问题。这在跨越硬件/软件边界时尤其明显(例如文件系统读取导致 DVD/SSD/机械硬盘上的多次重试,失败后在文件系统级别被重试),但在纯软件中似乎也普遍存在。
第 22 章:应对级联故障
典型故障场景
- 服务器过载
- 例如:有两台服务器
- 一台过载,开始失败
- 另一台现在获得所有流量,也开始失败
- 例如:有两台服务器
- 资源耗尽
- CPU/内存/线程/文件描述符等
- 例如:资源之间的依赖关系
- Java 前端有调优不佳的 GC 参数
- 前端因 GC 耗尽 CPU
- CPU 耗尽使请求变慢
- 增加的队列深度使用更多内存
- 整个前端的固定内存分配意味着可用于缓存的内存减少
- 命中率降低
- 更多请求到后端
- 后端耗尽 CPU 或线程
- 健康检查失败,开始级联故障
- 在停机期间很难确定原因
- 注意:避免服务错误的服务器的策略可能使事情更糟
- 可用的后端更少,获得过多请求,然后也不可用
防止服务器过载
- 负载测试!必须有真实环境
- 提供降级结果
- 过载时尽早廉价地失败
- 让高层系统拒绝请求(在反向代理、负载均衡器和任务级别)
- 进行容量规划
队列管理
- 队列在稳态下什么也不做
- 排队的请求消耗内存并增加延迟
- 如果流量相对稳定,最好保持较小的队列大小(例如线程池大小的 50% 或更少)
- 例如:Gmail 使用无队列服务器,在线程满时进行故障转移
- 对于突发性工作负载,队列大小应是线程数、每请求时间和突发大小/频率的函数
- 另见 自适应 LIFO 和 CoDel
优雅降级
- 注意测试优雅降级路径很重要,也许通过定期在接近过载的情况下运行一小部分服务器,因为这条路径在正常情况下很少被使用
- 最好保持简单易懂
重试
- 始终使用随机化指数退避
- 参见前一章关于仅在单个级别重试的内容
- 考虑拥有服务器级别的重试预算
截止时间
- 不要在截止时间已过时继续工作(级联故障的常见主题)
- 在每个阶段检查截止时间是否已被命中
- 截止时间应该传播(例如,甚至通过 RPC)
双模延迟
- 例如:长截止时间的问题
- 假设前端有 10 台服务器,每台 100 个线程(总共 1K 线程容量)
- 正常运行:1K QPS,请求耗时 100ms → 占用 100 个工作线程(1K QPS × 0.1s)
- 假设 5% 的操作未完成,截止时间为 100s
- 消耗 5K 线程(50 QPS × 100s)
- 前端超额 5 倍。成功率 = 1K / (5K + 95) = 19.6% → 80.4% 的错误率
使用截止时间而非超时很棒。我们应该更加系统地做这件事。
不允许系统被无意义的僵尸请求填满,通过设置合理的截止时间是"显而易见"的,但很多真实系统似乎有人为好记数字(30s、60s、100s 等)的任意超时,而不是考虑负载/级联故障而分配的截止时间。
级联故障的立即应对措施
- 增加资源
- 临时停止健康检查失败/死亡
- 重启服务器(仅在有帮组时——例如 GC 死亡螺旋或死锁)
- 丢弃流量——激进的,最后手段
- 进入降级模式——需要事先在服务中构建此功能
- 消除批量负载
- 消除不良流量
第 23 章:面向可靠性的分布式共识
核心问题
- 我们如何就以下问题达成一致:
- 哪个进程是一组进程的领导者?
- 一组中的进程集合是什么?
- 消息是否已成功提交到分布式队列?
- 进程是否持有特定租约?
- 数据存储中特定键的值是什么?
分裂脑示例
- 服务在不同机架中有复制的文件服务器
- 必须避免同时写入一对文件服务器以防止数据损坏
- 每对文件服务器有一个领导者和一个跟随者
- 服务器通过心跳监控彼此
- 如果一台服务器无法联系另一台,它发送 STONITH(射杀另一个节点)
- 但如果网络慢或丢包怎么办?两台服务器都发出 STONITH 怎么办?
这让我想起了我最喜欢的分布式数据库事后分析之一。数据库配置为环形,每个节点与 5 台服务器的"邻域"通信并复制数据。如果邻域中的一些机器宕机,其他服务器加入邻域,数据得到适当复制。听起来不错,但当一台服务器出故障并决定没有数据存在且所有邻居都有问题时,它可以比任何邻居更快地返回结果,还可以告诉邻居它们都有问题。因为坏服务器没有数据所以非常快,可以比邻居报告它有问题更快地报告邻居有问题。糟糕!
故障转移需要人工干预
- 高度分片的 DB 每个分片有一个主节点,复制到另一个 DC 中的次节点
- 外部健康检查决定主节点是否应故障转移到其次节点
- 如果主节点无法看到次节点,它使自己不可用以避免"分裂脑"中的问题
- 这增加了运维负载
- 问题相关联,这在人们忙于其他问题时相对可能遇到问题
- 如果有网络问题,没有理由认为人类会比系统中的机器对世界状态有更好的视野
不可能性结果
- CAP:P 在真实网络中不可能,所以选择 C 或 A
- FLP:异步分布式共识在有不可靠网络时不能保证进展
Paxos
- 一系列提案,可能被多数进程接受或拒绝
- 未被接受 → 失败
- 每个提案的序列号,必须在系统中唯一
- 提案过程:
- 提议者向接受者发送序列号
- 接受者如果没看到更高的序列号就同意
- 提议者可以用更高的序列号重试
- 如果提议者收到多数同意,通过发送带值的提交消息来提交
- 接受者在接受时必须日志记录到持久存储
模式
- 分布式共识算法是低层原语
- 可靠的复制状态机
- 数据配置/存储、锁定、领导者选举等的基本构建块
- 可靠的复制数据和配置存储
- 非分布式共识系统通常使用时间戳:有问题,因为无法保证时钟同步
- 参见 Spanner 论文作为使用分布式共识的例子
- 领导者选举
- 等价于分布式共识
- 当领导者的工作可以由一个进程完成或分片时,领导者选举模式允许像编写简单程序一样编写分布式系统
- GFS 和 Colossus 使用
- 分布式协调和锁定服务
- 屏障,例如在 MapReduce 中确保 Map 在 Reduce 继续之前完成
- 分布式队列和消息传递
- 队列可以容忍工作节点的故障,但系统需要确保认领的任务被处理
- 可以使用租约而非从队列中移除
- 使用 RSM 意味着即使队列宕机,系统也可以继续处理
性能
- 共识算法不能用于高吞吐量低延迟系统的传统观念是错误的
- 分布式共识是许多 Google 系统的核心
- 规模使这对 Google 来说比大多数其他公司更糟,但它仍然有效
Multi-Paxos
- 强领导者进程:除非尚未选出领导者或发生故障,只需一次往返即可达成共识
- 注意组中的其他进程可以随时提案
- 可能来回乒乓和伪活锁
- 标准解决方案是选出提议者进程或使用轮换提议者
扩展读密集型工作负载
- 例如:Photon 允许从任何副本读取
- 从陈旧副本读取需要额外工作,但不会产生不正确的结果
- 要保证读取是最新的,执行以下之一:
- 执行只读共识操作
- 从保证是最新的副本读取(稳定领导者可以提供此保证)
- 使用法定人数租约
Fast Paxos
- 设计用于在 WAN 上更快
- 每个客户端可以直接向一组接受者发送 Propose,而不是通过领导者
- 不一定比经典 Paxos 更快——如果到接受者的 RTT 很长,我们用一次慢链路消息加 N 次并行快链路消息交换了 N 次慢链路消息
稳定领导者
- "几乎所有为性能而设计的分布式共识系统都使用单一稳定领导者模式或轮换领导系统"
第 25 章:数据处理管道
- 例如 MapReduce 或 Flume
- 在正常情况下方便且容易推理,但脆弱
- 初始安装通常没问题,因为工作器大小、分块、参数都经过仔细调整
- 随着时间推移,负载变化,导致问题
第 26 章:数据完整性
数据完整性的定义
- 定义不一定显而易见
- 如果界面 bug 导致 Gmail 无法显示消息,从用户的角度来看这与数据丢失相同
- 99.99% 正常运行时间意味着每年 1 小时停机。对大多数应用可能可以
- 99.99% 好字节在 2GB 文件中意味着 200K 损坏。对大多数应用可能不行
备份不简单
- 可能有事务性和非事务性备份和恢复的混合
- 不同版本的业务逻辑可能同时在线
- 如果服务独立版本控制,可能有许多版本组合
- 副本不够——副本可能同步损坏
Google 19 次数据恢复工作的研究
- 最常见的用户可见数据丢失由删除或因软件 bug 导致的引用完整性丢失引起
- 最困难的情况是在数周到数月后才发现的低级别损坏
纵深防御
- 第一层:软删除
- 用户应该能够删除他们的数据
- 但这意味着用户会意外删除他们的数据
- 账户劫持等
- 意外删除也可能因 bug 发生
- 软删除将实际删除延迟一段时间
- 第二层:备份
- 需要确定恢复时丢失多少数据是可以接受的,恢复需要多长时间,备份需要追溯到多远
- 希望备份追溯到永远,因为损坏可能数月(或更长)未被发现
- 但代码和 schema 的变更可能使旧备份的恢复变得昂贵
- Google 通常有 30 到 90 天的窗口,取决于服务
- 第三层:早期检测
- 带外完整性检查
- 做好很难!
- 正确的变更可能导致检查器失败
- 但放松检查可能导致遗漏故障