可扩展互联网服务的设计和部署【译】


这篇译文源自当时还在微软Windows Live Services Platform部门的James Hamilton的经典论文 On Designing and Deploying Internet-Scale Services,他现在是Amazon Web Services团队的成员之一,他是数据库、架构、云计算等方面的大牛。

概述

系统和管理员的比例通常作为一个大概指标来衡量高扩展服务的管理成本。对于规划较小很少有自动化的服务这个比率可能会低至2:1,但在领先并高度自动化的服务,这个比率可能高达2500:1。在微软的服务里,Autopilot常被称作Windows Live搜索团队成就高系统管理员比率后面的魔法。既然自动化管理如此重要,最重要的因素其实是服务自身。是服务高效到自动化?什么是我们通常称作的运维友好?运维友好的服务要求很少的人工介入,并且大部分复杂、异常的检测和恢复不需要管理员的介入。本论文总结了来自MSN和Windows Live在大规模服务多年的最佳实践。

介绍

本篇论文总结了设计和部署运维友好的服务的一系列最佳实践。这是一个快速变化的主题领域,因此,最佳实践的任何列表都可能随着时间而变化。我们的目标是帮助那些:

  • 快速开发运维友好的服务,并且
  • 避免因非运维友好服务导致的凌晨电话骚扰以及和不爽的客户开会

该工作基于我们过于20年在高扩展数据中心软件系统和可扩展互联网服务的经验,大多数来自近期领导Exchange Hosted Services团队(当时,一个中等规模的服务有近700台服务器和220万以上的用户)。我们也吸收了来自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团队以及Microseft Global Fuundation Services Operations团队的经验。好几个上述的服务的用户量已经增长到超过2.5亿。本论文也严重依赖Berkeley在Recovery Oriented Computing以及Standford在Crash-Only Software方面所做的工作。

Bill Hoffman对本论文贡献了很多最佳实践,最下面这三个信条最值得放在前面来说:

  • 预期故障(Expect failures)。一个组件可能在任何时间都会崩溃或都停止。依赖的组件也可能在任何时间会出现故障或被停止。这可能是网络故障,磁盘空间不足。优雅地处理所有可能的故障。
  • 保持简单(Keep things simple)。复杂产生问题。简单的事情更容易做对。避免不必要的依赖。安装要简单。一个服务器失败不要影响数据中心的其它机器。
  • 一切自动化(Automate Everything)。人会制造错误。人需要休息。人会忘记事情。自动化过程是可测的,可固化的,因此会更可靠。如果可能尽量自动化。

这三个信条是下面讨论的主题。

建议

本文由十个部分组成,每一部分涵盖设计和部署运维友好服务的一个方面。包括:整体服务设计;面向自动化和供给的设计;依赖管理;发布周期和测试;硬件选型和标准化;运维和容量规划;审计,监控和报警;优雅降级和准入控制;客户和出版沟通计划;客户自供给和自助服务。

整体应用设计(Overall Application Design)

我们一直相信80%的运维问题源自设计和部署,因此整体服务设计是本文中最大和最重要的部分。当系统出现故障时,一个自然的倾向是首先去看运维状况,因为它是问题实际发生的地方。大多数运维问题,既然源自设计和部署,那么最好就是这里解决它们。

贯穿下面章节的一个共识是:严格区分开发,测试和运维在服务的世界里不是最有效的方式。我们看到很多低成本管理服务的趋势是和开发、测试、运维团队的紧密工作密切相关。

除了这里讨论的最佳服务设计实践之外,随后的章节,“面向自动化和供给的设计”,在服务设计上也有重要的影响。高效的自动化管理和供给通常仅在一个约束的服务模型里才能达成。这里有一个始终重复的主题:简单是高效运维的核心。在硬件选择,服务设计,和部署模型上的关联约束是减少管理成本和提升服务可用性的极大驱动力。

