《SRE:Google运维解密》-笔记

序言

  • 可靠性就像安全性,越早关注越好。
  • 这就意味着一些小型创业公司,在应付日常面临的种种挑战时,也应该抽出一部分精力来面对可靠性这个话题。这与盖房子有些类似,如果一开始将整个地基打好并保持继续修缮,要比盖好房子之后再重新修改设计要容易得多
  • Margaret 曾经说过:“无论对一个软件系统运行原理掌握得多么彻底,也不能阻止人犯意外错误。”
  • 只有靠着对细节的不懈关注,做好充足的灾难预案和准备工作,时刻警惕着,不放过一切机会去避免灾难发生。这就是SRE 最重要的理念!

第1章 介绍

  • 不能将碰运气当成战略。

Google的解决之道:SRE

  • SRE就是让软件工程师来设计一个新型运维团队的结果。

  • 目前来看,UNIX 系统内部细节和1~3层网络知识是Google最看重的两类额外的技术能力。

    • (a)对重复性、手工性的操作有天然的排斥感。
    • (b)有足够的技术能力快速开发出软件系统以替代手工操作。
  • 从本质上来说,SRE 就是在用软件工程的思维和方法论完成以前由系统管理员团队手动完成的任务。这些SRE倾向于通过设计、构建自动化工具来取代人工操作。

  • Google的经验法则是,SRE团队必须将50%的精力花在真实的开发工作上。

  • 由于SRE模型中为了提高可靠性需要采取一些与常规做法违背的做法,所以需要强有力的管理层支持才能推行下去。例如:由于一个季度内的错误预算耗尽而停止发布新功能的决定,可能需要管理层的支持才能让产品研发部门重视起来。

  • 我们可以认为DevOps是SRE核心理念的普适版,可以用于更广范围内的组织结构、管理结构和人员安排。同时,SRE是DevOps模型在Google的具体实践,带有一些特别的扩展。

SRE方法论

  • 一般来说,SRE团队要承担以下几类职责:可用性改进,延迟优化,性能优化,效率优化,变更管理,监控,紧急事务处理以及容量规划与管理。

  • 事后总结应该包括以下内容:事故发生、发现、解决的全过程,事故的根本原因,预防或者优化的解决方案。

  • Google 的一项准则是“对事不对人”,事后总结的目标是尽早发现和堵住漏洞,而不是通过流程去绕过和掩盖它们。

  • 一般来说,任何软件系统都不应该一味地追求100% 可靠。因为对最终用户来说,99.999% 和 100% 的可用性是没有实质区别的

  • 如果100% 不是一个正确的可靠性目标,那么多少才是呢?这其实并不是一个技术问题,而是一个产品问题。

  • 如果一个服务的可靠性目标是99.99%,那么错误预算就是 0.01%。这意味着产品研发部门和SRE部门可以在这个范围内将这个预算用于新功能上线或者产品的创新等任何事情。错误预算可以用于什么范畴呢?研发团队需要用这个预算上线新功能,吸引新用户。理想情况下,我们应该使用错误预算来最大化新功能上线的速度,同时保障服务质量。这个基本模型建立起来之后,许多常见的战术策略,例如灰度发布、1% AB测试等就全说得通了。这些战术性手段都是为了更合理地使用整个服务的错误预算。通过引进“错误预算”的概念,我们解决了研发团队和SRE团队之间的组织架构冲突。SRE团队的目标不再是 “零事故运行”,SRE团队和产品研发团队目标一致,都是在保障业务服务可靠性需求的同时尽可能地加快功能上线速度。这个改动虽小,意义却很大。一次“生产事故”不再是一件坏事,而仅仅是创新流程中一个不可避免的环节,两个团队通过协作共同管理它。

  • 一个需要人工阅读邮件和分析警报来决定目前是否需要采取某种行动的系统从本质上就是错误的。监控系统不应该依赖人来分析警报信息,而是应该由系统自动分析,仅当需要用户执行某种操作时,才需要通知用户。

  • 一个监控系统应该只有三类输出。紧急警报(alert)意味着收到警报的用户需要立即执行某种操作,目标是解决某种已经发生的问题,或者是避免即将发生的问题。工单(ticket)意味着接受工单的用户应该执行某种操作,但是并非立即执行。系统并不能自动解决目前的情况,但是如果一个用户在几天内执行这项操作,系统不会受到任何影响。日志(logging)平时没有人需要关注日志信息,但是日志信息依然被收集起来以备调试和事后分析时使用。正确的做法是平时没人会去主动阅读日志,除非有特殊需要。

  • 任何需要人工操作的事情都只会延长恢复时间。一个可以自动恢复的系统即使有更多的故障发生,也要比事事都需要人工干预的系统可用性更高。当不可避免地需要人工介入时,我们也发现与“船到桥头自然直”的态度相比,通过事先预案并且将最佳方法记录在“运维手册(playbook)”上通常可以使MTTR 降低3倍以上。初期几个万能的工程师的确可以解决生产问题,但是长久看来一个手持“运维宝典”经过多次演习的on-call工程师才是正确之路。

  • 虽然不论多么完备的“运维手册”也无法替代人的创新思维,但是在巨大的时间压力和产品压力下,运维手册中记录的清晰调试步骤和分析方法对处理问题的人是不可或缺的。因此,Google SRE将大部分工作重心放在“运维手册”的维护上,同时通过“Wheel of Misfortune”等项目[2]不断培训团队成员。

  • 变更管理SRE的经验告诉我们,大概 70% 的生产事故由某种部署的变更而触发。变更管理的最佳实践是使用自动化来完成以下几个项目:● 采用渐进式发布机制。● 迅速而准确地检测到问题的发生。● 当出现问题时,安全迅速地回退改动。

  • 容量规划有几个步骤是必需的:● 必须有一个准确的自然增长需求预测模型,需求预测的时间应该超过资源获取的时间。● 规划中必须有准确的非自然增长的需求来源的统计。● 必须有周期性压力测试,以便准确地将系统原始资源信息与业务容量对应起来。

个人总结

  1. 尽早关注安全性;关注细节和充分的准备
  2. 自动化工具替代人工操作
  3. 事后总结的若干要点;事后总结“对事不对人”
  4. 使用错误预算来最大化新功能上线的速度;目标不再是 “零事故运行”
  5. 自动恢复的系统;事先预案和“运维手册;自动化减少人为事故
  6. 容量规划;周期性压力测试和调整

第3章 拥抱风险

  • 你可能认为Google会试图构建一个百分之百可靠的服务。事实证明,超过一定值后,再提高可靠性对于一项服务(和它的用户)来说,结果可能会更差而不是更好!极端的可靠性会带来成本的大幅提升:过分追求稳定性限制了新功能的开发速度和将产品交付给用户的速度,并且很大程度地增加了成本,这反过来又减少了一个团队可以提供的新功能的数量。

  • 此外,用户通常不会注意到一项服务在高可靠性和极端可靠性之间的差异,因为用户体验主要是受较不可靠的组件主导,例如手机移动网络或者他们正在使用的设备。简单地说,用户在一个有着99%可靠性的智能手机上是不能分辨出99.99%和99.999%的服务可靠性的区别的!基于这一点,SRE旨在寻求快速创新和高效的服务运营业务之间的风险的平衡,而不是简单地将服务在线时间最大化。这样一来,我们可以优化用户的整体幸福感,平衡系统的功能、服务和性能。

管理风险

  • 不可靠的系统会很快侵蚀用户的信心,所以我们想要减少系统出故障的几率。然而,经验表明,在构建系统的过程中,可靠性进一步提升的成本并不是线性增加的—可靠性的下一个改进可能比之前的改进成本增加100倍。

度量服务的风险

  • Google标准做法是通过一个客观的指标来体现一个待优化的系统属性。

  • 可用性=系统正常运行时间/(系统正常运行时间+停机时间)

  • 在Google内部,基于时间的可用性通常是毫无意义的。
    我们通过请求成功率来定义服务可用性。
    可用性=成功请求数/总的请求数

  • 使用请求成功率指标量化计划外停机时间使得这种指标更适合在不直接服务终端用户的系统中使用。

  • 通常,我们会为一项服务设定季度性的可用性目标,每周甚至每天对性能进行跟踪。我们通过寻找、跟踪和调整重要的、不可避免的偏差来使服务达到一个高层次的可用性目标。

服务的风险容忍度

  • 为了辨别服务的风险容忍度,SRE必须与产品负责人一起努力,将一组商业目标转化为明确的可以实现的工程目标。

  • 尽管当时YouTube已经有了一个很出色的产品,但它仍然在不断变化和快速发展着。因此,我们为YouTube设定了一个相比我们企业的产品更低的可用性目标,因为快速发展更加重要。

  • 对于一项给定的服务的故障预期是另一个需要重点考虑的因素。我们的业务对于服务的停机时间的容忍程度有多高?持续的低故障率或者偶尔发生的全网中断哪一个会更糟糕?这两种类型的故障可能会导致绝对数量上完全相同的错误被返回,但可能对于业务的影响相差很大。下面这个例子说明了一个提供私人信息的系统中自然发生的完全和部分服务中断的区别。假设有一个联系人管理应用程序,一种情况是导致用户头像显示失败的间歇性故障,另一种情况是将A用户的私人联系列表显示给B用户的故障。第一种情况显然是一个糟糕的用户体验,SRE会努力去快速地解决这个问题。然而,在第二种情况下,暴露私人数据的风险可能会破坏基本的用户信任。因此,在第二种情况下,在进行调试和事后的数据清理时,完全停止该服务更加恰当。

  • 对于Google提供的其他服务,有时候,我们可以接受计划内的常规的服务中断。几年前,Ads前端曾经就是这样的一种服务。它是广告商和网站创建者用来建立、配置、运行和监控他们的广告活动的服务。因为这项工作大部分发生在正常工作时间内,我们认为维修窗口中发生的偶然的、正常的、计划之中的故障是可以接受的,并且我们把这些故障看作计划内停机时间,而不是计划外停机时间。

使用错误预算的目的

  • 产品研发的绩效是如何很大程度通过产品研发速度体现的,这会激励员工尽可能快地创建新的代码。同时, SRE 的绩效表现取决于该服务的可靠性,这意味着SRE 会对高频率的更改提出抗议。两个团队之间的信息不对称进一步加剧了这种内在的紧张局势。产品开发者更了解编写和发布他们的代码所需的时间,而SRE则更关注服务可靠性程度(以及生产环境中的其他相关事项)。

  • 软件对故障的容忍度对意外事件的容忍程度有多高?做得太少,我们就只能设计出一个脆弱无用的产品。做得太多,我们的产品可能没有人会使用(但运行非常稳定)。

  • 测试新发布的代码的最好做法就是在一个典型工作负载的服务子集中进行测试,这种做法通常被称为金丝雀测试。

  • 一项决策越是基于数据做出的,常常就越好。

  • 错误预算提供了一个明确的、客观的指标来决定服务在一个单独的季度中能接受多少不可靠性。这个指标在SRE与产品研发部门的谈判中将政治因素排除。

  • 我们的实际做法如下:● 产品管理层定义一个SLO,确定一项服务在每个季度预计的正常运行时间。● 实际在线时间是通过一个中立的第三方来测算的:我们的监控系统。● 这两个数字的差值就是这个季度中剩余的不可靠性预算。● 只要测算出的正常在线时间高于SLO,也就是说,只要仍然有剩余的错误预算,就可以发布新的版本。

  • 错误预算的主要好处就是它能够激励产品研发和SRE一起找出创新和可靠性之间合理的平衡点。

  • 只要系统符合SLO,就可以继续发行新版本。如果频繁地违反SLO 导致错误预算被耗尽,那么发布就会暂停,同时需要在系统测试和开发环节投入更多资源使得系统更有弹性,以使性能得到提升。

  • 有比这种简单的开/关技术更巧妙和有效的方法:[2]例如,当SLO 违规导致错误预算接近耗尽时,将发布的速度减慢,或者回退到上一版本。

  • 例如,如果产品研发人员想要在测试上节约时间或者想要提高发布速度并且SRE 表示反对时,那么就可以通过错误预算指导决策。当预算剩余很多时,产品研发人员就可以承担更多的风险。如果预算接近耗尽,产品研发人员自身将会推动更多的测试或者放慢发布的速度,因为他们不想冒着用尽预算的风险和拖延他们的程序上线。实际上,产品开发团队这样就开始进行自我监管。他们知道预算还剩多少,并且可以控制自己的风险。(当然,这要求SRE在SLO达不到的时候有权停止程序的发布。)如果网络中断或者数据中心发生故障影响了SLO,怎么办?这样的事件也会给错误预算带来不良的影响,会使本季度剩余部分的发布将会减少。整个团队会支持这种发布频率的降低,因为每个人都有义务保障服务正常运行。

  • 利用错误预算可以同时找到制定得过高的可用性目标,显示出它们所导致的灵活性和创新速度方面的问题。如果团队无法发布新的功能,他们可以选择降低SLO(从而增加错误预算)来提高创新速度。

  • 管理服务的可靠性主要在于管理风险,而且管理风险的成本可能很高。

  • ● 100%可能永远都不是一个正确的可靠性目标:不仅是不可能实现的,而且它通常比一项服务的用户期望的可靠性大得多。我们要将服务风险和愿意承担的业务风险相匹配。● 错误预算在SRE和产品研发团队之间调整激励,同时强调共同责任。错误预算使得讨论发布速率更容易,同时可有效地减少任何关于事故的讨论。这样,多个团队可以毫无怨言地对生产环境风险度达成一致。


第4章 服务质量目标

  • 如果不详细了解服务中各种行为的重要程度,并且不去度量这些行为的正确性的话,就无法正确运维这个系统,更不要说可靠地运维了

  • 我们需要利用一些主观判断结合过去的经验以及对服务的理解来定义一些服务质量指标(SLI)、服务质量目标(SLO),以及服务质量协议(SLA)。

服务质量术语

  • SLO的选择和公布可以帮助设立用户对服务质量的预期。

  • 该策略可以应对那些没有根据的抱怨—“服务太慢了”。如果没有一个明确的SLO,用户经常会按照自己的理解设置一个服务性能的预期,即使这可能跟运维人员或者设计者所想的完全不同。这种问题可能会导致对某个服务的过度依赖—用户错误地认为这个服务会比实际情况更可靠

  • 由于真正的全球Chubby服务故障出现的频率太低,以至于其他服务负责人开始认为全球Chubby服务永远不会出故障,从而不停地将更多的服务依赖于此。
    Chubby全球服务的高可靠性实际上提供了一种安全假象,因为这些服务实际上在Chubby全球服务不可用的时候不能正常工作,不管这种情况是多么罕见。

  • 每个季度,如果真实故障没有将可用性指标降低到SLO之下,SRE会有意安排一次可控的故障,将服务停机。利用这种方法,我们可以很快找出那些对Chubby全球服务的不合理依赖,强迫服务的负责人尽早面对这类分布式系统的天生缺陷。

  • 不管某个服务是否具有SLA,定义SLI与SLO,并且用它们来管理服务质量都是很有价值的。

