201706 孙鸣老师关于《On Designing and Deploying Internet Scale Services》的导读 - xiaoxianfaye/Learning GitHub Wiki

孙鸣老师 导读 2017.6 陈雅菲 整理 2017.6

Preface

接下来的一段时间,我会带着大家一起来读James Hamilton的《On Designing and Deploying Internet-Scale Services》,这篇论文可以说是非常经典,蕴含了作者的真知灼见。 原文:http://mvdirona.com/jrh/TalksAndPapers/JamesRH_Lisa.pdf, 在导读过程中,大家可以参照。

先来说说作者James Hamilton,一个传奇的人物,一个开了挂的人生。他的履历表列出来,丰富得就像咱们双11的购物清单。他目前是亚马逊AWS的VP和杰出工程师,专注于基础设施的效率、可靠性和可伸缩性。在加入AWS之前,任职微软,众多微软产品都打下了他的烙印,是微软未来数据中心团队的架构师,在此之前,是WindowsLive平台服务团队的架构师,这篇文章写成时,他还服务于这个团队。再往前,他负责管理Microsoft EHS团队,EHS之前,服务于SQL Server团队,SQL Server团队之前,他是Windows NT操作系统组的架构师....,点睛之笔是,在70年代末和80年代的早期,他曾是兰博基尼和法拉利的专业汽修工,Cool!!

Overview

文章总结的是如何设计与部署互联网络规模服务的一系列经验和最佳实践,所以,在摘要中,他首先提出了一个可以粗略度量大规模服务管理开销的标准--“系统:管理员”比例。对于那些自动化程度不高或者鲜有自动化的服务来说,这个比率可能低至2:1,而对于那些业界领先,高度自动化的服务来说,该比率可高达2500:1。所以,Autopilot(微软的一个自动化服务)通常被认为是Windows Live Search团队得以达成高“系统管理员”比例背后的魔法。

但作者同时也认为,自动化管理固然重要,但更为重要的还是服务本身。服务能否高效地自动化?是否运维友好?运维友好的服务几乎不需要人工干预,除了极个别的故障,其余的都无需管理员的干预就能被自动检测到并恢复。本文就总结了过去20年在MSN和Windows Live这些超大型服务运营多年积累下来的最佳实践。

在文章的导引中,作者提到,大规模服务的设计和部署目前仍是一个不断演化的领域,因此,文中的最佳实践列表随着时间的推移也会不断地演化。列出这些实践的目的是:

  1. 快速交付运维友好的服务。
  2. 避免凌晨收到短信和电话的骚扰、还有客户永远的抱怨。

Three Basic Principles

文中的很多最佳实践由Bill Hoffman提出,他同时也给出了三个在系统设计之初就需要考虑的三条基本原则,这三条原则将贯穿文后大多数讨论的主线。

  1. Expect failures:故障随时会发生,组件和它依赖的组件都可能随时挂掉,网络故障,磁盘空间耗尽等也会发生,要能优雅地处理它们。(应该要极力避免软件世界中惯常的思维方式:“这怎么可能?”,它会阻碍我们正确地判断和解决问题。)

  2. Keep things simple:保持简单,复杂易滋生问题。简单的事情才容易做正确;避免不必要的依赖;安装要简单;单台服务器上的故障不要影响集群的其它部分。

  3. Automate everything:自动化一切,人会犯错;人需要睡觉;人会忘记东西。而自动化的过程是可测试的,确定的,因而也最可靠。尽可能全部自动化。

Ten sub-sections

文章从以下十个方面来阐述运维友好的服务在设计和部署时要遵循和考虑的要求:

  1. 服务的总体设计
  2. 面向自动化和指配的设计
  3. 依赖管理
  4. 发布周期和测试
  5. 硬件选型和标准化
  6. 运维和容量规划
  7. 审计、监控和报警
  8. 优雅降级和准入控制
  9. 客户和媒体沟通计划
  10. 客户自配置和自助

Overall Application Design (总体设计)

我们都知道,80%的运维问题源自设计和开发。因此服务总体设计这一节是文章篇幅最多,也最重要的一节。故障发生时,会首先审视运维工作,因为这是问题实际产生的地方。但是,绝大多数的运维问题都可以归因为设计或开发,或者比较适合在设计和开发阶段解决。而且,我们也希望能够达成共识:在服务领域,严格地区分开发、测试和运维不是最有效的方式,它们之间应该紧密相关。

接下来,作者列出来了一些对服务整体设计影响最大的基本原则。

  1. Design for failure,面向错误设计,这是构建大型服务时最核心的观念。

组件会会频繁发生故障;一旦服务规模超过10,000台服务器和50,000个磁盘后,一天内都会发生多次故障;如果每个硬件故障的发生都需要紧急人工干预,何谈低成本和可靠的服务伸缩;所以,整个服务要具备自愈能力。