一些运维友好的基本准则在整体服务设计上有最大的影响:

  • 面向故障的设计(Desing for failure)。这是在开发由许多相互协作的组件组成的大型服务系统时的最核心概念。这些组件会出问题并且会不断地出问题。这些组件也不会总是独立地协作和失败。当服务扩展到1万台服务器和5万块硬盘以上时,每天都会发生多次故障。如果一个硬件故障需要立即有人工介入,这个服务就无法低成本地扩展和可靠。整个服务必须具备没有人工介入也能处理故障的能力。故障恢复必须是一个简单的路径,并且该路径必须经过不断的测试。Standford的Armando Fox曾经对测试故障最佳的办法是不要正常关闭服务有过讨论。就要通过暴力让它出问题。这个听起来有点违反直觉,但如果一个故障路径没有不断的被使用,当需要的时候它很可能无法工作。

  • 冗余和故障恢复(Redundancy and fault recovery)。大型主机模式是买一个非常巨大,非常昂贵的服务器。主机有冗余的电源供应,热切换的CPU,和在一个单一双路的系统内能够提供可观的I/O吞吐量的外部总线架构。这些系统显而易见的问题是它们的费用。并且即使所有成本高昂的工程师都在,它们也无法足够可靠。为了获得5个9的可靠性,冗余是必须的。即使在一个单系统部署上获得4个9的可靠性也很困难。这个概念在工业界相当容易理解,但依然经常看到有服务构建在脆弱的、非冗余的数据层上面。设计一个在任何时间任何系统都可能崩溃(或者服务当掉)但仍然能达到服务等级协议(SLA)的服务需要非常小心的工程师。能够完全接受这个设计准则的反向测试如下:运维团队能够在任意时间没有先降低工作负载的情况下下掉服务中的任何一台服务器吗?如果是,这就是同步冗余(没有数据丢失),故障检测,和自动故障转移。作为一个设计办法,我们建议一个通常使用的办法来发现和纠正潜在的服务安全问题:安全威胁建模。在安全威胁建模中,我们考虑每一个可能的安全威胁,对每一个给给予足够的减缓。同样的办法能被用作故障恢复设计。由此文档化所有可想到的组件故障模式和混合情况。对于每一个故障,确保服务能继续运行,并且没有不可接受的服务质量损失,或者确定这个故障风险对特定的服务是可接受的(比如,在一个非跨地区冗余的服务中丢失所有数据)。非常反常的组合也许不太可能被充分地查明,确保系统能通过它们正常运行是不经济的。但当做此决定时要小心。当运行上千台服务器每天有上百万的机会可能产生组件故障时我们惊讶于非正常事件是多频繁的发生。罕见的组合会变得很平常。

  • 廉价硬件(Commodity hardware slice)。所有的服务组件都会对应到廉价硬件切片上。比如,轻存储的服务器会是全双工,2~4核的系统,一个启动盘,$1000~$2500范围内,重存储的服务器通常会用16到24个磁盘。最重要的观察如下:

    • 大规模的廉价服务器集群比它们替换掉的小规模的大型服务器便宜多,
    • 服务器性能会比I/O性能的增长快很多,让特定数量磁盘的小型服务器使用更平衡的系统,
    • 电力消耗会随着服务线性增长但和时钟频率呈立方,使用较高性能的服务器运行成本会更贵,并且
    • 一个小型服务器当机时对整体服务处理能力的影响比例也更小
  • 单一版本软件(Single-version software)。两个因素导致一些服务比大多数打包好的产品开发成本更低并且进行更快:

    • 软件只需要单一内部部署,并且
    • 对面向企业的产品来说不需要支持前一个版本十年。 单一版本软件相对来说更容易和一个客户服务集成,特别是免费提供的。但当销售基于订金的服务给非客户时它也是同等重要。当企业部署新版本时(特别地慢)他们相对于他们的软件提供商有显著的影响和完全的控制权。既然那么多的软件版本需要支持,这会提升他们的运营成本和支持成本。最经济的服务是不要给客户版本的控制权,仅支持一个版本。维持单一版本软件线要求:
    • 关注非产生重要用户体验改变的发布,并且
    • 乐于允许需要这个控制级别的客户要么内部托管或者切换到愿意提供对人员敏感的多版本支持的应用服务提供商
  • 多租户(Multi-tenancy)。多租户是指所有公司或终端用户的服务托管在同一个服务中不要做物理隔离,而单租户是在一个独立的集群中做用户分组的隔离。对多租户的争论和对单版本支持几乎是一样的,并且是基于提供基础的、构建在自动化和大规模可扩展的低成本服务。

总结一下,我们在上面提到的基本的设计准则和考虑点如下:

  • 面向失败的设计,
  • 实现冗余和故障恢复,
  • 基本商业硬件,
  • 支持单版本软件,
  • 支持多租户

我们正在约束服务设计和运维模型以便最大化我们自动化的能力并减少整体的服务成本。我们和那此应用服务提供商或IT外包有明显的区别。那商业模式趋向于用更多的人以及更多的意愿运行复杂的、客户定制的配置。