指标在实践中的应用

  • 只有理解用户对系统的真实需求才能真正决定哪些指标是否有用。指标过多会影响对那些真正重要的指标的关注,而选择指标过少则会导致某些重要的系统行为被忽略。一般来说,四五个具有代表性的指标对系统健康程度的评估和关注就足够了。

  • 常见的服务,根据它们的相关SLI通常会归类为以下几个大类。● 用户可见的服务系统,例如莎士比亚搜索服务的前端服务器通常关心可用性、延迟,以及吞吐量。换句话说:是否能正常处理请求?每个请求花费的时间是多少?多少请求可以被处理?● 存储系统通常强调:延迟、可用性和数据持久性。换句话说:读写数据需要多少时间?我们是否可以随时访问数据?数据是否一段时间内还能被读取?扩展讨论参见第26章。● 大数据系统,例如数据处理流水线系统,一般来说关心吞吐量和端到端延迟。换句话说:处理了多少数据?数据从输入到产出需要多少时间?(某些流水线任务还会关注某个单独处理阶段的延迟。)● 所有的系统都应该关注:正确性。是否返回了正确的回复,是否读取了正确的数据,或者进行了正确的数据分析操作。正确性是系统健康程度的一个重要指标,但是它更关注系统内部的数据,而不是系统本身,所以这通常不是SRE直接负责的。指标的收集利用某种监控系统,大部分指标数据都在服务器端被收集,例如Borgmon(具体参见第10章)或者Prometheus。或者利用某种日志分析系统,例如分析日志中HTTP 500回复所占的比例。然而,某些系统可以加入对客户端数据的收集,否则可能会错失一些不影响服务器端指标,但是对用户产生影响的问题。例如,只关注莎士比亚服务器搜索后端的延迟可能会错失由页面JavaScript脚本导致的用户可见的延迟问题。在这个例子中,度量页面在浏览器中可用的延迟是度量用户体验的一个更好的指标。

  • 平均请求延迟可能看起来很简单,但是却掩盖了一个重要的细节;很可能大部分请求都是很快的,但是长尾请求速度却很慢。

  • 大部分指标都应该以“分布”,而不是平均值来定义。

  • 响应时间的分布越分散,意味着普通用户受到长尾请求延迟的影响就越明显,这可能预示了负载过高情况下出现的排队问题。

  • 一般来说,SRE更倾向于分析一组数据的百分比分布,而非其算术平均值。长尾效应比算术平均值更有特点,使用百分比分布能够更清晰地进行分析。因为计算机系统的本身特质决定,数据是具有特定分布特点的 ——例如,请求延迟必须大于0,同时如果超时设置为1000ms,则不可能有成功请求超过这个时间。因此,我们不能假设算术平均值和中位数是相等的——它们甚至可能相差甚远!

  • 汇总间隔:每1分钟汇总一次● 汇总范围:集群中的全部任务● 度量频率:每10秒一次● 包含哪些请求:从黑盒监控任务发来的HTTP GET请求● 数据如何获取:通过监控系统获取服务器端信息得到● 数据访问延迟:从收到请求到最后一个字节被发出

目标在实践中的应用

  • 我们应该从思考(或者调研)用户最关心的方面入手,而非从现在能度量什么入手。

  • 不要仅以目前的状态为基础选择目标

  • 了解系统的各项指标和限制非常重要,但是仅仅按照当前系统的标准制定目标,而不从全局出发,可能会导致团队被迫长期运维一个过时的系统,没有时间去推动架构重构等任务。

  • 避免绝对值虽然要求系统可以在没有任何延迟增长的情况下无限扩张,或者“永远”可用是很诱人的,但是这样的要求是不切实际的。就算有一个系统能够做到这一点,它也需要花很长时间来设计和构建,同时运维也很复杂—最关键的是,这可能比用户可以接受的(甚至是很开心地接受的)标准要高太多。

  • 不要追求完美我们可以随着时间流逝了解系统行为之后优化SLO的定义。刚开始可以以一个松散的目标开始,逐渐收紧。这比一开始制定一个困难的目标,在出现问题时放松要好得多。

  • 如果服务一切正常,可能力量应该花在其他的优先级上,例如消除技术债务、增加新功能,或者引入其他产品等。

个人总结

  1. 没有根据的抱怨—“服务太慢了”;指标制定
  2. 大部分指标都应该以“分布”,而不是平均值来定义。
  3. 如果服务一切正常,可能力量应该花在其他的优先级上,例如消除技术债务、增加新功能,或者引入其他产品等。

第5章 减少琐事

  • 如果系统正常运转中需要人工干预,应该将此视为一种Bug。

  • “正常”的定义会随系统的进步而不断改变。要把更多的时间花费在长期项目研发上而非日常运维中。因为术语日常运维可能会被误解,我们在这里使用一个专门的词语——琐事(toil)。

琐事的定义

  • 琐事不仅仅代表“我不喜欢做的工作”。

  • 到底什么是琐事?琐事就是运维服务中手动性的,重复性的,可以被自动化的,战术性,没有持久价值的工作。而且,琐事与服务呈线性关系的增长。并不是每件琐事都有以上全部特性,但是,每件琐事都满足下列一个或多个属性:手动性

  • 例如手动运行脚本以便自动执行一些任务。运行一个脚本可能比手动执行脚本中的每一步要快,但具体运行脚本所花费的手动的时间(而非脚本所需要的运行时间)应该被认为是琐事。重复性的如果某件事是第一次做,甚至第二次做,都不应该算作琐事。琐事就是不停反复做的工作。如果你正在解决一个新出现的问题或者寻求一种新的解决办法,不算作琐事。

  • 可以被自动化的如果计算机可以和人类一样能够很好地完成某个任务,或者通过某种设计变更来彻底消除对某项任务的需求,这项任务就是琐事。如果主观判断是必需的,那么很大程度上这项任务不属于琐事。[8]

  • 战术性的琐事是突然出现的、应对式的工作,而非策略驱动和主动安排的。处理紧急警报是琐事。我们可能永远无法完全消除这种类型的工作,但我们必须继续努力减少它。没有持久价值如果在你完成某项任务之后,服务状态没有改变,这项任务就很可能是琐事。如果这项任务会给服务带来永久性的改进,它就不是琐事。一些繁重的工作—比如挖掘遗留代码和配置并且将它们清理出去也不是琐事。与服务同步线性增长如果在工作中所涉及的任务与服务的大小、流量或用户数量呈线性增长关系,那这项任务可能属于琐事。一个良好管理和设计的服务应该至少可以应对一个数量级的增长,而不需要某些一次性工作(例如增加资源)之外的额外工作。

为什么琐事越少越好

  • SRE的一个公开目标是保持每个SRE的工作时间中运维工作(即琐事)的比例低于50%。SRE至少花50%的时间在工程项目上,

  • 以减少未来的琐事或增加服务功能。增加服务功能包括提高可靠性、性能,或利用率,同时也会进一步消除琐事。

什么算作工程工作

  • 工程工作通常是有创新性和创造性的,着重通过设计来解决问题,解决方案越通用越好。

琐事繁多是不是一定不好

  • 琐事不会总是让每个人都不开心,特别是不太多的时候。已知的和重复性的工作有一种让人平静的功效。完成这些事可以带来一种满足感和快速胜利感。琐事可能是低风险低压力的活动,有些员工甚至喜欢做这种类型的工作。

  • 琐事的存在并不总是坏事,但是每个人都必须清楚,在SRE所扮演的角色中,一定数量的琐事是不可避免的,这其实是任何工程类工作都具有的特点。少量的琐事存在不是什么大问题。但是一旦琐事的数量变多,就会有害了。如果琐事特别繁重,那就应该非常担忧,大声抱怨。在许多琐事有害的原因中,有如下因素需要考虑:职业停滞如果花在工程项目上的时间太少,你的职业发展会变慢,甚至停滞。Google确实会奖励做那些脏活累活的人,但是仅仅是该工作是不可避免,并有巨大的正面影响的时候才会这样做。没有人可以通过不停地做脏活累活满足自己的职业发展。

  • 士气低落每个人对自己可以承担的琐事限度有所不同,但是一定有个限度。过多的琐事会导致过度劳累、厌倦和不满。另外,牺牲工程实践而做琐事会对SRE组织的整体发展造成损害。

  • 原因如下:造成误解。我们努力确保每个SRE以及每个与SRE一起工作的人都理解SRE是一个工程组织。如果个人或者团队过度参与琐事,会破坏这种角色,造成误解。进展缓慢琐事过多会导致团队生产力下降。如果SRE团队忙于为手工操作和导出数据救火,新功能的发布就会变慢。开创先例如果SRE过于愿意承担琐事,研发同事就更倾向于加入更多的琐事,有时候甚至将本来应该由研发团队承担的运维工作转给SRE来承担。其他团队也会开始指望SRE接受这样的工作,这显然是不好的。促进摩擦产生即使你个人对琐事没有怨言,你现在的或未来的队友可能会很不开心。如果团队中引入了太多的琐事,其实就是在鼓励团队里最好的工程师开始寻找其他地方提供的更有价值的工作。违反承诺那些为了项目工程工作而新入职的员工,以及转入SRE的老员工会有被欺骗的感觉,这非常不利于公司的士气。

小结

  • 如果我们都致力于每一周通过工程工作消除一点琐事,就可以持续性地整顿服务。我们就可以将更多的力量投入到扩大服务规模的工程工作上去,或者是进行下一代的服务的架构设计,又或者是建立一套跨SRE使用的工具链。让我们多创新,少干琐事吧!

个人总结

  • 琐事应该尽量自动化,把时间创造性的事情上!

为什么要监控

  • 紧急警报的处理会占用员工的宝贵时间。如果该员工正在工作时间段,该警报的处理会打断他原本的工作流程。如果该员工正在家,紧急警报的处理则会影响他的个人生活,甚至是把他从睡眠中叫醒。当紧急警报出现得太频繁时,员工会进入“狼来了”效应,怀疑警报的有效性甚至忽略该警报,有的时候在警告过多的时候甚至会忽略掉真实发生的故障。由于无效信息太多,分析和修复可能会变慢,故障时间也会相应延长。高效的警报系统应该提供足够的信息,并且误报率非常低。

对监控系统设置合理预期

  • 但是监控系统中最重要的一点就是整个“生产故障,人工处理紧急警报,简单定位和深入调试”过程必须要保持非常简单,必须能被团队中任何一个人所理解。

现象与原因

  • 监控系统应该解决两个问题:什么东西出故障了,以及为什么出故障。

  • “现象”和“原因”的区分是构建信噪比高的监控系统时最重要的概念

黑盒监控与白盒监控

  • 黑盒监控是面向现象的,代表了目前正在发生的—而非预测会发生的—问题,即“系统现在有故障”。白盒监控则大量依赖对系统内部信息的检测,如系统日志、抓取提供指标信息的HTTP节点等。白盒监控系统因此可以检测到即将发生的问题及那些重试所掩盖的问题等。

  • 这里应该注意,在一个多层系统中,某一个服务的现象是另外一个服务的原因。

  • 白盒监控有时是面向现象的,有时是面向原因的,这取决于白盒系统所提供的信息。

  • 黑盒监控可以保证系统只在某个问题目前正在发生,并且造成了某个现象时才会发出紧急警报。

4个黄金指标

  • 监控系统的4个黄金指标分别是延迟、流量、错误和饱和度(saturation)。

  • 延迟增加是饱和度的前导现象。99% 的请求延迟(在某一个小的时间范围内,例如一分钟)可以作为一个饱和度早期预警的指标。

  • 如果我们度量所有这4个黄金指标,同时在某个指标出现故障时发出警报(或者对于饱和度来说,快要发生故障时),能做到这些,服务的监控就基本差不多了。

关于长尾问题

  • 区分平均值的“慢”和长尾值的“慢”的一个最简单办法是将请求按延迟分组计数(可以用来制作直方图):延迟为0~10ms之间的请求数量有多少,30~100ms之间,100~300ms之间等。

度量指标时采用合适的精度

  • 如果我们的监控目标需要高精度数据,但是却不需要极低的延迟,可以通过一些内部采样机制外部汇总的方式降低成本。

  • 这种方式使我们可以观测到短暂的CPU热点,但是又不需要为此付出高额成本进行收集和保留高精度数据

简化,直到不能再简化

  • 那些不常用的数据收集、汇总,以及警报配置应该定时删除(某些SRE团队的标准是一个季度没有用到一次即将其删除)。

将上述理念整合起来

  • ● 每当收到紧急警报时,应该立即需要我进行某种操作。每天只能进入紧急状态几次,太多就会导致“狼来了”效应。● 每个紧急警报都应该是可以具体操作的。● 每个紧急警报的回复都应该需要某种智力分析过程。如果某个紧急警报只是需要一个固定的机械动作,那么它就不应该成为紧急警报。● 每个紧急警报都应该是关于某个新问题的,不应该彼此重叠。

小结

  • E-mail警报的价值通常极为有限,很容易变成噪声。我们应该倾向于构建一个良好的监控台页面,直接显示所有的非紧急的异常情况。

个人总结

  1. 避免太多“紧急警报”造成“狼来了”效应
  2. 监控系统应该解决两个问题:什么东西出故障了,以及为什么出故障。
  3. 监控系统的4个黄金指标分别是延迟、流量、错误和饱和度(saturation)。

自动化的价值

  • 任何一个人或者一群人执行数百次动作时,不可能保证每次都用同样的方式进行:没有几个人能像机器一样永远保持一致。这种不可避免的不一致性会导致错误、疏漏、数据质量的问题和可靠性问题。在这个范畴内—一致性地执行范围明确、步骤已知的程序—是自动化的首要价值。

自动化的应用案例

  • 广泛使用的工具有Puppet、Chef、cfengine,甚至 Perl都提供了自动化完成特定任务的方法,主要区别在于对帮助进行自动化的组件的抽象层次不同。

可靠性是最基本的功能

  • 民航[20]或工业应用中—经常会指出高效的自动化的缺点[21]:随着时间的推移,操作员与系统的有用的、直接接触会逐渐减少,因为自动化会覆盖越来越多的日常活动。不可避免的,当自动化系统出现问题时,操作员将无法成功地操作该系统。

小结

  • 团队应该在开发流程开始时就留出一定资源进行发布工程工作。尽早采用最佳实践和最佳流程可以降低成本,以免未来重新改动这些系统。

个人总结

  1. 自动化很重要,但也要避免自动化工具出问题时,人工无法处理

第9章 简单化

  • 可靠性只有靠对最大程度的简化不断追求而得到。

  • 软件系统本质上是动态的和不稳定的。[25]只有真空中的软件系统才是永远稳定的。如果我们不再修改代码,就不会引入新的Bug。如果底层硬件或类库永远不变,这些组件也就不会引入Bug。如果冻结当前用户群,我们将永远不必扩展系统。事实上,一个对SRE管理系统的方法不错的总结是:“我们的工作最终是在系统的灵活性和稳定性上维持平衡。”

乏味是一种美德

  • 必要复杂度是一个给定的情况所固有的复杂度,不能从该问题的定义中移除,而意外复杂度则是不固定的,可以通过工程上的努力来解决。
  • 例如,编写一个Web服务器需要处理快速提供Web页面的必要复杂度。但是,如果我们用Java编写该服务器,试图减少GC的影响就可能会引入意外复杂度。

我绝对不放弃我的代码

  • 那些由于功能开关没有启用而没有被执行的代码,就像一个定时炸弹一样等待爆炸,正如Knight Capital的痛苦经历
  • 审查代码以确保它确实符合商业目标,定期删除无用代码,并且在各级测试中增加代码膨胀检测。

“负代码行”作为一个指标

  • 我曾经做过的一些最令人满意的编码工作就是删除了数千行已经没用的代码。

最小 API

  • 法国诗人 Antoine de Saint Exupery 曾写道,“不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美。”
    (参见文献[Sai39])这个原则同样适用于软件的设计和构建。API是这个规则应该遵循的一个清晰的例子。

  • 书写一个明确的、最小的API 是管理软件系统管理简单性必要的部分。我们向API消费者提供的方法和参数越少,这些API就越容易理解,我们就能用更多的精力去尽可能地完善这些方法。同时,一个反复出现的主题是:有意识地不解决某些问题可以让我们能够更专注核心问题,使得我们已有的解决方案更好。在软件工程上,少就是多!一个很小的,很简单的API通常也是一个对问题深刻理解的标志。

