“二郎禅”(2016)

2020-07-19 19:51:49

这是一份松散的文字记录(或冗长的转述?)。在ConnectDev大会上的一次演讲,这是Genetec组织的一次会议,我应邀在会上发言。

我想这里的大多数人都没有用过二郎,可能听说过,也许只听说过它的名字。因此,本演示文稿将只涵盖Erlang的高级概念,即使您从未接触过Erlang语言,它也可能对您的工作或副项目有用。

如果你以前看过Erlang,你一定听说过“让它崩溃”的座右铭。我第一次遇到它让我纳闷这到底是怎么回事。Erlang被认为具有很好的并发性和容错性,而这里我被告知要让事情崩溃,这与我实际上希望在系统中发生的情况完全相反。这一命题令人惊讶,但二郎禅宗却与之直接相关。

在某种程度上,让它坠毁对于Erlang来说,就像将它炸毁在火箭科学中一样有趣。在火箭科学领域,炸毁它可能是你最不希望看到的事情--挑战者号灾难就是对这一点的鲜明提醒。话又说回来,如果你从不同的角度来看,火箭及其整个推进机制是关于处理可能并将会爆炸的危险可燃物(这是危险的一部分),但要以受控的方式做到这一点,以便它们可以用来为太空旅行提供动力,或者在轨道上发送有效载荷。

这里的重点实际上是关于控制;你可以尝试并将火箭科学视为一种恰当地利用爆炸-或者至少是其力量-来做我们想要的事情的一种方式。因此,让它崩溃可以在同样的灯光下看到:这都是关于容错的。我们的想法不是到处都有不受控制的故障,而是将故障、异常和崩溃转化为我们可以使用的工具。

背部燃烧和受控燃烧是以火还火的真实例子。在我来自的萨盖奈-拉克圣让地区,蓝莓田地通常会被有控制地烧毁,以帮助鼓励和恢复它们的生长。为了防止森林火灾,森林中不健康的部分被火灾清理的情况相当频繁,以便在适当的监督和控制下进行。这里的主要目标是以实际野火不能进一步传播的方式移除可燃材料。

在所有这些情况下,穿过庄稼或森林的大火的破坏力被用来确保庄稼的健康,或防止对林区造成更大的、不受控制的破坏。

我想这就是“让它崩溃”的意义所在。如果我们能够接受故障、崩溃和异常,并以非常好的控制方式做到这一点,它们就不再是这个需要避免的可怕事件,而是成为组装大型可靠系统的强大基石。

因此,问题就变成了我们如何确保崩溃是促进器而不是破坏器。在Erlang中,这方面的基本游戏部分是过程。Erlang的进程是完全隔离的,它们没有任何共享。任何进程都不能进入另一个进程的内存,也不能通过损坏它所操作的数据来影响它正在做的工作。这很好,因为这意味着死亡的进程基本上可以保证将其问题隐藏在自己身上,这为您的系统提供了非常强的故障隔离。

Erlang的进程也是极其轻量级的,因此您可以使用成千上万的进程而不会出现问题。我们的想法是根据需要使用尽可能多的进程,而不是尽可能多地使用。通常的比较是说,如果你有一种面向对象的语言,在任何给定的时间你只能有32个对象在运行,你很快就会发现用这种语言构建程序过于约束,而且相当荒谬。拥有许多小进程确实可以确保更高的细分粒度,并且在我们想要利用这些故障的力量的情况下,这是很好的!

现在,描绘这些过程的确切工作方式可能有点奇怪。当您编写C程序时,您有一个很大的main()函数,它可以做很多事情。这是您进入程序的入口点。在二郎村,没有这回事。任何进程都不是程序的指定主控。它们中的每一个都运行一个函数,该函数在单个进程中扮演main()的角色。

我们现在有了那群蜜蜂,但如果它们不能以任何方式交流,很可能很难指示它们加强蜂巢。在蜜蜂跳舞的地方,Erlang进程传递消息。