更加具体的运维友好的服务设计最佳实践如下:

  • 快速服务健康检查(Quick service health check)。这是一个构建验证测试的服务版本。它是一个能够在开发者的系统中快速运行的小测试,以及保证该服务不会被任何重要步骤打断。并非所有边界用例都会被测到,但是一但健康检查通过,代码就可以入库。
  • 在完整的环境中开发(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)。通常的桥段是在发生灾难性的事件或其它紧急情况时迁移用户的数据。系统设计时永远不要让人工参与,但要理解在组合的故障场景下或者未预料到的故障场景需要人工介入也是有可能出现的。这些事件在这些状况下出现和操作错误是导致灾难性数据丢失的源头。一个运维工程师在压力下于凌晨2点工作将会制造错误。设计系统的第一原则是在大多数情况不需要人工介入,但是如果他们需要介入,能够有恢复计划地做运维工作。相对于把这些固化成几个步骤,把他们写成脚本并在生产环境中测试它们并确保他们能正常工作会更好。不在生产环境测试就不会正常工作,因此运维团队需要定期使用这些工具进行故障演练。如果服务可用性风险极度高危,那么无效的投资已在设计、开发和测试这些工具时埋下种子。
  • 保持事情简单和健壮(Keep things simple and robust)。复杂的算法和组件相互影响会增加调试、部署等的难度。简单和足够直接在高扩展的服务中总是更好-在复杂的优化开始之前相互影响的大量问题已让人望而却步。我们总的原则是带来重大提升的优化是值得考虑的,但部分或者甚至只有很小的收益是不值得的。
  • 在所有层面做强制准入控制(Enforce admission control at all levels)。任何好的系统都会在入口设计准入控制。这个是根据久经考验的原则:不要让已经过载的系统接收更多的工作比继续接收工作并开始崩溃要好。一些节流或准入控制在进入服务时是很常见的,但是应该在所有主要组件的边界都做准入控制。尽管整体服务可以接受的负载水平继续运行,但是工作负载的改变最终还是会导致子组件的过载。可以参考2.8紧急开关这节,在过载时把优雅降级作为一种解决办法。总的原则是优雅降级优于直接失败,在给所有用户提供统一的糟糕服务前阻止它进入服务。
  • 分区服务(Partitions the service)。分区应该是无限可调整和细粒度的,并且不会被任何真实世界的实体(人,组织...)所限制。如果按公司做分区,那么一家大公司就会超过很多单一个体的数量。如果按名字前缀做分区,比如所有以P开头的,就无法集中在一台服务器上。我们推荐在中间层使用一个查询表来做细粒度的实体映射,典型的像用户到他们的数据被管理的系统。这些细粒度的分区可以自由地在服务器之间进行移动。
  • 分析吞吐量和延迟(Analyze througput and latency)。对用户使用的核心服务进行吞吐量和延迟分析能够了解它的影响。做些其它操作,比如常规的数据库维护,操作配置(新用户增加,用户迁移),服务调试等。靠周期性的管理任何可以帮助捕获异常。对每一个服务来说,都应该有一个为容量规划而出现的指标,比如每一个系统的每一秒的用户请求数,每一个系统的在线用户并发数,或者映射工作负载到资源需求的一些指标。
  • 把运维工具做为服务的一部分(Treat operations utilities as part of the service)。开发、测试、程序管理和运维的工具都应该由开发来做代码审核,提交到主代码库,并追踪时间计划以及做同样的测试。通常这些工具是应付紧急任务还几乎没有被测试过。
  • 了解访问模式(Understand access patterns)。当准备新功能时,总是考虑他们要给后端存储增加什么样的负载。服务模型和服务开发人员总是变得远离存储,他们会忘记看放在下层数据库的负载。一个最佳实践是把它放在说明文档中,就像那章“这个功能会对剩余的基础设施有什么影响?”,然后当这个功能可用时评估和校验它的负载。
  • 一切版本化(Version everything)。做好运行在一个混合版本的环境中的准备。目标是运行单一版本的软件,但是在首次发布和生产测试时会存在多个版本。所有组件的版本n和n+1需要和平共处。
  • 保留上一次发布的单元/功能测试(Keep the unit/funcational tests from the previous release)。这些测试是验证上一个版本功能没有出现问题的一种非常好的方法。我们建议更进一步,不断地在生产环境运行服务验证测试(详见下面)。
  • 避免单点故障(Avoid single points of failure)。单点故障会导致服务或部分服务不可用。倾向于无状态的实现。不要把请求或客户端和具体的服务器绑死。代替的是,在一组能处理这样负载的服务器上使用负载匀衡。静态哈希或者任何静态工作分配经过一段时间后都可能会遇到数据或查询倾斜的问题。当同一等级的服务器可以互换时水平扩展是很容易的。数据库经常存在单点故障并且数据库的扩展仍然是设计可扩展的互联网服务中最难的点。使用细粒度的分区并且不支持跨分区操作的良好设计能跨许多数据库服务器高效扩展。

自动化管理和分发(Automatic Management and Provisioning)

很多用来作故障预警的服务在恢复时需要人工介入。这种模式的问题是24*7的运维人员是很昂贵的。更重要的是,如果运维工程师在压力下做决策,大概20%的概率会导致出问题。这种模式既昂贵又容易出错,并且会减少整体服务的可靠性。

面向自动化做设计,需要重要的服务模型约束。比如,今天一些大型服务依赖数据库到备份服务器的异步复制功能。在主机不能服务切换到备机时,会在做异步复制时丢掉一些客户数据。尽管如此,如果不切换到备机会导致服务当机影响那此数据存储在出故障的数据库服务器上的客户。在这个案例中,做自动化故障切换是比较困难的,因为它依赖人工判断和当机时间内丢失数据量的精确评估。一个自动化的系统补偿了同步复制的延迟和吞吐量成本。并且,这样做了,故障变成一个简单决策:如果主机当了,自动路由到备机。这种办法对自动化来说容易接受的多并且更少出错。

在设计和部署之后,一个服务的自动化管理可能非常困难。成功的自动化需要简单和清晰,容易做出操作决策。这个反过来依赖一个非常小心的服务设计,有必要时,牺牲一些延迟和吞吐量以更容易做自动化。这个权衡经常比较难以制定,但是在高扩展服务中可以节省大量管理成本。事实上,在大部分是人工和大部分自动化服务之间的差距主要是在人员成本上的巨大差异。

