当你部署一个分布式Erlang应用的时候,你最可能必须要回答的问题是:“哪些端口需要在防火墙里打开?”。除非进行配置,否则答案就是:
- epmd(Erlang端口映射守护进程)端口4369,
- Erlang节点自己的端口,一个不可预知的大数字端口。
这个答案通常都不能简单地应用到防火墙的规则。通常的解决方法是用环境变量 inet_dist_listen_min 和 inet_dist_listen_max (官方的kernel文档里有描述)来限制分布式端口在一个小范围或者甚至是一个单独的端口。
但是如果我们限制了Erlang节点在一个单独的端口,那么我们还真的需要一个端口映射吗?运行epmd有以下几个潜在的缺点:
- 我们可能想要以运行Erlang节点的用户来运行epmd,但是正如我们将见到的,这个很难得到保证。
- 我们可能想要配置epmd监听一个特定的端口,但是这依赖于在本机上我们如何第一次运行epmd。
- 任何人无需身份验证可以连接epmd并且可以列出本地的所有Erlang节点。
- 与epmd的连接都没有加密,即使你按 Erlang distribution over TLS 这篇文章的方法去做,与epmd的连接也是不受保护的,并且有被中间人攻击的潜在风险。
- 虽然epmd很稳定,它也有发生崩溃的可能。一旦它崩溃了,任何已经存在的分布式Erlang节点将继续运行,即使epmd恢复了,这些节点也不会重新连接empd。这意味着存在的分布式连接继续工作着,但是与这些节点的新的连接将不起作用。阅读俄文博客 Экстренная реанимация epmd 可能可以在不重启epmd的情况下解决这个问题。
那么我们来研究一下epmd是如何工作的,然后我们如何做就可以在不用epmd的情况下运行一个Erlang集群。
epmd是如何第一次启动的?
让我们来读读源码!epmd是在erlexec.c文件的start_epmd函数里被启动的。实际上,每一次一个Erlang分布式节点启动epmd都无条件地被启动。如果一个epmd实例已经在运行,新的epmd在侦听端口4369的时候将失败,它将因此悄无声息地结束运行。
事实上,这就是即使由另一个用户启动的epmd实例存在系统将发生的事情。任何epmd实例都非常乐意为由任何用户启动的Erlang节点服务,因此这个通常不会引起任何问题。
那么谁可以连接epmd?
任何人都可以!默认情况下,epmd侦听在所有有效的接口上,并且响应关于当前有哪些节点以及这些节点所侦听的端口的查询。一听到这个场景,系统管理员应该有点紧张。
你可以在epmd启动前通过手工指定 -address 选项或设定ERL_EPMD_ADDRESS 环境变量启动epmd来改变上述情况。这个方法在官方文档里有描述。这个方法需要在你第一次启动epmd的时候来做,否则,已经存在的epmd实例将不受影响地继续运行。
为什么我们需要一个端口映射守护进程?
很明显,epmd就是一个中间人。我们可以绕过这个中间人吗?
我们可以让每个Erlang节点侦听在一个大家熟知的端口–如果我们打算去除掉epmd的话,我们可以用empd默认保留的端口,4369。但是这就意味着我们只能够在每个主机上运行一个Erlang节点(当然,在某些场景下这就足够了)。
那么让我们指定一些其他端口。前面我们提到变量 inet_dist_listen_min 和 inet_dist_listen_max 。这两个变量定义一个端口范围,不过如果我们设置它们为相同的值,我们就把范围缩小到一个端口:
|
|
这个方法不错,不过我们也需要告诉其他节点不要再去询问epmd来获取端口号,而是只用这个端口号来代替。如果我们在同一个主机上有几个节点,我们需要为这些节点配置不同的端口号。
用别的方法
在Erlang/OTP 19.0里,有两个新的命令行选项:
- 当我们指定 -start_epmd false,Erlang将不会在启动一个分布式节点的时候尝试去启动epmd;
- 选项 -epmd_module foo 让你指定一个不同的模块替代默认的模块erl_epmd,用来节点名字注册和查询。
接下来我们来构建一些代码!
我想用一个无状态的方式来实现这个方法:既然发起连接的节点知道了它要连接的节点的名字,那么我就用这个名字作为资源来得到相应的端口号。我选择4370这个端口作为基准端口,它比epmd的端口大一。接着我从节点名称的“节点”部分抽取出数字,比如节点名称为 myapp3@foo.example.com 抽取的数字是3。最后我把这个数字和基准端口相加。由此得知,节点 myapp3@foo.example.com 侦听在端口4373。如果节点名字最后不是数字,我这种情况将处理为0。这意味着节点 myapp3 和 myotherapp3 不能运行在同一个主机上,但是我准备好了处理对这种场景。(感谢 Luca Favatella 的完美建议)
为此我们来写一个小模块:
|
|
同时我们还要写一个模块用作 -epmd_module。有一点小复杂就是因为OTP19.0期望这个模块导出 register_node/2 函数,而从OTP19.1开始则是期望导出 register_node/3 函数。我们把这两个函数都包含进去。
|
|
正如你所见,大多数事情基本完成了。
当这个模块被加为 erl_distribution 监督者的子进程的时候,start_link/0 将被调用。我们并不需要在这个启动一个进程,因此我们只是返回 ignore。
register_node函数通常会连接epmd并告诉它我们使用哪一个端口。epmd将返回一个“creation”数字。“creation”数字上一个介于1和3的整数。epmd跟踪每一个节点名字的“creation”数字并且当一个确定名字的节点重连的时候增加这个数字。这意味着它可以区分先前某一个节点“存活期间”的进程id。
既然我们没有epmd,那么我们就没有获得它跟踪节点生命周期的好处。在这里我们返回一个随机数,它有三分之二的机会不同于前面的“creation”数字。
port_please/2 获取远程主机的IP地址以便连接它的epmd,但是我们不关心这些;我们用我们自己的算法来算出我们的端口号。
我们也需要返回一个分布式协议版本号。从Erlang/OTP 6开始它就是5(参阅 分布式协议官方文档 ),就这么简单。
最后,names/1 函数被调用来返回某个主机上的Erlang节点列表。我们没有办法知道这些信息,所以让我们假装我们连接不上这个主机。
目前为止,一起都顺利。但是我们需要一个方法来确认我们正侦听在合适的端口上。我认为最好的方法是写一个新的分布式协议模块,该模块仅是设置端口号而让真正的协议模块做它自己的事情:
|
|
几乎所有的代码都在这。listen/1函数在将控制权交给真正的处理模块inet_tcp_dist前,根据我们的节点名来设置inet_dist_listen_min和inet_dist_listen_max两个变量。
(请注意:虽然inet_tcp_dist是默认的处理模块,但是它仅提供基于IPv4上的未加密连接。如果你想用IPv6,你应该用inet6_tcp_dist,而如果你想用基于TLS上的Erlang分布式功能,那就应该选择inet_tsl_dist或inet6_tsl_dist。增加这些弹性的尝试留给读者自己练习。)
我们准备好了!现在我们启动两个节点,foo1和foo2,并且使它们彼此互连:
|
|
系统能正常运行吗?
|
|
看起来没问题!
接着在Elixir下尝试
当然,因为Erlang和Elixir都运行在同样的虚拟机上,那么就没有什么可以阻止我们在Elixir里实现上述功能了。
在Elixir里,我们可以将所有代码放在一个单独文件里,编译器会将它编译成我们需要的不同的模块:
|
|
当我们启动Elixir,我们需要用 - -erl 来传递一些参数来使得功能启用:
|
|
让我们ping我们前面已经启动的两个节点:
|
|
它们都连通了并且没有epmd参与!
结论
本文描述的只是其中一种不需要epmd的Erlang分布式方式;我相信你可以用别的办法来更好地符合你的需求。我希望上述的示例代码是有用的指南!
原文链接: https://www.erlang-solutions.com/blog/erlang-and-elixir-distribution-without-epmd.html