故障恢复的步骤必须简单明了,并且要对该步骤频繁测试。Stanford的Armando Fox认为测试故障恢复最好的方法就是从不去正常地停掉服务,而是粗暴地让它挂掉。这听起来违反直觉,但如果故障恢复不被频繁使用和测试的话,真的需要它时,它也派不上啥用场。看到这段,有没有想到我们是怎样小心翼翼地对待我们的系统,下次,请大胆些吧,这样才能让它变得更robust。

  1. Redundancy and fault recovery,冗余与故障恢复。

尽管大型机具有冗余的电源、热插拔和CPU、独特的总线结构,并且提供了令人惊讶的IO吞吐量。但它也做不到足够可靠。要达到5个9的可靠性,冗余是必需的。即便只是4个9,单系统的部署也难以企及。

要设计一个任何组件随时都可以挂掉,但仍能满足相应SLA(Service Level Agreement)的服务,需要细致的工程化。我们可以用这个方法来做自检:运维团队是否愿意并且能够随时关掉任何一个服务器,而不用等待该服务器上的工作负载全部完成?如果可以,就说明服务实现了同步化冗余,故障检测和自动的故障恢复。

文中提到一种设计方法--安全威胁模型,通常用来发现和纠正服务的潜在安全问题。在安全威胁模型里,我们考虑每一种可能的安全威胁,并且针对性地给出解决方案。

列举出所有可能的组件故障模式,及其组合;对每一种故障,给出解决方案,确保服务质量仍能接受,又或者确定该故障的风险对这个服务是可接受的(比如,没有地理冗余服务的整个数据中心挂掉了)。

特别值得注意的是,对于一些非同寻常的故障组合,我们出于成本的考虑,会认为它们是不会发生的。但是,做出这种决定一定要非常谨慎,这些罕见的故障组合,在成千上万的服务器集群中,每天的组件故障数以百万计时,它们的出现会变得司空见惯。

  1. 所有组件都应当基于普通硬件,文中举例,一个轻量级存储的服务器配置可能是:一个双插槽1000-2500美元的单磁盘,2-4核;而一个重量级存储的服务器,配置应该和它差不多,只是会有16到24个磁盘。

之所以这么做,关键的考虑点有:

  • 大量廉价服务器组成的大规模集群要比少量大型服务器组成的便宜很多;
  • 服务器性能增长速度比IO性能增长速度快很多。对于给定容量的磁盘,小型服务器足以满足性能需求;
  • 耗电量跟服务器数量是线性关系,跟时钟频率是三次方关系,所以高性能服务器的运营成本更高。
  1. 单一版本软件:一些服务,之所以比绝大多数打包发行的软件开发成本更低,演化更快,是因为:
  • 只有一个单一的内部部署环境;
  • 之前的版本无需像面向企业的产品那样一支持就是十几年。

出于以上的考虑,最经济的服务不会将运行版本的控制权交给客户,并且只会保有一个版本。这种单版本的软件要考虑:

  • 不同版本的发布,用户体验变更不宜太大;
  • 对于那些需要版本控制的客户来说,支持内部部署,或者切换到愿意提供多版本支持的服务商。

单一版本软件要满足客户需求相对容易,尤其是免费提供。让企业客户去影响软件提供商并且对新版本的部署拥有完全的控制,都会极大地抬高了运维成本和支持成本。

我相信,看到单版本这一点,定会引起很大的争议,我的观点是,在软件世界中,“不可能”的事情也许(Faye:少了一个“不”字?)是不可能的,单版本应该是我们的目标,可能目前无法做到,那少量、可控版本是否可行呢?总之,不轻易排除任何可能性。

  1. Multi-tenancy 多租户是指不通过物理隔离的方式在同一个服务里容纳所有的公司或者终端用户,与之相比单租户则是指将用户进行分组放到隔离的集群中。多租户的理由几乎与单一版本软件相同,同时也可以为建立在自动化基础上的大规模服务从根本上降低成本。

以上就是服务总体设计方面的五条原则,可以看到,通过对服务设计和运维模型加以限制,可以最大化我们构建自动化和低成本的服务的能力。同时,也可以看到以上列出的这些目标和一些应用服务提供商或者IT外包商的目标的明显不同:那些企业通常是人力密集型的,更愿意运行复杂的,由客户定制的配置。

接下来就是在这些原则指导下的一些具体的,可以操作的实践。

  1. 快速服务健康检查

构建验证测试,可以运行在开发人员的系统环境上,确保服务没有遭受实质性的破坏。不要求所有的边界条件都会被测试到,但如果这个测试通过了,代码可以CI。

  1. 在完整的环境里开发

对自己开发的组件,开发人员除了进行单元测试,也需要将变更合入整个环境去验证整个服务。达成这个目标需要支持单服务器的部署以及前面提到的快速服务健康检查。

  1. 对依赖的组件零信任

