- 原文地址:As bad as anything else: Part 1
- 原文做者:Fred T-H
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:steinliber
- 校对者:kasheemlew, SergeyChang
我在由 Genetec 组织的 ConnectDev'16 会上受邀演讲,这个是我所介绍部分的一个松散的文字记录(或者也能够说是较长的释义)。html
我假定这里的大多数人都没有使用过 Erlang,或许可能已经据说过它,或许就只是知道这个名字。在这种状况下,这个介绍只会涵盖 Erlang 中的高层次理念,使用这种方式来说述的话即便你从未接触过这个语言,它也会对你的工做或副项目有所帮助。前端
若是你以前曾经了解过 Erlang,那你应该已经听过“就让它奔溃”的箴言。当我第一次遇到这句话时就在想这究竟是什么鬼玩意。Erlang 应该对并发和容错有着很好的支持,可是我却被告知就让它奔溃吧,这和我想要这个系统发生的彻底相反。这个主张让人难以想象,可是 Erlang 之禅却与之息息相关。android
在必定程度上,在 Erlang 中使用“就让它奔溃”这句话就和在火箭科学中使用“就让它爆炸”同样可笑。在火箭科学中“就让它爆炸”也许是你最不想发生的事 - 挑战者号空难就是一个鲜明的提醒。相对的若是你用不一样的方式来看待这件事,火箭和它的整个推动机制都是要处理危险且会爆炸的可燃物(这是其中危险之处),可是它经过可控的方式来使用这种能量来驱动空间旅行或者把载荷送上轨道。ios
这里的重点在于控制;你能够尝试把火箭科学看做是一种如何正确驾驭爆炸的方式 - 或者至少是驾驭其中包含的能量 - 用于作咱们想要的事情。从同一个角度看就让它奔溃也是同样的道理:它全部一切都是关于容错。这个想法并非说让不可控制的错误遍及各处,而是把失败,异常和奔溃转化为咱们可使用的工具。git
回燃法和受控制的燃烧是真实世界中用火来灭火的真实例子。在我出生的加拿大萨格内-圣约翰,蓝莓田会例行以受控的方式被烧毁,以帮助支持和延续它们以后的生长。为了防止森林火灾,使用火来清除森林中已经枯萎了的部分是很常见的行为,这样森林才能被适当地监管和控制。这里的主要目的是用这种方式来去除可燃材料,这样真实的火灾就不能进一步蔓延。github
在全部这些状况下,火对庄稼或者森林的破坏力被用于确保庄稼的健康或者是防止森林地区发生更大的没法控制的火灾。编程
我认为这就是“就让它奔溃”所想表达的。若是咱们能够经过一种很是好的控制方式来拥抱失败,奔溃和异常,它们就不会是须要避免的可怕事物,而能成为构建大型可靠系统的强大基石。后端
因此这个问题就变成想出一个办法确保奔溃是促进者而不是毁灭者。对于这个,Erlang 使用进程做为它的基本棋子。Erlang 的进程是彻底独立的,进程之间不共享任何东西。任何进程都不能访问其它进程的内存,或者经过修改它所操做的数据来影响它的工做。这样就很棒了,由于这意味着咱们能够保证一个进程死亡只会把问题保留在进程内,从而为你的系统带来了很是强大的故障隔离。安全
Erlang 的进程也很是轻量,你就算运行成千上万个也不是问题。这里的想法是使用你__须要__运行数量的进程,而不是你__能__运行数量的进程。这里一般的比较就是说,若是你有一个面向对象的语言,该语言在任何运行时间中只能有 32 个对象,你很快就会发现使用这种语言构建程序会受到过多的限制并且是很是荒谬的。拥有许多小的进程能够确保在拆分事物中有更高的粒度,并且在一个咱们想要利用这些失败力量的世界中,这很棒!服务器
如今想象这样 Erlang 中进程是如何工做的可能会有些奇怪。当你写一个 C 程序时,你有一个大的 main()
函数作大量的事情。这个函数是你程序的入口点。在 Erlang 中就没有这样的东西。没有进程是这个程序指定的主进程。每个进程都运行一个函数,这个函数在对应单个进程中扮演着 main()
函数的角色。
咱们如今有一群蜜蜂,可是若是它们之间不能经过任何方式沟通,那可能就很难管理它们加固蜂巢。蜜蜂是经过舞蹈进行沟通,而 Erlang 的进程是经过消息传递。
在并行环境中,进程间消息传递是最直观的通讯形式。这是咱们工做过最古老的通讯方式,从咱们开始写信经过邮差骑马来送到目的地的日子,到这个幻灯片中展现的拿破仑信号塔。在这种状况下,你只须要带着一群人进入塔楼,给他们一个消息,他们就会挥舞旗帜以比骑马更快的方式把消息传递到远方,但这很容易让人疲劳。最终这些都被电报取代了,以后又被电话和收音机取代了,如今咱们拥有全部这些很棒的技术来传递消息,而且能够传的很远、很快。
全部这些消息转递方式特别是过去的其中一个关键在于它们都是异步的,并且传输的消息都会被复制。没有人会为了等寄信的信使回来而在门廊上站好几天,也没有人(至少我认为)会坐在信号塔中等消息的响应发回来。你只是会把消息发送出去,而后回去作你的平常工做,最终会有人告诉你有回信了。
这很合理由于若是另外一边没有回应,你不会什么事都不作而是傻傻的在门廊前一直等到死去。相反,若是你死了,消息交流通道另外一边的收信人也不会奇迹般的立刻收到或改变你的消息。数据__应该__在被发送出以前被复制一遍。这两个原则确保通讯过程当中的失败不会致使一个损坏或者没法恢复的状态。Erlang 实现了这两个原则。
为了能阅读消息,每一个进程都有一个邮箱。任何一个进程均可以写消息到一个进程的邮箱,可是只有拥有这个邮箱的进程能查看它。这些消息会默认以它们到达的顺序被读取,可是也有可能经过模式匹配[咱们在先前的演讲中讨论过这个]之类的特性来让进程临时只关注一种,从而驱动不一样优先级消息的执行顺序。
大家之中的部分人会注意到我刚才所提到的一些事项;我一再重申隔离和独立性是伟大的,这样一个系统的组件就能够在不影响其它组件的状况下死亡和奔溃,同时我也提到了在多个进程或者代理之间进行交流。
每当有两个进程开始交流,咱们在它们之间就建立了一个隐性的依赖。系统中会有一些隐性的状态将它们绑定在一块儿。若是进程 A 向进程 B 发送了一个消息,可是 B 进程已经死亡而没有响应 A,那么 A 进程所能作的要么就是永远等着,要么就是在一段时间后放弃和 B 进程的交流。后者是一个有效的策略,可是它也是一个十分模糊的策略:它并不知道远端的那个进程是已经死了还只是处理时间比较长,解除绑定以后远端进程的消息才发送到你的邮箱。
相反,Erlang 为咱们提供了两种机制来处理这种状况:监视器和连接
监视器所作的所有就是做为一个观察者,一个攀缘植物。你决定去留意一个进程,若是该进程出于任何缘由死亡了,你就能够在你的信箱中获取到关于这个的消息。你就能够对此做出反应而且经过你新发现的信息来作出决策。其它的进程永远也不会知道你对它作了什么。若是你是一个观察者或者关注对等进程的状态,那么监视器能够是很是棒的工具。
连接是双向的,创建一个连接就会将其所相连的两个进程命运绑定。当其中一个死亡时,任何与之连接的进程都会收到退出信号,这个退出信号会杀死这些进程。
如今这就真变得颇有趣了,由于咱们使用监控器来快速地检测到失败,并且我还能使用连接做为一个架构构造够把多个进程绑定在一块儿做为一个共同的失败单元。不管什么时候我独立构建的模块开始有相互之间的依赖,我可以开始把这些依赖写入个人代码中。这颇有用,由于这样就能够防止个人系统意外奔溃进入到一个不稳定的局部状态。连接是一种工具,它可让开发人员确保当其中一件事失败时,最终会彻底失败并留下一个空的白板,而不会影响这个运行中没有牵涉到的组件。
在这个幻灯片中,我选了一张爬山者经过绳子连在一块儿的照片。如今若是爬山者之间只有这个连接,他们恐怕会陷入一个糟糕的境地。任什么时候间你队伍里的一个爬山者滑倒,队里的其余人也会立刻滑倒死去。这并非一个作事情的好办法。
相反,Erlang 可让你指定某些进程是特殊的,这些进程会使用 trap_exit
选项做为标记。而后他们能够接受连接发送过来的退出信号而且将它们转化为消息。这可让它们从错误中恢复过来而且可能启动一个新的进程来作以前死掉进程的工做。不像爬山者那样,这种特殊的进程不能阻止一个对等进程奔溃;这是这个对等进程的责任来确保本身不会挂掉,好比说经过 try ... catch
表达式。一个收到退出信号的进程仍是没有办法进入另外一个进程的内存而后保存这些内存,可是它能够避免由于这个而死去。
这成为了实施监督者的关键特性。若是你历来没有据说过这些,咱们很快就会接触到这些。
在进入监督者这部分以前,咱们仍须要一点调料才能成功地烘焙出一个系统,这个系统利用奔溃来得到自身的优点。其中之一与进程如何调度有关。对于这方面,我想提到的真实世界例子是阿波罗 11 号的登月计划。
阿波罗 11 号是在 1969 年的登月任务。在这个幻灯片中,咱们看到 Buzz Aldrin 和 Neil Armstrong 的登月舱,这张照片我认为是 Michael Collins 拍的,在此次任务中他留在了指挥舱中。
在他们登月的途中,登月舱将由 Apollo PGNCS(主要指挥,导航和控制系统) 所引导。这个指导系统有多个任务在上面运行,它们的运行周期数是被仔细斟酌过的。NASA 也指出全部任务运行只占用了处理器 85% 的容量,还剩下 15% 的空间。
如今,为了在应对须要终止计划的状况,宇航员们须要制定一个完善的备份计划。因而他们还用处理器运行了一个交会雷达以防万一它能派上用场,这会用掉 CPU 所剩容量中的一大部分。当 Buzz Aldrin 输入指令时会出现大量关于溢出和容量耗尽的报错信息。若是控制系统所以失控,它将没法正常工做,而且害死两名宇航员
这主要是因为雷达存在已知的硬件错误会使它的运行频率和指挥计算机不匹配,这就致使它窃取了比其原本所应该有的更多的运行周期。固然 NASA 的人也不是白痴,在这种关键的任务中他们重用了他们所知道以前用过不多发生错误的组件,而不是研发一个新的技术。可是更重要的是,他们设计了优先级调度。
这意味着即便由于这种雷达或者输入命令致使处理器过载的状况下,若是它们的运行优先级与性命攸关的事情相比很低,那么这些任务将被杀死,从而把 CPU 运行周期给真正迫切须要它的任务。那是在 1969 年;在今天仍然有大量的语言或者框架给你的__只是__合做调度,除此以外别无他有。
Erlang 并非一种用于构建生命攸关系统的语言 - 它只遵循软实时时间约束,而不是实时时间约束因此在这些场景中使用它并非一个好主意。可是 Erlang 为你提供了抢先试调度以及相应的进程优先级。这就意味着做为一个开发者或者系统设计人员,你并不__须要__去关心确保每一个人都仔细统计了他们全部组件的(包括使用的库)CPU 使用量以避免使整个系统变慢。他们并无这个能力。并且若是你须要一些重要任务在它必须运行时总能运行,你也能实现这个。
这彷佛并非一个大的或者通用的需求,人们仍是能经过协做式的并发任务开发真正成功的项目,可是它确实十分有价值,由于它可使你免受他人和你本身错误的影响。它还为像自动负载平衡,惩罚和奖励好或者坏的进程或者给予须要作大量工做的进程更高优先级提供了实现机制。这些东西最终都会使你的系统更好的适应生产环境负载和处理意外事件。
我想讨论得到优雅容错性的最后一个调料是网络意识。在咱们开发的任何须要长时间运行的系统中,让多台计算机快速的运行这个系统是一个先决条件。你不会想坐在由钛门锁在里面的金色机器旁边,却不能忍受任何方式引发的中断影响到你的用户。
因此最终你须要两台计算机,这样一台机器能够在另外一台破环时继续提供服务,若是你想要在损坏计算机仍是你系统一部分的时候部署,那么你或许就须要第三台。
这个幻灯片中的飞机是 F-82 双生野马,这是一架在第二次世界大战期间设计的飞机,用于护送大多数其它战机没法覆盖范围内的轰炸机。它有两个驾驶舱,这样随着时间推移,当一个驾驶员累的时候另外一个能够互相替换;在一些状况下他们也能相互配合,其中一我的飞行的时候,另外一个能够操做雷达做为拦截者的角色。现代飞机仍然在作相似的一些事情;他们有数不清的备用方案,常常有机组人员在飞行期间的途中睡觉,以确保总有人能时刻警戒准备好驾驶飞机。
当这个说法用于编程语言或者开发环境,它们中大多数的设计都彻底忽略了分布式,尽管人们都知道若是你写的是服务器栈,那么你须要的就不止一台服务器。然而,若是你要使用文件,这些变成语言就会有标准库帮你完成这些事情。大多数语言更进一步就是给你一个套接字库或者 HTTP 客户端。
Erlang 意识到了分布式这个事实而且为你提供了一个实现,这个实现是有文档记录并且透明的。这可让人们为故障转移设或者是接管奔溃的应用配置所想要的逻辑从而提供更高的容错性,甚至可让其它语言伪装它们是 Erlang 的节点来构建多边形系统。
全部这些就是 Erlang 之禅食谱的基本调料。这整个语言的目的在于得到崩溃和失败,并使它们如此易于管理,从而有可能将它们看成工具。就让它崩溃开始有道理起来了,这里看到的原则大部分都是能够在非 Erlang 系统中做为灵感重用的。
如何将它们组合在一块儿是下一个挑战。
监管树描述的是如何实施你 Erlang 程序的架构。它们源自于一个简单的观念,有一个监管者,它惟一的工做就是启动进程,关注进程的运行,而后在它们运行失败时重启它们。顺便提下,监管者是 ‘ OTP ’ 的核心组件之一,它是被普遍使用的开发框架,名字叫作 ‘ Erlang/OTP ’。
这样作的目的是建立一个层级结构,在这个结构中全部重要必须稳定运行的东西越接近树的根部,而全部易变或正在转移的部分则会积累在叶子部分。事实上,这就是现实生活中大多数树木的样子:树叶不是固定的,树上会有不少树叶,在秋天它们都会飘落下来,而这个树仍然活着。
这意味着当你构建 Erlang 程序时,任何你以为脆弱的容许运行失败的进程应该处于这个层级的更深处,而稳定并且可靠性要求很高的应该移到层级上面。
监管者是经过使用连接和捕获退出来实现这个功能的。它们的工做从一次启动它们的子进程开始,从上往下,从左到右。只有当一个子进程彻底开始以后它才会返回上个层级开始建立下一个子进程。每个子进程都会被自动连接。
每当一个子进程死亡时,有如下三个策略可供选择。第一个策略在这个幻灯片中就是 ‘一对一’,经过替换死去的子进程来实现。这是用于监管者的全部子进程相互之间都独立时的策略。
第二个策略是‘一便是所有’。这个策略用于子进程之间存在相互依赖关系。当它们中的任何一个死去时,监管者就会在把它们所有从新启动以前把其它全部子进程都杀掉。当失去一个特殊的子进程会使其它进程陷入一个不肯定的状态时,你就可使用这个策略。让咱们想象三个进程进行一个对话,该对话以投票结束的。若是在投票过程当中其中一个进程死亡,那么可能咱们并无编写任何代码来处理这个问题。用一个新的替换死去的进程会在表格上带来一个新的同伴,而其中的全部进程彻底不知道接下来该作什么。
若是咱们没有真正定义当一个进程在投票过程当中形成严重出故障时要怎么作,那么这种不一致的状态多是有危险的。相比于这个,杀死全部的进程可能会更安全,而后从已知稳定的状态从新开始。经过这样作咱们就能够限制错误的范围:在错误发生时早点及时奔溃会比慢慢且长时间毁坏数据要更好。
当进程之间根据它们的启动顺序有依赖关系时一般能够用最后这种策略。它的命名叫作‘一个所剩下的’,当一个子进程死亡时,在以后它后面启动的进程会被杀死。而后进程就会像以前预期的那样从新启动。
每一个监管者还额外有可配置的控制和忍耐级别。一些监管者可能中断以前天天只能忍受一个故障,而其它的或许能够每秒承受 150 个故障。
在我提到监管者以后你们一般都会说起的评论就是“可是若是个人配置文件就是错的,重启并不能解决任何问题!”。
这彻底正确。重启有效的缘由在于生产环境系统中所遇到的错误性质。为了讨论这个问题,我必须说起 Jim Gray 在 1985 年提出的 ‘ Bohrbug ’ 和 ‘ Heisenbug ’ 这两个术语(我建议你尽量多读下 Jim Gray 的论文,它们都写的很棒!)。
基本上来看,一个 bohrbug 是一个稳定的,可观察的并且可复现的错误。它们倾向于能够被开发者容易地推测出问题的缘由。相反 Heisenbug 具备不可靠的行为,它不会在肯定的条件中出现,并且若是只是采起简单的行为尝试去观测这些问题时它们可能会被隐藏起来。好比说在系统中使用每一个操做都会被循序执行的调试器时,并发错误就没法查找出来。
Heisenbugs 是这些在一千次,百万次,十亿次或者万亿次错误中才会出现一次的使人讨厌的错误。当你看到有人打印了一页又一页的代码以及在它们中填上一大堆标记时,你就知道他已经处理这种类型的错误有一段时间了。
定义了这些术语以后,让咱们来看看它们的出现频率应该是多少。
在这里,我把 bohrbugs 列为可重复的错误类型,把 heisenbugs 列为暂时的错误类型。
若是你在你系统的核心功能中有 bohrbugs,那么当这个系统到达生产环境以前它们应该能很容易被找出来。经过可重复性,以及这类错误一般在程序运行的关键路径上,你应该早晚会遇到它们,并且在到达下一个阶段以前修复它们。
那些发生在次要的,更少使用的功能上的错误,更像是提醒和错过的事。每一个人都认可的是修复软件中的所有错误是一件艰苦的战争,为此获得的收益是递减的;随着你继续编写代码,除去其中的小缺陷可能要花愈来愈多的时间。一般状况下,这些次要功能每每会收到较少的关注,不只由于较少的客户会使用它们,还由于它们对满意度的影响并无那么重要。或者也许它们只是要晚些时候被安排修复并且把时间表拖后最终会下降开发人员处理这个的重要度。
在任何状况下,它们在必定程度上都挺容易找到的,咱们只是没有时间或者资源来作这件事。
Heisenbugs 几乎不可能在开发过程当中发现它们。像形式证实,模型检查,穷举测试或者基于属性的测试这些很棒的技术可能会增长发现其中一部分或者所有问题(取决于所用方法)的可能性,可是坦白讲,除非手头上的任务是很是关键的,不然咱们中不多有人使用这些技术。在数十亿次中出现一次的问题就须要大量的测试和验证才能发现,并且若是你已经看到过这个错误,那么极可能没那么好运气再次产生这个错误。
更多内容请见本文第二部分:Erlang 之禅:第二部分。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。