OTP 23 亮点

2020年5月13日 · 作者:Kenneth Lundin

OTP 23 已经发布(2020年5月13日)。这是一个漫长的过程,在最终发布之前,分别在二月、三月和四月发布了三个候选版本。我们非常感谢大家对候选版本的反馈,这些反馈揭示了一些我们内部测试没有发现的错误和缺陷。

这篇博客文章将介绍 OTP 23 中的一些新亮点。

您可以在这里下载描述更改的自述文件:OTP 23 自述文件。或者,像往常一样,查看您感兴趣的应用程序的发行说明。例如,这里:OTP 23 Erts 发行说明

语言 #

在 OTP 23 中,我们为语言和编译器添加了一些新功能,其中一个自从引入位语法以来就一直存在,另一个是来自社区的建议。

匹配语法改进 #

二进制匹配 #

在二进制匹配中,现在允许匹配的段大小是一个保护表达式。在下面的示例中,变量 Size 被绑定到前 8 位,然后在表达式 (Size-1)*8 中用于以下二进制的大小。

example1(<<Size:8,Payload:((Size-1)*8)/binary,Rest/binary>>) ->​
    {Payload,Rest}.

在映射上匹配 #

在当前的映射匹配语法中,映射模式中的键必须是单个值或文字。如果映射中的键是复杂术语,这会导致不自然的代码。

在 OTP 23 中,映射匹配中的键可以是保护表达式,如您在 new_example2 中看到的那样。

唯一的限制是键表达式中使用的所有变量都必须事先绑定。

以前你必须这样做

example2(M, X) ->
    Key = {tag,X},
    #{Key := Value} = M,
    Value.

现在你可以这样做

new_example2(M, X) ->​
    #{ {tag,X} := Value} = M,​
    Value.​

下面是一个非法示例,显示仍然不支持使用未绑定的变量作为键模式表达式的一部分。在这种情况下,Key 未绑定,并且要求键表达式中使用的所有变量都必须事先绑定。

