面向年轻一代的分布式系统笔记(2013)

2020-05-31 22:27:10

我一直在思考分布式系统工程师在工作中学到的教训。我们的很多指示都是通过在生产交通中犯下的错误留下的伤疤。当然,这些伤疤是有用的提醒,但最好是有更多的工程师对他们的手指了如指掌。

新的系统工程师将发现分布式计算的缺陷和CAP定理作为他们自我教育的一部分。但这些都是抽象的东西,没有经验不足的工程师开始行动所需的直接、可行的建议。令人惊讶的是,新工程师在开始行动时只得到了很少的背景信息。

以下是我作为一名分布式系统工程师学到的一些经验教训,值得告诉新工程师。有些是微妙的,有些是令人振奋的,但没有一个是有争议的。这份清单是为新上任的分布式系统工程师提供的,以指导他们对所从事领域的思考。虽然不全面,但这是一个良好的开端。

这份清单最糟糕的特点是集中在技术问题上,很少讨论工程师可能遇到的社会问题。由于分布式系统需要更多的机器和更多的资金,它们的工程师倾向于与更多的团队和更大的组织合作。社交工作通常是任何软件开发人员工作中最困难的部分,也许在分布式系统开发中尤其如此。

我们的背景、教育和经验使我们倾向于技术解决方案,即使社交解决方案会更有效、更令人愉快。我们一定要改正这一点。人们没有计算机那么挑剔,即使他们的界面没有那么标准化。

#分布式系统不同,因为它们经常出现故障。当被问及分布式系统与其他软件工程领域的区别时,这位新工程师经常提到延迟,认为这就是分布式计算困难的原因。

但他们错了。分布式系统工程的不同之处在于失败的概率,更糟糕的是,部分失败的概率。如果格式良好的互斥解锁失败并出现错误,我们可以认为该进程不稳定并使其崩溃。但是分布式互斥锁的解锁失败必须内置于锁定协议中。

没有从事过分布式计算的系统工程师会想出这样的想法:“嗯,它只会把写操作发送到两台机器上”,或者“它会一直重试写操作,直到成功”。这些工程师并没有完全接受(尽管他们通常在智力上认识到)联网的系统比只存在于一台机器上的系统故障更多,而且故障往往是局部的,而不是全部的。一次写入可能成功,而另一次失败,那么现在我们如何获得一致的数据视图呢?这些部分失败的原因要难得多。

交换机关闭、垃圾收集暂停使领导者“消失”、套接字写入在另一台机器上看似成功但实际上已失败、一台机器上的磁盘驱动器过低会导致整个群集中的通信协议爬行,依此类推。从本地存储器读取比跨几个交换机读取更稳定。

#编写健壮的分布式系统比编写健壮的单机系统成本更高。创建健壮的分布式解决方案比创建单机解决方案需要更多的资金,因为有一些故障只在许多机器上才会发生。虚拟机和云技术使分布式系统工程变得更便宜,但不如能够在您已经拥有的计算机上设计、实现和测试那么便宜。而且有一些故障情况很难在一台机器上复制。无论是因为它们只出现在比共享机器所能容纳的数据集大得多的数据集上,还是在数据中心发现的网络条件下,分布式系统往往需要实际的(而不是模拟的)分布来清除它们的错误。当然,模拟是非常有用的。#。

#健壮的开源分布式系统远不如健壮的单机系统常见。长时间运行多台机器的成本是开源社区的负担。业余爱好者和业余爱好者是开源软件的引擎,他们没有可用的财政资源来探索或修复分布式系统将会遇到的许多问题。业余爱好者利用他们已经拥有的机器,在空闲时间编写开源代码来取乐。很难找到愿意启动、维护和购买大量机器的开源开发人员。

为法人实体工作的工程师已经填补了部分空缺。但是,他们组织的优先事项可能与您组织的优先事项不一致。

虽然开放源码社区中的一些人已经意识到了这个问题,但它还没有得到解决。这太难了。#。

