> * 原文地址：[As bad as anything else: Part 2](https://ferd.ca/the-zen-of-erlang.html)
> * 原文作者：[Fred T-H](https://ferd.ca)
> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/the-zen-of-erlang-2.md](https://github.com/xitu/gold-miner/blob/master/TODO1/the-zen-of-erlang-2.md)
> * 译者：[7Ethan](https://github.com/7Ethan)
> * 校对者：[K.Lew](https://github.com/kasheemlew), [satansk](https://github.com/satansk)

# 就像其它的任何事物一样糟糕

## Erlang 之禅

**如果你还没看本文的第一部分，请先阅读第一部分：[Erlang 之禅：第一部分](https://github.com/xitu/gold-miner/blob/master/TODO1/the-zen-of-erlang-1.md)。**

![生产环境中的 Bug](https://ferd.ca/static/img/zen-of-erlang/015.png)

我在这部分想要阐述的是以我的经验去说说在生产中每种类型错误的出现频率。没有任何明显的证据表明利用查找错误和错误的发生概率有联系。但是我的直觉告诉我，这种关系是存在的。

首先，在核心特性中容易复现的错误不应该出现在产品中。如果这些（容易复现的）bugs 确实在生产环境中出现，那么你实际上已经发布了一个破产品，再多的重新启动或者技术支持都不会帮到你的用户。这种问题需要修改代码，并且可能是生产该产品的组织内部一些根深蒂固的问题的后果。

边缘特性中的可复现 bugs 很可能会流入生产环境，我认为这是没有花足够的时间去合理测试它们的结果。但当涉及到部分重构时，次要功能往往会被摆在次要位置，或者设计者没有充分考虑这些功能要与系统的其他部分保持一致。

另一方面，瞬变错误一直存在。吉姆·格雷发明了这些术语，他报告说，在给定的客户站点中，132 个错误里只有一个是波尔 Bug（可复现的 bug）。生产环境中遇到的 bug 中有 131/132 是海森堡Bug（不可复现的 bug）。它们很难被触发，如果它们是真正的错误，可能每一百万次就只出现一次，那么你的系统就会一直需要一些负载来捕捉它们;在一个每秒处理 10 万请求的系统中，10 亿之一的 bug 每 3 小时出现一次，百万分之一的 bug 每 10s 就会出现一次，但在测试环境中，类似 bug 很少出现。

如果处理不当，就将会有很多的错误和失败。

![通过重启来处理 bug](https://ferd.ca/static/img/zen-of-erlang/016.png)

那么，重启作为一种策略有多有效呢？

对于核心功能上的可复现的 bug，重新启动是没用的。对于不经常使用的代码路径中的可复现的 bug，这取决于不同情况;如果这个功能对于非常少的用户来说非常重要，那么重新启动不会有太大的作用。如果这是每个人都使用的一个小功能，但在某种程度上他们并不太在意，那么重新开始或忽略失败就够了。例如，如果facebook 的 ‘poke’ 功能失效(不知道这个问题是否还存在)，也不会对很多用户的体验有影响。

对于瞬态错误，重启是非常有效的，而且它们往往是我们遇到的常见错误。由于它们难以复现，所以它们的出现通常依赖于特定情况或系统中状态的交织，并且它们的出现往往只占所有操作的一小部分，重启往往会使它们消失。

回滚到已知的稳定状态，再重复一次相同的操作，不太可能碰到导致这种情况的奇怪上下文。因此，可能发生的灾难只不过是系统的一个小插曲，用户很快就学会适应了。

然后，你可以使用日志记录、跟踪或各种自检工具(这些工具在 Erlang 中都是现成的)来查找、理解和修复问题，以保证它们不再发生。或者你可以决定容忍它们，因为解决问题需要付出巨大的努力。

![臭名昭著的 bsd](https://ferd.ca/static/img/zen-of-erlang/017.png)

这个问题是在一个论坛上提出来的，当时我正在讨论编程内容和 Erlang 模型。我一字不差地摘录了它，因为这是一个很好的例子，很多人听到重启和 Erlang 的特性时都会问这个问题。

我想通过一个现实的例子来具体说明如何在 Erlang 中设计一个系统，这更能突出它的特性。

![监控树示例](https://ferd.ca/static/img/zen-of-erlang/018.png)

通过监督者(圆角矩形)，我们可以开始创建深层次的流程。这里我们有一个选举系统，有两棵树:一棵计数树和一棵实时报告树。计数树负责计数和存储结果，而实时报告树则是让人们连接到它以查看结果。

通过定义子节点的顺序可以知道，计数树启动后实时报告树才会开始运行。除非存储层可用，否则分区子树（关于每个分区的计数结果）将不会运行。如果存储工作者池（将连接到数据库）可用，则只能启动存储的缓存。

我前面提到的监督策略让我们在程序结构中对这些需求进行编码，并且它们在运行时仍然存在的，而不仅仅是在启动时。例如，管理人员可能会采用一对一策略，这意味着各区域可能各自失败，而不会影响彼此之间的计数。相比之下，每个地区(魁北克和安大略的管理者)都可以采取休息策略。 因此，这一策略可以确保 OCR 程序始终可以将检测到的投票发送给“计数”工作人员，并且即使经常崩溃也不会对其造成影响。 另一方面，如果计数工作人员无法保存和存储状态，它的停止会中断 OCR 程序，确保没有任何数据丢失。

这个 OCR 进程本身可能是用 C 语言编写的监视代码，作为独立的代理，并与其链接。 这将进一步隔离该 C 语言代码与虚拟机的故障，以实现更好的隔离或并行化。

我要指出的另一件事是，每个主管都有对失败的可配置容忍度;区域主管可能非常宽容，每分钟处理 10 次故障，而存储层如果预期是正确的，则可能对故障相当不宽容，如果我们希望它是正确的，则在每小时 3 次崩溃后永久关闭。

在这个程序中，关键的功能更接近树的根，这样能更少的移动和更加坚固。他们不受兄弟节点消亡的影响，但他们自己的失败影响到其他人。叶子完成了所有的工作，并且可以很好地丢失 —— 一旦它们吸收了数据并在上面进行光合作用，它就可以进入核心。

因此，通过定义所有这些，我们可以将危险的代码隔离在一个具有高容忍度或正在被监控的进程中，并在数据进入系统时将数据移至更稳定的进程。 如果 C 语言中的 OCR 代码有危险，它可以失败并安全地重新启动。 当它工作时，它将其信息传输到 Erlang OCR 进程。 该过程可以进行验证，也可以自行崩溃，也许不会。 如果信息是可靠的，则将其移至 Count 过程，该进程的任务是保持非常简单的状态，并最终通过存储子树将该状态刷新到数据库，这是安全独立的。

如果 OCR 进程死亡，它会自动重启。如果它奔溃得太频繁，它就会将自己的管理器关闭，子树的那一部分也会重新启动 —— 不会影响系统的其他部分。如果这能解决问题，很好。如果没有，这个过程就会不断重复，直到它工作，直到整个系统停止，因为某些东西显然出错了，我们无法通过重新启动来处理它。

以这种方式构建系统具有巨大的价值，因为错误处理被嵌入到系统的结构中。这意味着我可以不用在边缘节点中编写恶心的防御代码 —— 如果出了问题，让其他人(或程序的结构)来决定如何反应。如果我知道如何处理一个错误，那么我可以对那个特定的错误这么做。否则，就让它崩溃吧!

这种方式倾向于转换代码。慢慢地，你会发现它不再包含大量的 if/else 或 switch 或 try/catch 表达式。相反，它包含了清晰的代码，解释当一切正常时代码应该做什么。它不再包含许多形式的猜测，你的软件可读性更强。

![监督子树](https://ferd.ca/static/img/zen-of-erlang/019.png)

当我们退一步看我们的程序结构时，我们可能会发现，在黄色环绕的每个子树中，在它们所做的事情上似乎都是相互独立的;它们的依赖关系大多是合乎逻辑的:例如，报表系统需要一个存储层进行查询。

例如，如果我可以交换存储实现或在其他系统中独立使用它，那也是非常好的。将实时报告系统隔离到不同的节点或开始提供替代手段（例如 SMS）也可能很整洁。

我们现在需要的是找到一种方式来打破这些子树，并将它们转化为我们可以组合，重用的逻辑单元，并且我们可以独立配置，重新启动或开发。

![OTP apps](https://ferd.ca/static/img/zen-of-erlang/020.png)

Erlang 将 OTP 用作解决方案。OTP 应用程序是构建这种子树的代码以及一些元数据。该元数据包含基本内容，如版本号和应用程序的描述，以及指定应用程序之间的依赖关系的方法。 这非常有用，因为它可以让我的存储应用程序与系统的其他部分保持独立，但仍然对计数应用程序在运行时的需要进行编码。我可以保留我在系统中编码的所有信息，但现在它是由独立块构建的，这些块更容易理解。

实际上，人们认为 OTP 应用程序是 Erlang 的库。 如果您的代码库不是 OTP 应用程序，那么它在其他系统中不可重用。 [旁注：有许多方法可以指定实际上不包含子树的 OTP 库，只是由其他库重用的模块]

搞掂一切后，我们的 Erlang 系统现在已经定义了以下所有属性：

*   对系统的生存至关重要或不重要
*   什么是可以失败的，以及在不再可持续之前它能够以何种频率这样做
*   软件应该如何根据哪些保证以及以什么样的顺序启动
*   软件应该如何失败，这意味着它定义了你所处的部分失败的合法状态，以及如何在发生这种情况时回滚到已知的稳定状态
*   软件如何升级（因为它可以根据监督结构进行实时升级）
*   组件如何相互依赖

这是非常有价值的。更有价值的是迫使每个开发人员在早期就从这种角度去考虑。你的防守代码较少，发生奔溃时系统会继续运行。你只需要查看日志或实时系统状态，并花时间修复问题（如果您觉得这是值得的时间）。

![晚上睡觉](https://ferd.ca/static/img/zen-of-erlang/021.png)

完成这一切后，我应该可以安稳的睡大觉了，对吧？希望是的。我这里展示的是我们几年前在 Heroku 上部署的一个像素图表。

图的最左边是在 9 月左右。那时，我们的新代理层([vegur](https://github.com/heroku/vegur)）已经投入生产了大约 3 个月，我们已经解决了其中的大部分问题。用户没有问题，过渡进行得很顺利，新的功能正在被使用。

在某个时候，一个团队成员为我们用来聚合异常的日志记录服务收到了非常昂贵的信用卡帐单。 那时候我们看了一眼，看到了图表最左边的恐怖：我们每天产生 500,000 到 1,200,000 个异常！额滴神，这太多了吧。 但是呢？ 如果问题是一个 heisenbug，而我们的系统每秒收到 100,000 个请求，那么它发生的几率是多少？在 1/17000 到 1/7000 之间。这很频繁，但是因为它对服务没有影响，所以直到带宽和存储账单来了我们才注意到它。

我们花了一点时间才弄清错误，然后我们修正了错误。你可以看到，此后的异常率仍然很低，可能每天几十万。他们都是我们所知道的，但是没有影响。两年后，我们还没有着手解决这个问题，因为尽管如此，系统还是可以正常工作的。

![预料失败](https://ferd.ca/static/img/zen-of-erlang/022.png)

与此同时，你不可能总能安稳的睡大觉。尽管你采用了最佳的设计方法，但失败可能会失控。

几年前，我乘坐过一趟飞往温哥华的航班。当飞机下降时，飞行员在广播里说道：“这是机长，我们马上就要着陆了。不要惊慌，因为我们会在停机坪上停留几分钟，而消防部门会检查飞机。我们有一些液压元件失效了，他们想要确保没有发生火灾的危险。我们有两个备用系统，我们应该没问题。”

我们都没事。在这种情况下，这架飞机设计得非常好。

这张幻灯片上的图片并不是那个航班，而是我两周前乘坐的另一架，当时美国东部正被埋在 24 英寸厚的雪中。这架飞机(联合 734 航班)，我确信它同样可靠，降落在跑道上。但到了休息的时候，它发出了很大的噪音，我猜是 ABS 的飞机，但它还是继续前进。

我们跑过了跑道尽头的红灯，你在照片上看到了，在停机坪的尽头，飞机滑出跑道，错过了斜坡，前轮在草地上消失了。每个人都没事，但这是一个伟大的工程不能每次都能正常运作的例子。

![危险区](https://ferd.ca/static/img/zen-of-erlang/023.png)

事实上，操作始终是成功部署系统的一个重要因素。这张幻灯片很受理查德·库克( Richard Cook )的演讲启发(实际上是被偷了)。如果你不认识他，我建议你去 youtube 上看他演讲的视频，这些视频非常棒。

正确的系统架构和开发实践仍然无法被取代，或者可能因不适当的操作而被打破; 工具，剧本，监控，自动化等的效率和有用性，都趋向隐式依赖于知识和操作条件的完全考虑（如吞吐量，负载，过载管理等）。如果定义了这些，这些操作限制会让你知道什么时候事情会变坏，什么时候再变好。

这些限制的问题在于，当操作员习惯了这些限制，并且习惯了频繁地破坏它们而不产生负面后果，就有可能慢慢地将极限推到危险区域的边缘，在那里会发生严重的大规模故障。你的反应时间和和余地将受到更高的负载会的侵蚀，最终被终结在一个不断被破坏的位置，却没有任何喘息的机会。

所以我们必须小心，注意这类事情，以及重视人们使用和操作软件的重要性。要想扩大一个优秀团队的规模，总是比扩大一个项目要困难。即使不发生紧急情况也要做好计划预防它们奔溃，当这样的事情发生时你可以轻松的运行模拟程序并且有完备的方法去修复它们。

![飞机应急措施](https://ferd.ca/static/img/zen-of-erlang/024.png)

就像我说的，在我的飞行中没有人受伤。尽管如此，这仍是一场为了大家而上演的闹剧:巴士护送乘客返回航站楼，因为运送滞留的飞机可能存在风险。 很多随车将巴士安全地从跑道护送至码头。其中有警车，一大堆消防车，还有那辆我不知道它做什么的黑色汽车，但我相信它非常有用。

尽管每个人没事，尽管飞机非常可靠，但他们还是部署了所有这些设备。他们做了正确的事情。

![其他好处](https://ferd.ca/static/img/zen-of-erlang/025.png)

这里有另外一些你使用 Erlang 获得的东西。对他们没什么好说的，只是我倾向于对切换使用它有一些兴趣，所以就是这样。

最后一点值得评论。在他们的系统设计方法中非常灵活的语言中发生的风险之一是，你使用的库可能不会按照你认为合理的方式执行任何操作。这样的情形下你只好不用库，又或者用不连贯的设计来操作代码库。这在 Erlang 中不会发生，因为每个人都使用相同的经过验证的方法来完成任务。

![组件如何交互](https://ferd.ca/static/img/zen-of-erlang/026.png)

简而言之，Erlang 之禅和 “让它崩溃”，其实就是搞清楚组件如何相互作用，弄明白什么是关键的，什么不是关键的，什么状态可以保存、保留、重新计算或丢失。在所有的情况下，你都必须想出最坏的情况以及如何度过它。通过使用具有隔离，链路和监视器以及监视器的故障快速机制来限制所有这些最坏情况的规模和传播，你将让它成为一个非常容易理解的常规故障案例。

这听起来很简单，但却有奇效。如果你认为你可以理解的常规失败案例是可行的，那么你所有的错误处理都可以适用于该案例。你不再需要担心或编写防御代码。你只要编写代码应该做什么，并让程序的结构决定其余部分。随它崩溃去吧。

![Erlang 之禅](https://ferd.ca/static/img/zen-of-erlang/027.png)

这就是 Erlang 的精髓：首先建立互动，确保可能发生的最坏情况仍然是可行的。那么在你的系统中几乎没有错误或失败会让你紧张（当它发生时，你可以在运行时自省一切！），那样你就可以坐下来放松了。


---

> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。