illegal_example(Key, #{Key := Value}) -> Value.

带下划线的数字字面量 #

现在允许在数字字面量的数字之间使用下划线以提高可读性。但下划线的位置不是完全自由的,有一些规则。请参见下面允许使用的示例

305441741123_456
1_2_3_4_5
123_456.789_123
1.0e1_23
16#DEAD_BEEF
2#1100_1010_0011

在下面的示例中,我们有一些不允许放置下划线的示例

_123  % variable name
123_
123__456  % only single ‘_’
123_.456
123._456
16#_1234
16#1234_

分布式 spawn 和新的 erpc 模块 #

改进的 spawn #

对于分布式情况,即在另一个节点上生成进程时,spawn 操作在可伸缩性和性能方面得到了改进。

还添加了新功能,例如分布式 spawn_monitor() BIF。此函数创建一个新进程并原子地设置监视器。

spawn_opt() BIF 还将支持监视器选项,以便在另一个节点上创建进程时原子地设置监视器。

我们还添加了新的 spawn_request() BIF,用于异步生成进程。spawn_request() 支持 spawn_opt() 已支持的所有选项。

上述 spawn 改进还可以用于优化和改进 rpc 模块中的许多函数,但由于新函数不是 100% 兼容,我们决定引入一个名为 erpc 的新模块,并保留旧的 rpc 模块。

erpc 模块实现了 rpc 模块提供的操作的增强子集。

增强的意义在于,它可以区分返回值、引发的异常和其他错误。

erpc 也比原来的 rpc 实现具有更好的性能和可伸缩性。这是通过利用新引入的 spawn_request() BIF 实现的。

rpc 模块现在与 erpc 共享相同的实现,因此 rpc 的用户将自动受益于 erpc 中所做的性能和可伸缩性改进。

下图说明了旧的和新的 rpc:call() 实现,并说明了为什么新的实现更高效且可伸缩。

“旧”的 rpc:call 实现: 旧 rpc 说明

新的 rcp:call 实现,在分布协议(spawn 请求)中支持 新的 rpc 说明

正如您在上面的“旧”实现中所见,rpc:call 依赖于接收节点上的 rex 进程来生成一个临时进程以执行被调用的函数。如果有许多同时发送到该节点的 rpc:calls,这将使 rex 成为瓶颈。

新的解决方案根本不使用 rex,而是让生成的进程解码调用的参数,从而避免了“旧”实现中发生的一些不必要的数据复制。

gen_tcp 和新的 socket 模块 #

在 OTP 22 中,我们引入了新的实验性 socket API。此 API 背后的想法是拥有一个稳定的中间 API,可用于创建不属于更高级别 gen_* API 的功能。

我们现在在用 socket 作为可选后端来使用 gen_tcp API 的计划中又前进了一步,从而使替换 inet 驱动程序成为可能。

为了使使用 gen_tcp 的现有代码易于测试,可以使用新的选项 {inet_backend, socket | inet} 来选择 socket 实现而不是默认的 inet 实现。此选项必须放在函数:gen_tcp:listengen_tcp:connectgen_tcp:fdopen 的选项列表中的第一位,这些函数都是创建套接字的函数。例如,像这样

{ok,Socket} = gen_tcp:connect(Addr,Port,[{inet_backend,socket}|OtherOpts])

返回的 Socket'$inet' 标记的 3 元组,而不是端口,因此所有其他 API 函数都将使用正确的套接字实现。

更通用的覆盖方法是使用 Kernel 配置变量 inet_backend 并将其设置为 socketinet。例如,在 erl 命令行上,如下所示

erl -kernel inet_backend socket

或者使用以下命令设置

ERL_FLAGS="-kernel inet_backend socket"

shell 中的帮助 #

我们已经实现了 EEP 48,它指定了 BEAM 语言使用的 API 文档的存储格式。通过标准化 API 文档的存储方式,可以编写跨语言工作的工具。

普通的文档构建已扩展,为所有 OTP 模块生成 .chunk 文件。您可以运行 make docs DOC_TARGETS=chunks 仅构建 EEP 48 chunk。不设置 DOC_TARGETS 变量仅运行 make docs 将构建所有格式(html、man、pdf、chunks)。

基于这些新功能,我们已经在 shell 中添加了带有以下函数的在线帮助

h(Module) 
h(Module,Function), 
h(Module,Function,Arity)

还有相应的函数 ht/1,2,3hcb/1,2,3 来获取有关类型和回调函数的帮助

我们在 stdlib 中添加了一个新的模块 shell_docs,其中包含用于为 shell 呈现文档的函数。这可以例如被基于语言服务器协议 (LSP) 的开发环境使用。

code 模块还获得了一个新函数 get_doc,该函数返回文档 chunk 而无需加载模块。

请参见下面的示例,以获取 lists:sort/2 的文档

4> h(lists,sort,2).

  -spec sort(Fun, List1) -> List2
                when
                    Fun :: fun((A :: T, B :: T) -> boolean()),
                    List1 :: [T],
                    List2 :: [T],
                    T :: term().

  Returns a list containing the sorted elements of List1,
  according to the ordering function Fun. Fun(A, B) is to
  return true if A compares less than or equal to B in the
  ordering, otherwise false.
ok

改进的 Tab 补全 #

shell 中的 Tab 补全也得到了改进。以前,模块的 Tab 补全仅适用于已加载的模块,现在已扩展为适用于代码路径中可用的所有模块。补全还扩展为在“help”函数 h、ht 和 hcb 中工作。例如,您可以按下 Tab 键,如下面的示例所示,并获取所有以 l 开头的模块

5> h(l
lcnt                      leex                      lists                     
local_tcp                 local_udp                 log_mf_h                  
logger                    logger_backend            logger_config             
logger_disk_log_h         logger_filters            logger_formatter          
logger_h_common           logger_handler_watcher    logger_olp                
logger_proxy              logger_server             logger_simple_h           
logger_std_h              
logger_sup

或者,像这样完成 lists 模块中所有以 s 开头的函数

5> h(lists,s
search/2     seq/2        seq/3        sort/1       sort/2       split/2      
splitwith/2  sublist/2    sublist/3    subtract/2   suffix/2     sum/1        

“容器友好”功能 #

考虑 CPU 配额 #

在决定默认的在线调度程序数量时,现在会考虑 CPU 配额。

因此,自动使 Erlang 成为应用配额的容器环境(例如带有 --cpus 标志的 docker)中的良好公民。

EPMD 独立性 #

在基于云和容器的环境中,在不使用 epmd 的情况下运行分布式 Erlang 节点并使用硬编码端口或替代服务发现可能很有趣。因此,我们引入了一些方法,使在没有 epmd 的情况下启动和配置系统更容易。

握手 #

我们改进了 Erlang 分布式协议中连接设置期间的握手。现在可以在不依赖 epmd 或其他对等节点版本的先验知识的情况下就协议版本达成一致。

动态节点名称 #

与新握手一起引入的另一个功能是动态节点名称。通过使用选项 -name Name-sname Name 并将 Name 设置为 undefined 来选择动态节点名称。

这些选项使 Erlang 运行时系统成为分布式节点。这些标志调用了节点成为分布式节点所必需的所有网络服务器;请参阅 net_kernel。还确保在 Erlang 启动之前在当前主机上运行 epmd;请参阅 epmd 和 -start_epmd 选项。

OTP 23 中的新功能是将 Name 设置为 undefined,然后该节点将以特殊模式启动,该模式优化为另一个节点的临时客户端。启用后,该节点将从它连接的第一个节点请求动态节点名称。此外,还将暗示这些分布设置

erl -dist_listen false -hidden -dist_auto_connect never

因为 -dist_auto_connect 设置为 never,所以系统必须手动调用 net_kernel:connect_node/1 才能启动分布。如果使用动态节点名称时分布通道关闭,则该节点将停止分布,并且必须再次调用 net_kernel:connect_node/1。请注意,如果分布被删除然后再次建立,则节点名称可能会更改。

注意! 从 OTP 23 开始支持动态节点名称功能。临时客户端节点和第一个连接的对等节点(提供动态节点名称)都必须至少是 OTP 23 才能正常工作。

控制 epmd 使用的新选项 #

为了让用户更好地控制 epmd 的使用,为 inet 分布添加了一些新选项。

  • -dist_listen false 设置分布通道,但不侦听传入连接。当您想要使用当前节点与同一台机器上的另一个节点交互,而无需它加入整个集群时,这很有用。

  • -erl_epmd_port Port 配置内置 EPMD 客户端应返回的默认端口。这允许本地节点知道集群中任何其他节点要连接的端口。
  • -remsh Node 启动 Erlang,并连接到 Node 的远程 shell。如果没有给出 -name-sname,则节点将使用 -sname undefined 启动。如果 Node 使用长名称,则应给出 -name undefined。如果 Node 不包含主机名,则会自动从 -name-sname 选项中获取一个。

    注意 在 OTP-23 之前,用户需要为 -remsh 提供有效的 -sname-name 才能工作。如果目标节点运行的不是 OTP-23 或更高版本,则仍然是这种情况。

# starting the E-node test
erl -sname test@localhost 

# starting a temporary E-node (with dynamic name) as a remote shell to
# the node test
erl -remsh test@localhost 

erl_epmd 回调 API 也已扩展,允许返回 -1 作为创建号,这意味着节点将创建一个随机的创建号。

此外,还添加了一个名为 listen_port_please 的新回调函数,允许回调返回分发应使用的监听端口。如果要从外部服务获取监听端口,则可以使用此函数代替 inet_dist_listen_min/max

erl_call 的新选项 #

erl_call 是一个 C 程序,最初作为 erl_interface 应用程序中的示例捆绑在一起。erl_interface 包含用于与 Erlang 节点通信并使 C 程序表现得像 Erlang 节点的 C 库。它们被称为 C 节点。erl_call 已经变得很流行,主要用于产品中,用于管理同一主机上的 Erlang 节点。在 OTP 23 中,erl_call 安装在与 erl 相同的路径下,使其在路径中可用,而无需考虑 erl_interface 版本。erl_call 的另一个新功能是 address 选项,它可用于直接连接到节点,而无需依赖 epmd 来解析节点名称。

据我所知,erl_call 正被用于即将发布的 relx 版本(由 rebar3 使用)的 node_tool 功能中。

TLS 增强和更改 #

现在支持 TLS-1.3(在 OTP 22 中,我们将其归类为实验性的),但尚未完全实现所有功能。支持的关键功能是

  • 会话票证
  • 刷新会话密钥
  • RSASSA-PSS 签名
  • 中间件兼容性。

尚不支持“早期数据”功能。早期数据是 TLS 1.3 中引入的优化,允许客户端在连接的第一个往返过程中向服务器发送数据,而无需等待 TLS 握手完成(如果客户端最近与同一服务器通信过)。

在 OTP 23 中,TLS 1.3 默认由客户端和服务器声明为首选协议版本。未显式配置 TLS 版本的用户应注意这一点,因为它可能会影响互操作性。

ssl:cipher_suites/2,3 提供了一个新选项 exclusive,并扩展了 ssl:versions 以更好地反映 Erlang/OTP 当前设置可用的 TLS 版本。

另请注意,我们已删除对旧版 TLS 版本 SSL-3.0 的支持。

SSH #

两个值得注意的 SSH 功能是由开源用户通过 Pull Request 提供的,即支持从 ssh-agents 获取密钥和 TCP/IP 端口转发。端口转发有时称为隧道或 tcp-forward/direct-tcp。在 OpenSSH 客户端中,端口转发对应于选项 -L 和 -R。

Ssh 代理存储的密钥提高了安全性,而端口转发通常用于在两个主机之间获得加密隧道。在密钥处理方面,默认密钥插件 ssh_file.erl 已重写并使用 OpenSSH 文件格式“openssh-key-v1”进行了扩展。到目前为止的一个限制是新格式的密钥无法加密。默认插件现在还使用端口号,这提高了安全性。

现在可以在 Erlang 配置文件中配置 SSH 应用程序。这使得可以例如在不更改代码的情况下更改支持的算法集。

加密 #

在 OTP-22.0 中引入了一个新的加密 API。引入新 API 的主要原因是使用 OpenSSL libcrypto EVP API,该 API 允许在机器支持的情况下进行硬件加速。加密算法的命名也已系统化,现在遵循 OpenSSL 中的模式。

Crypto 应用程序的某些部分正在使用非常旧的 API,而其他部分正在使用最新的 API。事实证明,以新的方式使用旧的 API,同时保持向后兼容性是不可能的。

因此,旧的 API 暂时保留,但它是使用新的原语实现的。旧的 API 在 OTP-23.0 中已被弃用,将在 OTP-24.0 中删除。