个人总结

  1. 我们的工作最终是在系统的灵活性和稳定性上维持平衡
  2. 必要复杂度是业务固有的,而意外复杂度则是不固定的,可以通过工程上的努力来解决
  3. “不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美。”

第Ⅲ部分 具体实践

  • 正确的解决方案不一定是当场把问题一次性修复好,而可以靠降低系统准确度、关闭一些不重要的功能,或者将用户流量导向其他没有问题的任务实例等手段暂时缓解问题。解决方案的细节肯定是和每个服务和团队相关的。但是如何有效地应对紧急问题的方法论是每个团队都适用的。

第10章 基于时间序列数据进行有效报警

  • 一个大型系统不应该要求运维人员持续关注其中使用的无数个小组件,而是应该自动汇总所有的信息,自动抛弃其中的异常情况。监控系统应该主要从高级服务质量目标层面进行报警,但是也应该保持足够的粒度,可以追踪到某个具体组件。

Borgmon的起源

  • Prometheus 都是开源软件中与Borgmon 基于时间序列数据报警理念类似的系统。
  • Prometheus[3]与Borgmon十分类似,尤其是当你对比其中的规则计算语法时。变量收集和规则计算的理念在所有这些项目中都很类似。希望借助这些工具,读者可以自行试验、部署本章内描述的一些想法。

黑盒监控

  • 白盒监控只能看到已经接收到的请求,并不能看到由于DNS故障导致没有发送成功的请求,或者是由于软件服务器崩溃而没有返回的错误
  • 探针程序使用应用级别的自动请求探测目标是否成功返回。

配置文件的维护

  • Borgmon提供一种类似于宏的模板机制,允许用户创造出一些可重用的规则库,进一步减小了配置文件中的重复程度,降低配置文件出错的可能性。

个人总结

  1. Prometheus
  2. 黑盒监控(类似APM)
  3. 保证监控系统的维护成本与服务的部署规模呈非线性相关增长是非常关键的。

  • SRE团队和纯运维团队十分不一样的地方在于,SRE团队非常强调用工程化手段来应对运维问题。而这些运维问题,当达到一定规模时,也确实只有采用软件工程化手段才能解决。

  • 我们为SRE花在纯运维事务上的时间设立了50%的上限。SRE至少要花50%的时间进行工程项目研发,以便能够研发出更好的自动化和服务优化手段来更好地服务整个业务。

安全感

  • 现代理论研究指出,在面临挑战时,一个人会主动或非主动(潜意识)地选择下列两种处理方法之一(参见文献[Kah11]):● 依赖直觉,自动化、快速行动。● 理性、专注、有意识地进行认知类活动。

  • 当处理复杂系统问题时,第二种行事方式是更好的,可能会产生更好的处理结果,以及计划更周全的执行过程。为了确保on-call工程师可以保持在第二种处理方式范围内,我们必须要减轻on-call所带来的压力感。

  • 在应急事故处理过程中,凭直觉操作和快速反应(例如服务出现问题就先重启服务器)看起来都是很有用的方法,但是这些方法都有自己的缺点。直觉很可能是错误的,而且直觉一般都不是基于明确的数据支持的。因此,在处理问题的过程中,on-call工程师很有可能由于凭直觉去解释问题产生的原因而浪费宝贵的时间。快速反应主要是由习惯而产生的,习惯性的快速反应的动作后果一般都没有经过详细考虑,这可能会将灾难扩大。在应急事件处理过程中,最理想的方法论是这样的:在有足够数据支撑的时候按步骤解决问题,同时不停地审视和验证目前所有的假设。

  • 让on-call SRE知道他们可以寻求外部帮助,对减轻on-call压力也很有帮助。最重要的资源有:● 清晰的问题升级路线。● 清晰定义的应急事件处理步骤。● 无指责,对事不对人的文化氛围(参见文献[Loo10]和[All12])

  • 最后,在应急事件处理结束时,仔细评估哪些地方有问题,哪些地方做得好是非常关键的。而且应该采取措施避免再次发生同样的问题。SRE团队必须在大型应急事件发生之后书写事后报告,详细记录所有事件发生的时间线。这些事后报告都是对事不对人的,为日后系统性地分析问题产生的原因提供了宝贵数据。犯错误是不可避免的,软件系统应该提供足够的自动化工具和检查来减少人为犯错误的可能性(参见文献[Loo10])。

避免运维压力过大

  • 控制on-call工程师收到的针对同一起事故的报警总数也很重要。有的时候,一个异常情况可能会触发多条报警,所以合理地分组汇总报警信息是很重要的。

  • 奸诈的敌人—运维压力不够虽然给一个非常安静的系统on-call值班是很幸福的事情,但是当一个系统太稳定,或者SRE on-call的周期太长会发生什么呢?SRE团队运维压力不够也是一个不良现象。长时间不操作生产环境会导致自信心问题,包括自信心太强以及自信心不够。这些现象只有在下一次发生问题时,才会显现出来。为了避免这种问题,应该控制SRE团队的大小,保证每个工程师每个季度至少参与oncall一次,最好两次。这样可以保证团队成员有足够的生产环境操作经验。“命运之轮”(见第28章)也是一种有助提高技能和共享知识的团队活动。同时,Google每年举办一次持续数天的全公司灾难恢复演习(DiRT),针对理论性和实际性的灾难进行演练。

个人总结

  1. 理性,逐步验证解决
  2. 清晰定义的应急事件处理步骤和升级流程
  3. 定期演练避免一旦突发时生疏

第12章 有效的故障排查手段

  • 值得警惕的是,理解一个系统应该如何工作并不能使人成为专家。只能靠调查系统为何不能正常工作才行。

  • 新手们常常不能有效地进行故障排查,是因为这个过程理想情况下同时需要两个条件。

    • 1.对通用的故障排查过程的理解(不依靠任何特定系统)。2.对发生故障的系统的足够了解。

理论

  • 我们不断提出一个造成系统问题的假设,进而针对这些假设进行测试和排除。

实践

  • 有效的故障报告应该写清预期是什么,实际的结果是什么,以及如何重现

  • 合理判定一个问题的严重程度需要良好的工程师判断力,同时,也需要一定程度的冷静。

  • 在大型问题中,你的第一反应可能是立即开始故障排查过程,试图尽快找到问题根源。这是错误的!不要这样做。正确的做法应该是:尽最大可能让系统恢复服务。

  • 这可能需要一些应急措施,比如,将用户流量从问题集群导向其他还在正常工作的集群,或者将流量彻底抛弃以避免连锁过载问题,或者关闭系统的某些功能以降低负载。缓解系统问题应该是你的第一要务。在寻找问题根源的时候,不能使用系统的用户并没有得到任何帮助。当然,快速定位问题时仍应该及时保存问题现场,比如服务日志等,以便后续进行问题根源分析时使用。

  • 这种方法也同样适用于计算机系统:如果一个Bug有可能导致不可恢复的数据损坏,停止整个系统要比让系统继续运行更好。对新SRE来说,这个想法是反直觉,令人不安的。对以前曾经有过产品研发背景的人来说更有点难以接受

  • 在日志中支持多级记录是很重要的,尤其是可以在线动态调整日志级别。

最后一个修改

  • 计算机系统有惯性存在:我们发现,一个正常工作的计算机系统会维持工作,直到某种外力因素出现,例如一个配置文件的修改,用户流量的改变等。检查最近对系统的修改可能对查找问题根源很有帮助。

  • 某项测试可能产生有误导性的结果。例如:防火墙规则可能只允许某些特定IP访问,所以在你的工作机上ping 数据库可能会失败,而实际从应用服务器上ping数据库可能是成功的。

  • 将你的想法明确地记录下来,包括你执行了哪些测试,以及结果是什么。

  • 尤其是当你处理更加复杂的问题时,良好的文档可以让你记住曾经发生过什么,可避免重复执行。

神奇的负面结果

  • 我们在设计实验的时候应该将可能的负面结果考虑在内,因为一个可靠的、应用广泛的负面结果对其他人更有帮助。

  • 公布负面结果有助于提升整个行业的数据驱动风气。

  • 公布结果。如果你对一项测试的结果感兴趣,那么很有可能其他人也感兴趣。当你公布结果的时候,其他人不需要再重新设计和运行一套类似试验。

  • 更多的试验压根没有公布数据,因为很多人错误地认为负面结果意味着没有价值。

使故障排查更简单

  • 使故障排查更简单有很多方法可以简化和加速故障排查过程。可能最基本的是:● 增加可观察性。在实现之初就给每个组件增加白盒监控指标和结构化日志。● 利用成熟的、观察性好的组件接口设计系统。

个人总结

  1. 理解一个系统应该如何工作并不能使人成为专家。只能靠调查系统为何不能正常工作才行。
  2. 有效的故障报告应该写清预期是什么,实际的结果是什么,以及如何重现
  3. 先让系统恢复服务,再排查问题,尽量保存现场相关数据或日志等。
  4. 发生问题时,留意“最后一个修改”
  5. 排查问题后文档记录,避免以后重复执行
  6. 实验的负面结果也有价值

第13章 紧急事件响应

  • 东西早晚要坏的,这就是生活。
  • 将紧急事件的处理过程变成一个学习机会。

当系统出现问题时怎么办

  • 当系统出现问题时怎么办首先,别惊慌失措!这不是世界末日,你也并不是一个人在战斗!作为一个专业人士,你已经接受过如何正确处理这样的情况的训练。通常来说,我们处理的问题一般不涉及真实的物理危险,只是计算机系统出现了问题。最差的情况下,也只是半个互联网都停止运转了(Google网络规模已经是全球排名前三)。所以请深吸一口气,慢慢来。

测试导致的紧急事故

  • 这次测试带来的严重后果促使开发者对代码类库中的问题进行了一次彻底的修复,同时制定了一个周期性测试机制来保证这类严重问题不再重现。

  • 由于我们没有在测试环境中测试回滚机制,没有发现这些机制其实是无效的,导致了事故总时长被延长了。我们现在要求在大型测试中一定先测试回滚机制。

变更部署带来的紧急事故

  • 在这些带外通信系统之外,Google还有命令行工具和其他的访问方式确保我们能够在其他条件无法访问的时候进行更新和变更回滚。这些工具和访问方式在这次事故中起到了重大作用,但是工程师应该更加频繁地测试,以便更为熟悉它们。

  • 具有讽刺意味的是,我们本来计划在下个季度提高部署测试流程和自动化的优先级。这次事故的发生直接将他们的优先级提高了,同时强调不管风险看起来有多小,也要经过严格完整的部署测试。

流程导致的严重事故

  • 我们在机器管理自动化上投入了大量的时间和精力,以使我们能很轻松地在整个集群里运行、停止和重新配置大量任务。但是当意外来临时,自动化的效率有时可能是很可怕的。

所有的问题都有解决方案

  • 时间和经验一再证明,系统不但一定会出问题,而且会以没有人能够想到的方式出问题。Google学到的最关键的一课是,所有的问题都有对应的解决方案,虽然对一个面对着疯狂报警的工程师来说,它可能不是那么显而易见。如果你想不到解决办法,那么就在更大的范围内寻求帮助。找到更多团队成员,寻求更多的帮助,做你需要做的一切事情,但是要快。最高的优先级永远是将手头问题迅速解决。很多时候,触发这个事故的人对事故了解得最清楚,一定要充分利用这一点。

  • 非常重要的是,一旦紧急事件过去之后,别忘了留出一些时间书写事后报告。

向过去学习,而不是重复它

  • 向过去学习,而不是重复它为事故保留记录没有什么比过去的事故记录是更好的学习资料了。历史就是学习其他人曾经犯的错误。在记录中,请一定要诚实,一定要事无巨细。尤其重要的是,提出关键的问题。时刻寻找如何能在战术及战略上避免这项事故的发生。公布和维护事后报告,确保全公司的每个人都能从中学到你所学到的知识。在事故结束后,确保自己和其他人切实完成事故中总结的待办事项。这样能够避免未来再次发生以同样的因素触发的同样的事故。一旦开始仔细学习过去的事故,我们就能更好地避免未来的事故。

  • 鼓励主动测试面对失败,理论和实践是两个完全不同的领域。直到你的系统真的失败的那一刻,你并不真的了解它,以及依赖它的系统,或者它的用户会如何应对。不要预设任何假设,也不要依赖任何没有经过测试的假设。你是希望这个系统在星期六凌晨两点钟,公司大部分同事都还在参加黑森林中的团建时出现故障,还是希望和最可靠和最聪明的同事在一起仔细监控着它们上周详细评审过的测试时出现故障呢?

一次流程管理良好的事故

  • Josephine上线之后,发现Robin也自愿加入进来。Sabrina 提醒Robin和Josephine,他们应该优先处理任何Mary交给他们的工作,同时他们必须告知Mary他们进行的任何操作。Robin和Josephine 通过阅读实时事故状况文档,很快熟悉了目前的情况。到下午5点时,Sabrina开始寻找接下来负责处理事故的替代人,因为她和她的同事们快要到下班时间了。她更新了事故状况文档。在5点45分时,她召开了一个简短的电话会议,让所有人都清楚目前的情况。在6点时,他们与他们的姐妹团队(另外一个办公室同团队的人)进行了职责交接。Mary第二天早上回到公司时,发现她身处大西洋另一端的同事已经定位了具体问题,缓解了问题,同时将事故做了了结,已经开始写事后总结了。问题解决了!她冲了点咖啡,开始规划一些结构性改变,使得这类问题在未来不会再重现。

什么时候对外宣布事故

  • 先宣布事故发生,随后找到一个简单解决方案,然后宣布事故结束,要比在问题已经持续几个小时之后才想起流程管理更好。应当针对事故设立一个明确的宣布条件。Google团队依靠下面几个宽松的标准——如果下面任何一条满足条件,这次事故应该被及时宣布。● 是否需要引入第二个团队来帮助处理问题?● 这次事故是否正在影响最终用户?● 在集中分析一小时后,这个问题是否依然没有得到解决?

  • 如果平时不经常使用,事故流程管理的可靠性萎缩得很快。所以怎么使工程师不忘记他们的流程管理技能呢?难道一定要制造更多事故吗?幸运的是,事故流程管理框架常常也适用于其他的跨时区、或者跨团队的常规运维变更实施。如果我们经常使用流程管理框架处理生产变更请求,那么在事故来临时,就可以很好地利用流程管理框架管理它。如果你的组织经常进行灾难恢复演习(你应该这样做!参见文献[Kir12]),事故流程管理应该包含在其中。Google经常针对之前发生的灾难进行角色扮演式演习,比如演习另外一个地区的团队处理过的问题,以更好地熟悉事故流程管理体系。

小结 !!!

  • 我们发现,通过事前准备一个事故流程管理策略,并确保平稳实施,以及经常测试,我们能够降低事故的平均恢复时间(MTTR),同时减轻处理紧急事故的人的工作压力。任何对服务可靠性关注的组织团队都会从类似策略上获得帮助。事故流程管理最佳实践划分优先级:控制影响范围,恢复服务,同时为根源调查保存现场。事前准备:事先和所有事故处理参与者一起准备一套流程。信任:充分相信每个事故处理参与者,分配职责后让他们自主行动。反思:在事故处理过程中注意自己的情绪和精神状态。如果发现自己开始惊慌失措或者感到压力难以承受,应该寻求更多的帮助。考虑替代方案:周期性地重新审视目前的情况,重新评估目前的工作是否应该继续执行,还是需要执行其他更重要或者更紧急的事情。练习:平时不断地使用这项流程,直到习惯成自然。换位思考:上次你是事故总控负责人吗?下次可以换一个职责试试。鼓励每个团队成员熟悉流程中的其他角色。

