作者:James Hamilton – Windows Live Services Platform 2007
原文:http://www.mvdirona.com/jrh/TalksAndPapers/JamesRH_Lisa.pdf
[序:James Hamilton,连线,主页,blog。James Hamilton目前是亚马逊AWS的VP和杰出工程师,专注于基础设施的效率、可靠性和可伸缩性。
在加入AWS之前,他是微软未来数据中心团队的架构师。在此之前,他是Windows Live平台服务团队的架构师。再往前,他负责管理Microsoft EHS(Exchange Hosted Services)团队。在加入EHS之前,他是SQL server的架构师以及SQL Server Security和Incubation团队的leader。他是在SQL server 7.0还在开发状态的时候加入的,此后的许多年里他领导了包括SQL语言编译器、查询优化器、查询执行引擎、DDL处理、元数据和目录管理、安全、服务器端XML、网络协议、服务器端游标、全文检索、公共语言运行时集成等在内的很多开发团队。在加入SQL server团队之前,他是Windows NT操作系统组的架构师。
在加入微软之前,James曾在IBM多伦多实验室作为IBM DB2 UDB主架构师,负责将DB2移植到众多的操作系统平台上,包括AIX OS / 2、Windows NT、Windows 95、Sinix、HP/ UX和Solaris。在加入DB2之前,James创建并领导了IBM的第一个C++编译器的开发。在70年代末和80年代的早期,他曾是兰博基尼和法拉利的专业汽修工。
]
说明:
摘要
“系统-管理员”比例通常可以作为一种理解大规模服务的管理开销的粗略度量方式。对于那些越小,越缺乏自动化的服务来说,这个比率越低可能低至2:1,而对于那些业界领先,高度自动化的服务来说,该比率已可高达2500:1。在微软的众多服务之中,Autopilot [1] 经常被认为是Windows Live Search团队成功达到高的“系统-管理员”比例背后的魔法。尽管自动化管理确实很重要,但最重要的还是服务本身。服务是否能够高效地进行自动化?是否是运维友好的(operations-friendly)?运维友好的服务几乎不需要人工干预,同时除了极个别最难解的故障外都可以在无管理员干预的情况下被自动检测到并恢复。本文总结了MSN和Windows Live在支撑一些超大型服务过程中多年积累下来的最佳实践。
导引
本文总结了用于设计和开发运维友好的服务的一系列最佳实践。大规模服务的设计和部署目前仍是一个快速发展中的领域,因此,这样的一个最佳实践列表随着时间的推移也会不断的演化。我们的目的是帮助人们:
1. 快速交付运维友好的服务
2. 远离那些非运维友好的服务可能带来的:凌晨的电话报警,充满了客户抱怨声的恼人会议
这里的内容吸收了我们过去20年在大型数据中心软件系统和Internet级服务方面的经验,其中大部分来自于最近领导Exchange Hosted Services(一个中等规模的service,有大约700台服务器和2百多万用户)团队期间。同时其中也包含了来自Windows Live Search, Windows Live Mail, Exchange Hosted Services, Live Communications Server, Windows Live Address Book Clearing House (ABCH), MSN Spaces, Xbox Live, Rackable Systems Engineering Team, Messenger Operations,Microsoft Global Foundation Services Operations等诸多团队的经验。此外本文也深受Berkeley关于Recovery Oriented Computing[2,3]和Stanford关于Crash-Only Software[4,5]的研究工作的影响。
Bill Hoffman[6]给本文贡献了很多最佳实践,此外还有三条很值得一开始就介绍下的简单信条:
1. Expect failures。一个组件可能在任意时间挂掉或停止工作。它所依赖的组件也可能在任何时间挂掉或停止工作。会发生网络故障,磁盘空间耗尽等。需要优雅地处理所有的故障。
2. Keep things simple。复杂导致问题。简单的东西更容易做正确。避免不必要的依赖。安装应该很简单。单台服务器的故障应该对集群其它部分没有影响。
3. Automate everything。人会犯错。人需要睡觉。人会忘记东西。自动化的过程是可测试的,可固定的,因此最终更可靠。尽可能地将所有地方自动化。
这三条原则形成了贯穿后面大多数讨论的主线。
建议
本节被分成如下10小节,每小节都覆盖了设计和部署运维友好的服务所需遵守的不同方面的要求:
? 总体服务设计
? 自动化管理和配置
? 依赖管理
? 发布周期和测试
? 硬件选型和标准化
? 运维和容量规划
? 审计监控和报警
? 优雅降级和准入控制
? 客户和媒体沟通计划
? 客户自配置和自助
总体设计
我们相信,80%的运维问题源自设计和开发。因此服务总体设计这一节是篇幅最多,也最重要的一节。系统发生故障时,很自然会先去检查运维过程,因为那是问题实际发生的地方。但是,大多数的运维问题要么是设计或开发导致的,要么是最好在设计和开发阶段解决。
在下面的小节中有一个共识:严格地区分开发、测试和运维,这在服务的世界里并不是最有效的工作方式。我们在很多服务那里看到低成本的管理跟开发、测试和运维团队间合作的紧密程度密切相关。
除了本节讨论的最佳实践,下一节的“面向自动化和预防式的设计”,对服务设计也有很大的影响。高效的自动化管理和配置只有在特定的服务模型上才能达成。这是一个反复强调的主题:简单性是高效运维的关键。在硬件选择,服务设计,部署模型上的合理限制,可以大大降低管理成本,提高服务可靠性。
对服务整体设计影响最大的一些运维友好的基本原则如下:
l Design for failure。这是一个开发由很多协作组件构成的大型服务时的核心观念。那些组件会发生故障,而且会频繁发生故障。一旦服务规模超过10,000台服务器和50,000个磁盘后,一天内都会发生多次故障。如果一个硬件故障发生后需要马上进行人工处理,那么服务就根本没办法低成本和可靠地进行扩展。整个服务必须有能力在没有人工干预的情况下在故障发生时还能继续工作。故障恢复必须是非常简单的路径,并且经常对该路径进行测试。Stanford的Armando Fox认为最好的测试故障路径的方式就是从来都不去正常地停掉服务,而是直接让它挂掉。这看起来违反直觉,但如果故障路径没有频繁被使用的话, 在真的需要它的时候它根本就不能工作。斯坦福的Armando Fox[4,5]认为测试故障路径的最好方式就是从来都不正常地停掉服务。而是直接让它挂掉(Just hard-fail it)。这看起来违反直觉,但如果故障路径不常被使用的话, 那么在真的需要时它根本就不能工作。
l 冗余和故障恢复。大型机模型是要购买非常大非常昂贵的服务器。大型机具有冗余的电源、热插拔的CPU、独特的总线结构,在一个单一、紧耦合的系统中提供了令人惊讶的IO吞吐量。这种方式明显的问题是成本高,但即使这样,它也不是足够可靠。要达到5个9的可靠性,冗余是必需的。即使要达到4个9,单系统的部署方式也是很难做到的。这个观念已经在业界被广泛接受,但还是能经常看到一些部署在脆弱的、非冗余数据层上的服务。
要设计一个任何组件在任何时间都可以挂掉(或者是关掉服务),但仍能满足相应SLA(service level agreement)的服务,需要细致的工程化。要判断是否遵循了这条设计原则,可以采用如下方法:运维团队是否愿意并且也能够随时关掉服务的任何一个服务器,同时也不用等待将上面的工作负载排除?如果可以,那么说明服务实现了同步化冗余(没有数据丢失),故障检测和自动failover。
作为一种设计方法,我们介绍一下常用来发现和纠正服务的潜在安全问题的安全威胁模型。在安全威胁模型里,我们考虑每一种可能的安全威胁,并且针对性地给出缓解方案。同样的方法可以用在容错和故障恢复的设计上。
列举出所有可能的组件故障模式,及其组合。对每一种故障,确保服务能够在没有无法接受的服务质量损失情况下继续运作,或者确定该故障的风险对这个服务是可接受的(比如,没有地理冗余的服务的整个数据中心挂掉了)。一些非常不寻常的故障组合,要处理它们在成本上是不可接受的,可能会被认为它们是不可能发生的。但是,做这种决定时要谨慎。我们已多次惊讶的发现某些不寻常的”事件的组合,如何频繁发生在运行成千上万的服务器每天数以百万计的组件故障的情况下了。罕见的组合可以 变得司空见惯。
l Commodity hardware slice。服务的所有组件都应当是基于普通硬件的。比如,一个具有轻量级存储的服务器可能是具有dual socket(双插槽),单磁盘的价值1000-2500美元的2-4核系统;一个具有重量级存储的服务器,与之配置相似,但是会具有16到24个磁盘。关键的洞察如下:
1. 由大量廉价服务器组成的大规模集群要比由少量大型服务器组成的便宜很多。
2. 服务器性能比IO性能的增长快得多。同样数量的磁盘,小型服务器的性能更均衡。
3. 耗电量跟服务器数量是线性关系,跟时钟频率是三次方关系,使得高性能服务器运营成本更高。
4. 小服务器failover时只会影响服务整体工作负载中的一小部分。
l 单一版本软件。有两个因素让服务比大多数打包发行的软件开发成本更低,进化更快,对于服务来说:
n 软件只需要面向一个单一的内部部署环境
n 之前的版本不需要像面向企业的产品那样一支持就是10几年
单一版本软件相对容易为消费者提供服务,尤其是免费提供。但是在向非消费者销售基于订阅的服务时,这一点也是同等重要的。企业已经习惯了强力地影响它们的软件提供商,并且习惯了在部署新版本时拥有完全的控制(通常是非常缓慢的)。这抬高了运维成本和支持成本,因为有很多版本需要支持。
最经济的服务不会让客户可以控制它们的运行版本,同时只会保有一个版本。控制这种单一版本软件时需要:
n 发布要避免造成用户体验发生根本改变
n 对于那些想要控制版本升级的客户来说,要么自己部署一套,要么切换到愿意提供多版本支持的服务商。
l 多租户(Multi-tenancy)。多租户是指将所有公司或终端用户都囊括在同一个没有物理隔离的服务里,而与之相比单租户则是指将用户进行分组放到独立的集群中。进行多租户的理由几乎与单一版本软件相同,同时也是因为这样可以为建立在自动化基础上的大规模服务从根本上降低成本。
回顾一下,我们上面已经提出的基本设计原则和考虑点是:
l Design for failure。
l 冗余和故障恢复
l 基于廉价硬件
l 提供单一版本软件
l 实现多租户
通过对服务设计和运维模型进行限制,可以最大化我们构建自动化和低成本的服务的能力。在我们的这些目标和应用服务提供商或者IT外包商的那些目标之间有一个明显的区别。那些企业通常是人力密集型的,同时更愿意运行复杂的,由客户定制的配置。
一些更具体的设计运维友好的服务的最佳实践如下:
l 服务健康状况快速检查。服务版的构建验证测试(BVT),这是一个可以在开发人员的系统环境上运行的嗅探性测试,确保服务没有遭受实质性的破坏。不是所有的边界条件都会被测试到,但如果这个测试通过了,代码可以ci。
l 在完整的环境里开发。开发除了需要进行自己模块的单元测试,还要对包含了他们的变更后的整个服务进行测试。要高效的实现这一点需要支持单机部署 (见2.4节),以及前面的那条最佳实践—服务健康状况快速检查。
l 对底层组件零信任。假定依赖组件会挂掉,同时要确保组件能恢复并继续提供服务。恢复技术是与服务本身相关的,但是也有一些常用的恢复技术:
n 以只读模式继续访问缓存的数据
n 继续对所有用户提供服务,除了那些受故障影响的一小部分用户
l 不在多个组件里实现同样的功能。要预见未来的交互方式是很困难的,同时如果系统中存在代码冗余的话那就需要在系统的多个部分中进行修复。服务总在快速的增长和演化。如不小心,代码库会迅速恶化。
l 一个隔舱或集群不应该影响其它隔舱或集群。大部分服务都是由位于多个隔舱或子集群的相互协作的多个系统组成。隔舱间要尽可能100%独立,避免关联故障。那些全局性的服务即使具有冗余备份也是一个单点。有时候这种情况可能无法避免,但是还是要尽可能把一个集群依赖的东西都放到集群里面。
l 允许(极少情况下的)紧急人工干预。常见的场景是灾害后或其他紧急情况下的用户数据迁移。将系统设计的永远不需要人工干预,但是也要理解一些组合故障或者意料之外的故障发生时会需要人工干预。这些情况会发生,同时在这些情况下的操作错误是很多灾难性数据丢失的常见来源。一个在凌晨2点顶着压力干活的PE,可能犯错。系统的设计首先要让绝大多数情况不需要PE干预,但对于需要PE干预的情况,要跟PE一起确定预案。预案不能是记在文档里的多步骤的、容易出错的过程,应该将它们写成脚本,在生产环境测试,确保可用。没有在生产环境里测试过的东西是无法work的。所以运维团队应该周期性地组织演习来使用这些工具。如果演习对服务可用性的风险很大,说明对于这些工具的设计、开发、测试的投入不够。
l 保持简单和健壮。复杂的算法和组件交互会将调试和部署等方面的困难加倍。在大规模服务中,故障模式的数量在进行复杂的优化之前就够让人害怕的了。一个总体原则是:超过一个数量级的改进才值得考虑,只有几个百分点甚至更少的改进是不值得去做的。
l 在所有层执行准入控制。所有好系统都会在前门设计有准入控制。这遵循了一个长期以来被人们所接受的原则:与继续接受新任务导致系统震荡相比,更好的方式是不要让更多任务进入过载的系统。在服务入口处通常都有某种形式的流控或准入控制,但是在所有重要组件的边界上也都应该有准入控制。负载特点的变化常常会导致某个子组件过载,尽管整体服务负载可能还在一个可以接受的水平上。下面第2.8节里提到的“big red switch”就是一种在过载情况下的优雅降级方式。总体原则是尝试优雅降级而不是让系统直接挂掉,在所有用户受到影响之前阻止进入。
l 对服务进行分区(Partition the service)。分区应该是细粒度的,无限可调的,并且不绑定在任何现实实体上(比如人,团体等)。比如如果按照公司进行划分,那么一个规模很大的公司就会超过单个分区的大小。再比如如果按照人名前缀进行划分,那么最后那些以P开头的,采用单个服务器将会存不下。我们推荐用一个中间层的查找表把细粒度实体(典型的如用户)映射到相应的数据管理系统。然后这些细粒度分区就可以在服务器间自由移动。
l 理解网络设计。尽早进行测试以了解什么样的负载会发生在机架内、跨机架、跨数据中心。应用程序开发人员必须了解网络的设计,并且要提早与运维团队里的网络专家们一起对它进行review。
l 分析吞吐率和延迟。为了理解其影响,必须要对核心服务用户交互的吞吐量和延迟进行分析。并将这件事与其他一些操作像日常数据库维护、运维配置管理(新用户添加,用户迁移)、服务调试一样进行常规化。这将有助于发现因某些周期性管理任务引发的问题。对于每个服务来说,所需的度量量可以与容量规划相结合,比如系统的每秒用户请求数、系统的并发在线用户数或者是某些可以将相关工作负载映射到资源需求的度量量。
l 把运维工具作为服务的一部分。由开发、测试、PM、PE编写的运维工具都要在开发过程中进行代码review,checkin到代码主干,跟代码一起进行跟踪维护和测试。常见的情况是,这些工具非常关键,但是却几乎没被测试过。
l 理解访问模式。规划新feature时,一定要考虑它会给后端存储带来怎样的负载。通常服务模型和服务开发者所在的抽象层次已经脱离了底层存储,这使得他们无法注意到负载会给底层数据库带来怎样的影响。一个最佳实践是给SPEC (Standard Performance Evaluation Corporation,系统性能评估测试)加上一节:“这个feature对系统其它部分有什么影响?” ,然后在feature上线时验证负载的情况是否符合。
l 将所有东西版本化。要假设系统是运行在一个多版本混合的环境里。目标是运行单一版本软件,但在上线和生产测试时会有多版本共存。所有组件的n和n+1版本都要能和平共处。
l 保留上次发布的UT和FT。这些测试是用来验证n-1版本的功能没有被破坏掉的重要手段。更进一步地,我们强烈推荐要持续在生产环境跑验证测试(后面还会详细解释)。
l 避免单点故障。单点故障会导致故障发生时服务或服务的多个部分不可用。优先采用无状态的实现。不要把请求或用户指定给特定的服务器。而是要对能够对负载进行处理的一组服务器进行负载均衡。静态hash或者任何的静态服务器任务分配方式,随着时间的流逝都会导致数据和/或查询的倾斜。当同一组内的机器都是可互换的时候,就可以很容易地进行水平扩展。数据库通常都是一个单点,同时在进行互联网级别的服务的设计时,如何进行数据库的扩展依然是最困难的问题。好的设计会使用细粒度分区,同时会禁止跨分区操作使得可以高效地通过多个数据库服务器进行扩展。对所有的数据库状态冗余地存储(至少有一个)到全冗余的热备服务器上,同时要经常在产品环境中进行failover测试。
自动化管理和配置
很多服务实现地需要在故障时向运维发报警,依赖人工干预来恢复服务。这种方式的问题首先在于人力成本高,需要7*24小时的运维人员成本。更重要的是,当运维人员需要顶着巨大压力做出重要决定时,大概有20%的概率会出现失误。这种模型既昂贵又容易出错,同时还降低了整体服务的可靠性。
但面向自动化的设计需要给服务模型强加很多限制。比如,今天的某些大规模服务依赖于一个使用了异步复制进行备份的数据库系统。这样在主本发生故障无法提供服务而需要切换到副本时,会因为复制的异步性丢失一些客户数据。但是如果不failover切换副本则会导致那些数据存储在发生故障的数据库服务器上的用户服务被中断。很难将是否在这种情况下进行fail over进行自动化,因为它需要人为做出判断,并且需要精确地估算出可能丢失的数据量和可能导致的服务中断时长。要想实现自动化,就要付出同步复制的延迟和吞吐率代价。如果这样做了的话,那么failover就变成了一个非常简单的决定:如果主本挂了,那么就将请求路由给副本处理。 这种方式更易于自动化,并且更不容易出错。
在完成设计和部署后再进行服务的自动化是很困难的。成功的自动化应当是简单和清晰的,易于做出运维判断的。这反过来又取决于细致的服务设计,必要时可以牺牲一些延迟和吞吐量来方便自动化。通常要在这里面的进行权衡也很难的,但是对于大规模服务来说,所带来的运维方面的节省通常也是数量级上的差异。实际上我们看到,当前最手动与最自动的服务在人力开销上的差异多达两个数量级。
面向自动化设计的最佳实践如下:
l Be restartable and redundant。所有操作都应是可重做的(restartable),所有持久化状态都需要进行冗余性存储。
l 支持地理分布式。所有的大规模服务都应当支持跨数据中心运行。坦率地讲,我们这里提到的自动化和大多数的高效率即使是在没有地理分布的情况下也是可能的。但是跨数据中心部署能力的缺乏将会大幅推高运维成本。没有地理分布式的支持,就很难通过将负载迁到另一个数据中心来减轻当前数据中心的压力。地理分布式的缺乏,是一种会推高成本的运维限制。
l 自动配置和安装。配置和安装,如果是手动完成的话,成本高且易发生问题,同时细小的配置差异会慢慢延伸到整个服务,使得问题越来越难以定位。
l 将配置和代码作为一体。保证:
n 开发团队将配置和代码作为同一个单元进行提供
n 该单元的部署测试会以与完全与运维人员线上部署方式相一致的形式进行
n 运维人员将它们作为同一个单元进行部署
那些将配置和代码作为同一个单元,且对它们一块进行变更管理的服务会更可靠。
l 如果必须要在产品环境中进行某项配置变更,要保证所有的变更都会有相应的审计记录,以确保清楚地知道进行了哪些变更,以及谁,何时进行的,受影响的服务器有哪些(见2.7节)。经常扫描所有服务器以确保它们的状态与期望状态相一致。这有助于捕获安装和配置故障,尽早检测到服务器配置错误,发现未经审计的服务器配置变化。
l Manage server roles or personalities rather than servers。每个role或personality应该能支持按需增减服务器数。
l 多系统同时故障会经常发生。要意识到某些故障可能会同时影响到很多主机(电力,交换机,上线)。不幸的是,具有状态的服务,也不可避免的是对拓扑敏感的。关联故障仍会是现实存在的。
l 在服务级进行恢复。在服务级别上进行故障处理和错误纠正,保证完整的上下文执行环境可用,而不是更低的软件层上进行。比如,将冗余构建到服务中,而不是依赖于在更低的软件层上进行的恢复。
l 永远不要依赖本机存储不可恢复的信息。始终坚持对非临时的服务状态进行备份。
l 保持部署过程的简单。文件拷贝是理想方案,因为它提供了最大的部署灵活性。最小化外部依赖。避免复杂部署脚本。应当避免禁止在同一个服务器上运行不同组件或相同组件的不同版本的行为。
l Fail services regularly。停掉数据中心,关掉机架,以及关闭服务器电源。定期引入人为的故障,不断暴露出系统,网络和服务的弱点。不想在生产环境下进行测试的服务,也是没有信心保证服务可以经历故障的考验的。同时如果没有生产测试,故障恢复就没办法在需要的时候工作。
依赖管理
大规模服务中的依赖管理通常得不到所应有的关注。一般来说,小的组件或服务的依赖又不足以证明或显示依赖管理所带来的复杂性。只有在如下情况下,依赖才会显得有意义:
1. 依赖的组件很大或者很复杂
2. 依赖的服务的价值在于它是单一中心实例
第一种情况的实例是存储和一致性算法实现。第二种情况的实例是身份和组管理系统。这些系统的价值在于,它们是单一的共享实例,无法采用多实例避免这种依赖。
假设依赖关系满足上述规则,一些管理它们的最佳实践如下:
l Expect latency。调用外部组件可能会花很长时间完成。不要让一个组件或服务的延迟造成完全不相关的地方的延迟。确保所有的交互有合适的超时,避免长期占用某项资源。操作幂等性允许超时后重启请求,即使请求已经部分或完全完成。确保汇报所有的请求重启。限制重启次数,避免反复失败的请求消耗过多系统资源。
l 故障隔离。站点的架构必须能够防止级联失败。总是“fail fast”的。依赖的服务发生了故障,就标记为不可用并不再使用它们,以免线程一直等待失败的组件。
l 使用经验证的稳定的组件。经过验证的技术总是要好过刀口舔血。稳定版的软件总是要比尝鲜版更好,无论新feature看起来多么诱人。该规则也同样适用于硬件。
l 实现内部服务的监控和报警。如果服务正在使得它所依赖的服务过载,被依赖的服务需要能够知道这种情况,并且如果它不能自动对过载进行处理还需要发送报警。如果运维人员也无法快速解决问题,还需要能够迅速联系到来自两个团队的工程师。所有关联团队都应该保证有工程师可以被随时联系到。
l 被依赖的组件需要有同样的设计出发点。被依赖的服务以及被依赖的组件的生产者至少需要跟依赖者达成一样的SLA。
l 组件解耦。在可能的情况下,确保组件在其他组件发生故障时可以继续工作,可能是以降级模式。例如,不是对每个连接都重新做认证,而是维护一个会话密钥,每几个小时更新一下连接状态。这样认证服务器的负载会更一致,同时也保证了在短暂网络故障后的重连不会造成登录风暴。