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

Posted

技术标签:

【中文标题】您如何设计基于 Erlang/OTP 的分布式容错多核系统的架构?【英文标题】:How do you design the architecture of an Erlang/OTP-based distributed fault-tolerant multicore system? 【发布时间】:2011-11-10 13:55:41 【问题描述】:

我想构建一个基于 Erlang/OTP 的系统来解决“令人尴尬的并行”问题。

我已经阅读/略读:

向您学习一些 Erlang; Erlang 编程(阿姆斯壮); Erlang 编程(Cesarini); Erlang/OTP 在行动。

我已经掌握了进程、消息传递、主管、gen_servers、日志记录等的要点。

我确实了解某些架构选择取决于所关注的应用程序,但我仍然想知道一些 ERlang/OTP 系统设计的一般原则。

我应该从几个带有主管的 gen_servers 开始并在此基础上逐步构建吗?

我应该有多少个主管?我如何决定系统的哪些部分应该基于流程?我应该如何避免瓶颈?

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

Erlang/OTP 分布式容错多处理器系统架构的一般方法是什么?

【问题讨论】:

【参考方案1】:

我应该从几个 gen_servers 和一个主管开始,然后在此基础上逐步构建吗?

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

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

设计 Erlang 系统的第一步是决定需要哪些应用程序。有些可以原样从网上提取,我们可以将它们称为库。其他你需要自己写的(否则你不需要这个特定的系统)。我们通常将这些应用程序称为业务逻辑(通常您还需要自己编写一些库,但区分这些库和将所有内容联系在一起的核心业务应用程序很有用)。

我应该有多少个主管?

对于要监控的每种流程,您都应该有一名主管。

一群一模一样的临时工?一个主管来统治他们。

不同的进程,不同的职责和重启策略?每个不同类型流程的主管,在正确的层次结构中(取决于什么时候应该重新启动以及需要哪些其他流程与它们一起关闭?)。

有时可以将一堆不同的进程类型置于同一个主管之下。当您有几个始终运行的单例进程(例如,一个 HTTP 服务器主管、一个 ETS 表所有者进程、一个统计收集器)时,通常会出现这种情况。在这种情况下,为每个人配备一名主管可能太麻烦了,因此通常会添加一名下属的主管。请注意在执行此操作时使用特定重启策略的含义,因此您不要关闭统计过程,例如,以防万一您的 Web 服务器崩溃(one_for_one 是在这种情况下使用的最常见策略)。请注意不要在one_for_one 主管中的进程之间存在任何依赖关系。如果一个进程依赖于另一个崩溃的进程,它也可能崩溃,过于频繁地触发监督者的重启强度,并且太快地让监督者本身崩溃。这可以通过使用两个不同的主管来避免,这将完全按照配置的强度和周期来控制重启 (longer explanation)。

我如何决定系统的哪些部分应该基于流程?

系统中的每个并发活动都应该在它自己的进程中。错误的并发抽象是 Erlang 系统设计者一开始最常犯的错误。

有些人不习惯处理并发;他们的系统往往太少了。一个进程,或几个巨大的进程,按顺序运行所有内容。这些系统通常充满代码异味,代码非常死板,难以重构。这也使它们变慢,因为它们可能不会使用 Erlang 可用的所有内核。

其他人立即掌握并发概念但未能最佳应用它们;他们的系统倾向于过度使用进程概念,使许多进程处于空闲状态,等待其他正在工作的进程。这些系统往往过于复杂且难以调试。

本质上,在这两种变体中,您都会遇到相同的问题,您不会使用所有可用的并发性,也不会从系统中获得最大的性能。

如果您坚持single responsibility principle 并遵守规则,为系统中的每个真正并发活动都有一个流程,那么您应该没问题。

拥有空闲进程是有正当理由的。有时它们保持重要状态,有时您想暂时保留一些数据然后丢弃该过程,有时它们等待外部事件。更大的陷阱是通过一长串大部分不活动的进程传递重要消息,因为它会因大量复制和使用更多内存而减慢系统速度。

我应该如何避免瓶颈?

很难说,很大程度上取决于您的系统和它在做什么。不过,一般来说,如果您在应用程序之间有良好的责任划分,您应该能够将似乎是瓶颈的应用程序与系统的其余部分分开扩展。

这里的黄金法则是测量、测量、测量!在你衡量之前不要认为你有什么需要改进的地方。

Erlang 的伟大之处在于它允许您将并发隐藏在接口后面(称为隐式并发)。例如,您使用一个功能模块 API,一个普通的module:function(Arguments) 接口,它可以反过来产生数千个进程,而调用者不必知道这一点。如果您的抽象和 API 正确,则在开始使用库后,您始终可以并行化或优化库。

话虽如此,以下是一些通用指南:

尝试直接向收件人发送消息,避免通过中间过程引导或路由消息。否则系统只会花时间移动消息(数据)而没有真正工作。 不要过度使用 OTP 设计模式,例如 gen_servers。在很多情况下,你只需要启动一个进程,运行一段代码,然后退出。为此,gen_server 是多余的。

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

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

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

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 具有多项高级功能,应该涵盖您需要记录的大多数情况。还有较旧但仍被广泛使用的第 3 方库 Lager。

Erlang/OTP 分布式容错多处理器系统架构的一般方法是什么?

总结上面所说的:

将您的系统划分为应用程序 根据流程的需要和依赖关系,将流程置于正确的监督层次结构中 为系统中的每一个真正并发的活动都有一个进程 为系统中的其他组件维护一个功能性 API。这让您: 重构您的代码而不更改正在使用它的代码 事后优化代码 在需要时分发您的系统(只需调用 API 后面的另一个节点!调用者不会注意到!) 更轻松地测试代码(设置测试工具的工作更少,更容易理解如何使用它) 开始使用 OTP 中提供给您的库,直到您需要不同的东西(到时候您就会知道)

常见的陷阱:

进程过多 进程太少 路由过多(转发消息、链式进程) 应用程序太少(实际上我从未见过相反的情况) 没有足够的抽象(使得重构和推理变得困难。这也使得测试变得困难!)

【讨论】:

很好的答案,+100(如果我能做到的话) +100,我希望我在设计我的一个系统之前就看到了这个,我最终想出了与这个几乎完全相同的东西,但我为此遇到了很多麻烦。不幸的是,我必须等待 23 小时才能获得赏金。 您是否有任何我可以阅读的资源来获得对 Erlang 的这种理解,或者您是否能够纯粹根据 Erlang 的经验写出这个答案? 这只是经验之谈,不过我会推荐Learn You Some Erlang 和Erlang in Anger 的后面章节。还有a handful of other books,比其他的更高级。 另一个很棒的新资源和即将推出的资源是采用 Erlang 电子书:adoptingerlang.org/docs/development/otp_high_level

以上是关于您如何设计基于 Erlang/OTP 的分布式容错多核系统的架构?的主要内容,如果未能解决你的问题,请参考以下文章

1.Erlang/OTP平台

EMQ --集成搭建

使用 Erlang/OTP 构建容错软实时 Web 应用程序

使用 OTP/Erlang 作为 Web 应用程序基于组件的架构的一部分

Erlang/OTP 消息可靠吗?消息可以复制吗?

Erlang 细节记录