目前,在超时或连接丢失发生后,没有轻量级的机制来阻止服务器向客户端发送延迟回复。今天防止延迟回复的唯一方法是通过代理进程发送请求。
提出的进程别名功能是一种轻量级机制,可以解决上述问题。进程别名类似于一个注册名称,在请求未完成时临时使用。如果请求超时或与服务器的连接丢失,则别名将被停用,从而阻止延迟回复到达客户端。
本文档已置于公共领域。
别名是 Erlang 类型 reference()
,可以在使用 !
操作符发送时或使用 erlang:send()
和 erlang:send_nosuspend()
BIF 发送时用作目标。别名可以在分布式系统中本地节点和远程节点上使用。别名标识一个存在于或曾经存在于节点上的进程,节点名称由 node(Alias)
返回。
从现在开始,所有引用都将被接受为上述消息发送操作中的目标。如果引用不是别名或已停用的先前别名,则消息将被静默丢弃。
引入了以下新的 BIF
alias/0
,alias/1
。alias()
BIF 创建并返回一个别名,该别名可以在向调用 alias()
BIF 的进程发送消息时使用。alias/1
BIF 接受一个选项列表作为参数,其中包含以下接受的选项
explicit_unalias
- 别名将保持活动状态,直到被 unalias/1
BIF 停用。reply
- 当接收到使用别名发送的回复消息时,别名将自动停用。unalias/1
。unalias(Alias)
BIF 停用标识调用进程的别名。如果别名 Alias
标识了调用进程并因此被停用,则 BIF 返回 true
;否则,别名状态没有发生更改,并返回 false
。
monitor/3
。monitor/3
BIF 是 monitor/2
BIF 的扩展,其中第三个参数是一个选项列表。自引入以来,它接受两个选项
{alias, UnaliasOpt}
。二元组的第一个元素表示我们希望返回的监视器引用也作为别名工作。第二个元素确定别名应如何停用
explicit_unalias
- 别名将保持活动状态,直到被 unalias/1
BIF 停用。demonitor
- 当监视器停用时,别名将被停用。也就是说,当在监视器上调用 demonitor()
BIF 时,或者当监视器通过接收 'DOWN'
消息自动停用时。仍然可以通过调用 unalias/1
BIF 在此之前停用别名。reply_demonitor
- 当监视器停用或收到使用别名传递的消息时,别名将被停用。如果别名由于使用别名传递的消息而停用,则监视器也会停用,就像调用了 demonitor()
BIF 一样。{tag, UserDefinedTag}
。这将替换默认的 Tag
,在触发监视器时传递的监视器消息中使用 UserDefinedTag
。例如,在监视进程时,down 消息中的 'DOWN'
标签将被 UserDefinedTag
替换。
spawn_opt()
和 spawn_request()
BIF 也已扩展为接受选项 {monitor, MonitorOpts}
,其中 MonitorOpts
对应于 monitor/3
BIF 的选项列表。
有关这些 BIF 和选项的完整文档,请通过 pull request #2735 获取,其中包含参考实现。
无法检索由别名标识的进程的进程标识符,也无法测试引用是否为别名。
如前所述,可以通过使用代理进程将回复转发给客户端来防止延迟回复。通过生成代理进程并将其进程标识符发送到服务器,而不是客户端自己的进程标识符,可以在操作超时或连接丢失时终止代理。由于代理进程不处于活动状态,因此回复将被静默丢弃,并且不会有任何错误消息到达之前的请求客户端。然而,这使得代码更加复杂,并且效率低于实际需要。低效率来自于需要创建、调度、执行和终止代理进程,以及通过代理进程额外复制数据。
当客户端代码的作者完全控制客户端进程时,可以无需代理即可处理此类延迟回复,因为代码可以意识到这些潜在的错误消息并在收到时将其丢弃。然而,在实现库代码时这是不可能的。那么您要么需要使用代理进程,如 gen_statem
行为所做的那样,要么接受客户端进程可能会在调用后收到错误消息,如 gen_server
行为所做的那样。
进程别名以非常小的开销解决了这些问题。
这或多或少就是引用数据类型的用途。一种可以标识大量不同实体的数据类型。引用是唯一的,并且包含一个节点标识符,用于标识其来源节点。这使得在标识同一进程创建的不同别名的同时,可以轻松地标识特定节点上的特定进程。嵌入的节点标识符使提供分布透明性变得容易。
预期的最常见用例是客户端服务器请求。例如 gen_server:call()
。Erlang 中的客户端服务器请求通常是在客户端监视服务器时进行的。为了最大程度地减少请求中产生和发送的数据,我们希望重用为标识监视器而创建的引用,使其也可以用作别名。由于监视器标识符被记录为引用且不透明(人们可能会认为这是引入监视器时的设计错误),因此很难不将别名的类型记录为引用。
原因有两个。分布透明性和可扩展性。
分布透明性确实是可取的,因为无论它是节点本地操作还是节点远程操作,用户都可以使用相同的功能。名称注册 API 不是分布透明的。
关于可扩展性。由于名称注册 API 的设计方式,我们需要某种表才能实现该 API。此表将由并行执行的进程写入和读取。在我们关注的用例中,名称(别名)预计是临时的,并且会大量创建。也就是说,来自不同处理器上执行的进程将对该表进行大量修改。这将使实现一个可良好扩展的表变得具有挑战性。
在提出的解决方案中,将消息路由到正确位置所需的信息保存在别名本身(引用)中。确定是否应丢弃或传递通过别名传递的消息所需的信息保存在由别名标识的进程中。也就是说,所有需要的信息都分配到需要它的地方,而不是集中在节点全局表中。这种分布式信息的方法在完全实现后(下面会详细介绍)完全不会引入任何新的同步点,这将实现非常好的扩展。基于节点全局表的实现从可扩展性的角度来看 永远 无法与之竞争。
使用这种分布式信息方法无法实现已有的注册名称的功能,但需要集中存储名称。也就是说,无法使用已有的 API。
除了节点标识符之外,今天的引用还包含三个 32 位的数据字,换句话说,是 96 位的数据。由于历史原因,在这 96 位中,只有 82 位允许通过分布传递到另一个节点。当引用驻留在本地时,它可以包含更多或更少无限量的数据。82 位不足以在本地节点上创建唯一的引用,同时唯一地标识节点本地进程。为了能够将所有需要的信息存储在别名中,需要扩展引用数据类型。
在提出的解决方案中,用作别名的引用扩展为在 64 位架构上使用五个 32 位字,在 32 位架构上使用四个 32 位字。由于引用中的大量数据今天无法通过分布传递,因此参考实现将活动的别名保存在节点全局表中。当节点本地别名通过分布进入本地节点时,需要在该表中查找它,以便能够将其恢复为实际值。当别名在本地传递时,不需要在此表中查找。
参考实现还修改了分发协议,允许引用最多包含五个 32 位的值。出于向后兼容性的原因,在引入别名时,不能立即使用对分发协议的此修改。这是因为我们需要能够在一段时间内与之前版本的旧节点通信。当它在系统中存在足够的时间(预计是 OTP 26)后,我们就可以开始发送最多包含五个 32 位字的引用,并删除表映射引用到别名的用法。也就是说,只有当这种情况发生时,别名实现才算完全完成。
最重要的是,为了解决别名旨在解决的问题,无需知道别名所指进程的 PID。用户应该在一个协议中使用别名,在该协议中知道一个引用是否是别名,并且不需要知道它所指的进程的 PID。
除此之外,此功能还存在其他问题。引用的内容只是一个大的整数。为了保持分发的透明性,要么必须指定如何解释这个整数,要么需要与标识进程所在的节点进行同步信号。同步信号将非常昂贵。通过指定如何解释引用整数,我们将阻止将来对如何解释引用整数进行更改,这可能会阻止未来的优化、改进和新功能。在可以通过分发传递包含五个 32 位字的大引用之前,同步通信也是实现此功能的唯一选择。
如果我们应该模仿已注册名称 API 的 whereis()
函数,您也可以看到名称是否当前已注册,那么除了与别名标识的进程进行同步信号之外,没有其他选择。
原因与为什么无法获取别名所指进程的 PID 相同。
这样的功能可以解决别名旨在解决的相同问题,但是这种方法存在问题。
除 PID、端口和引用之外的项的数据类型中没有嵌入节点标识符。对于此类数据类型,您需要其他方法来标识注册名称的节点。在当前将原子作为注册名称的情况下,这是通过将名称包装在包含节点名称的二元组中来完成的。对于除普通 PID、端口和引用之外的所有其他项,都需要类似的东西。这也引入了一个问题。二元组只是一个名称还是一个名称加上一个节点标识符?
是否应该可以将 PID 注册为另一个进程的名称?这将迫使所有发送操作在执行操作之前首先在注册名称表中查找 PID。这将导致所有发送操作的性能下降。端口也是如此。
我们认为不应实现任意项的注册,因为会出现问题。当前仅允许原子的注册功能在您需要为同一服务注册多个进程时可能有点过于受限。一种选择可能是允许注册包含原子和整数的二元组。也许还应该允许其他项(例如字符串),但不应允许任意项。
允许将引用用作注册名称意味着别名 API 中不存在的可扩展性瓶颈。也就是说,这将是解决我们着手解决的问题的一种较差的解决方案。
人们可能希望扩展名称注册以允许比原子更多的项,但这只是为了解决别名旨在解决的问题之外的其他问题。名称注册 API 不适合别名,因此我们认为不应将别名与此类注册 API 的扩展结合使用。别名解决方案解决了我们着手解决的问题,因此此 EEP 仅限于此。
当在 spawn_request()
调用中使用 monitor 选项 alias
时,您会遇到不必要的延迟,因为在获得带有子进程进程标识符的 spawn 回复之前,您无法与子进程共享别名。相反,您通常希望在 spawn_request()
调用之前显式创建别名,并将其作为参数传递给子进程。
在典型场景中,您希望收到操作的响应或错误。但是,如果在 spawn_request()
操作之前显式创建一个别名,则监视器引用和别名将是不同的引用。这将阻止编译器优化接收(跳过在创建引用时消息队列中存在的消息),因为并非所有接收子句都将匹配相同的引用。
我们通过使用 tag
监视器选项以及 reply_tag
spawn 请求来解决此问题。以下是在具有别名原型实现的系统上使用此方法的完整 rpc 实现
rpc(Node, M, F, A) ->
Alias = alias([reply]),
ReqId = spawn_request(Node,
fun () ->
Result = apply(M, F, A),
Alias ! {{result, Alias}, Result}
end,
[{monitor, [{tag, {'DOWN', Alias}}]},
{reply_tag, {spawn_reply, Alias}},
{reply, error_only}]),
receive
{{result, Alias}, Result} ->
demonitor(ReqId, [flush]),
Result;
{{'DOWN', Alias}, ReqId, process, _, Error} ->
rpc_error_cleanup(Alias, Error);
{{spawn_reply, Alias}, ReqId, error, Error} ->
rpc_error_cleanup(Alias, Error)
end.
rpc_error_cleanup(Alias, Error) ->
case unalias(Alias) of
true ->
%% No flush needed since we used the 'reply' option
%% to alias(), and the alias was still active...
error({rpc_error, Error});
false ->
%% Flush a possible result message...
receive {{result, Alias}, Result} -> Result
after 0 -> error({rpc_error, Error})
end
end.
tag
监视器选项也可以在其他情况下使用,以便获得一个在来自一组进程的所有类型的响应中都存在的单个引用。这些进程可能是预先存在的,也可能不是。然后,可以利用此引用来确定消息是否对应于对特定进程组执行的特定操作。
计划扩展接收优化,以便所有子句中匹配相同引用的多个接收可以使用优化。这也将提高在此类实现中接收与相同引用匹配的多个消息的性能。
监视器消息中使用的标记本地存储在设置监视器的进程中,无需在进程之间进行通信。最重要的是,在分布式情况下,它不必通过网络发送。这也意味着它也可以在监视不支持此功能的旧节点上的进程时使用。
别名功能是一个纯粹的扩展,因此不存在真正的向后兼容性问题。
为了能够通过以前版本的 Erlang 节点传递别名,我们无法通过分发传递大型引用,因此需要在节点全局表中保留有关别名的信息。该实现得益于能够在分发过程中传递更大的引用,但直到我们可以强制要求能够处理如此大的引用时才会这样做。OTP 24 和 OTP 25 都能够通过分发处理大型引用,并且由于我们仅保证与向后和向前的两个最近版本的分发兼容性,因此我们可以在 OTP 26 中强制使用大型引用。
与使用进程的 PID 发送相比,此节点全局别名表在使用别名时会引入开销。这是由于表结构的分配和操作。与使用代理进程来防止错误消息的现有解决方案相比,此节点全局别名表的开销很小。幸运的是,此节点全局表也仅需要临时存在,并且可以在 OTP 26 中删除。
参考实现由 pull request #2735 提供。
除了别名功能的实现之外。pull 请求还包含在 gen 行为(例如 gen_server)中对别名的使用。因此,现在也可以实现类似于 erpc:receive_response()
的 receive_response()
功能,该功能也已实现
gen_server:receive_response/2
gen_statem:receive_response/2
gen_event:receive_response/2
tag
监视器选项。alias/1
的 once
选项更改为 reply
。alias/1
的 unalias
选项更改为 explicit_unalias
。在 alias
监视器选项的 UnaliasOpt
部分中,unalias
更改为 explicit_unalias
。