个人总结

  1. 东西早晚要坏的,这就是生活。
  2. 将紧急事件的处理过程变成一个学习机会。
  3. 在大型测试中一定先测试回滚机制。
  4. 时间和经验一再证明,系统不但一定会出问题,而且会以没有人能够想到的方式出问题。所有的问题都有对应的解决方案。
  5. 向过去学习,而不是重复它(事后报告)
  6. 事前准备一个事故流程管理策略

第15章 事后总结:从失败中学习

  • 一篇事后总结是一次事故的书面记录,包括该事故造成的影响,为缓解该事故采取的措施,事故的根本原因,以及防止未来问题重现的后续任务。

Google的事后总结哲学

  • 书写事后总结的主要目的是为了保证该事故被记录下来,理清所有的根源性问题,同时最关键的是,确保实施有效的措施使得未来重现的几率和影响得到降低,甚至避免重现。

  • 书写事后总结不是一种惩罚措施,而是整个公司的一次学习机会。但是书写事后总结的过程确实需要消耗团队的一定时间和精力,所以我们在选择上很严格。每个团队都有一些内部灵活性,但是基本的事后总结条件为:● 用户可见的宕机时间或者服务质量降级程度达到一定标准。● 任何类型的数据丢失。● on-call 工程师需要人工介入的事故(包括回滚、切换用户流量等)。● 问题解决耗时超过一定限制。● 监控问题(预示着问题是由人工发现的,而非报警系统)。

  • 在SRE的文化中,最重要的就是事后总结“对事不对人”。一篇事后总结必须重点关注如何定位造成这次事件的根本问题,而不是指责某个人或某团队的错误或者不恰当的举动。一篇对事不对人的事后总结假设所有参与事件处理的人都是善意的,他们在自己当时拥有的信息下做了正确的举动。如果因为某些“错误的”举动就公开指责或者羞辱某个人或团队,那么人们就会自然地逃避事后总结。

  • 我们不能“修好”某个人,但是可以通过改善系统和流程从而更好地协助他在设计和维护大型复杂系统时,做出更多“正确”的判断。

  • 当一次事故发生时,我们不能把事后总结当成例行公事。我们的工程师将事后总结看作一个修复问题,一个使Google变得更可靠的机会。一篇“对事不对人”的事后总结不应该简单地指责或者抱怨某个团队,而应该确实提出服务如何能够获得进步。

  • 下面是两个例子。指责“我们需要重写整个复杂后端系统。在过去三个季度中,它每周都在出问题。我们对一点一点修复它的问题已经烦透了!真的,如果我再收到一个报警,那我就自己重写了。”对事不对人“通过重写整个后端系统可能可以避免这些烦人的报警信息继续发生,目前版本的维护手册非常冗长,学习成本很高。相信通过重写,可以减少报警信息,未来的oncall工程师会感谢我们的。”

  • 最佳实践:避免指责,提供建设性意见。

  • 对事不对人的事后总结有的时候比较难写,因为事后总结的格式清晰地表明了触发事故的原因。从事后总结中排除指责的因素可以使人们在向上级汇报问题的时候更有自信。同时我们也不应该因为某个团队和个人经常写事后总结而对他们产生怀疑。一个充满相互指责风气的环境很容易让人将事故和问题掩盖起来,从而对整个组织酿成更大的灾难。

协作和知识共享

  • 最佳实践:所有的事后总结都需要评审

  • 一旦所有的事故参与者都对文档和其中的代办事项表示了肯定,这篇事后总结会被添加到该团队或者整个组织的文档汇总中。

  • 透明化的共享机制保证了每个人都可以很容易地找到和学习以前的事故。

建立事后总结文化

  • 最佳实践:公开奖励做正确事的人

  • 最佳实践:收集关于事后总结有效性的反馈

个人总结

  • 最佳实践:避免指责,提供建设性意见。
  • 一篇“对事不对人”的事后总结不应该简单地指责或者抱怨某个团队,而应该确实提出服务如何能够获得进步。
  • 一个充满相互指责风气的环境很容易让人将事故和问题掩盖起来,从而对整个组织酿成更大的灾难。

第16章 跟踪故障

  • 提高可靠性的唯一可靠的方法论是建立一个基线(baseline),同时不断跟踪改变。

  • 通过找到基础设施中造成故障最多的一部分,可以更好地知道如果提高该部分的稳定性或性能会带来多大帮助。

  • 甚至有的时候,我们需要人为制造一些宕机时间,以免给内部用户造成某种假象(通常指某些服务设计的架构已经决定发生故障会耗时很久才能解决,但是经常由于运气因素而造成非常稳定的假象。某些团队选择定期制造人为宕机时间,以避免用户过于依赖该服务)。

个人总结

  1. 定时宕机,避免把依赖当作理所当然。

第17章 测试可靠性

  • 如果你还没有亲自试过某件东西,那么就假设它是坏的。

  • 测试的数量直接取决于所服务系统的可靠性要求。随着代码的测试覆盖度上升,每次改动的不确定性和降低系统可靠度的可能性都降低了。足够的代码测试覆盖度可以让我们对系统做出更多的改动,而不会使系统可靠度下降到可接受水平之下

  • 测试和平均修复时间的关系系统通过某项测试或者一系列测试并不一定能证明系统是稳定的,但是失败的测试通常证明了系统不可靠。

软件测试的类型

  • 软件测试基本分为两大类:传统测试和生产测试。传统测试在软件开发过程中很常见,主要用来在开发过程中离线评估软件的正确性。生产测试在生产Web服务器上进行,用来评估一个已经部署的软件系统工作是否正常。

  • 单元测试单元测试(unit test)是最小、最简单的软件测试形式。这些测试用来评估某一个独立的软件单元,比如一个类,或者一个函数的正确性。这些测试不考虑包含该软件单元的整体系统的正确性。单元测试同时也是一种规范,用来保证某个函数或者模块完全符合系统对其的行为要求。单元测试经常被用来引入测试驱动开发的概念。集成测试通过独立的单元测试的软件组件被组装成大的系统组件。工程师通过在这个组件中运行一个集成测试(integration test)来检验该组件的功能的正确性。依赖注入(dependency injection),利用类似Dagger[53]这样的工具,我们可以创建出复杂依赖的mock(测试中替代真实逻辑的伪组件),用以方便地测试某个系统组件。一个常见的例子是通过依赖注入来用轻便的mock替换一个有状态的数据库,同时保持一模一样的行为特征。

  • 系统测试系统测试(system test)是一个在未部署的系统上运行的大型测试。某个组件的所有模块(Module)都会被装载到系统中(例如通过集成测试的软件服务器)。接下来工程师可以端到端地测试系统功能。系统测试包括以下几种类型。冒烟测试(smoke test)工程师在冒烟测试中可检测非常简单但是非常重要的系统行为。这是最简单的一种系统测试形式。冒烟测试有时也被称为理性测试,如果该测试不通过,那么其他更昂贵的测试可以不用运行了。性能测试(performance test)一旦冒烟测试通过,系统基本的正确性已经得到了保障。下一步通常是通过某个系统测试的变形来保证整个系统的性能自始至终保持在可接受范围内。因为系统的响应时间和资源要求可能在开发过程中大量改变,该系统必须接受某些测试以确保它不会在没人知道的情况下逐渐变慢(在发布到最终用户之前)。例如,一个程序可能随着改变开始需要32GB内存,而以前只需要8GB。或者该程序的响应时间由10ms变成了50ms,随后变成了100ms。性能测试可以保证随着时间推移系统性能不会下降,或者资源要求不会升高。回归测试(regression test)另外一种系统测试可保证之前的Bug不会重现。回归测试可以被理解为曾经发生过的,导致系统故障或产生错误信息的Bug列表。通过将这些Bug记录为系统测试或者集成测试,重构代码的工程师可以保证他们不会偶然间将他们曾经辛苦调查和修复的Bug又带回来。很重要的是,每个测试都有成本,时间成本和计算资源成本。在一个极限上,单元测试非常便宜,通常可以在毫秒级和很少的资源上(例如一个笔记本电脑上)完成。而在另一个极限上,将一个完整的软件服务器设立起来,同时包括它所有的依赖系统(或者是mock),然后运行相关的测试可能会需要很长时间—几分钟到几小时—一般还需要专属的运算资源。时刻关注这些测试的成本,是软件开发效率提升的重要因素,同时也鼓励程序员更有效地利用我们的测试资源。生产测试生产测试和一个已经部署在生产环境中的业务系统直接交互,而不是运行在密闭的测试环境中。这些测试和黑盒监控在很多地方十分类似(参见第6章),有的时候也被称为黑盒测试。生产测试对运行一个可靠的生产环境来说是必要的。

小结

  • 测试是工程师提高可靠性投入回报比最高的一种手段。

个人总结

  • 测试分类查资料总结

基于意图的容量规划

  • 向大型团队推广内部软件工具需要以下几点:● 持续的和完整的推广方案。● 用户的拥护。● 资深工程师和管理层的赞助,因为他们看到了项目的实用潜力。

使用DNS进行负载均衡

  • DNS负载均衡。最简单的方案是在DNS回复中提供多个A记录或者AAAA记录,由客户端任意选择一个IP地址使用

负载均衡:虚拟IP

  • 虚拟IP地址(VIP)不是绑定在某一个特定的网络接口上的,它是由很多设备共享的。

识别异常任务:流速控制和跛脚鸭任务

  • 从一个客户端的视角来看,某个后端任务可能处于下列任一种状态中:健康后端任务初始化成功,正在处理请求。拒绝连接后端任务处于无响应状态。这可能是因为任务正在启动或者停止,或者是因为后端正处于一种异常状态(虽然很少有后端任务在非停止状态下停止监听端口)。

  • 跛脚鸭状态后端任务正在监听端口,并且可以服务请求,但是已经明确要求客户端停止发送请求。

  • 当某个请求进入跛脚鸭状态时,它会将这个状态广播给所有已经连接的客户端。但是那些没有建立连接的客户端呢?

  • 在Google的RPC框架实现中,不活跃的客户端(没有建立TCP连接的客户端)也会定期发送UDP健康检查包。这就使跛脚鸭状态可以相对较快地传递给所有的客户端—通常在一到两个RTT周期内—无论它们处于什么状态下。

  • 允许任务处于这种半正常的跛脚鸭状态的好处就是让无缝停止任务变得更容易,处于停止过程中的任务不会给正在处理的请求返回一个错误值。能够无影响地停止一个活跃的后端任务可以让处理代码推送、设备维护活动,和机器故障问题导致的任务重启变得对用户透明。这个停止过程通常按照以下步骤进行:1.任务编排系统发送一个SIGTERM信号给该任务。2.后端任务进入跛脚鸭状态,同时请求它的所有客户端发送请求给其他后端任务。这通过SIGTERM信号处理程序中调用RPC实现中的API完成。3.任何在后端进入跛脚鸭状态时正在进行的请求(或者在进入状态之后,但是其他客户端收到通知之前)仍会继续进行。4.随着请求回复被发送回客户端,该后端任务的活跃请求逐渐降低为0。5.在配置的时间过后,该后端程序要么自己干净地退出,要么任务编排系统主动杀掉它。该时间应该被设置为一个足够大的值,以便一般的请求可以有足够的时间完成。每个服务的该数值都不同,一般来说取决于客户端的复杂程度,10s到150s是一个不错的选择。这个策略使客户端可以在后端程序进行耗时较长的初始化过程中(这时后端程序还不能服务请求)就建立连接。如果后端程序等到服务可以接受请求的时候才建立链接,就增加了一些不必要的延迟。一旦后端程序可以提供服务了,它就会主动通知所有客户端。

利用划分子集限制连接池大小

  • 利用划分子集限制连接池大小在健康管理之外,负载均衡另外要考虑的一个因素就是子集划分:限制某个客户端任务需要连接的后端任务数量。我们的RPC系统中的每个客户端都会针对后端程序维持一个长连接发送请求。这些连接通常在客户端启动的时候就建立完成,并且保持活跃状态,不停地有请求通过它们,直到客户端终止。另外一个方案是针对每个请求建立和销毁后端连接,这样会带来极大的资源成本和造成延迟问题。在极端情况下,如果某个连接闲置时间非常长,我们的RPC实现可以自动将该连接转为“不活跃”状态,转为UDP模式连接,而非TCP模式。

QPS陷阱

  • Google在多年的经验积累中得出:按照QPS来规划服务容量,或者是按照某种静态属性(认为其能指代处理所消耗的资源:例如某个请求所需要读取的键值数量)一般是错误的选择。就算这个指标在某一个时间段内看起来工作还算良好,早晚也会发生变化。有些变动是逐渐发生的,有些则是非常突然的(例如某个软件的新版本突然使得某些请求消耗的资源大幅减少)。这种不断变动的目标,使得设计和实现良好的负载均衡策略使用起来非常困难。更好的解决方案是直接以可用资源来衡量可用容量。

客户端侧的节流机制

  • 拒绝一个执行简单内存查询的请求可能跟实际执行该请求消耗内存差不多(因为这里主要的消耗是在应用层协议解析中,结果的产生部分很简单)。就算在某些情况下,拒绝请求可以节省大量资源,发送这些拒绝回复仍然会消耗一定数量的资源。

  • 当某个客户端检测到最近的请求错误中的一大部分都是由于“配额不足”错误导致时,该客户端开始自行限制请求速度,限制它自己生成请求的数量。超过这个请求数量限制的请求直接在本地回复失败,而不会真正发到网络层。

  • 我们使用一种称为自适应节流的技术来实现客户端节流。具体地说,每个客户端记录过去两分钟内的以下信息:请求数量(requests)应用层代码发出的所有请求的数量总计(指运行于自适应节流系统之上的应用代码)。请求接受数量(accepts)后端任务接受的请求数量。

重要性

  • 重要性重要性(criticality)是另外一个在全局配额和限制机制中比较有用的信息。某个发往后端的请求都会被标记为以下4类中的一种,这说明了请求的重要性。最重要 CRITICAL_PLUS为最重要的请求预留的类型,拒绝这些请求会造成非常严重的用户可见的问题。重要 CRITICAL生产任务发出的默认请求类型。拒绝这些请求也会造成用户可见的问题,但是可能没有CRITICAL_PLUS那么严重。我们要求服务必须为所有的CRITICAL和CRTICAL_PLUS流量配置相应的资源。可丢弃的SHEDDABLE_PLUS这些流量可以容忍某种程度的不可用性。这是批量任务发出的请求的默认值。这些请求通常可以过几分钟,或者几小时之后重试。可丢弃的SHEDDABLE这些流量可能会经常遇到部分不可用情况,偶尔会完全不可用。

  • 我们同时增强了RPC系统,可以自动传递重要性信息。如果后端接收到请求A,在处理过程中发出了请求B 和C给其他后端,请求B和C会使用与A相同的重要性属性。

  • 在过去一段时间内,Google内部的许多系统都逐渐产生了一种与重要性类似的属性,但是通常不能跨服务兼容。通过标准化和在RPC系统中自动传递,我们现在可以在某些特定节点处统一设置重要性属性。这意味着,我们相信依赖的服务在过载情况下可以按正确的优先级来拒绝请求,不论它们处于整个处理栈中多深的位置。于是我们一般在离浏览器或者客户端最近的地方设置优先级—通常在HTTP前端服务器上。同时,我们可以在特殊情况下在处理栈的某处覆盖优先级设置。