自动化设计的最佳实践包括:

  • 可重复的和冗余(Be restartable and redundant)。所有的操作都必须能够重复执行并且所有的持久化状态必须冗余存储。
  • 支持跨地区分布(Support geo-distribution)。所有高扩展的服务都应该支持跨数据中心的运行。为了公正起见,我们在这里描述的自动化和大部分提升效率的东西都是不涉及到跨地区分布的。但是缺少跨多数据中心的支持,部署会戏剧性地提升运维成本。没有跨地区分布,就很难利用一个数据中心的空闲能力去减轻另一个数据中心服务的负载。跨地区分布的缺失不利于约束运维成本的提升。
  • 自动化分发和安装(Automatic provisioning and installation)。分发和安装,如果手工来做,成本很高,会有太多问题,并且微小的配置差异会慢慢地放大导致定位问题会难的多。
  • 配置和代码一体(Configuration and code as a unit)。确保:
    • 部署团队要把代码和配置作为一体来传递,
    • 测试过是以和运维完全一样的方式部署,并且
    • 运维也是把它们当做一体来部署 把配置和代码作为一体对待并且只能同时改变它们的服务通常更可靠。如果在生产环境一个配置必须被变更,确保所有的变更都会生成审计日志,清楚地记录变更了什么内容,何时变更,是谁变更,哪些服务器受到了影响。不断地扫描所有的服务器确保它们当前的状态是符合预期的状态。这能帮助捕获安装和配置问题,尽早地检测到服务器的错误配置,并且发现没有审计的服务器配置变更。
  • 管理服务器角色或个性化甚于服务器本身(Manage server roles or personalities rather than servers)。每一个系统角色或个性化都应该支持按需部署在或多或少的服务器上。
  • 多系统故障很常见(Multi-system failures are common)。预计很多主机在同一时间出现问题(电力,网络交换,和首次发布)。不幸的是,有状态的服务不得不知道拓扑结构。实际的生活中总是有相关联的问题存在。
  • 服务级别恢复(Recover at the service level)。在执行上下文都可用的服务级别处理异常和修正错误比在低层给的软件层面更好。比如,做服务的冗余优于依赖低层软件的恢复。
  • 永远不要靠本地存储做不可恢复信息的存储(Never rely on local storage for non-recoverable information)。总是对所有的非短暂的服务状态进行复制。
  • 保持部署简单(Keep deployment simple)。能够给予最大限度部署灵活性的文件拷贝是比较理想的。最小化外部依赖。避免复杂的安装脚本。任何阻碍在同一个服务器上运行不同组件或者同一组件的不同版本行为都应该避免。
  • 有计划地让服务失败(Fail services regularly)。当掉数据中心,当掉机柜,把服务器断电。常规的有控制的当机都会充分暴露服务、系统和网络的脆弱性。这此在生产环境中的异常测试仍不能确保在出问题时服务仍能继续运行。并且,没有生产测试,在真正使用时无法正常恢复。

依赖管理(Dependency Management)

在高扩展服务中依赖管理经常被忽视。总的原则是,依赖小的组件或者服务不会充分节省管理他们的复杂性。依赖在下面的情况时才有意义:

  • 被依赖的组件在大小或复杂性上非常可观,或者
  • 被依赖的服务在是单个、处于中心位置的实例上已收益。

第一层级的例子是存储和连续算法实现。第二层级的例子是识别和分组管理系统。这些系统的整体价值是他们是一个单个的、共享的实例,因此避免依赖的多实例不可选。

假设根据上面的规则依赖已被验证,管理它们的最佳实践如下:

  • 预期延迟(Expect latency)。调用外部组件也许会花费较长时间。不要让一个组件或服务的延迟影响所有不相关的领域。确保所有的关联影响都有适当的超时机制,避免资源长时间无法释放。操作的幂等性允许超时后重新请求,即使这些请求部分或全部完成。确保所有的重新请求会被报告和处理,避免重复的失败请求消耗过多的系统资源。
  • 隔离故障(Isolate failures)。网站架构必须能够阻止级联故障。总是“快速失败(fail fast)”。当依赖的服务故障时,把它们标记为不可用并停止使用它们以阻止线程在失败组件上的无谓等待。
  • 使用可移植和经过证明的组件(Use shipping and proven components)。经过证明的技术总是比在危险边缘操作要靠谱。稳定的软件比早最新的版本要好,不管新功能带来的价值有多大。这个规律对硬件同样有效。卷移植的稳定硬件总是比有小幅性能提升、早期发布的硬件要好。
  • 实现服务内的监控和报警(Implement inter-service monitoring and alerting)。如果一个服务给依赖的服务造成过大压力,被依赖的服务自己需要知道,并且如果它不能自动回退,就要报警。如果运维不能快速解决问题,需要很方便地从两个团队联系到工程师。所有被依赖的团队都应该有工程师的联系方式在依赖的团队。
  • 依赖服务需要同一设计标准(Dependent services require the same design point)。被依赖服务和依赖组件的生产者需要至少和依赖服务同一SLA。
  • 组件解耦(Decouple components)。在任何可能的地方,保证组件在别的组件失败时也能继续运行,也许在降级模式。比如,相对为每一个连接进行再次认证,保持一个会话并不管连接状态如何每隔几个小时刷新更好。重连时,仅用已存在的会话主键。这种方案在认证服务器上的负载更加稳定并且短暂的网络异常和相关事件发生时也不会因此而产生登陆风暴。

发布周期和测试(Release Cycle and Testing)

生产环境的测试是个事实并且需要成为可扩展互联网服务质量保证的一部分。大部分服务至少有一个测试实验环境,它和生产环境尽可能的相似,所有好的工程师团队用生产环境的工作负载来让测试系统更加真实。我们的经验是,测试实验环境无论如何的好,也无法完全保真。它们总是和生产环境有细微的差别。当测试环境在保真度上接近生产环境时,会快速接近生产环境的成本。

相反地我们建议这样做新服务的发布,做标准的单元测试、功能测试和生产测试环境测试,然后到受限的生产环境做最后的测试步骤。很清楚的是我们不想软件到了生产环境还不能工作或者有数据风险,所以必须很小心的处理。接下来的是必须要遵守的原则: - 生产系统必须做充分的冗余,如果新服务失败导致灾难性的事故,状态能很快恢复, - 数据问题或者状态相关的失败不会极度失控(功能测试必须首先要通过), - 错误必须能够被检测到并且工程师团队(而不是运维)必须能够在测试中监控代码的系统状况,并且 - 所有的变更都必须能够很快回滚并且回滚必须在进到生产环境之前做充分的测试。

这听起来很危险。但是我们发现使用这样的技术在新服务发布时实际上能提升客户体验。相比尽快部署,我们会把一个系统放到一个单一的数据中心的生产环境中几天。然后我们把新系统部署到每一个数据中心。接着把一些小模块部署到整个数据中心的生产环境。最后,如果质量和性能满足预期,我们再做全局部署。这种方法能够在服务出现风险之前发现问题,并且能够通过版本迁移给客户提供更好的体验。大规模的部署是非常危险的。