#协调非常困难。尽可能避免协调机器。这通常被描述为“水平可伸缩性”。水平可伸缩性的真正诀窍在于独立性--能够将数据转移到机器上,从而将机器之间的通信和共识保持在最低限度。每当两台机器必须就某件事达成一致时,服务就变得更难实现。信息的传播速度有一个上限,网络交流比你想象的更不稳定,你关于什么构成共识的想法可能是错误的。了解两位将军和拜占庭将军的问题在这里是有用的。(哦,Paxos确实很难实现;这不是脾气暴躁的老工程师认为他们比您更了解。)#。

#如果你能把你的问题放在内存中,那么它可能是微不足道的。对于分布式系统工程师来说,一台机器上的本地问题是很容易的。当数据刚刚切换离开而不是几个指针取消引用时,更难弄清楚如何快速处理数据。在分布式系统中,自计算机科学诞生以来就有文献记载的老套的效率技巧不再适用。对于在一台机器上运行的算法,有大量的文献和实现,因为大多数计算都是在单一的、不协调的机器上完成的。对于分布式系统,存在的数量明显更少。#。

#“它很慢”是你调试过的最难的问题。“它很慢”可能意味着执行用户请求所涉及的一个或多个系统很慢。这可能意味着流水线中的一个或多个部分在许多机器上的转换速度很慢。“它很慢”很难,部分原因是问题陈述没有为缺陷的位置提供太多线索。部分故障潜伏在黑暗的角落里,这些故障不会出现在您通常查找的图表上。而且,在降级变得非常明显之前,您不会收到那么多的资源(时间、金钱和工具)来解决它。达珀和齐普金的诞生是有原因的。#

#在整个系统中实施反压。反压是从服务系统到请求系统的故障信号,以及请求系统如何处理这些故障以防止自身和服务系统过载。背压设计是指在过载和系统故障期间限制资源的使用。这是创建健壮的分布式系统的基本构件之一。

反压力的实现通常包括要么将新消息丢弃在地板上,要么在资源变得有限或发生故障时将错误发回给用户(在这两种情况下都会增加度量)。到其他系统的连接和请求的超时和指数回退也很重要。

如果没有适当的反压机制,可能会出现级联故障或意外的消息丢失。当一个系统无法处理另一个系统的故障时,它往往会向依赖它的另一个系统发出故障。#。

#找到部分可用的方法。部分可用性是指即使系统的某些部分出现故障,也能够返回一些结果。

搜索是在这里探索的理想案例。搜索系统在搜索结果的好坏和让用户等待多长时间之间进行权衡。典型的搜索系统设置了搜索文档的时间限制,如果该时间限制在搜索完所有文档之前过期,它将返回收集到的任何结果。这使得搜索在面对间歇性减速和错误时更容易扩展,因为这些失败被视为无法搜索其所有文档。该系统允许将部分结果返回给用户,并且增强了它的弹性。

并考虑Web应用程序中的私人消息收发功能。在某一时刻,无论您做什么,足够多的私人消息存储机器都会在您的用户注意到的同时停机。那么,我们希望在这个系统中出现什么样的局部故障呢?

这需要考虑一下。人们通常对私人信息为他们(可能还有其他一些用户)关闭更无所谓,而不是对所有用户都刮掉他们的一些信息而感到不安。如果服务超载或其中一台机器出现故障,只出现一小部分用户群的故障比丢失较大部分的数据要好。而且,最重要的是,我们可能不想因为私信出现问题而影响一些不相关的功能,比如公共图片上传。我们愿意做多少工作来保持这些故障域的分离?

能够在部分可用性中识别这些类型的权衡是很好的,您的工具箱中有这样的工具。#

#衡量标准是完成工作的唯一途径。公开度量标准(如延迟百分位数、增加某些操作的计数器、变化率)是跨越您认为的系统在生产中所做的和它实际正在做的事情之间的差距的唯一方法。了解系统在第20天的行为与在第15天的行为有何不同,是成功的工程和失败的萨满教之间的区别。当然,度量标准对于理解问题和行为是必要的,但它们不足以知道下一步要做什么。