资源利用率信号

  • 资源利用率信号我们的任务过载保护是基于资源利用率(utilization)实现的。

  • 在多数情况下,资源利用率仅仅是指目前CPU的消耗程度(目前CPU使用量除以全部预留CPU数量)。但是在某些情况下,同时也会考虑内存的使用率。随着资源利用率的上升,我们开始根据请求的重要性来拒绝一些请求(高重要性的请求对应高阈值)。

处理过载错误

  • 决定何时重试

连接造成的负载

  • 连接造成的负载连接造成的负载是最后一个值得一提的因素。有时候我们仅仅考虑后端处理接收的请求所造成的负载(这也是用QPS来建模负载的一个问题),然而却忽略了其他因素,比如维护一个大型连接池的CPU和内存成本,或者是连接快速变动的成本。这样的问题在小型系统中可以忽略不计,但是在大型RPC系统中很快就会造成问题。

  • 我们的RPC协议需要不活跃的客户端定期执行健康检查。当某个连接空闲一段可配置的时间后,客户端放弃TCP连接,转为UDP健康检查。不幸的是,这种行为会对大量请求率很低的客户端造成问题:健康检查需要比实际处理请求更多的资源。

  • 通过仔细调节连接参数(如,大幅降低健康检查频率)或者动态创建和销毁连接可以优化这些场景。

小结

  • 利用不同的技术手段(确定性算法、加权轮询、客户端侧的节流、用户配额等)更平均地将负载分散到数据中心中。然而这些手段都依赖于在分布式下的状态传递机制。虽然大部分情况下都表现良好,但是在真实情况下,某些应用遇到了一些困难。所以,我们认为保护某个具体任务,防止过载是非常重要的。简单地说:一个后端任务被配置为服务一定程度的流量,不管多少额外流量被指向这个任务,它都应该保证服务质量。

  • 一个常见的错误是认为过载后端应该拒绝和停止接受所有请求。然而,这个假设实际上是与可靠的负载均衡目标相违背的。我们实际上希望客户端可以尽可能地继续接受请求,然后在有可用资源时才处理。某个设计良好的后端程序,基于可靠的负载均衡策略的支持,应该仅仅接受它能处理的请求,而优雅地拒绝其他请求。

个人总结

  • RPC框架设计的重要参考 TODO
  • 如何优雅地重试:https://mp.weixin.qq.com/s/6IkTnUbBlHjM3GM_bT35tA 这个实现类似
    策略 说明
    重试熔断 请求失败 / 成功 > 0.1 时停止重试
    链路上传错误标志 下层重试失败后上传错误标志,上层不再重试
    链路下传重试标志 重试请求特殊标记,下层对重试请求不会重试
    DDL 当剩余时间不够时不再发起重试请求
    框架熔断 微服务框架本身熔断、过载保护等机制也会影响重试效果


第22章 处理连锁故障

  • 如果请求没有成功,以指数型延迟重试。

  • 连锁故障是由于正反馈循环(positive feedback)导致的不断扩大规模的故障。[72]连锁故障可能由于整个系统的一小部分出现故障而引发,进而导致系统其他部分也出现故障。例如,某个服务的一个实例由于过载出现故障,导致其他实例负载升高,从而导致这些实例像多米诺骨牌一样一个一个全部出现故障。

连锁故障产生的原因和如何从设计上避免

  • RPC超时服务器过载时,对客户端RPC的回复会变慢,最终会超过客户端所设置的超时时间。这会导致服务器对请求实际进行的处理都被浪费了,而客户端可能会重试RPC,造成更严重的过载。

  • CPU缓存效率下降CPU使用得越多,任务被分配到多个CPU核心上的几率越大,从而导致CPU核心本地缓存的失效,进而降低CPU处理的效率。

  • 缓存命中率下降可用内存的减少可能会导致应用层缓存的命中率降低,导致向后端发送更多的RPC,可能会导致后端任务过载。

  • 线程线程不足可能会导致错误或者导致健康检查失败。如果服务器为此增加更多线程,这些线程可能会占用更多内存。在极端情况下,线程不足可能会导致进程ID数不足(Linux的进程ID数是有限的)。文件描述符文件描述符(file descriptor)不足可能会导致无法建立网络连接,进而导致健康检查失败。

  • 假设如下场景:1.某Java前端服务器GC参数没有被调优。2.在高负载(但是在期待范围内)情况下,前端由于GC问题导致CPU不足。3.CPU不足导致请求处理变慢。4.同时处理的请求增多导致内存使用上升。5.内存压力上升,同时由于固定内存分配比例的原因,用于缓存的内存数量减少。6.缓存数量降低意味着缓存中键值数量下降,从而导致命中率下降。7.缓存命中率下降导致更多的请求被发往后端进行处理。8.后端服务器CPU或者线程不足。9.CPU不足导致健康检查失败,从而触发了连锁故障。在上述这个复杂情景下,发生故障时可能没有时间仔细分析因果关系。尤其是在前端和后端由不同团队运维时,判断后端崩溃是由于前端缓存命中率下降可能非常困难。

  • 会自动避免产生错误的软件服务器的负载均衡策略会将这个问题加剧—某几个后端任务产生了错误,会导致负载均衡器不再向它们发送请求,进而使得其余软件服务器的负载上升,从而再次触发滚雪球效应。

防止软件服务器过载

  • 防止软件服务器过载下面描述了避免过载的几种策略,大致以优先级排序。使用负载压力测试得出服务器的极限,同时测试过载情况下的失败模式

  • 提供降级结果给用户返回低质量的,但是更容易计算的结果。

  • 在过载情况下主动拒绝请求软件服务器应该保护自己不进入过载崩溃状态。

上层系统应该主动拒绝请求

  • 在反向代理层,通过针对请求的某种特性进行数量限制(如IP地址),来缓解和避免拒绝服务攻击,避免攻击性客户端的影响。● 在负载均衡器层,在服务进入全局过载时主动丢弃请求。

  • 进行容量规划好的容量规划可以降低连锁反应发生的可能性。容量规划应该伴随着性能测试进行,以确定可能导致服务失败的负载程度。

  • 进行容量规划只能减少触发连锁反应的可能性,但是并不能完全避免。当一个计划内或者计划外的事件导致大部分集群容量同时下线时,连锁反应是不可避免的。负载均衡问题、网络分区事件,或者突发性流量增长,都会创造意料之外的负载问题。有些系统可以根据需要动态增加容量,这可能防止过载发生,但是适当地进行容量规划还是必要的。

  • 在这个理想化的情景下,只有在请求速率超过单个请求的处理速率时,请求才会进入队列,这种情况会导致线程池和队列的同时饱和。

  • 对一个流量基本稳定的服务来说,队列长度比线程池大小更小会更好(如 50% 或更小)。当服务处理速度无法跟上请求到达速率时,尽早拒绝请求会更好。

  • 流量抛弃和优雅降级流量抛弃(load shedding)是指在软件服务器临近过载时,主动抛弃一定量的负载。

  • 一种简单的流量抛弃实现方式是根据CPU使用量、内存使用量及队列长度等进行节流。

  • 简化了一些细节,[75]但是这里很好地展现了重试是如何摧毁一个系统的。注意临时性的过载升高,或者使用量的缓慢增加都有可能造成这种情况。

  • 一定要使用随机化的、指数型递增的重试周期。

  • 如果重试不是随机分布在重试窗口里的,那么系统出现的一个小故障(某个网络问题)就可能导致重试请求同时出现,这些请求可能会逐渐放大

  • 限制每个请求的重试次数。不要将请求无限重试。● 考虑使用一个全局重试预算。例如,每个进程每分钟只允许重试60次,如果重试预算耗尽,那么直接将这个请求标记为失败,而不真正发送它。这个策略可以在全局范围内限制住重试造成的影响,容量规划失败可能只是会造成某些请求被丢弃,而不会造成全球性的连锁故障。

  • 从多个视角重新审视该服务,决定是否需要在某个级别上进行重试。这里尤其要避免同时在多个级别上重试导致的放大效应:高层的一个请求可能会造成各层重试次数的乘积数量的请求。如果服务器由于过载不能提供服务,后端、前端、JavaScript层各发送3次重试(总计4次请求),那么一个用户的动作可能会造成对数据库的64 次请求(43)。在数据库由于过载返回错误时,这种重试只会加重问题。

  • 使用明确的返回代码,同时详细考虑每个错误模式应该如何处理。例如,将可重试错误和不可重试错误分开。

  • 设置一个截止时间通常是明智的。不设置截止时间,或者设置一个非常长的截止时间通常会导致某些短暂的、已经消失的问题继续消耗服务器资源,直到重启。

  • 截止时间设置得太长可能会导致框架中的高层级部分由于低层级的问题而持续消耗资源。截止时间设置得太短可能会导致某些比较重型的请求持续失败。恰当的截止时间设置,需要在多个限制条件中选择一个平衡点。

  • 截止时间传递与其在发送RPC给后端服务器时自拟一个截止时间,不如让软件服务器采用截止时间传递和取消传递的策略。

  • 可使用截止时间传递机制,截止时间在整个服务栈的高层设置(如,前端服务器)。由初始请求触发的整个RPC树会设置同样的绝对截止时间。

  • 同时,我们可能还会将传递出去的截止时间减少一点(如几百毫秒),以便将网络传输时间和客户端收到回复之后的处理时间考虑在内。

  • RPC取消的传递可以避免某些泄露情况,如果某个初始RPC设置了一个很长的截止时间,但是底层之间的RPC只有短暂的截止时间,超时失败了。使用简单的截止时间传递可能会导致初始RPC虽然无法继续处理,却继续消耗服务器资源直到超时。

  • 请求延迟的双峰分布(Bimodal)

  • 使用100s的截止时间,5%的请求会消耗5000个线程(50QPS * 100 seconds),但是前端服务器并没有这么多可用线程。忽略副作用,前端也仅能够处理19.6%的请求(1000可用线程 /(5000+95)线程工作),这会造成 80.4%的错误率。因此,不仅 5% 的请求受到了影响(那些由于后端问题不可能成功的请求),实际上大部分的请求都受到了影响。

  • 如果无法完成的请求能够尽早返回一个错误而不是等完整个截止时间,我们就可以避免这个问题。例如,如果一个后端服务器不可用,经常立刻返回一个错误值是最好的,而不是等待这个后端服务器变得可用。如果RPC层支持快速失败的选项,一定要启用它。

  • 假设你的后端要处理来自不同客户端的性能和特征各异的请求,我们可以考虑限制一个客户端只能占用25% 的线程总数,以便在某个异常客户端大量产生负载的情况下提供一些公平性。

慢启动和冷缓存

  • 慢启动和冷缓存进程在刚刚启动之后通常要比稳定状态下处理请求的速度慢一点。慢的原因可能是由下列一个或多个原因导致:必需的初始化过程在接收到第一个请求后,需要跟后端服务器建立连接。运行时性能优化,尤其是JavaJIT 编译过程,热点优化,以及类延迟加载机制。同样的,有些服务器会在缓存没有充满之前效率很低。

  • 过量配备(overprovision)该服务。区分延迟类缓存和容量类缓存是很重要的:当使用延迟类缓存时,服务器可以在空缓存的情况下仍然处理预期的请求负载,但是使用容量类缓存时,该服务将不能够在空缓存下处理请求负载。

保持调用栈永远向下

  • 假设一个用户有一个主后端和一个预先选择好的另外集群中的一个热备后端,主后端在底层出现错误或者延迟上升的情况下将请求代理给热备后端。如果整个系统都处于过载状态,那么从主到副的这种代理可能会增多,会给系统带来更多的负载,因为请求通常要被解析两次,还需要主后端消耗资源等待副后端任务。

  • 在用户的请求路径中最好能够避免使用同层通信—也就是避免通信路径中出现环。

  • 应该由客户端来进行这种通信。例如,如果一个前端需要和后端通信,但是猜错了后端任务,后端不会代理请求给正确的后端,而是通过返回错误使得前端在正确的后端任务上重试它的请求。

连锁故障的触发条件

  • 根据请求数量和可用容量来动态调节任务的同时更新数量可能是个好办法。

  • 在发生连锁故障时,检查最近的改变以及回滚通常是明智的,尤其在这些改变会影响容量或者更改请求特点的情况下。

  • 自然增长在很多情况下,连锁故障不是由于某个特定的服务改变导致的,而是由于使用量的天然上升,却没有进行对应的容量调整导致的。

连锁故障的测试

  • 设计良好的组件应该可以拒绝一小部分请求而继续存活。

解决连锁故障的立即步骤

  • 莎士比亚搜索服务的连锁故障某个关于莎士比亚作品的纪录片在日本上映了,同时特别指明了莎士比亚搜索服务是进行进一步研究的最佳工具。随着这次广播,亚洲数据中心的流量激增,超过了服务容量。服务容量的问题伴随着当时正在进行的大型更新而变得更严重了。幸运的是,一些安全防护措施帮助缓解了可能的故障。生产环境准备评审(production readiness review)流程指出了一些问题,开发团队已经解决。例如,开发者为服务加入了优雅降级功能。当容量不够时,服务不再返回照片,或者不再返回解释故事发生位置的小地图。取决于RPC的目的,超时的RPC要么不再重试(例如,之前提到的图片),要么采用随机指数型延迟进行重试。即使有这些保护措施的存在,任务还是一个接一个地失败了,然后被Borg系统重启,这导致正常工作的任务数量持续减少。由于这个原因,服务监控页面上的某些图表变成了红色,并且SRE收到了紧急警报。为了解决这个问题,SRE临时向亚洲数据中心增加了一些服务容量,调整了莎士比亚搜索任务的任务数量。通过这种操作,成功恢复了亚洲数据中心的莎士比亚搜索服务。接下来,SRE书写了一篇事后总结,详细说明了触发问题的事件,哪些做得好,哪些可以做得更好,和一系列待办事项来避免这个情景重现。例如,在服务过载的情况下,GSLB负载均衡器可以将一些流量导入邻近的数据中心。同时,SRE团队启用了自动伸缩机制,于是任务的数量可以自动跟着流量增长,这样他们就不用再操心这类问题了。

小结

  • 小结当一个系统过载时,某些东西总是要被牺牲掉。一旦一个服务越过了临界点,服务一些用户可见错误,或者低质量结果要比尝试继续服务所有请求要好。理解这些临界点所在,以及超过临界点系统的行为模式,是所有想避免连锁故障的运维人员所必需的。如果不加小心,某些原本为了降低服务背景错误率或者优化稳定状态的改变反而会让服务更容易出现事故。在请求失败的时候重试、负载自动转移、自动杀掉不健康的服务器、增加缓存以提高性能或者降低延迟:这些手段原本都是为了优化正常情况下的服务性能,但是也可能会提高大规模的服务故障的几率。一定要小心评估这些改变,否则灾难就会接踵而至。

第22章 处理连锁故障

  • 如果请求没有成功,以指数型延迟重试。

  • 连锁故障是由于正反馈循环(positive feedback)导致的不断扩大规模的故障。[72]连锁故障可能由于整个系统的一小部分出现故障而引发,进而导致系统其他部分也出现故障。例如,某个服务的一个实例由于过载出现故障,导致其他实例负载升高,从而导致这些实例像多米诺骨牌一样一个一个全部出现故障。

