如何设计基于Erlang/OTP的分布式容错多核系统架构?

43
我想构建一个基于Erlang/OTP的系统,用于解决“尴尬并行”问题。
我已经阅读/浏览过:
- 学习Erlang; - Erlang编程(Armstrong); - Erlang编程(Cesarini); - Erlang/OTP实战。
我已经了解了进程、消息传递、监督器、gen_servers、日志记录等内容。
我确实明白某些架构选择取决于特定应用程序,但仍然希望了解一些ERlang/OTP系统设计的一般原则。
我应该从带有监督器的几个gen_servers开始,并逐步构建吗?
我应该有多少监督器?我如何决定哪些部分应该基于进程?我应该如何避免瓶颈?
我应该后添加日志记录吗?
Erlang/OTP分布式容错多处理器系统架构的一般方法是什么?
1个回答

116

我应该从几个gen_servers和一个监督者开始,并逐步构建吗?

在Erlang架构中,您缺少一个关键组件:应用程序!(即OTP应用程序的概念,而不是软件应用程序)。

将应用程序视为组件。系统中的组件解决特定问题,负责一组相关资源或从系统中抽象出重要或复杂的内容。

设计Erlang系统的第一步是决定需要哪些应用程序。有些可以从Web中提取,我们可以将其称为库。其他应用程序需要自己编写(否则您就不需要这个特定的系统)。这些应用程序通常被称为业务逻辑(通常还需要自己编写一些库,但保持库与核心业务应用程序之间的区别很有用)。

我应该有多少监督者?

您应该为要监视的每种进程拥有一个监督者。

一堆相同的临时工人?一个监督者来管理它们。

不同的进程具有不同的职责和重启策略?每种不同类型的进程都应有一个监督者,在正确的层次结构中(取决于何时重新启动以及其他进程何时需要关闭?)。

有时候把不同的进程类型放在同一个监管者下是可以的。当您有一些单例进程(例如,一个HTTP服务器监管者,一个ETS表所有者进程,一个统计收集器)总是运行时,通常会出现这种情况。在这种情况下,为每个进程都建立一个监管者可能会太过繁琐,因此将它们添加到一个监管者下是很常见的。只要注意使用特定的重启策略时的影响,以免在Web服务器崩溃时导致统计进程崩溃(在这种情况下,使用“one_for_one”是最常见的策略)。请注意,在“one_for_one”监管者中不要有任何进程之间的依赖关系。如果一个进程依赖于另一个崩溃的进程,它也可能会崩溃,从而触发监管者的重启强度过高,使监管者本身过早崩溃。这可以通过拥有两个不同的监管者来避免,它们将完全控制由配置的强度和周期引起的重启(更长的解释)。
如何决定系统的哪些部分应该基于进程?
系统中的每个并发活动都应该在自己的进程中。错误的并发抽象是Erlang系统设计者在开始时最常见的错误。
有些人不习惯处理并发;他们的系统往往缺乏并发性。一个进程,或几个巨大的进程,按顺序运行所有内容。这些系统通常充满了代码气味,代码非常僵硬和难以重构。它们也会变慢,因为它们可能没有使用Erlang可用的所有内核。
其他人可能立即掌握并发概念,但无法最优地应用它们;他们的系统往往过度使用进程概念,导致许多进程处于等待其他正在工作的进程而空闲的状态。这些系统往往过于复杂且难以调试。
本质上,在两种变体中,你都会遇到同样的问题,即没有充分利用所有可用的并发性,并且无法从系统中获得最大的性能。
如果你坚持单一责任原则并遵守为系统中每个真正并发活动设置一个进程的规则,那么你应该没问题。
有时候保留空闲进程是有合理的原因的。有时它们保留重要状态,有时你想暂时保留一些数据然后丢弃进程,有时它们在等待外部事件。更大的陷阱是通过长链传递重要消息的大部分不活跃进程,因为这会通过大量复制减慢系统速度并使用更多内存。
我应该如何避免瓶颈?
很难说,这取决于你的系统和它正在做什么。通常来说,如果你在应用程序之间有良好的责任划分,你应该能够将看起来是瓶颈的应用程序与系统的其余部分分开扩展。
这里的黄金法则是测量、测量、测量!在进行测量之前,不要认为你有什么可以改进的。

Erlang非常棒,因为它允许您将并发隐藏在接口后面(称为隐式并发)。例如,您可以使用函数模块API,一个普通的module:function(Arguments)接口,该接口可以反过来生成数千个进程,而调用者无需知道。如果您正确使用了抽象和API,则始终可以在开始使用之后并行化或优化库。

话虽如此,以下是一些一般性的指导原则:

  • 尝试直接向收件人发送消息,避免通过中间进程传递或路由消息。否则,系统只会花费时间移动消息(数据),而不是真正地工作。
  • 不要过度使用OTP设计模式,例如gen_servers。在许多情况下,您只需要启动一个进程,运行一些代码,然后退出。对于这个,gen_server就过度了。

还有一个额外的建议:不要重复使用进程。在Erlang中生成进程是如此便宜和快速,以至于在其生命周期结束后重新使用进程是没有意义的。在某些情况下,重用状态可能是有意义的(例如,文件的复杂解析),但最好将其规范地存储在其他位置(在ETS表、数据库等中)。

我应该稍后添加日志记录吗?

您现在应该添加日志记录!Erlang/OTP从21版本开始带有一个称为Logger的强大内置API:

logger:error("The file does not exist: ~ts",[Filename]),
logger:notice("Something strange happened!"),
logger:debug(#{got => connection_request, id => Id, state => State},
             #{report_cb => fun(R) -> {"~p",[R]} end}),

这个新API有几个高级功能,应该可以满足大多数需要记录日志的情况。还有旧的但仍广泛使用的第三方库Lager
Erlang/OTP分布式容错多处理器系统架构的一般方法是什么?
总结以上内容:
- 将系统划分为应用程序 - 根据进程的需求和依赖关系将其放在正确的监控层次结构中 - 对于系统中每个真正并发的活动都要有一个进程 - 向系统中的其他组件维护一个功能API。这样可以:
- 重构代码而不改变使用它的代码 - 之后优化代码 - 在需要时分发您的系统(只需调用API后面的另一个节点!呼叫者不会注意到!) - 更轻松地测试代码(设置测试工具更少,更容易理解如何使用它)
- 开始使用OTP中可用的库,直到需要其他东西(当时机成熟时,你会知道的) 常见问题:
- 进程过多 - 进程过少 - 路由过多(转发消息,链接进程) - 应用程序过少(实际上我从未见过相反的情况) - 缺乏足够的抽象(使重构和推理变得困难。它也使测试变得困难!)

1
很棒的答案,+100(如果我可以这样做) - Andriy Tylychko
+100,真希望我在设计我的系统之前看到了这个。最终我也设计出了几乎完全相同的内容,但是我为此付出了很多努力。不幸的是,我必须等待23个小时才能授予此赏金。 - Seth Carnegie
你有没有什么资源可以让我阅读,以获得对Erlang的这种理解,还是你能够纯粹地根据自己的经验撰写这个答案? - dimiguel
2
这仅仅是我个人的经验,但我强烈推荐阅读《Learn You Some Erlang》的后续章节,并阅读《Erlang in Anger》。此外,还有其他几本书,其中一些更为高级。 - Adam Lindberg
1
另一个很棒的新资源是采用 Erlang 电子书: https://adoptingerlang.org/docs/development/otp_high_level/ - Adam Lindberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接