把注意力转移到伐木上。日志文件很好用,但它们往往会过时。例如,几个错误类的日志记录通常会占用日志文件中的很大一部分空间,但实际上,只有很小比例的请求会发生。因为记录成功在大多数情况下是冗余的(并且在大多数情况下会炸掉磁盘),并且因为工程师经常错误地猜测哪些类型的错误类是有用的,所以日志文件被各种各样的奇数位和浮点填满了。更喜欢记录Asif如果有人谁没有看到代码将阅读日志。

我看到另一位工程师(或我自己)延长了大量的停机时间,在没有首先对照度量进行检查的情况下过度强调了我们在日志中看到的一些奇怪的东西。我还见过另一位工程师(或我自己)夏洛克-福尔摩斯从几条日志中提取出一整套失败的行为。但请注意:首先,我们记住这些成功是因为它们非常罕见;其次,除非衡量标准或实验支持这个故事,否则你不是夏洛克。#。

#使用百分位数,而不是平均值。百分位数(第50、99、99.9、99.99)比绝大多数分布式系统中的平均值更准确、更具信息量。使用均值假设评估中的度量遵循钟形曲线,但在实践中,这描述了工程师关心的很少的度量。“平均延迟”是一个常见的度量标准,但我从未见过延迟遵循钟形曲线的分布式系统。如果度量不遵循钟形曲线,则平均值毫无意义,并导致错误的决策和理解。用百分位数说话来避开陷阱。默认为百分位数,您将更好地了解用户对您的系统的真实看法。#。

#学会估计自己的能力。因此,您将了解一天中有多少秒。知道你需要多少台机器才能完成一项任务,这是一个持久的系统和一个需要投入3个月的工作的系统之间的不同之处。或者,更糟糕的是,在完成生产之前需要更换。

考虑一下推特吧。在一台普通机器的内存中可以容纳多少个tweet id?嗯,2012年底一台典型的机器有24 GB的内存,操作系统需要4-5 GB的开销,另外至少还有一对夫妇来处理请求,一个tweet id是8个字节。这就是包络计算的后面部分,您会发现自己在做这项工作。杰夫·迪恩(Jeff Dean)的数字Everyone ShouldKnow幻灯片是一个很好的预期设定者。#。

#功能标志是基础设施的铺设方式。“功能标志”是产品工程师在系统中推出新功能的常见方式。功能标志通常与前端A/B测试相关联,在前端A/B测试中,它们用于仅向部分用户展示新的设计或功能。但它们也是更换基础设施的有力途径。

太多的项目失败了,因为它们进行了“大切换”或一系列“大切换”,然后这些“大切换”被发现的错误工具强迫回滚。相反,通过使用功能标志,您将获得对项目的信心,并降低失败的成本。

假设您要从单个数据库转向隐藏新存储解决方案详细信息的服务。使用功能标志,您可以在写入旧数据库的同时缓慢增加对新服务的写入,以确保其写入路径足够正确和快速。在写入路径为100%并且回填到服务的数据存储区完成后,您可以使用单独的功能标志开始从服务读取数据,而无需使用用户响应中的数据,以检查性能问题。另一个特征标志可用于在从旧系统和新系统读取数据时执行比较检查。并且可以使用最后一个标志来缓慢地增加来自新系统的“真实”读数。

通过将部署分解为多个步骤,并为您自己提供快速和部分的特性标志反应,您可以更容易地在升级过程中发现错误和性能问题,而不是在“大爆炸”发布时发现它们。如果出现问题,您只需立即将功能标志设置压回较低(可能为零)的设置即可。调整速率可以让你在不同的流量下进行调试和实验,因为你知道你遇到的任何问题都不是完全的灾难。有了功能标志,您还可以选择其他迁移策略,如基于每个用户移动请求,从而更好地洞察新系统。当您的新服务仍处于原型阶段时,您可以使用较低设置的标志来使您的新系统消耗更少的资源。

现在,对于训练有素的开发人员或受过良好培训的新工程师来说,功能标志听起来就像是一堆糟糕的条件。使用功能标志意味着接受拥有多个版本的基础设施和数据是一种规范,而不是罕见。这是一个深刻的教训。在单机系统中运行良好的东西在分布式问题面前有时会步履蹒跚。