另外一个反直觉的方法是相对晚上部署来说我们更偏爱白天中午部署。在晚上,出现错误的风险更大。并且,如果在大半夜部署出了问题,只能有很少的工程师来处理。目标是最小化工程师和运维和系统的交互次数,并且特别是正常的工作日之外,既能减少成本并能增强质量。

发布周期和测试的最佳实践包括:

  • 经常发布(Ship often)。直觉上会想发布越频繁越困难并且错误越多。然而我们发现,越频繁的发布越少重大变更。相应地,发布质量会更高并且客户体验会好很多。一次好发布的糟糕测试是用户体验也许已被改变但是在发布周期内围绕可用性和延迟的运维问题的数量却没改变。我们喜欢为期3个月的发布,但是对其它时间表来说可能会存在争议。我们从心底的感受是基准最终会小于3个月,并且很多服务的发布是以周为单位。超过3个月的周期是危险的。
  • 使用生产数据发现问题(Use production data to find problems)。在大规模可扩展系统中质量保证是数据挖掘和可视化问题,不是一个测试问题。每一个人都需要聚焦在如何发挥生产环境中数据的价值上。几个策略如下:
    • 可测量的发布标准(Measurable release criteria)。为想要的用户体验定义明确的标准,并持续监控它。假设可用性是99%,测量可用性到这个目标。如果在这之下,报警并诊断它。
    • 根据实际调整目标(Tune goal in real time)。相比陷入决定目标应该是99%或者是99.9%或者是任何其它目标的泥潭中,不如设置一个可接受的目标然后提升生产环境中系统的稳定性。
    • 总是收集真实的数字(Always collect the actual numbers)。收集真实的指标胜于红绿或者汇总报表。汇总报表和图表有用但更需要原始数据来做诊断。
    • 最小化误报(Minimize false positives)。当数据不正确时人们会很快停止注意。重要的是不滥报警否则运维人员学者忽略它。附带的损害经常被接受的话会隐藏真实问题,这点很重要。
    • 分析趋势(Analyze trends)。这能用来做问题预测。比如,当数据在系统中的移动和通常的比例不一样时,它通常预示着一个大问题。这归功于有用的数据。
    • 让系统健康高度可见(Make the system health highly visiable)。对整个组织来说,需要一个全局可见、实时显示的服务健康状况。有一个内部网站的话人们能够在任何时间了解当前的服务状况。
    • 持续监控(Monitor continuously)。人们必须每天看所有数据这并没有什么负担。每个人都应该这么做,但是最好明确地让团队中的一部分人做个事情。
  • 投资工程师(Invest in engeering)。好的工程师会最小化运维需求并在它们真正变成运维问题之前解决它们。太常见的事情是,组织增加运维处理可扩展的问题但从来没有花费时间建立一个可扩展、可靠的工程师团队。启动前没有充分思考的服务会有后顾之忧。
  • 支持版本回滚(Support version roll-back)。版本回滚是强制的并且在回滚前必须经过测试和验证。没有回滚,任何生产级别的测试都是非常高危的。回滚到之前的版本就像降落伞的开伞锁,应该总是在任何部署的时候都是可用的。
  • 保持向前和向后兼容(Maintain forward and backward compatibility)。这个要点和上面一个强烈相关。改变文件格式,接口,日志/调试,手段,监控和组件间的联接点都是潜在的风险。不要打破对旧文件格式的支持,直到将来没有机会会回滚到旧的文件格式。
  • 单服务器部署(Single-server deployment)。这既是测试也开发需求。整个服务必须很容易托管在一个单一系统中。一些组件(比如,外部非单点部署服务的依赖)做单服务器部署是不可能的,写一个模拟器以便做单服务器测试。如果没有这,单元测试会很困难并且无法触发所有。并且如果运行完整系统困难的话,开发人员会倾向于组件视角而不是全局视角看问题。
  • 压力测试(Stress test for load)。让生产系统中的一小部分运行两倍或以上的负载以确保系统行为比预期的更高时也能正常运行并且在系统负载增高时不会当机。
  • 在发布前做容量和性能测试(Perform capacity and performance testing prior to new releases)。既然负载特征会随时间变化,在服务级别和每个组件做容量和性能测试。问题和系统降级需要提前捕获。
  • 灰度和迭代式构建和部署(Build and deploy shallowly and iteratively)。在部署环节中尽早让整体服务的骨干版本可用。这个完整服务也许根本做不了什么并且也许包含显而易见的问题,但是它允许测试和开发人员提早进入生产状态,并让整体团队一开始就在用户级别思考问题。构建任何软件系统这都是一个好的实践,但是对服务更加重要。
  • 使用真实数据测试(Test with real data)。从生产环境克隆用户请求或者工作负载。从生产环境提取数据并把它放到测试环境。生产环境广泛多样的数据对于发现Bug更有创造性。很明显地,要承诺隐私的保护,因此这个数据永远不要回流到生产系统是至关重要的。
  • 运行系统级别验收测试(Run system-level acceptance tests)。本地测试提供了基本检查这会加速迭代部署。为了避免过重的维护成本他们仍然应该在系统级别做测试。
  • 在完整环境做测试和开发(Test and develop in full environments)。放置适当比例的硬件做测试。特别重要的是,使用同一数据集和生产环境中的数据挖掘技术以最大化投资。

硬件选择和标准化(Hardware Selection and Standardization)

