[elixir! #0080] 读 erlang 开发团队博客 之 N 对 1 并行消息的性能优化

自从 erlang OTP 团队开设技术博客以来,很多高质量的文章让我们有机会能够了解 erlang 内部的各种机制。 譬如最近的这篇 https://www.erlang.org/blog/p... ,就讲述了在 erlang 虚拟机中是如何对 “N对1” 的进程消息传递进行性能优化的。

本文只是站在笔者的角度对文章内容进行转述,如有理解错误或者不到位的地方,敬请在评论中指出。

[elixir! #0080] 读 erlang 开发团队博客 之 N 对 1 并行消息的性能优化_第1张图片

上面这张图很直观地表现了优化的效果,这是在多核机器上,很多进程同时向一个进程发送短消息的性能对比。其中横轴是进程数量,纵轴是每秒操作数。可以看到在优化后,已经实现了水平扩展,即进程数量越多,每秒操作数越多。而在优化之前,进程数越多,性能越低。

在深入了解这个优化是如何做到的之前,先来了解一下 erlang 虚拟机中的信号(signal)机制。

在 erlang 虚拟机中,实体(entity)代表所有并发执行的东西,包括进程、Port 等等。普通的进程消息也是一种信号。信号的顺序遵循以下规则:

如果实体 A 先发送信号 S1 给 B, 然后发送 S2 给 B。那么 S1 保证不会在 S2 之后到达。 

通俗地讲,想象一条N个车道的公路,不允许超车,那么在同一条车道上,汽车的顺序是一定的;而不同车道之间,汽车的前后总是在变化。

下图是在优化之前,一个进程内简略结构。

[elixir! #0080] 读 erlang 开发团队博客 之 N 对 1 并行消息的性能优化_第2张图片

进程发送消息的步骤是这样的:

  1. 分配一个链表的节点,其中包含信号
  2. 获取外信号队列(OuterSinalQueue)的锁
  3. 将信号节点添加到外信号队列的后面
  4. 释放锁。

进程收取消息的步骤是这样的:

  1. 获取外信号队列的锁
  2. 将外信号队列的内容添加到内信号队列(InnerSinalQueue)后面
  3. 释放锁。

以上是选项 {message_queue_data, off_heap} 开启时的机制。而默认的选项是 {message_queue_data, on_heap}, 本次的这个优化其实只作用于 off_heap 的情况,也就是如果我们没有对 message_queue_data 这个选项进行配置,那么这个优化就和我们无关。那么默认情况下的消息传递步骤是什么呢?虽然和这个优化无关,但文章里还是详细介绍了一下:

发送消息的步骤:

  1. 尝试用 try_lock 来获取主进程锁(MainProcessLock)。
    如果成功:
    1.在进程的主堆(main heap)上为信号分配空间,并将信号复制到那里
    2.分配一个链表节点,包含指向那个信号的位置的指针
    3.获取外信号队列锁
    4.将信号节点添加到外信号队列的后面
    5.释放外信号队列锁
    6.释放主进程锁
    如果失败:
    1.分配一个链表的节点,其中包含信号
    2.获取外信号队列锁
    3.将信号节点添加到外信号队列的后面
    4.释放外信号队列锁。

可以看出 on_heap 的好处就是在获取主进程锁成功的情况下,信号数据被直接复制到了进程的主堆上。坏处就是需要获取主进程锁,来防止在这个过程中发生垃圾回收。所以,在非常多的进程同时给一个进程发消息的时候,off_heap具有更好的扩展性,因为不需要去争抢接收者的主进程锁。

尽管如此,外信号队列锁依旧是一个性能瓶颈。

下面我们可以聊聊如何优化了。

回顾我们之前提到的 erlang 虚拟机对于信号顺序的要求,能看出我们需要的是一条N车道的公路,现在却只有一个收费站(接收者的外信号队列锁),车全堵在这了。优化的方案显然也呼之欲出了,就是增加“收费站“的数量。通过简单地对发送者进程的pid做哈希,将信号分流到64个 slot 队列中。

[elixir! #0080] 读 erlang 开发团队博客 之 N 对 1 并行消息的性能优化_第3张图片

只有在同时获取外信号队列的进程数量超过一定阈值的时候,此优化才会被触发。

此优化为我们在多核机器上进行 N 对 1 的大量消息传递提供了更好的性能。更多的细节请参见原文。

你可能感兴趣的:(elixirerlang)