消息传递是并发环境中最直观的通信形式。它是我们合作过的最古老的机器,从我们写信并通过信使骑马寄送信件来找到目的地,到幻灯片上显示的拿破仑信号灯这样更华丽的机制。在这种情况下,你只需要把一群人带到塔上,给他们一个信息,他们就会挥舞着旗子,以比马还快的方式远距离传递数据,这可能会让人感到疲惫。这最终被电报取代,被电话和收音机取代,现在我们有了所有奇特的技术来真正远距离、真正快速地传递信息。

所有这些消息传递的一个关键方面,特别是在过去,一切都是异步的,并且消息被复制。没有人会在他们的门廊上站上几天等待信使回来,也没有人(我怀疑)会坐在信号塔旁等待回应回来。你发送了这条消息,回到你的日常活动中,最终有人会告诉你你得到了回复。

这很好,因为如果对方没有回应,你就不会被困在门廊里无所事事,直到你死去。相反,如果你真的死了,通讯通道另一端的接收者不会像魔术一样看到新到达的信息消失或改变。发送消息时应复制数据。这两个原则确保通信过程中的故障不会产生损坏或不可恢复的状态。Erlang同时实现了这两个功能。

要阅读邮件,每个进程都有一个邮箱。每个人都可以写入进程邮箱,但只有所有者可以查看它。默认情况下,消息是按照它们到达的顺序读取的,但通过模式匹配(在之前的一次谈话中已经讨论过)等功能,可以仅或暂时专注于一种消息,并驱动各种优先级。

到目前为止,你们中的一些人可能已经注意到我提到的内容;我不断重复,孤立和独立是很棒的,因此系统的组件可以在不影响其他组件的情况下死亡和崩溃,但我也提到了跨许多进程或代理进行对话。

每次两个进程开始对话时,我们都会在它们之间创建隐式依赖关系。在系统中有一些隐含的状态将它们捆绑在一起。如果进程A向进程B发送消息,而B死后没有响应,则A可以永远等待,也可以在一段时间后放弃对话。后者是一个有效的策略,但它是一个非常模糊的策略:不清楚远程端是否已经死掉,或者只是需要很长时间,带外消息可能会到达您的邮箱。

监视器完全是一个观察者,一个令人毛骨悚然的人。您决定监视某个进程,如果该进程因任何原因而终止,您的邮箱中会有一条消息告诉您。然后你就可以对此做出反应,并用你新发现的信息做出决定。另一个过程永远不会想到您正在做这一切。因此,如果你是观察者或关心同龄人的状态,监视器是相当不错的。

链接是双向的,建立一个链接会将它们之间建立的两个过程的命运捆绑在一起。每当进程死亡时,所有链接的进程都会收到退出信号。该退出信号将反过来杀死其他进程。

现在,这变得非常有趣,因为我可以使用监视器快速检测故障,我可以使用链接作为体系结构构造,使我可以将多个进程联系在一起,以便它们作为一个单元出现故障。每当我的独立构建块之间开始有依赖关系时,我就可以开始将其编码到我的程序中。这很有用,因为我可以防止我的系统意外地崩溃到不稳定的部分状态。链接是一种工具,可以让开发人员确保最终,当一件事情失败时,它会完全失败,并留下一张白纸,仍然不会影响练习中没有涉及的组件。

对于这张幻灯片,我选了一张登山者被绳子绑在一起的照片。现在,如果登山者之间只有联系,他们就会处于可悲的境地。每次你的登山队中有一名登山者滑倒,其他登山者就会立即死亡。这不是一种很好的做事方式。

相反,Erlang允许您指定某些进程是特殊的,可以使用TRAP_EXIT选项进行标记。然后,它们可以获取通过链路发送的退出信号,并将其转换为消息。这使它们可以恢复故障,并可能启动一个新进程来完成前一个进程的工作。与登山者不同的是,这种特殊的过程不能防止同龄人崩溃;这是同龄人的责任,通过使用TRY……。例如,捕捉表达式。一个捕获出口的进程仍然没有办法在另一个进程的内存中运行并保存它,但它可以避免死于它。

事实证明,这是实施监管者的一个关键特征。如果你还没有听说过它们,我们很快就能找到它们。

在去找监管者之前,我们仍然有一些因素能够成功地烹饪一个利用崩盘为自己谋利的系统。其中之一与进程的调度方式有关。对于这一次,我想引用的现实世界用例是阿波罗11号登月。