连锁故障产生的原因和如何从设计上避免

  • RPC超时服务器过载时,对客户端RPC的回复会变慢,最终会超过客户端所设置的超时时间。这会导致服务器对请求实际进行的处理都被浪费了,而客户端可能会重试RPC,造成更严重的过载。

  • CPU缓存效率下降CPU使用得越多,任务被分配到多个CPU核心上的几率越大,从而导致CPU核心本地缓存的失效,进而降低CPU处理的效率。

  • 缓存命中率下降可用内存的减少可能会导致应用层缓存的命中率降低,导致向后端发送更多的RPC,可能会导致后端任务过载。

  • 线程线程不足可能会导致错误或者导致健康检查失败。如果服务器为此增加更多线程,这些线程可能会占用更多内存。在极端情况下,线程不足可能会导致进程ID数不足(Linux的进程ID数是有限的)。文件描述符文件描述符(file descriptor)不足可能会导致无法建立网络连接,进而导致健康检查失败。

  • 假设如下场景:1.某Java前端服务器GC参数没有被调优。2.在高负载(但是在期待范围内)情况下,前端由于GC问题导致CPU不足。3.CPU不足导致请求处理变慢。4.同时处理的请求增多导致内存使用上升。5.内存压力上升,同时由于固定内存分配比例的原因,用于缓存的内存数量减少。6.缓存数量降低意味着缓存中键值数量下降,从而导致命中率下降。7.缓存命中率下降导致更多的请求被发往后端进行处理。8.后端服务器CPU或者线程不足。9.CPU不足导致健康检查失败,从而触发了连锁故障。在上述这个复杂情景下,发生故障时可能没有时间仔细分析因果关系。尤其是在前端和后端由不同团队运维时,判断后端崩溃是由于前端缓存命中率下降可能非常困难。

  • 会自动避免产生错误的软件服务器的负载均衡策略会将这个问题加剧—某几个后端任务产生了错误,会导致负载均衡器不再向它们发送请求,进而使得其余软件服务器的负载上升,从而再次触发滚雪球效应。

防止软件服务器过载

  • 防止软件服务器过载下面描述了避免过载的几种策略,大致以优先级排序。使用负载压力测试得出服务器的极限,同时测试过载情况下的失败模式。

  • 提供降级结果给用户返回低质量的,但是更容易计算的结果。

  • 在过载情况下主动拒绝请求软件服务器应该保护自己不进入过载崩溃状态。

上层系统应该主动拒绝请求

  • 在反向代理层,通过针对请求的某种特性进行数量限制(如IP地址),来缓解和避免拒绝服务攻击,避免攻击性客户端的影响。● 在负载均衡器层,在服务进入全局过载时主动丢弃请求。

  • 进行容量规划好的容量规划可以降低连锁反应发生的可能性。容量规划应该伴随着性能测试进行,以确定可能导致服务失败的负载程度。

  • 进行容量规划只能减少触发连锁反应的可能性,但是并不能完全避免。当一个计划内或者计划外的事件导致大部分集群容量同时下线时,连锁反应是不可避免的。负载均衡问题、网络分区事件,或者突发性流量增长,都会创造意料之外的负载问题。有些系统可以根据需要动态增加容量,这可能防止过载发生,但是适当地进行容量规划还是必要的。

  • 在这个理想化的情景下,只有在请求速率超过单个请求的处理速率时,请求才会进入队列,这种情况会导致线程池和队列的同时饱和。

  • 对一个流量基本稳定的服务来说,队列长度比线程池大小更小会更好(如 50% 或更小)。当服务处理速度无法跟上请求到达速率时,尽早拒绝请求会更好。

  • 流量抛弃和优雅降级流量抛弃(load shedding)是指在软件服务器临近过载时,主动抛弃一定量的负载。

  • 一种简单的流量抛弃实现方式是根据CPU使用量、内存使用量及队列长度等进行节流。

  • 简化了一些细节,[75]但是这里很好地展现了重试是如何摧毁一个系统的。注意临时性的过载升高,或者使用量的缓慢增加都有可能造成这种情况。

  • 一定要使用随机化的、指数型递增的重试周期。

  • 如果重试不是随机分布在重试窗口里的,那么系统出现的一个小故障(某个网络问题)就可能导致重试请求同时出现,这些请求可能会逐渐放大

  • 限制每个请求的重试次数。不要将请求无限重试。● 考虑使用一个全局重试预算。例如,每个进程每分钟只允许重试60次,如果重试预算耗尽,那么直接将这个请求标记为失败,而不真正发送它。这个策略可以在全局范围内限制住重试造成的影响,容量规划失败可能只是会造成某些请求被丢弃,而不会造成全球性的连锁故障。

  • 从多个视角重新审视该服务,决定是否需要在某个级别上进行重试。这里尤其要避免同时在多个级别上重试导致的放大效应:高层的一个请求可能会造成各层重试次数的乘积数量的请求。如果服务器由于过载不能提供服务,后端、前端、JavaScript层各发送3次重试(总计4次请求),那么一个用户的动作可能会造成对数据库的64 次请求(43)。在数据库由于过载返回错误时,这种重试只会加重问题。

  • 使用明确的返回代码,同时详细考虑每个错误模式应该如何处理。例如,将可重试错误和不可重试错误分开。

  • 设置一个截止时间通常是明智的。不设置截止时间,或者设置一个非常长的截止时间通常会导致某些短暂的、已经消失的问题继续消耗服务器资源,直到重启。

  • 截止时间设置得太长可能会导致框架中的高层级部分由于低层级的问题而持续消耗资源。截止时间设置得太短可能会导致某些比较重型的请求持续失败。恰当的截止时间设置,需要在多个限制条件中选择一个平衡点。

  • 截止时间传递与其在发送RPC给后端服务器时自拟一个截止时间,不如让软件服务器采用截止时间传递和取消传递的策略。

  • 可使用截止时间传递机制,截止时间在整个服务栈的高层设置(如,前端服务器)。由初始请求触发的整个RPC树会设置同样的绝对截止时间。

  • 同时,我们可能还会将传递出去的截止时间减少一点(如几百毫秒),以便将网络传输时间和客户端收到回复之后的处理时间考虑在内。

  • RPC取消的传递可以避免某些泄露情况,如果某个初始RPC设置了一个很长的截止时间,但是底层之间的RPC只有短暂的截止时间,超时失败了。使用简单的截止时间传递可能会导致初始RPC虽然无法继续处理,却继续消耗服务器资源直到超时。

  • 请求延迟的双峰分布(Bimodal)

  • 使用100s的截止时间,5%的请求会消耗5000个线程(50QPS * 100 seconds),但是前端服务器并没有这么多可用线程。忽略副作用,前端也仅能够处理19.6%的请求(1000可用线程 /(5000+95)线程工作),这会造成 80.4%的错误率。因此,不仅 5% 的请求受到了影响(那些由于后端问题不可能成功的请求),实际上大部分的请求都受到了影响。

  • 如果无法完成的请求能够尽早返回一个错误而不是等完整个截止时间,我们就可以避免这个问题。例如,如果一个后端服务器不可用,经常立刻返回一个错误值是最好的,而不是等待这个后端服务器变得可用。如果RPC层支持快速失败的选项,一定要启用它。

  • 假设你的后端要处理来自不同客户端的性能和特征各异的请求,我们可以考虑限制一个客户端只能占用25% 的线程总数,以便在某个异常客户端大量产生负载的情况下提供一些公平性。

慢启动和冷缓存

  • 慢启动和冷缓存进程在刚刚启动之后通常要比稳定状态下处理请求的速度慢一点。慢的原因可能是由下列一个或多个原因导致:必需的初始化过程在接收到第一个请求后,需要跟后端服务器建立连接。运行时性能优化,尤其是JavaJIT 编译过程,热点优化,以及类延迟加载机制。同样的,有些服务器会在缓存没有充满之前效率很低。

  • 过量配备(overprovision)该服务。区分延迟类缓存和容量类缓存是很重要的:当使用延迟类缓存时,服务器可以在空缓存的情况下仍然处理预期的请求负载,但是使用容量类缓存时,该服务将不能够在空缓存下处理请求负载。

保持调用栈永远向下

  • 假设一个用户有一个主后端和一个预先选择好的另外集群中的一个热备后端,主后端在底层出现错误或者延迟上升的情况下将请求代理给热备后端。如果整个系统都处于过载状态,那么从主到副的这种代理可能会增多,会给系统带来更多的负载,因为请求通常要被解析两次,还需要主后端消耗资源等待副后端任务。

  • 在用户的请求路径中最好能够避免使用同层通信—也就是避免通信路径中出现环。

  • 应该由客户端来进行这种通信。例如,如果一个前端需要和后端通信,但是猜错了后端任务,后端不会代理请求给正确的后端,而是通过返回错误使得前端在正确的后端任务上重试它的请求。

连锁故障的触发条件

  • 根据请求数量和可用容量来动态调节任务的同时更新数量可能是个好办法。

  • 在发生连锁故障时,检查最近的改变以及回滚通常是明智的,尤其在这些改变会影响容量或者更改请求特点的情况下。

  • 自然增长在很多情况下,连锁故障不是由于某个特定的服务改变导致的,而是由于使用量的天然上升,却没有进行对应的容量调整导致的。

连锁故障的测试

  • 设计良好的组件应该可以拒绝一小部分请求而继续存活。

小结

  • 小结当一个系统过载时,某些东西总是要被牺牲掉。一旦一个服务越过了临界点,服务一些用户可见错误,或者低质量结果要比尝试继续服务所有请求要好。理解这些临界点所在,以及超过临界点系统的行为模式,是所有想避免连锁故障的运维人员所必需的。如果不加小心,某些原本为了降低服务背景错误率或者优化稳定状态的改变反而会让服务更容易出现事故。在请求失败的时候重试、负载自动转移、自动杀掉不健康的服务器、增加缓存以提高性能或者降低延迟:这些手段原本都是为了优化正常情况下的服务性能,但是也可能会提高大规模的服务故障的几率。一定要小心评估这些改变,否则灾难就会接踵而至。

个人总结

  1. 使用负载压力测试得出服务器的极限以及过载降级策略
  2. 一定要使用随机化的、指数型递增的重试周期。
  3. 使用明确的返回代码,同时详细考虑每个错误模式应该如何处理。例如,将可重试错误和不可重试错误分开。
  4. 设置一个截止时间通常是明智的。RPC之间传递。
  5. 限制一个客户端对线程总数占有比例,以便在某个异常客户端大量产生负载的情况下提供一些公平性。
  6. 慢启动和冷缓存;区分延迟类缓存和容量类缓存。
  7. 保持调用栈永远向下(减少转发次数和后端资源消耗)。
  8. 设计良好的组件应该可以拒绝一小部分请求而继续存活。

第23章 管理关键状态:利用分布式共识来提高可靠性

  • 在构建可靠的、高可用的系统过程时,我们发现分布式共识系统适合用来维护某个强一致的系统状态

  • 当你需要实现领头人选举(leader election)、关键性共享状态或分布式锁等时,我们建议采用某种正式证明过的、详尽测试过的分布式共识系统来实现。如果不严肃对待这个问题很有可能会导致事故,在更糟的情况下,将造成某种非常难以修复的数据一致性问题,这可能会大大加长系统事故的处理时间。

  • 网络分区问题迟早会发生(光缆被切断,数据包由于拥塞造成丢失或延迟升高,硬件故障,网络组件配置错误等),理解如何构建分布式共识实际上就是理解某个服务应用如何实现强一致性和高可用。由于商业的压力,很多服务都需要很高的可用性,这些应用程序通常都需要对数据访问的强一致性。

  • 最终一致可能会带来意想不到的问题(参见文献[Lu15]),尤其是当时钟漂移(在分布式系统中,这是不可避免的),或者网络分区(参见文献[Kin15])发生的时候。

  • Jeff Shute(参见文献[Shu13])曾经说过,“我们发现开发者通常花费了大量的时间构建一个极为复杂和容易出错的机制来应对最终一致性下可能过时的数据。我们认为对开发者来说这是一种无法接受的负担,数据一致性的问题应该在数据库层解决。”

使用共识系统的动力:分布式系统协调失败

  • 在网络分区情况下—造成问题不一定是由完全分区导致的,而是:● 网络非常慢。● 某些消息可以通过,但是某些消息被丢弃了。● 单方面的节流措施。

  • 如果网络质量真的很差,分布式共识系统也无法正确选举出主实例时,作为工程师可能也没有什么好办法来做出正确决策。

分布式共识是如何工作的

  • 拜占庭式问题指的是当某个进程由于程序Bug或者恶意行为发送不正确的消息的问题,这种问题相对来说处理成本更高,同时也更少见。

  • 严格来讲,在有限时间内解决异步式分布式共识问题是不可能的。正如Dijkstra 的获奖文章—FLP impossibility result(参见文献[Fis85])写的那样,在不稳定的网络条件下,没有任何一种异步式分布式共识算法可以保证一定能够达成共识。在实际操作中,我们通过保证给系统提供足够的健康的副本,以及良好的网络连接状态来保障分布式共识算法在大多数情况下是可以在有限时间内达成共识的。同时整个系统还需要加入随机指数型延迟。

  • 这样,我们可以保障重试不会造成连锁反应,以及本章后面会提到的角斗士(dueling proposers)问题。

  • 这个协议在一个有利环境下能够保障安全性,同时提供足够的冗余度。

  • 最初的分布式共识问题的解决方案是Lamport的Paxos 协议(参见文献[Lam98]),但是也有其他的协议能够解决这个问题,包括 Raft(参见文献[Ong14])、Zab(参见文献[Jun11]),以及 Mencius(参见文献[Mao08])。Paxos本身也有很多变种尝试提升性能(参见文献[Zoo14])。这些变种常常只是在某一个小细节上不同,例如给某个进程一个特殊的领头人角色以简化协议实现等。

分布式共识的系统架构模式

  • 可靠的复制状态机一个复制状态机(replicated state machine,RSM)是一个能在多个进程中用同样顺序执行同样的一组操作的系统。RSM 是一个有用的分布式系统基础组件,可以用来构建数据和配置存储,执行锁机制和领头人选举(接下来会详细描述)。

  • 在RSM系统上进行的操作是通过共识算法来全局排序的。

  • 时间戳在分布式系统中问题非常大,因为在多个物理机器上保证时间同步是不可能的。Spanner(参见文献[Cor12])通过针对最差情况下的不确定性建模,同时在必要时减慢处理速度来解决这个问题。

  • 分布式系统的领头人选举是跟分布式共识等价的问题。复制多份服务并使用一个唯一的领头人(leader)来进行某种类型的工作是很常见的设计。唯一的领头人是一种保证粗粒度互斥性的方法。