对SKU通常的讨论是大量采购能够节省可观的费用。这毋庸置疑。需要的标准化硬件越多,服务部署越快成长越快。如果每一个服务都购买私有的基础设施,那么每个服务不得不: - 确定哪个硬件当前最具性价比, - 下单,并 - 一但硬件在数据中心安装完毕,做资产登记和软件部署

这通常要花一个月的时间并且很容易会更多。

比较好的办法是“服务打包”,包含少量的硬件SKU和自动管理以及基础设施,在这上面所有运行所有的服务。如果测试集群需要更多的机器,通过一个Web服务请求并快速可用。如果一个小服务越成功,新资源越能够从存在的池子里增加。这个办法保证了两个重要原则: - 所有的服务,即使比较小的,使用自动化管理和基础设施,并且 - 新服务能够很快地多地测试和部署。

硬件选择的最佳实践包括:

  • 只用标准SKU(Use only standard SKUs)。生产环境拥有单个或者少量的SKU能够让资源在服务间按需流动。大多数最花成本的模式是开发一个标准的服务托管框架,包括自动化管理和供给,硬件和标准的一组共享服务。标准SKU是达成该目标的核心需求。
  • 购买完整的机架(Purchase full racks)。购买完整配置和测试过的机架或者多个大块机架。机架和机柜成本在大多数数据中心都是不可能思议地贵,因此让系统制造商做这个事情并能扩展。
  • 写到硬件抽象层(Write to a handware abstraction)。在一个抽象硬件描述层编写服务。相对完全暴露的硬件SKU,服务既不应该暴露SKU也不应该依赖它的具体信息。这允许2路4块硬盘的SKU以更好的性价比随时间而升级。SKU应该是一个虚拟描述,包括CPU和磁盘数量,和一个内存的最低限度。关于SKU的细粒度信息都不应该被暴露。
  • 抽象网络和命名(Abstract the network and naming)。尽可能快地抽象网络和命名,使用DNS和CNAME。始终使用CNAME。硬件会中断,过期,和另作它用。在代码的任何部分永远都不要依赖机器名。改变DNS中的CNAME比改变文件,甚至更糟糕的,生成环境的代码都要容易的多。如果你需要避免刷新DNS缓存,记得设置TTL足够低,以确保变更尽可能的被推送过去。

运维和容量规划(Operations and Capacity Planning)

高效运维服务的关键是构建系统时消除大多数运维管理的相互影响。目标应该是24*7 高可用的服务应该由一个小规模的8*5的运维团队来运维。

然而,非正常的故障会发生并且当系统或一组系统不能被在线回退时会加倍。理解这种可能性,让程序自动地把损坏的系统移出。依赖运维人工地更新SQL表或者用特定技术迁移数据是个现场灾难。错误会在关键时刻产生。预先想好这些补救措施,运维团队需要先制定、编写和测试这些程序。总的来说,开发团队自动化紧急恢复措施并必须经过测试。很明显不是所有的问题都能被预料到,但是典型地一小组恢复措施能被用来恢复大部分的问题。重要的,构建和测试能够在不同情况下使用和组合的“恢复内核”依赖于灾难的范围和本质情况。

恢复脚本需要在生产环境测试。总的来说如果没有做过不断的测试,团队是不敢使用的。如果在生产环境测试太危险,说明脚本没有准备好或者对紧急使用不够安全。这里的关键问题是灾难发生并且一个小灾难多大程度上会变成没有按照预期执行恢复步骤的大灾难是令人惊奇的。预料这些事件并让工程师做自动化措施去让服务没有数据丢失或占用过多时间地重新上线。

  • 让开发团队负责(Make the development team responsible)。Amazon的口号是“谁构建谁管理”,他们也许是最典型的代表。这个立场也许比我们要采取的都要稍强一点,但是这很明显是总体正确的方向。如果开发不断在半夜被叫醒,自动化很可能就是最后的结果。如果运维被不断叫醒,很可能的结果是运维团队的扩充。
  • 只做逻辑删除(Soft delete only)。永远不要删除任何东西。仅做删除标记。当新数据进来时,记录这个请求。保持两周或更长时间的可滚动的完整变更历史,能够帮助从软件或管理错误中恢复。如果有人犯错并忘记什么原因做的删除语句(之前已经发生过并且将来还会发生),数据的所有逻辑备份被删除。不管是RAID或者镜像都无法阻止这种错误。数据恢复能力让高度麻烦的问题和几乎不明显的小错误有所有区别。对于已经做了离线备份的系统来说,这个额外的数据记录仅在最近一次备份后需要时才进到服务中。但是,要小心的是,我们建议无论如何都要做进一步的备份。
  • 追踪资源分配(Track resource allocation)。了解为了容量规划而增加的负载的成本。每个服务都需要开发一些要用的指标,比如在线并发用户,每秒用户请求,或者其它一些合适的指标。无论是什么指标,都必须是在负载的测量和硬件资源需求间有直接和已知的相关性。预估的负载数字应该来自销售和市场团队以及运维团队的容量规划。不同的服务有不同的变化速度,需要不同的下单周期。我们每90天根据市场预测对服务进行更新,并做容量规划的更新,每30天下单购买设备。
  • 每次只做一次变更(Make one change at a time)。解决问题时,每次只对环境做一次变更。这看起来容易,但是我们看到很多次,当有多个变更时意味着原因和影响可能是不相关的。
  • 一切可配置(Make everything configurable)。生产环境中任何变更应该是可配置的并且不需要代码变更就能够可调整的。即使是为什么要做生产环境中值的变更没有好的理由,尽可能让变更变得容易。这些不应该在将来的生产环境中被改变,并且应该用准备为生产环境提供的配置做彻底的系统测试。但是,当一个生产问题来临时,做一个简单的配置变更总是比编码、编译、测试和部署代码变更要容易、安全和快捷。