阿波罗11号是69年的登月任务。在这里的幻灯片中,我们看到登月舱,上面有巴兹·奥尔德林和尼尔·阿姆斯特朗,还有一张照片,是一个我认为是迈克尔·柯林斯的人拍的,他呆在指挥舱里执行任务。

在登月途中,登月舱由阿波罗PGNCS(初级制导、导航和控制系统)引导。制导系统有多个任务在其上运行,仔细计算了循环的次数。美国宇航局还规定,处理器只能使用85%的容量,剩下15%的空闲空间。

现在,因为这种情况下的宇航员想要一个像样的后备计划,以防他们需要中止,他们留了一个会合雷达,以防它会派上用场。这占据了CPU剩余容量的相当大一部分。当Buzz Aldrin开始输入命令时,将弹出有关溢出和基本耗尽容量的错误消息。如果系统在这个问题上失控,它很可能不能完成它的工作,我们最终可能会有两名宇航员死亡。

这主要是因为雷达有已知的硬件缺陷,导致它的频率与制导计算机不匹配,并导致它窃取了比其他情况下多得多的周期。现在,NASA的人员不是白痴,他们重复使用他们知道自己有罕见缺陷的部件,而不仅仅是为如此关键的任务批准新技术,但更重要的是,他们设计了优先时间表。

这意味着,即使在雷达或可能输入的命令使处理器过载的情况下,如果它们的优先级与绝对至关重要的任务相比太低,任务也会被终止,以便将CPU周期分配给真正需要它的东西。那是在1969年;今天,仍然有大量的语言或框架只给你提供协作调度,而不给你其他的。

Erlang不是一种对生命至关重要的系统使用的语言-它只尊重软实时约束,而不是硬实时约束,在这些场景中使用它不是一个好主意。但是Erlang确实为您提供了抢占式调度和进程优先级。这意味着,作为开发人员或系统设计人员,您不必关心确保绝对每个人都仔细计算他们将在其所有组件(包括您使用的库)上执行的所有CPU使用量,以确保他们不会使系统停滞。他们就是不会有那样的能力。如果你需要一些重要的任务总是在它必须运行的时候运行,你也可以得到它。

这可能看起来不是一个大的或常见的需求,人们仍然只有通过并发任务的协作调度才能交付真正成功的项目,但它肯定是非常有价值的,因为它可以保护您免受他人的错误以及您自己的错误的影响。它还为自动负载平衡、惩罚或奖励好的和坏的进程,或者为那些有大量工作等待他们的进程提供更高的优先级等机制打开了大门。这些东西最终可能会为您提供相当适应生产负载和不可预见事件的系统。

在获得良好的容错能力方面,我想讨论的最后一个因素是网络感知。在我们开发的任何需要长时间工作的系统中,拥有一台以上的计算机快速运行是一个先决条件。你不会想坐在那里,把你自己的黄金机器锁在钛门后面,不能容忍任何对你的用户产生重大影响的干扰。

因此,您最终需要两台计算机,这样一台计算机才能在出现故障的另一台计算机中幸存下来,如果您希望部署系统的一部分出现故障的计算机,可能还需要第三台计算机。

滑梯上的飞机是F-82双胞胎野马,这种飞机是在第二次世界大战期间设计的,目的是护送轰炸机越过大多数其他战斗机无法覆盖的范围。它有两个驾驶舱,这样飞行员就可以在疲惫时接手并在一段时间内相互中继;在某种程度上,他们也会安装它,这样一个人就可以驾驶,另一个人就可以充当拦截者的角色。现代飞机仍然在做类似的事情;它们有无数的故障转移,而且经常有机组人员在飞行期间在飞行过程中睡觉,以确保总是有人时刻保持警觉,准备驾驶飞机。

当谈到编程语言或开发环境时,尽管人们知道如果您编写服务器堆栈,您需要不止一台服务器,但大多数语言或开发环境的设计完全忽略了分布。然而,如果您要使用文件,标准库中将会有相应的内容。大多数语言走得最远的地方就是为您提供了一个套接字库或HTTP客户端。