分布式共识系统的性能问题

  • 世界上并没有某个性能“最优”的分布式共识和状态机复制算法,因为算法性能通常取决于与系统负载有关的多个因子,以及系统性能的目标和系统的部署情况。

  • 复合式Paxos:消息流过程详解复合式Paxos(Multi-Paxos)协议采用了一个强势领头人(strong leader)进程的概念:除非目前没有选举出任何的领头人进程,或者发生了某种错误,在正常情况下,提议者只需要对满足法定人数的进程发送一次消息就可以保障系统达成共识。这种强势领头人在很多共识协议中都适用,在系统需要传递大量消息的时候非常合适。

  • 很多共识系统使用TCP/IP作为通信协议。TCP/IP是面向连接的,同时针对消息的先进先出(FIFO)顺序提供了强保障。但是在发送任何数据之前,都需要先建立新的TCP/IP连接,也就是一次网络往返需要进行三次握手协议。TCP/IP慢启动机制也限制了连接的初始带宽。常见的TCP/IP窗口大小在4~15KB之间。

  • TCP/IP的慢启动问题对共识组中的进程来说可能不是问题:因为这些进程会互相建立一个连接而且保持这些连接,因为通信将会很频繁。但是,对拥有非常多的客户端的系统来说,可能不能做到让每个客户端都和共识组所有进程都保持一个活动连接。因为TCP/IP需要消耗一定资源,包括文件描述符,以及对应的KeepAlive数据流量。对高度分片、拥有几千个副本的数据存储系统,以及更大规模的客户端来说,这种成本是无法接受的。一个解决方案是使用地域性的代理池,如图23-9所示。该代理池的进程与共识组建立持久的TCP/IP进程,以降低客户端的开销。同时代理池也是包装分片逻辑和负载均衡逻辑,以及共识系统的服务发现逻辑的好地方。

  • 快速Paxos协议:性能优化快速Paxos协议(Fast Paxos,参见文献[Lam06])是Paxos协议的一个变种,意在优化Paxos算法在广域网络中的性能。使用该协议的每个客户端可以直接向组内的接收者们发送Propose消息,而不需要像经典Paxos和复合Paxos那样通过一个领头人进程发送。

  • 稳定的领头人机制上文已经描述过,复合Paxos是如何通过选举一个稳定的领头人进程来提高性能的。Zab(参见文献[Jun11])和Raft(参见文献[Ong14])协议是其他两个例子,它们也通过选举稳定的领头人进程来提高性能。这种方案可以进一步进行读操作优化,因为领头人始终拥有最新信息,但是也存在以下几个问题:● 所有的改变状态的操作都必须经由该领头人,这个要求使得距离领头人进程较远的客户端必须要增加额外的网络延迟。● 领头人进程的外发网络带宽是系统性能的瓶颈(参见文献[Mao08]),因为该领头人的Accept消息包含了每个提议的所有数据,而其他进程发送的消息仅仅包含了交易数字,而没有真正的数据。● 如果领头人进程恰好处于一台有性能问题的机器上,整个系统的吞吐量都会受到影响。几乎在所有关注性能的分布式共识系统设计中,都采用了单个稳定领头人进程机制,或者是某种领头人轮换机制—预先分配给每个副本一个独立的分布式算法编号(通常是对交易编号取模)。使用轮换机制的算法包括Mencius(参见文献[Mao08])和Egalitarian Paxos(参见文献[Mor12a0])。

  • 复合Paxos的信息流的过程在本章前面“复合式Paxos:消息流过程详解”一节中描述过了,但是并没有展示在什么情况下协议要求必须在磁盘中记录状态改变。在进程做任何一个承诺之前都必须进行磁盘写操作。在复合Paxos性能的关键点—协议的第二部分中,这些操作点处于接收者针对某个提议发送Accepted消息之前,以及提议者发送Accept消息之前—因为这条Accept消息其实是一个隐含的Accepted消息

  • 这就意味着在单个共识操作的延迟中,有以下几个操作:● 提议者的一次硬盘写入操作。● 并行消息发送给接收者。● 每个接收者的磁盘写操作(并行)。● 回复消息的传递。

  • 分布式共识算法经常用来实现一个复制状态机。RSM需要保留一份交易日志,以便用于灾难恢复(正如其他数据存储那样)。共识算法的日志可以和RSM的交易日志合为一个。将这两个日志合并可以避免不停地向磁盘上的两个不同位置交替写入(参见文献[Bol11]),也就降低了磁盘寻址操作消耗的时间。这些磁盘于是可以每秒处理更多操作,该系统也可以每秒处理更多操作。

  • 在数据存储系统中,磁盘在维护日志之外还有其他用处:数据通常也是存放于磁盘中的。日志的写操作必须直接刷写到硬盘,但是数据的改变通常可以写入内存缓存中,稍后再写入磁盘,可以进行重新排序以便更有效地写入(参见文献[Bol11])。

分布式共识系统的部署

  • 系统设计者部署共识系统时,最重要的决策在于选择副本的数量和对应的部署位置。

  • 副本的数量一般来说,共识系统都要采用“大多数”的法定过程。也就是说,一组2f+1副本组成的共识组可以同时承受f个副本失败而继续运行(如果需要容忍拜占庭式失败,也就是要能够承受副本返回错误结果的情况,则需要3f+1个副本来承受f个副本失败(参看文献[Cas99]))。针对非拜占庭式失败的情况,最小的副本数量是3—如果仅仅部署两个副本,则不能承受任何一个副本失败)。3个副本可以承受1个副本失败。大部分系统的不可用时间都是由计划内维护造成的

  • 3个副本使该系统可以在1个副本维护时继续工作(这里假设其余两个副本可以承受系统负载)。如果某个非计划内的失败情况在常规维护时发生了,那么共识系统就会不可用。而共识系统的不可用通常是不可接受的,于是在这种情况下,我们需要5个副本同时运行,于是可以允许系统在两个副本不可用的情况下继续运行。如果5个副本中有4个能正常运行,则不需要进行任何人工干预操作,但是如果仅仅只有3个能正常运行,那么我们需要立刻增加一个或者两个额外的副本。

  • 如果共识系统中大部分的副本已经无法访问,以至于无法完成一次法定进程,那么该系统理论上来说已经进入了一个无法恢复的状态。因为至少有一个副本的持久化日志无法访问。在这个状态下,有可能某个操作仅仅只在无法访问的副本上执行了。这时,系统管理员可以强行改变小组成员列表,将新的副本添加到小组中,使用其他可用成员的信息来填充。但是数据丢失的可能性永远存在—这种情况应该全力避免。在灾难处理过程中,系统管理员需要决定是否需要进行这种强制性的重新配置,或者仅仅是等待那些机器恢复可用。当决策做出后,对系统日志的处理(以及对应的监控)就变得非常重要。

  • 针对任何系统的副本数量的考虑都是基于以下几个因素中的一个进行妥协:● 对可靠性的要求● 计划内维护操作的频率● 危险性● 性能● 成本最后的决策每个系统都会不同:每个系统都有不同的可用性服务水平目标;某些组织会更频繁地进行维护性操作;而某些组织使用的硬件成本、质量和可靠性都有所不同。

  • 一个故障域(failure domain)是指系统中可能由于一个故障而同时不可用的一组组件。常见的故障域包括:● 物理机器。● 数据中心中用同一个供电设施的一个机柜。● 数据中心中的数个机柜,使用同一个网络设备连接。● 受单个光纤影响的一个数据中心。● 处于同一个地理区域的一组数据中心,可能被同一个自然灾害所影响。

  • 一般来说,随着副本之间的距离增加,副本之间的网络往返时间也会增加,但是系统能够承受的失败程度也会增加。对多数共识系统来说,网络往返时间的增加会导致操作延迟的增加。

  • 对延迟的敏感性和对灾难的承受程度,每个系统很不一样。在某些共识系统架构下并不需要特别高的吞吐量,或者特别低的延迟:例如,某个共识系统如果只是为了提供成员信息和领头人选举,服务一般不会负载很高,如果共识操作时间仅仅是领头人租约的一小部分时,性能并不是关键点。批处理居多的系统也很少会被延迟所影响:可以通过批处理数量的增加来提高吞吐量。

  • 当处理关键数据时,即使已经有了可靠的共识系统部署在不同的故障域范围内,我们也必须经常将数据备份在其他地方。因为有两个故障域是我们永远无法逃避的:软件本身和系统管理员的人为错误。软件系统中的Bug可能会在罕见情况下浮现造成数据丢失,而错误的系统配置也可能会造成类似情况。而人工操作可能会执行错误的命令,从而造成数据损坏。

  • 当决定副本位置的时候,记得最关键的因素是客户端可见性能:理想情况下,客户端和共识系统之间的RTT应该是最小化的。在广域网络中,无领头人的协议,例如 Mencius和 Egalitarian Paxos可能有一定的性能优势,尤其是当应用程序设计为可以对任意副本进行读操作而不需要进行共识操作时。

  • 向一个采取“大多数”法定仲裁过程系统中增加新的副本可能会降低系统的可用性,如图23-10所示。对ZooKeeper和Chubby来说,一个常见的部署使用5个副本,一个法定仲裁过程需要3个副本参与。整个系统可以在两个副本,也就是40% 不可用的情况下仍然正常工作。当使用6个副本时,仲裁过程需要4个副本:也就是超过33% 的副本不可用就会导致系统无法工作。

  • 为了将系统流量分布得更为均匀,系统设计者可以考虑采用5个副本,将其中2个副本放置于美国中心地带,1个放置于东海岸,另外两个放置于欧洲。这样分布可基本保证共识过程在北美洲的副本上完成,而不需要等待欧洲的回复。而从欧洲起始的共识过程可以仅仅跟美国东海岸的副本完成。东海岸的副本就像两个可能的仲裁组的关键轴,将两个组连接在了一起。

个人总结

  1. 复合式Paxos、快速Paxos协议
  2. 一个常见的部署使用5个副本,一个法定仲裁过程需要3个副本参与。
  3. 分布式共识协议学习参考 TODO

数据完整性的强需求

  • 数据完整性意味着用户可以维持对云服务的访问,用户对数据的访问能力是最重要的,所以这种访问能力的完整性非常重要。

  • 大多数云计算应用都是优化以下5项的某种组合:在线时间、延迟、规模、创新速度和隐私。

  • 为了避免应用程序的数据出现问题,影响到最终用户,一个带外系统、专门负责检查和平衡各种数据存储的系统就很有必要了。

  • 一般来说,公司会采用某种备份策略来“预防”数据丢失。然而,真正应该被关注的重点其实是数据恢复,这是区分备份与存档的重要区别。就像一句流行语说的那样:没有人真的想要备份数据,他们只想恢复数据。

  • 备份与存档最重要的区别就是,备份是可以直接被应用程序重新加载的。因此备份和存档的使用场景非常不同。

  • 存档的目的是将数据长时间安全保存,以满足审核、取证和合规要求。在这些情况下的数据恢复通常不需要满足服务的在线率要求。例如,我们可能需要将财务交易数据保存七年时间。为了达到这个目标,我们可以每个月将累积的审核日志迁移到离线的、异地的长期存档存储系统上。读取和恢复这样的信息可能需要一周时间,在长达一个月的财务审核过程中,这是可以接受的。相对存档,当灾难来临的时候,数据必须从真实的备份中快速恢复,最好能维持服务在线率的要求。否则的话,受影响的用户将由于数据问题无法使用应用程序,直到数据恢复过程完成。

  • 尤其考虑到大多数最新产生的数据直到安全备份结束之前都存在丢失的风险,这就意味着备份(而不是存档)应该至少每天进行一次,或者每小时,甚至更短时间内进行一次。备份应该同时使用完整备份、增量备份,或者甚至是流式持续备份手段。因此,当选择备份策略时,一定要考虑针对某个问题需要的恢复时间,以及可以丢失多少最新数据。

保障数据完整性和可用性:Google SRE的目标

  • 数据完整性是手段,数据可用性是目标数据完整性指的是在其生命周期中,数据的准确性和一致性。从数据被记录的那一刻开始,一直到数据被访问的时候,数据应该保持正确,不会以某种未预知的方式改变。

  • 从用户的角度来看,仅仅保障数据完整性,而没有保障数据的可用性是没有意义的。

  • 交付一个恢复系统,而非备份系统针对系统进行备份是一个长期被忽视、被拖延的系统管理任务。任何人都不会将备份作为一个高优先级任务进行—备份需要长期消耗时间和资源,却不能带来任何现在可见的好处。由于这个原因,在备份策略具体实施中如果出现了某些被忽视的细节问题,大家一般都能理解。甚至有的人说,就像其他针对低可能性的危险的保护措施那样,这样的态度是必然发生的。这里的核心问题其实是,我们没有意识到备份措施防护的问题虽然是不太可能发生的,但却是会造成严重问题的。当该服务的数据不可用时,备份系统对整个服务、整个产品,甚至整个公司来说,都是生死攸关的。

  • 相对于关注没人愿意做的备份任务来说,我们应该通过关注更重要的(但并不是更简单的)恢复任务来鼓励用户进行备份。备份其实就像纳税一样,是一个服务需要持久付出的代价,来保障其数据的可用性。我们不应该强调应该纳多少税,而是应该强调这些税会用来提供什么服务:数据可用性的保障。因此,我们不会强迫团队进行“备份”,而是要求:● 各团队需要为不同的失败场景定义一系列数据可用性SLO。● 各团队需要定期进行演练,以确保他们有能力满足这些SLO。

造成数据丢失的事故类型

  • 根源问题某种无法恢复的用户数据丢失是由以下几个因素造成的:用户行为、管理员的错误、应用程序的Bug、基础设施中的问题、硬件故障和部署区的大型事故。

  • Google根据一次针对19次数据恢复过程的研究得出,最常见的用户可见数据丢失场景是由于数据删除和软件Bug造成的引用完整性问题。

  • 在设计数据完整性保障机制时,必须要认识到复制机制和冗余并不意味着可恢复性。

扩展性问题:全量、增量以及相互竞争的备份和恢复机制

  • 当别人问你“是否有备份”的时候,一个经典的错误回答是“我们有比备份更好的机制—复制机制!”复制机制很重要,包括提高数据的本地性(locality),保护某个部署点单点事故等。但是有很多数据丢失场景是复制机制无法保护的。一个自动同步多个副本的数据存储多半会在你发现问题之前,将损坏的数据记录以及错误的删除动作更新到多个副本上。

Google SRE保障数据完整性的手段

  • 24种数据完整性的事故组合

  • 由于数据丢失类型很多(如上文所述),没有任何一种银弹可以同时保护所有事故类型,我们需要分级进行。分级防护会引入多个层级,随着层级增加,所保护的数据丢失场景也更为罕见。

  • 软删除机制可以大幅度减少支持人员的压力。软删除意味着被删除的数据立刻被标记为已删除,这样除了管理后台代码之外其他代码不可见。

  • 软删除还意味着一旦数据被标记为已删除,在某段时间以后会真的被删除。这个时间的长度,尤其是在有很多短期数据的情况下,取决于某个组织的政策和相关的法律条文、可用的存储空间和存储成本,以及产品的价格和市场定位等。常见的软删除时间是15、30、45或者60天。在Google的经验中,大部分账号劫持和数据完整性问题都会在60天内汇报或者检测到。因此,为软删除保存超过60天的需求可能没有那么强烈。

  • 应用中实现一个回收站机制,作为用户错误的主要防护手段。

  • 就算不考虑成本问题,频繁地进行全量备份也是非常昂贵的。最主要的是,这会给线上服务用户的数据存储造成很大的计算压力,以至于影响服务的扩展性和性能。为了缓解这种压力,我们需要在非峰值时间段进行全量备份,同时在繁忙时间段进行增量备份。

  • 没有任何办法可以100% 确定数据是正确的:虽然我们相信存储系统的实现,但是还是要校验!使这个问题变得更复杂的是,为了从低级数据损坏或者删除场景中恢复数据,我们必须从不同恢复点的不同备份中恢复不同子集的数据。同时,我们还要考虑到在一个不断变化的环境中,代码和数据结构的变化对老的备份数据的影响。

  • 安排一些开发者来开发一套数据校验流水线可能会在短期内降低业务功能开发的速度。然而,在数据校验方面投入的工程资源可以在更长时间内保障其他业务开发可以进行得更快。因为工程师们可以放心数据损坏的Bug没那么容易流入生产环境。和在项目早期引入单元测试效果类似,数据校验流水线可以在整个软件开发过程中起到加速作用。

  • 举一个具体的例子:Gmail拥有一系列数据校验器,每一个校验器都曾经在生产环境中检测到真实的数据完整性问题。Gmail开发者由于知道每个新引入的数据一致性问题都可以在24小时之内被检测到而感到非常安心。这些数据校验器的存在,结合单元测试及回归测试的文化使得Gmail开发者有足够的勇气每周多次修改Gmail生产存储系统的实现。

  • 带外数据校验比较难以正确实现。当校验规则太严格的时候,一个简单的合理的修改就会触发校验逻辑而失败。这样一来,工程师就会抛弃数据校验逻辑。如果规则不够严格,那么就可能漏过一些用户可见的数据问题。为了在两者之间取得恰当的平衡,我们应该仅仅校验那些对用户来说具有毁灭性的数据问题。

  • 大规模部署带外检测器可能成本较高。Gmail计算资源的很大一部分都被用来支持每日数据检测的运行。使这个问题变得更严重的是,校验器本身可能会造成软件服务器缓存命中率的下降,这就会造成用户可见响应速度的下降。为了避免这种问题,Gmail提供了一系列针对校验器的限速机制,同时定期重构这些校验器,以降低磁盘压力。

  • 我们可以保证每天运行的校验器持续寻找毁灭性的问题,而其他更严格的校验器可以以更低频率运行,以便满足延迟和成本的要求。

  • 大部分高速进行的小型开发团队都负担不起设计、构建和维护这样的系统所需的资源。就算强迫他们进行,最后的结果也经常是不可靠、不全面的,以及浪费性、一次性的方案,很快就会被抛弃。因此,最好的办法是单独组织一个中央基础设施组为多个产品和工作组提供一套数据校验框架。该基础设施组负责维护带外数据校验框架,而产品开发组负责维护对应的业务逻辑,以便和他们不断发展的产品保持一致。

  • 数据恢复流程中任何一个环节都有可能出问题,只有建立一个完善的端到端测试才能让人晚上放心睡觉。即使最近刚刚成功进行过一次数据恢复,下次执行时某些步骤仍然可能出问题。

  • 只有真正进行恢复操作之后,才能确定到底能否恢复最新数据。

  • 失败是不可避免的,不去主动寻找这些数据恢复失败的场景等于玩火自焚。只有通过进行测试来主动寻找这些弱点,才能够提前修复,关键时刻才不至于追悔莫及!

