“系统-管理员”的比例通常粗略的作为了解大规模服务中管理成本的指标。在低自动化水平的服务中这个比例可能低到2:1,而在行业领先的高度自动化的服务中,这个比例可以达到2500:1。在微软的众多服务之中,Autopilot经常被认为是Windows Live Search团队成功提高“系统-管理员”比的原因。自动化管理非常重要,但更重要的还是服务本身。服务是否能高效的进行自动化?是否是运维友好的(operations-friendly)?运维友好的服务几乎不需要人工的干预,除了极个别的故障外其他情况都可以被自动的检测并恢复。本文总结MSN和Windows Live在支撑一些超大型服务过程中多年积累下来的最佳实践。
这里的内容吸收了我们过去20年在大型数据中心软件系统和Internet规模的服务方面的经验,其中大部分来自最近领导Exchange Hosted Services团队期间。其中也包含了来自Windows Live Search,Windows Live Mail,Exchange Hosted Services,Live Communications Server,Windows Live Address Book Clearing House,MSN Spaces,Xbox Live,Rackable System Team,Messenger Operations team,Microsoft Global Foundtion Services Operations等诸多团队的经验。本文也深受Berkeley关于Recovery Oriented Computing和Stanford关于Crash-Only Software的影响。
Design for failure。这是开发需要多组件协作的大规模服务时的一个核心理念。这些组件会发生故障,而且是频繁的发生故障。当服务规模达到10000台机器,50000块磁盘时,一天内会发生多次故障。如果硬件的故障需要人工立即介入处理,那么服务就没办法低成本和可靠的进行扩展。整个服务需要在没有人工干预的情况下在发生故障的情况依旧能够继续提供服务。故障的恢复必须是非常简单的路径,并且频繁的对该路径进行测试。Stanford的Armando Fox的观点是最好的测试失败恢复路径的方式是永远不要正确的关闭服务,而是直接让它挂掉。这听起来有点反直觉,但是如果故障恢复路径不被经常的使用,那么在真正需要时它是无法工作的。
Redundancy and fault recovery。大型机模型是要购买大型的、非常昂贵的服务器。大型机拥有冗余的电源、热插拔的CPU、独特的总线架构,在一个单一的、紧耦合的系统中提供了令人惊讶的IO吞吐量。这种模式明显的问题就是成本高,但即使这样,它也不是足够的可靠。为了获得五个9的可靠性,冗余是必须的。即使是四个9的可靠性要求,单系统部署也是非常困难的。这个观点已经被工业界广泛接受,但是依旧能看到部署在非常脆弱的、非冗余的数据层上的服务。
Quick service health check。服务版本的构建验证测试是一个可以在开发人员的系统上运行的测试,确保服务的核心并没有被破坏。并不是所有的边界场景都会被测试,但如果这个测试通过了,代码就可以check in了。
Develop in the full environment。开发者需要对他们的组件进行单元测试,还要对包含他们的服务在变更后进行服务测试。要实现这一点需要支持单机部署,以及上一条的最佳实践:服务健康状态快速检查。
Zero trust of underlying components。假设依赖组件会挂掉,同时要确保组件能恢复并继续提供服务。恢复技术是和服务相关的,但是也有一些通用的恢复技术:
以只读模式继续访问缓存数据
继续对用户提供所有服务,除了那些受故障影响的用户
Do not build the same functionality in multiple components。要预见未来的交互方式是非常困难的,同时如果系统中存在代码冗余的话就需要在系统的多个部分中进行修复。服务总在快速的增长和变化,如果不小心,代码库就会快速的恶化。
One pod or cluster should not affect another pod or cluster。大部分服务是由位于多个集群或者自己群的系统共同协作组成的。他们之间要尽可能的100%独立,避免关联的故障。那些全局的服务即使具有冗余备份也是一个单点。有时候这种情况无法避免,但还是要尽可能把一个集群依赖的东西都放到集群里面。
Allow (rare) emergency human intervention。常见的情况是灾害后或其他紧急情况的用户数据迁移。要将系统设计的永远不需要人工干预,但是也要理解一些组合故障或者意料之外的故障发生时会需要人工干预。这些情况会发生,并且在这些情况下的错误操作通常是灾难性数据丢失的常见原因。运维工程师在凌晨两点,顶着压力工作会容易犯错误。系统设计时首先要保证绝大多数情况下不需要人工进行干预,对于需要人工干预的场景一定需要提前确定预案。预案不应该只是记录在文档中,需要执行多个步骤、容易出错的计划,需要将它们写成脚本,并且在生产环境进行验证保证它们可以正确的执行。没有在生产环境验证过的东西是无法work的。运维团队需要周期性的演练这些工具。如果演练对服务的可用性会造成很大风险,说明在设计、开发、测试方面的投入还不够。
Keep things simple and robust。复杂的算法和组件交互会将调试和部署变得困难。在大规模服务中,故障模式的数量在进行复杂的优化之前就已经够让人害怕了。一个总体的原则是:超过一个数量级的优化才值得去做,只有几个百分点的提升和改进是不值得去做的。
Enforce admission control at all levels。任何好的系统都会在入口设计准入机制。这遵循了一个长期以来被人们所接受的原则:不要让更多的作业进入负载的系统比继续接受作业导致系统震荡要好。在服务入口通常有某种形式的流控或者准入控制,但是应该在所有重要的组件的边界上都应该有准入控制——即使整体负载在可接受范围内,但是负载的变化可能会导致其中某个子组件过载。下面2.8节提到的“big red switch”就是一种在过载情况下优雅降级的方案。总体方式是在所有用户收到影响之前尝试优雅降级,而不是直接挂掉。
Partition the service。分区应该是细粒度且无线可调的,不绑定在任务物理世界的实体上。如果分区绑定在公司上,那么一个超大的公司可能就会超过单个分区的大小限制。我们推荐使用一个中间层查找表把细粒度实体映射到相应的数据管理系统。然后这些细粒度的分区就可以在服务器之间自由的移动。
Understand the network design。尽早的进行测试以便了解机架内、机架之间、数据中心之间的负载情况。应用开发同学需要了解网络设计,并且尽早和网络专家一起Review。
Analyze throughput and latency。为了理解其影响,需要对核心服务用户交互的吞吐、延迟进行分析。并将这件事与其他的日常数据库维护、运维配置、服务调试一样进行常规化。这将有助于发现因为某些周期性管理任务引发的问题。对于每个服务来说,所需的度量值可以与容量规划相结合,比如系统每秒的用户请求数、系统并发的用户数,或者某些可以将工作负载映射到资源需求的度量值。
Treat operations utilities as part of the service。由开发、测试、运维开发的运维脚本都需要在开发过程中进行代码Review,checkin到代码主干,跟代码一起已经测试和维护。通常这个工具非常重要,但是几乎没有被测试过。
Version everything。要假设系统运行在一个多版本环境中。目标是运行单一版本,但是上线和生产测试时会有多版本共存的情况。所有组件的N和N+1版本都需要兼容。
Keep the unit/functional tests from the last release。这些测试是用来验证N-1版本的功能没有被破坏掉的重要手段。更进一步的,我们强烈推荐要持续在生产环境跑测试验证。
Aviod single points of failure。单点故障会导致故障发生时服务不可用或服务的多个部分不可用。优先采用无状态的实现。不要把请求或者用户指定给特定服务器。而是要对负载在一组服务器之间进行负载均衡。静态哈希或者其他静态的分配方式会随着时间的流逝而导致数据的倾斜。当同一组内的机器是可以相互替换的,就很容易能做到水平扩展。数据库通常都是一个单点,同时在进行互联网级别的服务设计时,如何进行数据库的扩展依然是最困难的问题。好的设计会使用细粒度分区,同时会禁止跨分区的操作使得可以高效地通过多个数据库服务器进行扩展。对所有数据库状态冗余的存储到热备的服务器上,同时要在生产环境经常进行failover测试来保证可靠性。
Be restartable and redundant。所有的操作必须可重启,并且所有持久化状态的存储都需要冗余。
Support geo-distribution。所有大规模的服务都应该支持跨数据中心运行。坦率的讲,我们这里提到的自动化和大多数的高效率即使是在没有地理分布的情况下也是可能的。但是跨数据中心部署的能力的缺失将会大幅推高运维成本。没有地理分布的支持,就很难通过将负载迁移到另一个数据中心来减轻当前数据中心的压力。地理分布式的缺失是一种会推高成本的运维限制。
Automatic provisioning and installation。配置和安装,如果是手动完成的话,成本高且易发生问题,同时细小的配置差异会慢慢散布到整个服务,使得问题越来越难以定位。
Use shipping and proven components。使用经过验证的组件总是好过那些使用起来像在刀口舔血的新技术。稳定的软件版本总是比尝鲜的版本更好,无论新的feature多么的诱人。改规则也同样适用于硬件。
Implement inter-service monitoring and alerting。如果服务正在使得它所依赖的组件过载,被依赖的组件需要能够知道这种情况。并且如果它不能自动对过载的情况进行处理,那么需要发送出告警。如果运维人员也无法快速解决问题,还需要能快速联系到两个团队的工程师。相关联的团队需要保证有工程师可以随时被联系到。
Dependent services require the same design point。被依赖的组件和服务只要需要和依赖者保持相同的SLA。
Use only standard SKUs。如果生产环境中只有单个或少量SKU,就可以实现资源在服务间的按需流转。成本效益最好的一种模式是开发一种包含了自动化管理和配置、硬件以及一组标准共享服务集合的标准服务托管框架。标准化SKU是实现该目标的核心要求。
Purchase full racks。采购硬件以机柜或者多个机柜为单位进行测试和验证。在大多数数据中心,机器组柜和堆垛成本是非常高的。
Write to a hardware abstraction。编写服务只依赖于硬件的抽象,不要暴露硬件SKU,服务既不暴露SKU也不应该依赖于它内部的细节。这样才可以在具有更高性价比的系统可用时将现有设备替换掉。SKU应当只是一个包含了CPU和磁盘数据,及最低内存要求的抽象描述。更细粒度上的SKU信息不应该被利用。
Abstract the network and naming。使用DNS和CNAMEs尽可能的将网络和命名进行抽象。始终坚持使用CNAME。硬件可能损坏、到期或者挪作它用。在代码的任何地方不要依赖于机器的名称。代表DNS中的CNAME与需要改变配置文件甚至是代码相比,要容易的多。如果需要避免更新DNS缓存,需要记得将TTL设置的足够低以保证变更尽可能快的被Push。
Make the development team responsible。在这方面,Amazon应该算是最激进的了,他们的格言是“you built it, you manage it”。他们的主张比我们现在的方式要强一些,但是无疑这是一个正确的方向。如果开发总是被在午夜吵醒,那么自动化会是必然的结果。但是如果是运维团队经常被吵醒,通常的反应是增加运维团队的人手。
Make one change at a time。碰到问题时应该一次只对环境做一个变更。这看起来显而易见,但是我们也多次看到过因为多个变更导致的因果关系无法确定的情况的发生。
Make Everything configurable。任何可能在系统中发生变更的东西都应该是在生产环境下可配置和调整的,而不需要改变代码。即使某个值看起来没有很好的在生产环境中发生变更的理由,如果很容易的话,也应该将它们做成可配置的。但是不要在生产环境中随意的改变它们,同时应该对系统连同哪些将在生产中使用的配置一块进行严格的测试。但是当生产环境出现问题时,与编码、编译、测试、部署相比,简单地修改配置总是要更简单、安全、快速。
Data is most valuable asset。如果对系统正常行为没有很好的理解,那么在它发生异常时也无法很好的进行处理。需要对关于系统行为的大量数据进行收集以确定系统状态是否正常。很多服务都经历过灾难性的故障,但是只有在电话铃响起时人们才意识到故障的发生。
Have a customer view of service。执行End-to-End测试。仅有Runners虽然不足,但是他们能用来确保服务在完整的运行。确保复杂和重要的路径能被Runners测试到。避免误报,如果Runners的失败可以被忽略,那么就修改测试使他们不会误报。再次强调,如果人们习惯了忽略数据,真实发生时也会被忽略。
Instrumentation required for production testing。对生产环境测试进行监测。要想安全的在生产环境进行测试,需要进行完整的测试和监控。如果组件发生了故障,需要能快速的监控发现。
Latencies are the toughest problem。比如像IO缓慢,虽然未造成故障但是会导致处理变慢之类的问题。这些问题很难发现,需要仔细的进行监控才能发现。
Have sufficient production data。为了发现问题,数据必须是有效的。及早的构建细粒度的监控,否则后面的改造成本会很高。我们所依赖的重要数据包括:
Use performance counters for all operations。至少将操作延迟,以及每秒的请求数记录下来。这些数据的起伏通常是一个危险的信号。
Audit all operations。当某人做了某事,尤其是重要之事,都要进行记录。这样做主要有两个目的:首先,日志可以用来进行挖掘以找出用户经常进行哪些操作;其次可以用它来在发现问题时帮助进行debug。
Track all fault tolerance mechanisms。容错机制会将故障隐藏掉。每当重试发生,或将数据从一个地方拷贝到另一个地方,或机器重启以及服务重启时,都进行跟踪。在容错机制隐藏了小故障时,要能了解到并对它进行跟踪,防止小故障演变成大故障。我们曾有一个2000台机器组成的服务在几天内慢慢地下降到只有400台可用,但是一开始却没人注意到。
Track operations against important entities。为发生在每个重要实体上的事件记录一个审计日志,无论实体是一个文档还是多个文档组成的集合。当执行数据分析时,通常能够通过数据发现异常。要了解数据的来源及其所经过的处理过程。到了项目后期再添加这些功能会变得困难。
Expose health information for monitoring。考虑多种从外部对系统健康状况进行监控的方法,并使之易于在生产环境下进行监控。
Make all reported errors actionable。问题总会发生,事情总会出错。如果代码中一个不可恢复的错误被检测并记录到了日志,或者报告为错误,错误信息应该能揭示错误产生的可能原因及建议的处理方法。不具备可操作性的错误报告是无用的,并且时间长了它们会被忽略掉,而真正的故障会被错过。
Enable quick diagnosis of production problem。
Give enough information to diagnose。当问题被标记出来,提供足以让人进行诊断的信息。否则,进入门槛将会更高,同时标记也会被忽略。比如不要仅仅说是“有10个查询没有返回结果”,还需要再加上“下面是它们的列表及问题发生的次数”——重要的是要给出失败的上下文信息便于定位问题。
Chain of evidence。确保有一个可以用于开发人员诊断问题的从头到尾的完整路径。这通常是通过日志实现的。
Debugging in production。我们喜欢这种模式,在这种模式中,系统几乎不会被包括运维在内的任何人接触,而debug是通过让系统产生快照,将内存内容dump出来,然后转移出生产环境再进行的。当在生产环境中进行Debug是唯一选择时,开发者是不二人选。确保他们对于可以在生产环境可以进行哪些操作是要经过严格的培训的。我们的经验表明,系统在生产环境中被触碰地越少客户满意度越高。因此我们建议一定要竭尽全力避免对生产环境中的系统进行改动。
Record all significant actions。每当系统执行重要动作时,尤其是收到网络的请求或者修改数据时,要记录下发生的事情。包括用户何时发送了命令以及系统内部做了什么。有了这些记录,对问题的调查大有裨益。更重要的是,还可以开发出挖掘工具来找到有用的聚合结果,比如当前用户正在执行哪些种类的查询。
Graceful Degradation and Admission Control
有些时候比如收到DOS攻击或者模式发生某些改变时,会导致负载突然爆发。服务需要能够进行优雅的降级及准入控制。如果在9.11期间,大多数新闻服务都崩溃了,无法为用户提供任何服务。与此相比,就算是仅能够提供所有文章的一部分自己也是一个更好的选择。两个最佳实践:“Big Red Switch”和准入控制,需要针对每个服务进行量身定制。但是这两个都是非常强大和必要的。
Support “Big Red Switch”。支持应急开关。大体来说“Big Red Switch”是一种当服务不再满足SLA或者迫在眉睫时,可以采取的经过设计和测试的动作。将优雅的降级比喻为“Big Red Switch”,稍微有些不太恰当,但核心的意思是指那种可以在紧急情况下摒弃那些非关键负载的能力。
“Big Red Switch”概念是以丢弃或者延迟非关键负载为代价,保证关键处理过程能继续进行的关键。根据设计,这种情况不应当发生,但是真的发生时不失为一个好办法。等待服务已经处于危险之下时在考虑这些就太迟了。如果有些负载可以被暂时放入队列后面再处理,那这也可以作为“Big Red Switch”的候选。另外如果在关闭高级查询的情况下,事务可以继续执行,那么这也可以作为一个候选。关键在于确定碰到问题时系统所需的最小集合,然后实现关闭那些不必要的服务的开关,并进行测试。需要注意的是,正确的“Big Red Switch”应该是可逆的。同时应对开关的重置进行测试,确保所有服务可以回到完整服务的状态,包括所有的批处理任务及先前被暂停的非关键工作。
Control Admission。第二重要概念是准入控制。如果当前的负载系统已经无法处理了,那么再往系统中增加负载也只是会让更多的用户产生糟糕的体验。如何实现这一点与系统本身有关,某些系统比其他系统更容易完成。以电子邮件处理服务为例,如果系统已经超负荷,并且开始排队,此时最好不要再接收邮件而是让它们在来源队列中等候。这样可以产生效果并能实际减少总体服务延迟的关键原因是,如果队列形成处理时间会变得更慢。如果我们不允许队列的建立,那么吞吐量会更高一些。另一种技术是:提供服务时高端用户优先级高于普通用户,注册用户高于访客用户,访客高于普通用户,如果以“使用和购买”模式访问的。
Meter Admission。另一个非常重要的概念是上面所确定的准入控制点的修改。如果系统发生故障并宕机,为确保一切正常恢复时必须能够缓缓进行。比如先让一个用户进入,然后每10秒允许10个用户进入,如此慢慢加大。对于每个服务来说,拥有一个细粒度的旋钮可以在重新上线或者从灾难性失败中恢复时缓慢增加使用量是至关重要的。在任何服务的第一个版本中很少会包含这种能力。对于一个有客户的服务来说,一定有方法来通知它的客户端暂时无法提供服务及何时恢复服务。这就允许客户端在可以接受的情况下继续修改本地数据,同时也可以让客户端暂时退下避免干扰服务,让服务可以尽快恢复上线。这样也同时为服务owner提供了一个直接与用户沟通的机会,并可借此调整他们的期望。另外的一个可以避免所有客户端同时涌向服务端的技巧是:故意制造抖动和对每个实体进行自动备份。