Erlang承认分发的现实,并为您提供了一个实现,这是有文档记录的,并且是透明的。这使人们可以为故障转移或接管崩溃的应用程序设置奇特的逻辑,以提供更高的容错能力,甚至让其他语言假装它们是Erlang节点来构建多语言系统。

这些都是二郎禅食谱中的基本成分。构建整个语言的目的是接受崩溃和失败,并使它们变得如此易于管理,以至于有可能将它们作为一种工具来使用。让它崩溃开始有意义,这里看到的原则在很大程度上是可以在非Erlang系统中作为灵感重用的东西。

监督树是将结构强加给Erlang程序的方式。他们从一个简单的概念开始,即主管,其唯一的工作是启动流程,查看流程,并在出现故障时重新启动流程。顺便说一句,主管是';OTP&39;的核心组件之一,OTP';是Erlang/OTP';名称中使用的通用开发框架。

这样做的目的是创造一个层次结构,在那里所有必须非常坚实的重要东西都会聚集在离树根更近的地方,而所有反复无常的东西,即可移动的部分,都会聚集在树叶上。事实上,这就是大多数树在现实生活中的样子:树叶是可移动的,它们很多,秋天它们都可以倒下,树还活着。

这意味着,当您构建Erlang程序时,您感觉到的一切都是脆弱的,应该允许失败的东西都必须深入到层次结构中,而稳定和关键的需要可靠的东西是更高层的。

监管者可以通过使用链接和陷阱出口来做到这一点。他们的工作首先是按照从左到右的顺序,按照深度优先的顺序开始他们的孩子。只有当一个孩子完全启动后,它才会回到一个级别,开始下一个级别。每个子项都会自动链接。

每当儿童死亡时,都会选择三种策略中的一种。幻灯片上的第一个是一对一,仅通过替换已死亡的子进程来实施。这是每当该主管的子代彼此独立时都要使用的策略。

第二个战略是人人共享。这是当孩子们相互依赖时使用的。当他们中的任何一个死了,监督员就会杀死其他孩子,然后再让他们全部回来。当丢失特定子进程会使其他进程处于不确定状态时,您可以使用此选项。让我们设想一个三个进程之间的对话,以投票结束。如果其中一个进程在投票过程中死亡,我们可能没有编写任何代码来处理它。用一个新的进程替换那个死的进程现在会将一个新的对等体带到一个也不知道发生了什么的表中!

如果我们还没有真正定义当一个进程通过投票程序造成严重破坏时会发生什么,那么这种不一致的状态可能是危险的。更安全的做法可能是直接杀死所有进程,然后从已知的稳定状态重新开始。通过这样做,我们重新限制了错误的范围:早而突然地崩溃比在长期基础上缓慢地损坏数据要好。

最后一种策略发生在根据引导顺序的进程之间存在依赖关系的时候。它的名称是Rest for one,如果一个子进程死了,那么只有那些在它被杀死之后启动的进程才会被终止。然后按预期重新启动进程。

每个主管都另外具有可配置的控制和容差级别。有些主管在中止之前可能每天只能容忍1次故障,而其他主管可能会容忍每秒150次故障。

在我提到主管之后,通常会立即出现评论,但如果我的配置文件已损坏,重新启动也解决不了任何问题!

这是完全正确的。重新启动Works的原因是由于生产系统中遇到的错误的性质。要讨论这一点,我不得不参考吉姆·格雷(Jim Gray)在1985年创造的两个术语:波尔虫(Bohrbug)和海森虫(Heisenbug)(我建议你尽可能多地阅读吉姆·格雷(Jim Gray)的论文,它们几乎都很棒!)。

基本上,bohrbug是一种坚固的、可观察到的、容易重复的bug。他们往往是相当简单的理由。相比之下,海森虫的行为不可靠,在某些条件下会显露出来,而这些行为可能会被试图观察它们的简单行为所掩盖。例如,在使用可能强制系统中的每个操作被序列化的调试器时,并发错误以消失而臭名昭著。

海森虫是千万亿亿亿万次才会发生一次的令人讨厌的虫子。你知道有人已经努力了一段时间,一旦你看到他们打印出一页页的代码,然后拿着一堆记号笔进城。

如果您的系统的核心功能中有错误,它们通常应该非常容易。

.