SRE的基本理念在数据完整性上的应用

  • 信任但要验证我们依赖的任何API都不可能一直完美工作。不管测试有多么复杂,工程质量有多么高,API都一定会存在某些问题。应该通过使用带外数据检测器来检查数据最关键、最核心的部分,哪怕API语义中说明了这些问题不存在。要记住,完美的理论不代表实现也是完美的。

个人总结

  • 备份与存档最重要的区别就是,备份是可以直接被应用程序重新加载的。
  • 通过使用带外数据检测器来检查数据最关键、最核心的部分。

不完美的机器

  • 不完美的机器从某种意义上讲,人类可以被称为不完美的机器。人会感觉无聊,人的处理器(指思维方式)工作原理不清楚,用户界面(指沟通方式)也不太友好,效率也不高。在工作安排中,能够认识到人的这些缺陷,取长补短是最好的。

  • 分心指数工程师能够被分心的方法太多了。例如,某个名字是Fred的SRE 星期一正常上班,他今天不是on-call,也不负责处理中断性事务。很明显,他希望能够进行他自己的项目工作。倒一杯咖啡,带上“不要打扰”的耳机,开始干活,一切正常,对吗?但是,下列任何一件事情都可能随时发生:● Fred的团队使用随机工单分配系统,某个需要今天处理的工单被分配给了他。● Fred的同事目前负责on-call,收到了一个紧急警报。发出警报的组件Fred最熟悉,所以这个同事过来咨询意见。● 某个用户将某个工单的优先级提高了,这个工单是上周Fred轮值on-call的时候就在处理的。● 某个已经持续了三四周的发布到Fred的时候突然出现了问题,Fred需要停下手中的事情检查发布的状态,回滚这个发布等。● 某个用户过来咨询一个问题,因为Fred为人和善。● 等等。最后结果是

  • 虽然Fred有一整天的时间分配给项目工作,他还是很容易被分心。他可以通过关闭E-mail、关掉IM,或者其他手段来减少一定的干扰。但是某些干扰是政策造成的,以及一些无法避免的责任造成的。我们可以说某种程度的干扰是不可避免的,也是有意为之的。这是正确的:每个人都有甩不掉的Bug,同事之间也会有关系的产生与职责的分配。然而,作为团队来说,是有一些管理方式能使更多的员工不受干扰的。

  • 极化时间为了限制干扰数量,我们应该减少上下文切换(指工作类型、环境等的改变)。某些中断性任务是无法避免的。然而,将工程师当成是可以随时中断、上下文切换没有成本是不正确的。给每次上下文切换加上成本的考虑。在项目工作中,一次20分钟的中断性任务需要进行两次上下文切换,而这种切换会造成数个小时的生产力的丧失。为了避免这种经常性的生产力丧失,我们应该延长每种工作模式的时间,一天甚至半天都可以。这种策略与“挤时间”(参见文献[Gra09])策略工作得很好。

  • 极化时间意味着当每个人来上班时,他们应该清晰地知道自己今天是否只是做项目工作,还是只是做中断性工作。这意味着他们可以更长时间地专注于手上的工作,不会不停地被那些他们本不应该负责的事情所打扰。

  • 一般性建议不论哪种中断性任务,如果中断性任务的量对一个人来说太高,那么应该增加一个人负责。这个概念很显然适用于工单,但是也适用于紧急警报。on-call工程师可以将紧急警报降级为工单,也可以找副on-call来共同处理。

  • 主on-call工程师应该专注于on-call工作。如果目前紧急警报较少,那么一些可以随时放下的工单,或者其他中断性事务应该由on-call人员处理。当某个工程师on-call一周时,这一周他应该完全排除在项目进度之外。如果某个项目非常重要,不能等待一周,那么这个人就不应该参与on-call。管理层应该介入,安排其他人替代on-call。管理层不应该期望员工在on-call的同时还能在项目上有所进展(或者其他高上下文切换成本的活动)。

  • 另外:总有一些清理工作要做,工单数量可能是0,但是总会有需要更新的文档,需要整理的配置文件等。未来的on-call工程师可以从这些活动中受益。他们就不会再来打扰你了!

  • 工单如果目前你是随机分配工单给团队成员,请立刻停止。这样做对团队的时间非常不尊重,和让成员尽可能不被打扰的目标背道而驰。工单处理应该由全职人员负责,同时保证占用合理的时间。如果团队目前的工单主oncall和副on-call都处理不完,那么需要重新架构整个工单的处理流程,保障任何时间都有两个全职员工处理工单。不要将复杂分散到整个团队中去。人不是机器,这样做只会干扰员工,降低他们的工作效率。

  • 持续性的运维工作尽可能地让整个团队共同负责这件事。例如变更发布、配置文件修改等,如果这项工作有非常清晰的流程定义,那么就没有任何理由让任何一个人专门负责这件事情。定义一个发布管理者的角色来由on-call和其他中断性任务处理者来承担。同时应该进行正式化交接过程—这有一定成本,但是却保障了接下来的人不受打扰。负责中断性事务,或者不负责有的时候,某个中断性任务只有某个目前不在值班的成员能够妥善处理。虽然理想情况下,这种情况不应该发生,但是还是偶尔会发生,我们应该尽一切努力避免这种情况。

个人总结

  1. 极化时间,减少干扰;整理必要的文档。
  2. 不要将复杂分散到整个团队中去。人不是机器,这样做只会干扰员工,降低他们的工作效率。

第30章 通过嵌入SRE的方式帮助团队从运维过载中恢复

  • Google的SRE团队的标准政策要求团队在项目研发和被动式工作之间均匀分配时间。在实际工作中,这种平衡可能会被每天工单数量的不断增加而被打乱,甚至持续数月之久。长时间承担非常繁重的Ops类型的工作是非常危险的,团队成员可能会劳累过度,不能在项目工作上取得任何进展。当一个团队被迫花费过多时间解决工单的时候,他们就会减少花在改进服务上的时间,服务的可扩展性和可靠性肯定会随之变差。解决这个问题的一种方式是给处于过载状态的团队临时调入一个SRE。调入该团队后,该SRE应该关注于改善这个团队的行事方式,而不是简单地帮助团队清理积压的工单。通过观察团队的日常工作,提出改善性意见,该SRE可以帮助团队用全新的视角来审视自己的日常工作。这往往是团队本身的成员做不到的。注意,当采用这个方法的时候,只需要调入一名SRE就够了。同时调入两名SRE不一定会产生更好的结果。对那些防御性比较强的团队来说,可能会激发问题的发生。

  • 当你开始构建自己的第一个SRE团队时,采用本章中介绍的方法可以帮助避免团队走入歧途,退化成那种只专注于工单处理的传统运维团队。

  • 在决定采用这种嵌入SRE的方式之前,你应该花一些时间来回顾一下Ben Treynor Sloss的介绍中提到的SRE理念和实践经验,同时应该看一下本书的第6章。

第一阶段:了解服务,了解上下文

  • 第一阶段:了解服务,了解上下文在嵌入团队的过程中,你的主要工作是清楚地表达团队目前的流程和工作习惯对于该服务的可扩展性有利或者有弊的原因。你应该提醒该团队,日益增加的工单不应需要更多的SRE来处理。SRE模型的目标是仅仅在系统复杂度上升的时候才增加新人。你应该尝试引导团队建立健康的工作习惯,这样能够减少花费在工单上的时间。这与指出该服务目前还可以自动化或者进一步简化同样重要。

  • SRE团队陷入Ops模式的原因是过分关注如何快速解决紧急事件而不是如何减少紧急事件的数量。

第二阶段:分享背景知识

  • 第二阶段:分享背景知识在了解团队内部动态和找到痛点之后,你应该通过建立一些最佳实践来为改善状况做准备。例如,建立书写事后总结的机制,辨别琐事的来源,确定每个琐事的最佳解决方案等。书写一个好的事后总结作为示范

  • 你应该将团队遇到的紧急事件分为琐事的和非琐事的。最后,将这个列表交给团队并且清晰地解释哪些紧急事件应该被自动化,而其他的紧急事件则是运维服务必须承担的工作。

第三阶段:主导改变

  • 第三阶段:主导改变保持团队健康是一个持续的过程。正因为此,这不是你可以通过个人英雄主义来解决的问题。为了确保团队在未来可以进行自我调节,我们需要帮助他们建立一个良好的SRE心理模型。

  • 对某项决策进行充分解释的例子:●“我反对最新版本的原因不是因为测试结果有问题,而是因为我们对发布所制定的错误预算已经耗尽了。”● “发布需要能够安全回退,因为我们的SLO非常高。要想达到SLO的要求,平均恢复时间必须非常短,这样在回退之前进行深入的现场调查是不现实的。”对于某项决策不充分的解释的例子:●“我不认为每一个服务器自己生成自身的路由配置是安全的,因为我不信。”这一决策可能是正确的,但是分析论证上是很薄弱的(也没有详细解释)。团队无法知道你心中所想,所以他们很可能模拟你这种不良行为。相反,尝试“[……]不安全,因为在该代码中的一个错误会导致其他服务同时受到影响,同时额外的代码也可能带来减慢回滚的速度的问题。”●“自动化机制在遇到部署不一致的情况时应该放弃进行。”

  • 和之前的例子一样,这一决策可能是正确的,但是是不充分的。相反,尝试“[……]因为我们这里的一个简化假设是全部的改变都通过自动化进行,这种情况的发生说明了有什么东西违反了这一规则。如果这种情况经常发生,我们应该找到和消除造成这种变化的根源。”

  • 提出引导性问题应该提出引导性问题,而非指责性的问题。当你和SRE团队交流时,试着用一种可以鼓励别人思考基本理念的方式来提出问题。

  • 引导性问题的例子:● “我看到任务失败的警报经常发生,但是on-call工程师通常什么都不做。这样会对SLO有什么影响?”● “这个上线过程看起来非常复杂。你知道为什么创建一个新的服务实例需要这么多的配置文件的更新吗?”引导性问题的反例:●“这些旧的、停滞的发布是什么情况?”●“为什么某个组件要做这么多的事情?”

小结

  • 你的最后一个任务是书写一份报告。报告中应该重申你的观点、例子和逻辑推理过程。同时,该报告应该向团队提供一些待办事项,来保证他们会实践你所传授的东西。你应该将报告组织成一份检查报告[12],解释成功路上的每一个重要的决策。大部分的工作现在已经完成了。虽然你的嵌入式工作正式结束,但仍然应该参与设计评审和代码评审。在未来几个月中持续关注这个团队,确保他们正在慢慢提高自己的容量规划能力、紧急事件处理能力和部署发布过程。

第31章 SRE与其他团队的沟通与协作

沟通:生产会议

  • 在两个SRE团队视频会议时,如果一个团队比另外一个大很多,我们建议从较小的团队一边选择主席。更大的一方会自然而然地安静下来,一些不平衡的团队规模所造成的不良影响(这些会由于视频会议的延迟而变得更糟)将得到改善。[14]我们不知道这是否有任何科学依据,但它确实有效。

SRE的内部协作

  • 不管这些角色定义得是否清晰,基本来说,技术负责人是负责团队技术方向的,可以以多种方式进行领导。他可以通过仔细评审每个人的代码,组织进行季度工作汇报,或者是在团队中推进共识的建立来引领团队方向等方式来领导团队。

  • 在Google内部,技术负责人和SRE经理的工作几乎一样,因为我们的SRE经理也具有高度的技术能力。但SRE经理在这之外还有两个特殊责任:绩效管理,以及其他一切其他人不能处理的事情。好的技术负责人、SRE经理以及项目经理组成了一个完整的管理团队,可以很好地组织项目进行讨论设计文档,必要的时候甚至亲自书写代码。

  • 建议只有在不得已的情况下才应该跨地域开发项目,但是这同时也会带来一定的好处。跨地域工作需要更多的沟通,工作完成得也更慢;但是好处是—如果你能协调好的话—则会有更高的产能。单地项目其实也可能会导致其他人不知道你正在做什么,所以这两种做法其实都有一定成本。


第33章 其他行业的实践经验

  • 在本章中,我们会讨论到许多SRE的核心指导思想。为了简化与其他行业最佳实践的比较,我们将这些理念分为4大类:● 灾难预案与演习● 书写事后总结的文化● 自动化与降低日常运维负载● 结构化的、理智的决策

附录A 系统可用性

附录B 生产环境运维过程中的最佳实践

附录C 事故状态文档示范

附录D 事后总结示范

附录E 发布协调检查列表


其他


看见有人不赞同,其实这些问题你们没有遇见过,产品着急上线,出现了一个极低概率的case,修复成本很高。上线后的回报很大,而这个case带来的损失很小,这个时候肯定带着bug上线啊。很多时候产品上线发现商业模式跑不通,这个bug就不用修了,或者迭代过程中才用了另外的实现方式避免了这个问题。错误预算都不一定能用上。
——评论

阿里的要求:可灰度、可监控、可回滚,大厂的思路基本都是一致的。
——评论

产品在不同的发展阶段,可用性目标应该是动态变化的,快速发展期的可用性往往低于成熟稳定期,以保证新功能的发布、试错等顺利进行。——评论

打天下和守天下,需要不同的节奏,优先级不同,方案和指标也不同。——评论

量化风险成本,建立一致性目标,风险共担,减少非必要的沟通扯皮成本,比较好的双赢模式。
对照着看传统运维和开发的独立承担指标就属于零和博弈了,只有政治斗争能化解。——评论