- 原文地址:As bad as anything else: Part 2
- 原文做者:Fred T-H
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:7Ethan
- 校对者:K.Lew, satansk
若是你还没看本文的第一部分,请先阅读第一部分:Erlang 之禅:第一部分。html
我在这部分想要阐述的是以个人经验去说说在生产中每种类型错误的出现频率。没有任何明显的证据代表利用查找错误和错误的发生几率有联系。可是个人直觉告诉我,这种关系是存在的。前端
首先,在核心特性中容易复现的错误不该该出如今产品中。若是这些(容易复现的)bugs 确实在生产环境中出现,那么你实际上已经发布了一个破产品,再多的从新启动或者技术支持都不会帮到你的用户。这种问题须要修改代码,而且多是生产该产品的组织内部一些根深蒂固的问题的后果。android
边缘特性中的可复现 bugs 极可能会流入生产环境,我认为这是没有花足够的时间去合理测试它们的结果。但当涉及到部分重构时,次要功能每每会被摆在次要位置,或者设计者没有充分考虑这些功能要与系统的其余部分保持一致。ios
另外一方面,瞬变错误一直存在。吉姆·格雷发明了这些术语,他报告说,在给定的客户站点中,132 个错误里只有一个是波尔 Bug(可复现的 bug)。生产环境中遇到的 bug 中有 131/132 是海森堡Bug(不可复现的 bug)。它们很难被触发,若是它们是真正的错误,可能每一百万次就只出现一次,那么你的系统就会一直须要一些负载来捕捉它们;在一个每秒处理 10 万请求的系统中,10 亿之一的 bug 每 3 小时出现一次,百万分之一的 bug 每 10s 就会出现一次,但在测试环境中,相似 bug 不多出现。git
若是处理不当,就将会有不少的错误和失败。github
那么,重启做为一种策略有多有效呢?数据库
对于核心功能上的可复现的 bug,从新启动是没用的。对于不常用的代码路径中的可复现的 bug,这取决于不一样状况;若是这个功能对于很是少的用户来讲很是重要,那么从新启动不会有太大的做用。若是这是每一个人都使用的一个小功能,但在某种程度上他们并不太在乎,那么从新开始或忽略失败就够了。例如,若是facebook 的 ‘poke’ 功能失效(不知道这个问题是否还存在),也不会对不少用户的体验有影响。编程
对于瞬态错误,重启是很是有效的,并且它们每每是咱们遇到的常见错误。因为它们难以复现,因此它们的出现一般依赖于特定状况或系统中状态的交织,而且它们的出现每每只占全部操做的一小部分,重启每每会使它们消失。后端
回滚到已知的稳定状态,再重复一次相同的操做,不太可能碰到致使这种状况的奇怪上下文。所以,可能发生的灾难只不过是系统的一个小插曲,用户很快就学会适应了。缓存
而后,你可使用日志记录、跟踪或各类自检工具(这些工具在 Erlang 中都是现成的)来查找、理解和修复问题,以保证它们再也不发生。或者你能够决定容忍它们,由于解决问题须要付出巨大的努力。
这个问题是在一个论坛上提出来的,当时我正在讨论编程内容和 Erlang 模型。我一字不差地摘录了它,由于这是一个很好的例子,不少人听到重启和 Erlang 的特性时都会问这个问题。
我想经过一个现实的例子来具体说明如何在 Erlang 中设计一个系统,这更能突出它的特性。
经过监督者(圆角矩形),咱们能够开始建立深层次的流程。这里咱们有一个选举系统,有两棵树:一棵计数树和一棵实时报告树。计数树负责计数和存储结果,而实时报告树则是让人们链接到它以查看结果。
经过定义子节点的顺序能够知道,计数树启动后实时报告树才会开始运行。除非存储层可用,不然分区子树(关于每一个分区的计数结果)将不会运行。若是存储工做者池(将链接到数据库)可用,则只能启动存储的缓存。
我前面提到的监督策略让咱们在程序结构中对这些需求进行编码,而且它们在运行时仍然存在的,而不只仅是在启动时。例如,管理人员可能会采用一对一策略,这意味着各区域可能各自失败,而不会影响彼此之间的计数。相比之下,每一个地区(魁北克和安大略的管理者)均可以采起休息策略。 所以,这一策略能够确保 OCR 程序始终能够将检测到的投票发送给“计数”工做人员,而且即便常常崩溃也不会对其形成影响。 另外一方面,若是计数工做人员没法保存和存储状态,它的中止会中断 OCR 程序,确保没有任何数据丢失。
这个 OCR 进程自己多是用 C 语言编写的监视代码,做为独立的代理,并与其连接。 这将进一步隔离该 C 语言代码与虚拟机的故障,以实现更好的隔离或并行化。
我要指出的另外一件事是,每一个主管都有对失败的可配置容忍度;区域主管可能很是宽容,每分钟处理 10 次故障,而存储层若是预期是正确的,则可能对故障至关不宽容,若是咱们但愿它是正确的,则在每小时 3 次崩溃后永久关闭。
在这个程序中,关键的功能更接近树的根,这样能更少的移动和更加坚固。他们不受兄弟节点消亡的影响,但他们本身的失败影响到其余人。叶子完成了全部的工做,而且能够很好地丢失 —— 一旦它们吸取了数据并在上面进行光合做用,它就能够进入核心。
所以,经过定义全部这些,咱们能够将危险的代码隔离在一个具备高容忍度或正在被监控的进程中,并在数据进入系统时将数据移至更稳定的进程。 若是 C 语言中的 OCR 代码有危险,它能够失败并安全地从新启动。 当它工做时,它将其信息传输到 Erlang OCR 进程。 该过程能够进行验证,也能够自行崩溃,也许不会。 若是信息是可靠的,则将其移至 Count 过程,该进程的任务是保持很是简单的状态,并最终经过存储子树将该状态刷新到数据库,这是安全独立的。
若是 OCR 进程死亡,它会自动重启。若是它奔溃得太频繁,它就会将本身的管理器关闭,子树的那一部分也会从新启动 —— 不会影响系统的其余部分。若是这能解决问题,很好。若是没有,这个过程就会不断重复,直到它工做,直到整个系统中止,由于某些东西显然出错了,咱们没法经过从新启动来处理它。
以这种方式构建系统具备巨大的价值,由于错误处理被嵌入到系统的结构中。这意味着我能够不用在边缘节点中编写恶心的防护代码 —— 若是出了问题,让其余人(或程序的结构)来决定如何反应。若是我知道如何处理一个错误,那么我能够对那个特定的错误这么作。不然,就让它崩溃吧!
这种方式倾向于转换代码。慢慢地,你会发现它再也不包含大量的 if/else 或 switch 或 try/catch 表达式。相反,它包含了清晰的代码,解释当一切正常时代码应该作什么。它再也不包含许多形式的猜想,你的软件可读性更强。
当咱们退一步看咱们的程序结构时,咱们可能会发现,在黄色环绕的每一个子树中,在它们所作的事情上彷佛都是相互独立的;它们的依赖关系大可能是合乎逻辑的:例如,报表系统须要一个存储层进行查询。
例如,若是我能够交换存储实现或在其余系统中独立使用它,那也是很是好的。将实时报告系统隔离到不一样的节点或开始提供替代手段(例如 SMS)也可能很整洁。
咱们如今须要的是找到一种方式来打破这些子树,并将它们转化为咱们能够组合,重用的逻辑单元,而且咱们能够独立配置,从新启动或开发。
Erlang 将 OTP 用做解决方案。OTP 应用程序是构建这种子树的代码以及一些元数据。该元数据包含基本内容,如版本号和应用程序的描述,以及指定应用程序之间的依赖关系的方法。 这很是有用,由于它可让个人存储应用程序与系统的其余部分保持独立,但仍然对计数应用程序在运行时的须要进行编码。我能够保留我在系统中编码的全部信息,但如今它是由独立块构建的,这些块更容易理解。
实际上,人们认为 OTP 应用程序是 Erlang 的库。 若是您的代码库不是 OTP 应用程序,那么它在其余系统中不可重用。 [旁注:有许多方法能够指定实际上不包含子树的 OTP 库,只是由其余库重用的模块]
搞掂一切后,咱们的 Erlang 系统如今已经定义了如下全部属性:
这是很是有价值的。更有价值的是迫使每一个开发人员在早期就从这种角度去考虑。你的防守代码较少,发生奔溃时系统会继续运行。你只须要查看日志或实时系统状态,并花时间修复问题(若是您以为这是值得的时间)。
完成这一切后,我应该能够安稳的睡大觉了,对吧?但愿是的。我这里展现的是咱们几年前在 Heroku 上部署的一个像素图表。
图的最左边是在 9 月左右。那时,咱们的新代理层(vegur)已经投入生产了大约 3 个月,咱们已经解决了其中的大部分问题。用户没有问题,过渡进行得很顺利,新的功能正在被使用。
在某个时候,一个团队成员为咱们用来聚合异常的日志记录服务收到了很是昂贵的信用卡账单。 那时候咱们看了一眼,看到了图表最左边的恐怖:咱们天天产生 500,000 到 1,200,000 个异常!额滴神,这太多了吧。 可是呢? 若是问题是一个 heisenbug,而咱们的系统每秒收到 100,000 个请求,那么它发生的概率是多少?在 1/17000 到 1/7000 之间。这很频繁,可是由于它对服务没有影响,因此直到带宽和存储帐单来了咱们才注意到它。
咱们花了一点时间才弄清错误,而后咱们修正了错误。你能够看到,此后的异常率仍然很低,可能天天几十万。他们都是咱们所知道的,可是没有影响。两年后,咱们尚未着手解决这个问题,由于尽管如此,系统仍是能够正常工做的。
与此同时,你不可能总能安稳的睡大觉。尽管你采用了最佳的设计方法,但失败可能会失控。
几年前,我乘坐过一趟飞往温哥华的航班。当飞机降低时,飞行员在广播里说道:“这是机长,咱们立刻就要着陆了。不要惊慌,由于咱们会在停机坪上停留几分钟,而消防部门会检查飞机。咱们有一些液压元件失效了,他们想要确保没有发生火灾的危险。咱们有两个备用系统,咱们应该没问题。”
咱们都没事。在这种状况下,这架飞机设计得很是好。
这张幻灯片上的图片并非那个航班,而是我两周前乘坐的另外一架,当时美国东部正被埋在 24 英寸厚的雪中。这架飞机(联合 734 航班),我确信它一样可靠,降落在跑道上。但到了休息的时候,它发出了很大的噪音,我猜是 ABS 的飞机,但它仍是继续前进。
咱们跑过了跑道尽头的红灯,你在照片上看到了,在停机坪的尽头,飞机滑出跑道,错过了斜坡,前轮在草地上消失了。每一个人都没事,但这是一个伟大的工程不能每次都能正常运做的例子。
事实上,操做始终是成功部署系统的一个重要因素。这张幻灯片很受理查德·库克( Richard Cook )的演讲启发(其实是被偷了)。若是你不认识他,我建议你去 youtube 上看他演讲的视频,这些视频很是棒。
正确的系统架构和开发实践仍然没法被取代,或者可能因不适当的操做而被打破; 工具,剧本,监控,自动化等的效率和有用性,都趋向隐式依赖于知识和操做条件的彻底考虑(如吞吐量,负载,过载管理等)。若是定义了这些,这些操做限制会让你知道何时事情会变坏,何时再变好。
这些限制的问题在于,当操做员习惯了这些限制,而且习惯了频繁地破坏它们而不产生负面后果,就有可能慢慢地将极限推到危险区域的边缘,在那里会发生严重的大规模故障。你的反应时间和和余地将受到更高的负载会的侵蚀,最终被终结在一个不断被破坏的位置,却没有任何喘息的机会。
因此咱们必须当心,注意这类事情,以及重视人们使用和操做软件的重要性。要想扩大一个优秀团队的规模,老是比扩大一个项目要困难。即便不发生紧急状况也要作好计划预防它们奔溃,当这样的事情发生时你能够轻松的运行模拟程序而且有完备的方法去修复它们。
就像我说的,在个人飞行中没有人受伤。尽管如此,这还是一场为了你们而上演的闹剧:巴士护送乘客返回航站楼,由于运送滞留的飞机可能存在风险。 不少随车将巴士安全地从跑道护送至码头。其中有警车,一大堆消防车,还有那辆我不知道它作什么的黑色汽车,但我相信它很是有用。
尽管每一个人没事,尽管飞机很是可靠,但他们仍是部署了全部这些设备。他们作了正确的事情。
这里有另一些你使用 Erlang 得到的东西。对他们没什么好说的,只是我倾向于对切换使用它有一些兴趣,因此就是这样。
最后一点值得评论。在他们的系统设计方法中很是灵活的语言中发生的风险之一是,你使用的库可能不会按照你认为合理的的方式执行任何操做。这样的情形下你只好不用库,又或者用不连贯的设计来操做代码库。这在 Erlang 中不会发生,由于每一个人都使用相同的通过验证的方法来完成任务。
简而言之,Erlang 之禅和 “让它崩溃”,其实就是搞清楚组件如何相互做用,弄明白什么是关键的,什么不是关键的,什么状态能够保存、保留、从新计算或丢失。在全部的状况下,你都必须想出最坏的状况以及如何度过它。经过使用具备隔离,链路和监视器以及监视器的故障快速机制来限制全部这些最坏状况的规模和传播,你将让它成为一个很是容易理解的常规故障案例。
这听起来很简单,但却有奇效。若是你认为你能够理解的常规失败案例是可行的,那么你全部的错误处理均可以适用于该案例。你再也不须要担忧或编写防护代码。你只要编写代码应该作什么,并让程序的结构决定其他部分。随它崩溃去吧。
这就是 Erlang 的精髓:首先创建互动,确保可能发生的最坏状况仍然是可行的。那么在你的系统中几乎没有错误或失败会让你紧张(当它发生时,你能够在运行时自省一切!),那样你就能够坐下来放松了。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。