审计,监控和报警(Auditing,Monitoring and Alerting)

运维团队在部署中无法改变一个服务。在开发时要足够努力以确保系统中的每一个组件都能产生性能数据,健康数据,吞吐量数据等。

任何时间,配置变更,确切的变更,谁做的,什么时间做的,都需要记到审核日志中。当出现生产问题,首先问一下最近做了什么变更。没有配置审计踪迹,答案总是“没有东西”变更,并且它几乎总是忘记做了什么变更是导致问题的根源。

报警是门艺术。任何事件都报警是一个趋势,开发期望他们可以发现有趣和经常产生从来没有看过的无用报警的服务。为了更加高效,每一个报警都要代表一个问题。否则,运维团队会忽略它们。除了动态地调整报警条件以确保所有高危事件能报警并且没事就不会报警,我们不知道任何其它的让报警正确的诀窍。为了让报警级别正确,两个指标能起到帮助并且值得追踪: - 报警和故障的比率(目标是接近1),并且 - 系统健康问题没有相应报警的数目(目标是接近0)。

最佳实践包括:

  • 一切仪表化(Instrument everything)。评测经过系统的每一个客户的交互或交易并报告反常。这有个“运行者”(模拟和生产中一个服务做用户交互的人造工作负载)的地方,但是他们不够充分。单独使用运行者,我们看到它花费了几天最终注意到一个严重的问题,自从标准运行者工作负载继续处理的很好之后,之后更多天后知道了为什么。
  • 数据是最重要的资产(Data is the most valuable asset)。如果正常的操作行为不能很好的被理解,不正常时很难做出响应。系统中发生了什么的大量数据需要被聚集起来了解它真地工作正常。许多服务已经出现灾难性的问题,然后当电话开始响起时才知道。
  • 以客户视角看服务(Have a customer view of service)。执行端到端的测试。运行者不足,但是他们需要确保服务是完整地工作。确保复杂和重要的路径比如像新用户登录能够被运行者测到。避免误报。如果一个运行者失败并不重要,改变测试到运行正常的。并且,一但人们变得习惯了忽略数据,损坏不会被立即注意。
  • 仪表化生产测试(Instrument for production testing)。为在生产环境中安全地测试,完善的监控和报警是必须的。如果一个组件有问题,需要能够快速检测到。
  • 延迟是最严重的问题(Latencies are the toughest problem)。例子是I/O慢和不完全失败但是处理缓慢。这些很难发现,因此小心地仪表化以确保它们能被检测到。
  • 拥有足够的生产数据(Have sufficient production data)。为了发现问题,数据必须可靠。在早期或者后面开始变得最贵时做细粒度的监控。我们依赖的最重要的数据包括:

    • 为所有操作使用性能计数器(Use performance counters for all operations)。至少要记录操作延迟和每秒的操作数量。这些值的警告是一个巨大的红色警示标识。
    • 对所有操作进行审计(Audit all operations)。每次有人做事,尤其是一些重要的事情,都做日志记录。这为两个目的服务:首先,能够挖掘日志发现用户做的事情的种类(在我们的案例中,是指他们做的里查询类型)。第二,有助于问题发现时的调试。一个相关的观点:如果每个人都使用同一个帐号管理系统这就不够好。一个非常糟糕的主意但并不罕见。
    • 追踪所有容错机制(Track all fault tolerance mechanisms)。对每个重要的特定的实体都做审计日志,形成一个或多个文档。当运行数据分析时,能够容易发现数据中的反常。知道数据来自哪里,它经过了什么处理。在项目的后面再增加会特别困难。
    • 断言(Asserts)。自由地使用断言并贯穿产品。收集结果日志或崩溃信息并调查它们。对于系统,在同一进程边界内运行不同的服务,无法做断言,写下追踪记录。无论如何实现,能够标记问题并不断挖掘不同的问题。
  • 可配置的日志(Configurable logging)。支持可配置的日志能够在需要调试问题时选择打开或关闭。为了额外的监控而不得不做新的构建部署非常危险。

  • 为监控暴露健康信息(Expose health information for monitoring)。思考外部监控服务健康的途径并让它在生产环境中容易被监控。

  • 让所有报错可处理(Make all reported errors actionable)。问题会发生。事情会被打破。如果代码中一个不可恢复的错误被检测到并记录或者报出,错误信息应该指出错误可能的原因并给出修正的建议方案。不能处理的错误报告没有用,并且随着时间过去他们会被忽略而真正的问题会错过。

  • 能够快速诊断线上问题(Enable quick diagnosis of production problems)

    • 给诊断足够的信息(Give enough information to diagnose)。当问题被标识后,给诊断的足够的信息。否则进入的门槛会太高而标识被会忽略。比如,不要仅仅说“10个查询没有返回结果”,增加“这里有列表以及他们发生的时间”。
    • 证据链(Chain of evidence)。确保从头到尾都有一条开发诊断问题的路径。这典型地由日志来做。
    • 在线调试(Debugging in production)。我们喜欢这样的模式:系统几乎没有被任何人接触过,包括运维人员,并且已经做了快照,dump内存,移到生产环境外。当在线调试是唯一的选择,开发人员是最好 的选择。确保他们被培训过,知道线上服务器哪些是被允许的。我们的经验是线上系统被接触的越少,客户通常更高兴。因此我们建议努力工作尽量不要碰线上生产系统。
    • 记录所有重要的动作(Record all significant actions)。每次系统做一些重要事情,尤其是像网络请求或者修改数据,记录发生了什么。这包括用户何时发了一个命令以及系统内部做了什么。有了这个记录在调试问题时极其有帮助。甚至更重要的是,能够构建挖掘工具发现有用的东西,比如,用户正在使用的查询类型(比如,哪个关键词,多少个关键词等)。

