我是否应该从几个带有主管的 gen_servers 开始并在此基础上逐步构建?
您在这里缺少 Erlang 架构中的一个关键组件:应用程序!(即OTP应用程序的概念,而不是软件应用程序)。
将应用程序视为组件。系统中的组件解决特定问题,负责一组连贯的资源或从系统中抽象出重要或复杂的东西。
设计 Erlang 系统的第一步是决定需要哪些应用程序。有些可以原样从网上提取,我们可以将它们称为库。其他你需要自己写的(否则你不需要这个特定的系统)。我们通常将这些应用程序称为业务逻辑(通常您还需要自己编写一些库,但保持库与将所有内容联系在一起的核心业务应用程序之间的区别很有用)。
我应该有多少个主管?
对于要监控的每种流程,您都应该有一名主管。
一群一模一样的临时工?一位主管来统治他们。
不同的进程,不同的职责和重启策略?每个不同类型的流程的主管,在正确的层次结构中(取决于什么时候应该重新启动以及需要哪些其他流程与它们一起下降?)。
有时可以将一堆不同的流程类型放在同一个主管下。当您有几个始终运行的单例进程(例如,一个 HTTP 服务器主管、一个 ETS 表所有者进程、一个统计收集器)时,通常会出现这种情况。在这种情况下,为每个人配备一名主管可能太麻烦了,因此通常会添加一名下属的主管。请注意在执行此操作时使用特定重启策略的含义,因此您不要关闭统计过程,例如,以防万一您的 Web 服务器崩溃(one_for_one
在这种情况下最常用的策略是)。注意不要在进程之间有任何依赖关系one_for_one
导师。如果一个进程依赖于另一个崩溃的进程,它也可能崩溃,过于频繁地触发监督者的重启强度,并且太快地让监督者本身崩溃。这可以通过拥有两个不同的监督者来避免,这将通过配置的强度和周期完全控制重新启动(更长的解释)。
我如何决定系统的哪些部分应该基于流程?
系统中的每个并发活动都应该在它自己的进程中。错误的并发抽象是 Erlang 系统设计者一开始最常见的错误。
有些人不习惯处理并发;他们的系统往往太少了。一个进程,或几个巨大的进程,按顺序运行所有内容。这些系统通常充满代码异味,代码非常死板,难以重构。这也使它们变慢,因为它们可能不会使用 Erlang 可用的所有内核。
其他人立即掌握了并发概念,但未能最佳地应用它们;他们的系统倾向于过度使用进程概念,使许多进程处于空闲状态,等待其他正在工作的进程。这些系统往往过于复杂且难以调试。
本质上,在这两种变体中,您都会遇到相同的问题,您没有使用所有可用的并发性,也没有从系统中获得最大的性能。
如果您坚持单一职责原则并遵守规则,为系统中的每一个真正并发的活动制定一个流程,那么您应该没问题。
拥有空闲进程是有正当理由的。有时它们保持重要状态,有时您想暂时保留一些数据然后丢弃该过程,有时它们等待外部事件。更大的陷阱是通过一长串大部分不活动的进程传递重要消息,因为它会因大量复制而减慢系统速度并使用更多内存。
我应该如何避免瓶颈?
很难说,很大程度上取决于你的系统和它在做什么。一般来说,如果您在应用程序之间有良好的责任划分,您应该能够将看起来是瓶颈的应用程序与系统的其余部分分开扩展。
这里的黄金法则是测量,测量,测量!在你测量之前不要认为你有什么需要改进的地方。
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 有几个高级特性,应该涵盖大多数需要记录的情况。还有较旧但仍被广泛使用的 3rd 方库Lager。
Erlang/OTP 分布式容错多处理器系统架构的一般方法是什么?
总结一下上面所说的:
- 将您的系统划分为应用程序
- 将您的流程置于正确的监督层次结构中,具体取决于它们的需求和依赖关系
- 为系统中的每一个真正并发的活动制定一个流程
- 为系统中的其他组件维护一个功能性 API。这让您:
- 在不更改使用它的代码的情况下重构您的代码
- 事后优化代码
- 在需要时分发您的系统(只需调用 API 后面的另一个节点!调用者不会注意到!)
- 更轻松地测试代码(设置测试工具的工作更少,更容易理解如何使用它)
- 开始使用 OTP 中可用的库,直到您需要不同的东西(到时候您会知道的)
常见的陷阱:
- 进程太多
- 进程太少
- 路由过多(转发消息、链式进程)
- 应用程序太少(实际上我从未见过相反的情况)
- 没有足够的抽象(使得重构和推理变得困难。这也使得测试变得困难!)