最好将功能标志理解为一种权衡,以牺牲局部复杂性(在代码中,在一个系统中)来换取全局简单性和弹性。#。

#明智地选择id空格。您为系统选择的ID空间将塑造您的系统。

获取数据所需的ID越多,对数据进行分区的选择就越多。获取数据所需的ID越少,使用系统输出就越容易。

考虑一下Twitter API的版本1。获取、创建和删除推文的所有操作都是针对每个推文的单个数字ID完成的。tweet id是一个简单的64位数字,它没有连接到任何其他数据。随着tweet数量的增加,很明显,如果同一用户的所有tweet都存储在同一台机器上,那么创建用户tweet时间轴和其他用户订阅的时间轴就可以有效地构建。

但是公共API要求每个tweet只能由twetid寻址。要按用户划分tweet,必须构造查找服务。知道哪个用户拥有哪个tweet id的人。如果有必要的话,这是可行的,但要付出不小的代价。

另一种API可以在任何推文查找中要求用户ID,并且最初简单地使用推文ID进行存储,直到用户分区的Storagecame在线为止。另一种替代方案将用户ID包括在推特ID本身中,代价是推特ID不再是k排序的和数字的。

注意您的ID中编码的是哪种信息,无论是明确的还是隐含的。客户端可能会使用您的ID结构来解除私有数据的匿名化,以意想不到的方式爬行您的系统(自动递增ID是非典型的痛点),或者执行大量其他攻击。#。

#利用数据局部性。数据的处理和缓存离其持久存储越近,处理就越高效,保持缓存的一致性和快捷性就越容易。与指针取消引用和Fread(3)相比,网络有更多的故障和更多的延迟。

当然,数据局部性意味着在空间上就近,但也意味着在时间上就近。如果多个用户几乎同时发出同样昂贵的请求,也许可以将他们的请求合并为一个。如果对同一类数据的多个请求彼此接近,则可以将它们合并为一个更大的请求。经常这样做可以降低被监听的通信,并更容易进行故障管理。#。

#将缓存数据写回永久存储是错误的。这发生在比你想象的更多的系统中。尤其是那些最初由没有分布式系统经验的人设计的。您将继承的许多系统都会有此缺陷。如果实现者谈论“俄罗斯玩偶缓存”,那么您很有可能会遇到非常明显的错误。这个条目本可以从列表中删除,但我心里对它有一种特别的憎恨。此缺陷的常见表现形式是用户信息(例如,屏幕名称、电子邮件和散列密码)神秘地恢复到以前的值。#。

#计算机的功能超出您的想象。在今天的领域里,没有太多经验的实践者对机器的能力有大量的错误信息。

2012年底,一台轻型Web服务器拥有6个或更多处理器、24 GB内存和超过您所能使用的磁盘空间。在一台机器上的现代语言运行时中,一个相对复杂的CRUD应用程序能够在几百毫秒内每秒处理数千个请求。这是一个很深的下限。就操作能力而言,在大多数情况下,每台机器每秒数百个请求并不值得吹嘘。

获得更好的性能并不难,特别是如果您愿意根据您的测量来分析您的应用程序并引入效率的话。#

#使用CAP定理来批判系统。CAP定理不是你可以用来构建系统的东西。这不是一个你可以作为第一原则并从中推导出工作系统的定理。它的概观太笼统了,可能的解决方案的空间也太广了。

但是,它非常适合于批评分布式系统设计,并了解需要进行哪些权衡。进行系统设计并迭代CAP对其子系统施加的约束,最终会给您留下更好的设计。作为家庭作业,将CAP定理的约束应用于俄罗斯玩偶缓存的真实实现。

最后一个注意事项:在C、A和P中,您不能选择CA。#。

#提取服务。“服务”在这里指的是“结合了比存储系统更高级的逻辑的分布式系统,并且通常具有请求-响应样式的API”。注意代码更改,如果将代码。

..