优雅降级和准入控制(Granceful Degradation and Admission Control)

当发生DOS攻击或者使用模式的一些改变时会导致工作负载的突然抖动。服务需要能够优雅降级和控制准入。比如,911期间大部分新闻网站都当掉而不能为任何用户提供可用的服务。可靠地传递一部分文章也许是更好的选择。两个最佳实践,一个“大红开关”和准入控制,需要被每个服务所定制。但是他们非常强大和必要。

  • 支持“大红开关”(Support a “big red switch”)。“大红开关”的主意来自Windows Live Search并且它有很多权力。我们稍微概括地说,更多的交易服务和搜索是显著不同的。但是这个主意非常强大并且适合任何地方。总的来说,“大红开关”是一个设计和测试过的动作,一但服务不再到达到或者接近SLA的要求就会触发。把优雅降级叫作“大红开关”是有点让人迷惑的术语,但是代表的意思是紧急情况下区分非紧急情况的能力。当区分或者延迟非紧急工作负载,大红开关的概念保留了重要的处理进步。按照设计,这不应该发生,但是当真发生时作为救援是好的。当服务十万火急时试着推测这些是危险的。如果有些负载能被队列化以后再处理,它就是大红开关的候选。当不允许高级查询时可能继续运行交易系统,这也是一个候选。最重要的事情是决定,如果系统出现问题时什么是最小化的要求,实现和测试关闭非重要服务的选择。注意一个正确的大红开关是可逆转的。重设开关应该被测试以确认完整的服务恢复正常,包括所有批量任务和之前其它被挂起的非重要工作。
  • 控制准入(Control admission)。第二重要的概念是准入控制。如果当前的负载系统已无法处理,给它更多的负载只会让更大范围内的用户得到一个糟糕的体验。怎么做到这一点依赖于系统和更容易做的一些东西。比如,处理邮件的最后一个服务。如果系统已过载并开始排队,不要接收更多的邮件到系统会更好,并让它在源头排队。这个主要原因是有意义的,并且实际上会消减整体服务的延迟,就像队列所做的那样,我们处理的更慢。如果不用队列,吞吐量会更高。另外一个技术是优先服务重点客户,或者用户而不是访客,或者访客而不是用户如果“试用”是商业模式的一部分。
  • 计量准入(Meter admission)。下一个至关重要的概念是上面所做的准入控制点的修改。如果系统失败并当掉,要能够慢慢地恢复以保证所有是正常的。它必须能让一个用户进来,然后让每秒10个用户进来,并慢慢恢复正常。当回到线上或者从灾难性故障恢复时,每个服务都应该有一个细粒度的抓手去慢慢恢复到可用,这非常重要。在服务接收客户端的地方,服务必须要能通知客户端它当掉了,什么时候可以起来。这允许客户端如果适当可以继续操作本地数据,让客户端回退,服务能更容易地恢复上线。这也给服务的所有者直接和用户进行沟通的机会并控制他们的期望。另一个能被用来阻止他们都同步冲击服务器的客户端检票机制被引入到有意的防范和每个实体自动化备份。

客户和出版沟通计划(Customer and Press Communication Plan)

系统失败,并且成倍延迟或者有其它问题时必须要和客户沟通。沟通应该通过各种渠道做有效沟通:RSS,网站,即时消息,email等。对于有客户的服务,服务能够具备和客户端沟通的能力时会非常有用。客户端能被请求回退到某个指定时间或一段时间。客户端能被请求在断开连接或缓存模式下运行,如果它支持的话。客户能给用户显示系统状态,如果人们知道发生了什么并且何时完整的功能会再次恢复正常。

即使没有客户端,比如用户和系统通过页面交互,系统状态仍能和他们沟通。如果用户理解发生了什么并且对服务何时被恢复有一个合理的期望,满意度会更高。对服务的所有者来说很自然的倾向是掩盖问题,但经过一段时间,我们确信让客户智能服务的真实状态总是会提供客户满意度。即使非付费的系统,如果人们知道发生了什么并且何时恢复,他们会更少出现抛弃服务的可能。

一些特定的类型会带来压力。如果准备的标准更高一点服务会表现的更好。像大块数据的丢失或损坏,安全违规,侵犯隐私,服务长时间当机都会带来压力。马上做一个沟通计划。知道呼叫谁、什么时间、怎么直接呼叫。沟通计划的主干应该已经制定好。每一种灾难都应该相应的方案,找谁,何时去找,怎么处理沟通。

客户自给和自助(Customer Self-Provisioning and Self-Help)

客户自给能显著地减少成本并能增加客户满意度。如果客户能去到网站,键入需要的数据就能开始使用服务,会比让他们浪费在呼叫队列的等待上更让他们高兴。我们一直感到主要的电话运营商丧失了节省和提升客户体验的机会,因为他们不允许给那些不想呼叫客户支持团队的人开通自助服务。

结论(Conclusion)

减少运维成本并提升大规模可扩展系统的服务可用性从写运维友好的服务开始。在本文中,我们定义了高可扩展服务的运维友好并且总结了服务设计、开发、部署和运维的最佳实践。

yikebocai /

Published under (CC) BY-NC-SA in categories tech  tagged with architecture