要认为依赖组件会挂掉,同时要确保组件能恢复并继续提供服务。恢复技术是服务相关的,但是也有一些常用的技术:

  • 以只读模式操作缓存数据;
  • 对绝大多数的用户继续提供服务,除了那些受失败组件影响的一小部分用户。
  1. 不在多个组件里构件同样的功能

服务的增长和演化非常迅速。一不小心,代码库会迅速恶化。

  1. 不同集群和Pod尽量不互相影响

大部分服务都是由系统中的子集群相互协作而成的。集群要尽可能100%独立,避免关联故障。那些全局性的服务即使具有冗余备份也是一个单点,所以有时候这种情况可能无法避免,但还是要尽可能把一个集群依赖的东西都放到该集群内部。

  1. 允许(极少情况下的)紧急人工干预

比较常见的场景是灾害后或其他紧急情况下的用户数据迁移。将系统设计成永不需人工干预,但也确实存在一些组合故障或者无法料及的故障发生需要人工干预。需要干预的情况要确定预案,预案不能步骤繁琐、易错;要尽可能写成脚本;还要频繁演习,形成惯性。

  1. 保持简单和健壮

复杂的算法和组件交互会加大调试和部署的困难;一个总的原则是:超过一个数量级的改进才值得考虑,只有几个百分点甚至更少的改进是不值得去做的。

  1. 在所有层执行准入控制

所有的好系统都会在入口处有准入控制。这遵循了一个长期大家都耳熟能详的原则:与其继续接受新任务引起系统震荡,最好是不要让更多的任务进入过载的系统。所以,在服务入口处通常都有某种形式的流控或准入控制,但其实在所有重要组件的边界上也都应该有准入控制。总的原则是尝试优雅降级而不是让系统直接挂掉,在所有用户都受到影响前阻止其进入。关于overload--过载处理,在前面一篇论文的导读中已经做过全面的阐述。

  1. 对服务进行分区

分区应该是细粒度的,无限可调的,并且不绑定在任何现实世界的实体上(比如人,群体等),因为这样的方式在实践中都被证明不具伸缩性。作者推荐的方法是用一个中间层的查找表将细粒度实体(如用户)映射到管理他们数据的系统上。这些细粒度分区就可以在服务器间自由移动了。目前用的比较多的是consistent hashing算法。

  1. 理解网络设计。

尽早开展测试以便了解机架内、跨机架、跨数据中心这些不同的场景下,机器间的负载是什么情况。应用开发人员必须了解网络的设计,提早与运维团队里的网络专家们一起进行评审。

  1. 分析吞吐率和延迟

要对核心服务用户交互的吞吐量和延迟进行分析,这些数据的收集要和其他运维数据的收集一样被常规化。对每个服务来说,需要的度量要能反映容量规划,比如系统的每秒用户请求数、系并发在线用户数、或者是某些可以将工作负载映射到资源需求的度量值

  1. 把运维工具作为服务的一部分

运维工具和产品代码同样重要,都要review,checkin到代码主干,并切和代码一起进行跟踪、维护和测试。其实,运维的代码要求甚至要高于产品代码,这个高主要指语义要严格、准确,最好能通过形式化的方法进行验证。

  1. 理解访问模式

规划新feature时,一定要考虑它会给后端存储带来怎样的负载。考虑到服务模型所在的抽象层次距离底层存储太远,所以,很容易忽略新feature的负载给底层数据库带来的影响。一个最佳实践是给该feature的SPEC中加上一节:“这个feature对系统其它部分有什么影响?” 在feature上线时度量和验证。

  1. 一切东西版本化

要假设系统是运行在一个多版本混合的环境里。目标是运行单一版本软件,但在试运行和生产测试时会有多版本共存。所有组件的n和n+1版本都要能和平共处。

  1. 保留上次发布的UT和FT

这些测试是用来验证前一个版本的功能没被破坏的重要手段。更进一步地,作者强烈推荐在生产环境持续运行验证测试。

  1. 避免单点故障

单点故障会导致故障发生时服务或服务的多个部分不可用。无状态的实现是优先采用的方式。不采用将请求或用户指定给特定服务器的方式,而是要能在一组服务器间负载均衡。静态hash或者任何静态的服务器分配方式,随着时间的流逝都会面临数据或查询的倾斜。如果同一级别的机器都可互换,就可以很容易地进行水平扩展。数据库通常都是一个单点,所以在做互联网规模服务的设计时,它的伸缩依然是最困难的问题。好的设计会使用细粒度分区,并禁止跨分区操作从而能够跨多台数据库服务器进行伸缩。所有的数据库状态都要冗余存储到全冗余的热备服务器上(至少一个),同时要经常在产品环境中进行failover测试。

⚠️ **GitHub.com Fallback** ⚠️