Windows Live! 平台团队:大规模服务设计部署经验(转载)
本文就设计和开发运营友好的服务的话题进行总结,得出一系列最佳实践。设计和部署大规模服务是一个高速发展的领域,因而随着时间的流逝,任何最佳实践集合都可能成熟并完善。我们的目的是为了帮助人们:
◆ 快速交付运营友好的服务;
◆ 避免清早电话铃声的骚扰,帮助备受运营不友好的服务侵扰的客户尽量摆脱窘境。
这篇论文是我们在过去的20年中在大规模以数据为中心的软件系统和互联网级大规模服务的智慧结晶,包括Exchange Hosted Services团队、Microsoft Global Foundation Services Operations团队以及Windows Live! 平台多个团队的经验。这些贡献经验的服务中,有不少规模已经增长到拥有超过二亿五千万名用户。同时,本论文也大量吸取了加州大学伯克利分校在面向恢复计算 (Recovery Oriented Computing)方面取得的成果和斯坦福大学在只崩溃软件(Crash-Only Software)方面的研究经验。
Bill Hoffman为本论文贡献许多最佳实践。此外,他还提出三条简单原则,值得大家在进入正题之前进行考量:
1.做好发生故障的心理准备。
2.保持简单化。
3.将所有的工作自动化。
这三条原则形成了贯穿后续讨论的主轴。
本文的十个小节涵盖了设计和部署运营友好服务所必须做到的各个方面。它们是:整体服务设计;以自动化和预置(Provisioning)为目标进行设计; 依赖关系管理;发布周期及测试;硬件的选择和标准化;运营和容量规划;审核、监控和警报;体面降级和管理控制;客户及媒体沟通计划;以及客户自我预置和自 我帮助。
整体服务设计
一直以来,人们都相信80%的运营问题源于设计和开发,因此本节关于整体服务设计的内容篇幅最长,也最重要。系统出故障时,人们很自然倾向于首先去审视运 营工作,因为这是问题实际产生的地方。不过,绝大多数运营问题都可以归因于设计和开发,或者最适合在设计和开发中解决。
随后的内容凸显一个共识,即在服务领域,将开发、测试和运营严格分离不是最有效的方式。在环顾众多服务之后,我们发现了这样一个趋势——管理的低成本与开发、测试和运营团队间协作的紧密程度密切相关。
除了在这里讨论的服务设计最佳实践以外,随后一节“以自动化管理和预置为目标进行设计”对服务设计也有实质性的影响。有效的自动化管理和预置通常以一个受 限的服务模型来实现。简单是高效率运营的关键,这是贯穿本文重复出现的主题。在硬件选择、服务设计和部署模型上的理性约束,是降低管理成本和提高服务可靠 性的强心针。
在运营友好的基础原则中,为整体服务设计带来最大影响的几条包括:
◆ 设计时为故障做好准备(Design for failure)。在开发包含多个协同运作的组件的大型服务时,这是一条核心概念。这些组件会有故障发生,而且故障的产生很频繁。它们之间不会总是稳定地 协作,也不会单独出现故障。一旦服务的规模达到10,000台以上的服务器和50,000块以上的磁盘,每天就会有多次故障发生。如果一有硬件故障产生就 得采取紧急措施来应对,那么服务就无法以合理的成本可靠地伸缩。整个服务必须有承受故障而无须人工干预的能力。故障恢复的步骤必须非常简单,而且这些步骤 应当进行频繁测试。斯坦福大学的Armando Fox 主张说,对故障恢复步骤进行测试的最佳方法,就是绝对不要用正常方式使服务停机,用粗暴的方式让它停转就可以了。乍听起来怎么做是违背直觉的,但是如果没 有频繁使用故障步骤,那么它们在真正临阵时就可能溃不成军 。
◆ 冗余和错误恢复(Redundancy and fault recovery)。大型机模型是指购买一台价高块头大的服务器。大型机拥有冗余的电源供应,CPU可以热交换,而且总线架构也超乎寻常,使得这样一个紧 密耦合的系统能够有可观的I/O吞吐量。这样的系统,最明显的问题就是它们的成本;而且即便有了所有这些费用高昂的设计,它们仍然不够可靠。为了达到 99.999%的可靠性,冗余是必须存在的。实际上,在一台机器上实现4个9的可靠性都是相当困难的。这个概念在整个业界都耳熟能详,不过,将服务构建在 脆弱而又非冗余的数据层之上的现象,到目前为止都屡见不鲜。
要保证设计的服务其中的任何系统可以随时崩溃(或者因为服务原因被停止)但又仍然能符合服务水平协定(Service Level Agreement,简称SLA),是需要非常仔细的设计的。保证完全遵守这项设计原则的严格测试(acid test)步骤如下:首先,运营团队是否有意愿并且有能力随时让任意一台服务器停机并且不会让负载被榨干?如果答案是确定的,那么肯定存在同步冗余(无数 据丢失)、故障侦测和自动接管。我们推荐一条普遍使用的设计方法,用于查找和纠正潜在的服务安全问题:安全威胁建模(Security Threat Modeling)。在安全威胁建模中,我们要考虑每一条潜在的安全威胁,并且相应实现恰当的缓和方案。同样的方法也适用于以错误适应和恢复为目标的设 计。
将所有可以想象到的组件故障模式及其相应组合用文档记录下来。要保证服务在每个故障发生后都能继续运行,且不会在服务质量上出现不可接受的损失;或者判断 这样的故障风险对于这样一个特定的服务是否可以接受(例如,在非地理冗余的服务中损失掉整个数据中心)。我们可能会认定某些非常罕见的故障组合出现的可能 性微乎其微,从而得出确保系统在发生这种故障之后还能继续运行并不经济的结论。但是,在做这样的决定时请谨慎从事。在运行数以千计的服务器的情况下,每天 都会有几百万种组件故障产生的可能,这时那些事件的“罕见”组合亮相的频繁程度,足以让我们瞠目结舌。小概率组合可能变成普遍现象。
◆ 廉价硬件切片(Commodity hardware slice)。服务的所有组件都应当以廉价硬件切片为目标。例如,存储量轻的服务器可以是双插槽的2至4核的系统,带有启动磁盘,价格在1,000至 2,500美元之间;存储量大的服务器则可以是带有16至24个磁盘的类似服务器。主要的观察结果如下:
▲ 大型的廉价服务器集群要比它们替代的少数大型服务器便宜得多;
▲ 服务器性能的增长速度依然要比I/O性能的增长速度快很多,这样一来,对于给定容量的磁盘,小型的服务器就成为了更为稳定的系统;
▲ 电量损耗根据服务器的数量呈线性变化,但随系统时钟频率按立方级别变化,这样一来性能越高的机器运营成本也越高;
▲ 小型的服务器在故障转移(Fail over)时只影响整体服务工作负荷的一小部分。
◆ 单版本软件(Single-version software)。使某些服务比多数打包产品开发费用更低且发展速度更快的两个因素是:
▲ 软件只需针对一次性的内部部署。
▲ 先前的版本无须得到十年的支持——针对企业的产品正是如此。
相对而言,单版本软件更容易实现,附带客户服务,特别是无须费用的客户服务。但是在向非客户人员销售以订阅为基础的服务时,单版本软件也是同样重 要的。企业通常习惯在面对他们的软件提供商时拥有重要的影响力,并且在部署新版本时(通常是个缓慢的过程),他们会习惯性想去掌握全部的控制权。这样做会 导致他们的运营成本和支持成本急剧上升,因为软件有许多版本需要得到支持。
最经济型的服务是不会把对客户运行的版本的控制权交给他们的,并且通常只提供一个版本。要把握好单一版本软件的产品线,必须:
▲ 注意在每次发布之间不要产生重大的用户体验变更。
▲ 愿意让需要相应级别控制能力的客户可以在内部托管,或者允许他们转向愿意提供这类人员密集型支持服务的应用服务提供商。
◆多重租赁(Multi-tenancy)。多重租赁是指在同一个服务中为该服务的所有公司或最终用户提供主机服务,且没有物理上的隔离;而单一 租赁(Single-tenancy)则是将不同组别的用户分离在一个隔离的集群中。主张多重租赁的理由基本上和主张单版本支持的理由一致,且它的基础论 点在于提供从根本上成本更低、构建在自动化和大规模的基础之上的服务。
回顾起来,上面我们所展示的基本设计原则和思考如下:
◆ 设计时为故障做好准备
◆ 实现冗余和错误恢复
◆ 依靠廉价硬件切片
◆ 支持单一版本软件
◆ 实现多重租赁
我们约束服务设计和运营的模型,以此最大化自动化的能力,并且减少服务的总体成本。在这些目标和应用服务提供商或IT外包商的目标之间,我们要划一道清楚的界限。应用服务提供商和IT外包商往往人员更加密集,并且更乐于运行面向客户的复杂配置。
设计运营友好的服务更具体的最佳实践包括:
◆ 快速服务健康测试。这是构建验证测试的服务版本。这是一个嗅探型测试,可以快速在开发者的系统上运行,以保证服务不会以独立方式出错。要保证所有的边界条件都被测试到,是不可能的,但如果快速健康测试通过的话,那么代码就可以检入了。
◆ 在完整的环境中开发。开发人员不但应当对他们的组件进行单元测试,而且还要对出现组件变更的整个服务进行测试。要高效实现这个目标,必须得有单服务器的部署,以及前一条最佳实践——快速的服务健康测试。
◆ 对下层组件的零信任。设想下层组件会出现故障,并且确保组件会有能力恢复并继续提供服务。恢复的技巧和服务相关,但常见的技巧包括:
▲ 在只读模式中依靠缓存的数据继续运转;
▲ 在服务访问故障组件的冗余拷贝的短暂时间内,继续向用户群一小部分的所有人提供服务。
◆ 不要把同一个功能构建在多个组件中。预见未来的交互是一件极其困难的事情,如果不慎引入冗余的代码,那么将来就不得不在系统的多处做修复。服务的增长和发展是很快的,稍不留神代码的质量就会很快恶化。
◆ 不同的集群之间不能互相影响。大多数服务由小型集合或者系统的子集群组成,它们之间相互协作,共同提供服务,其中每个小型集合都可以相对独立地运作。每个 小型集合应达到接近100%的独立程度,且不会有跨群的故障。全局服务,甚至包括冗余,是故障的中心点。有时候,这样的问题是不可避免的,但还是要设法保 证每个集群都拥有各自所需的资源。
◆ 允许(少量)在紧急情况的人工干预。常见场景是在灾难性事件或者其他紧急情况下移动用户数据。把系统设计成完全无须人工交互,但也要清楚小概率事件可能会 发生,其中组合的故障或者未预期的故障都会需要人工交互。这些事件是会发生的,而在这些情况下,操作员的错误往往是导致灾难性数据丢失的常见来源。一名在 半夜2点顶压工作的运营工程师可能会犯错误。将系统设计成一开始在多数情况下无须运营干预,但请和运营团队协作制定在他们需要进行干预时的恢复计划。比起 将这些计划写进文档,变成多步骤易出错的过程,更好的办法是把这些规则写成脚本,并在生产环境中进行测试,以确保它们正常工作。没有经过产品环境试验的代 码是不可行的,因此运营团队应当定时指挥使用这些工具进行“防火演习”。如果演习的服务可用性风险非常高,那么可以认为之前在工具的设计、开发和测试上的 投资不足。
◆ 保持一切简单健壮。复杂的算法和组件交互会给调试和部署带来成倍困难。简单到近乎傻瓜式的结构在大规模服务中几乎总是更胜一筹,因为在复杂的优化工作交付 之前,交互中故障模式的数量早就足以磨灭人们的信心。通常我们的惯例是,能够带来一个数量级以上改善的优化工作才值得考虑,而只有百分之几或者甚至于只是 低系数级别的提升,就不值得了。
◆ 全面推进准入控制。所有良好的系统会在设计时开门见山地引入许可控制,这样符合一条长期以来为人们所认可的原则,那就是避免将更多的工作引入一个已经过载 的系统,要比持续接受工作然后开始翻来覆去地检查好一些。在服务入口引入某些形式的节流或者准入控制是很常见的做法,但在所有的主要组件边界上都应该有准 入控制。工作性质的变更最终会导致子组件的过载,即使整体服务仍然运行在可接受的负载级别。总体的惯例就是尝试采用优雅降级的方式,而不用在统一给所有用 户低质量服务之前进行硬停机并阻断服务的入口。
◆ 给服务分区。分区应当可以无限调整,并且高度细粒度化,并且不受任何现实实体(人、集合等)的限制。如果按公司分区,那么对于大的公司,就有可能超过单个 分区的规模;而如果按名称前缀进行分区,那么例如所有以P打头的最终一台服务器就可能会装不下。我们推荐在中间层使用一张查询表,将细粒度的实体,通常是 用户,映射到其数据相应被管理的系统上。这些细粒度的分区随后就可以自由在服务器之间移动。
◆ 理解网络的设计。提早进行测试,了解机柜内的服务器之间、跨柜的服务器之间以及跨数据中心之间有哪些负载。应用程序开发人员必须理解网络的设计,且设计应当尽早交给来自运营团队的网络专员审核。
◆ 对吞吐量和延迟进行分析。应当对核心服务的用户交互进行吞吐量和延迟的分析,从而了解它们的影响。结合其他运营操作,比如定期数据库维护、运营配置(加入 新用户,用户迁移)和服务调试等,进行吞吐量和延迟的分析。这样做对于捕捉由周期性管理任务所带动的问题是颇有裨益的。对于每个服务,都应当形成一个度量 标准,用于性能规划,比如每个系统的每秒用户访问数,每个系统的并发在线人数,或者某些将关联工作负载映射到资源需求的相关度量标准。
◆ 把运营的实用工具作为服务的一部分对待。由开发、测试、项目管理和运营所产生的运营实用工具应当交给开发团队进行代码审查,提交到主源码树上,用同一套进度表跟踪,进行同样的测试。最频繁出现的现象是,这样的实用工具对于任务有至关重要的影响,但几乎都没有经过测试。
◆ 理解访问模式。在规划新特性时,一定要记得考虑它们会给后端存储带来哪些负载。通常,服务模型和服务开发人员与存储抽象得非常开,以至于他们全然忘记了它 们会给后端数据库带来的额外负载。对此的最佳实践把它作为规范建立起来,里面可以有这样的内容:“这项功能会给基础结构的其他部分带来什么样的影响?”然 后在这项特性上线之后,对它进行负载的测量和验证。
◆ 让所有工作版本化。做好在混合版本的环境中运行的准备。我们的目标是运行单版本的软件,但是多个版本可能会在首次展示和生产环境测试的过程中并存。所有组件的n版和n+1版都应当能够和平共存。
◆ 保持最新发布版的单元/功能测试。这些测试是验证n+1版本的功能是否被破坏的重要方法。我们推荐大家更进一步,定期在生产环境中运行服务验证(后面会详细介绍)。
◆ 避免单点故障。单点故障的产生会导致整个服务或者服务的某些部分停工。请选择无状态的实现。不要将请求或者客户端绑定在特定的服务器上,相反,将它们负载 均衡在一组有能力处理这样负载的服务器上。静态哈希或者任何静态的服务器负载分配都会时不时遭受数据和/或查询倾斜问题的困扰。在一组机器可以互换时,要 做到横向伸展是非常容易的。数据库通常会发生单点故障,而数据库伸缩仍然是互联网级大规模服务的设计中最为困难的事情之一。良好的设计一般会使用细粒度的 分区,且不支持跨分区操作,以在多台数据库服务器之间进行高效伸展。所有的数据库状态都会进行冗余存储(至少在一台)完全冗余的热待机服务器上,并且在生 产环境中会频繁进行故障转移测试。
自动管理和预置
许多服务编写的目的是为了在故障时向运营部门发出警报,以得到人工干预完成恢复。这种模式凸显出在24 x 7小时运营人员上的开支问题;更重要的是,如果运营工程师被要求在充满压力的情况下做出艰难的决定,那么有20%的可能他们会犯错误。这种模式的代价高 昂,容易引入错误,而且还会降低服务的整体可靠性。
然而,注重自动化的设计会引入明显的服务模型约束。比如说,当前某些大型服务依靠的数据库系统,会以异步方式复制到次级备份服务器,因为复制以异步方式完 成,在主数据库无法服务请求后,故障转移到次级数据库会引起部分客户数据丢失。然而,不把故障转移到次级数据库,则会引起数据被储存在故障服务器上的用户 面临服务停工。在这种情况下,要自动化故障转移的决策就很困难了,因为这个决策依赖于人为判断,以及对数据损失量和停机大致时长相比的准确估计。注重自动 化设计出的系统会在延迟和同步复制的吞吐量开销上付出代价。在做到这一步以后,故障转移变成了一个很简单的决策:如果主服务器宕机,将请求转到次服务器 上。这种方式更适用于自动化,而且被认为更不容易产生错误。
在设计和部署后将服务的管理过程自动化可能是一件相当具有难度的工作。成功的自动化必须保证简单性,以及清晰并易于确定的运营决策;这又要依靠对服务的谨 慎设计,甚至在必要时以一定的延迟和吞吐量为代价作出牺牲,让自动化变得简单。通常这样的折中方案并不容易确定,但是对于大规模服务来说在管理方面的节约 可能不止数量级。事实上,目前根据我们的观察,在人员成本方面,完全手动管理的服务和完全自动化的服务之间的差别足足有两个数量级。
注重自动化的设计包含以下最佳实践:
◆ 可以重启动,并保持冗余。所有的操作都必须可以重新启动,并且所有持久化状态也必须冗余存储。
◆ 支持地理分布。所有大规模服务都应当支持在多个托管数据中心运行。我们所描述的自动化和绝大多数功效在无地理分布的情况下仍是可行的。但缺乏对多服务中心 部署方式的支持,会引起运营成本显著提升。没有了地理分布,很难使用一个数据中心的空闲容量来减缓另外一个数据中心所托管的服务的负载。缺乏地理分布是一 项会导致成本提高的运营约束。
◆ 自动预置与安装。手动进行预置和安装是相当劳民伤财的,故障太多,而且微小的配置差异会慢慢在整个服务中蔓延开来,导致问题的确定越来越困难。
◆ 将配置和代码作为整体。请确保做到:
▲ 开发团队以整体单元的形式交付代码和配置;
▲ 该单元经过测试部门的部署,并严格按照运营部门将会部署的方式;
▲ 运营部门也按照整体单元的方式部署。
通常说,将配置和代码作为一整个单元处理并且只把它们放在一起修改的服务会更可靠。
◆ 如果配置必须在生产环境中变更,那么请保证所有的变更都要产生审核日志记录,这样什么东西被修改,在什么时候被修改,哪些服务器受到影响,就一目了然了。 频繁扫描所有的服务器,确保它们当前的状态与预期状态相符。这样做对捕获安装和配置故障颇有裨益,而且能在早期侦测到服务器的错误配置,还能找到未经审核 的服务器配置变更。
◆ 管理服务器的角色或者性质,而不是服务器本身。每个系统的角色或性质都应当按需要支持尽可能多或少的服务器。
◆ 多系统故障是常见的。请做好多台主机同时发生故障的准备(电源、网络切换和首次上线)。遗憾的是,带有状态的服务必须得注意它们的拓扑分布。有相互联系的故障一直以来都是不可避免的。
◆ 在服务级别上进行恢复。在服务级别处理故障,相比软件底层来说,其中的服务执行上下文更加完整。例如,将冗余纳入服务当中,而不是依靠较低的软件层来恢复。
◆ 对于不可恢复的信息,绝对不要依赖于本地存储。保证总是复制所有的非瞬时服务状态。
◆ 保持部署的简单性。文件复制是最理想的方式,因为这样能带来最大的部署灵活性。最小化外部依赖性;避免复杂的安装脚本;避免任何阻止不同组件或者同一个组件不同版本在同一台机器运行的情况。
◆ 定期使服务停转。停掉数据中心,关闭柜式服务器,断掉服务器电源。定期进行受控的关闭操作,能够主动暴露出服务器、系统和网络的缺陷。没有意愿在生产环境 中测试的人,实际上是还没有信心保证服务在经历故障时仍能继续运转。此外,若不进行生产测试,在真正出事时,恢复不一定能派上用场。
文/James Hamilton 译/赖翥翔
(来自:《程序员》杂志 http://www.programmer.com.cn/)