自从 erlang OTP 团队开设技术博客以来,很多高质量的文章让我们有机会能够了解 erlang 内部的各种机制。 譬如最近的这篇 https://www.erlang.org/blog/p... ,就讲述了在 erlang 虚拟机中是如何对 “N对1” 的进程消息传递进行性能优化的。
本文只是站在笔者的角度对文章内容进行转述,如有理解错误或者不到位的地方,敬请在评论中指出。
上面这张图很直观地表现了优化的效果,这是在多核机器上,很多进程同时向一个进程发送短消息的性能对比。其中横轴是进程数量,纵轴是每秒操作数。可以看到在优化后,已经实现了水平扩展,即进程数量越多,每秒操作数越多。而在优化之前,进程数越多,性能越低。
在深入了解这个优化是如何做到的之前,先来了解一下 erlang 虚拟机中的信号(signal)机制。
在 erlang 虚拟机中,实体(entity)代表所有并发执行的东西,包括进程、Port 等等。普通的进程消息也是一种信号。信号的顺序遵循以下规则:
如果实体 A 先发送信号 S1 给 B, 然后发送 S2 给 B。那么 S1 保证不会在 S2 之后到达。
通俗地讲,想象一条N个车道的公路,不允许超车,那么在同一条车道上,汽车的顺序是一定的;而不同车道之间,汽车的前后总是在变化。
下图是在优化之前,一个进程内简略结构。
进程发送消息的步骤是这样的:
- 分配一个链表的节点,其中包含信号
- 获取外信号队列(OuterSinalQueue)的锁
- 将信号节点添加到外信号队列的后面
- 释放锁。
进程收取消息的步骤是这样的:
- 获取外信号队列的锁
- 将外信号队列的内容添加到内信号队列(InnerSinalQueue)后面
- 释放锁。
以上是选项 {message_queue_data, off_heap}
开启时的机制。而默认的选项是 {message_queue_data, on_heap}
, 本次的这个优化其实只作用于 off_heap
的情况,也就是如果我们没有对 message_queue_data
这个选项进行配置,那么这个优化就和我们无关。那么默认情况下的消息传递步骤是什么呢?虽然和这个优化无关,但文章里还是详细介绍了一下:
发送消息的步骤:
- 尝试用
try_lock
来获取主进程锁(MainProcessLock)。
如果成功:
1.在进程的主堆(main heap)上为信号分配空间,并将信号复制到那里
2.分配一个链表节点,包含指向那个信号的位置的指针
3.获取外信号队列锁
4.将信号节点添加到外信号队列的后面
5.释放外信号队列锁
6.释放主进程锁
如果失败:
1.分配一个链表的节点,其中包含信号
2.获取外信号队列锁
3.将信号节点添加到外信号队列的后面
4.释放外信号队列锁。
可以看出 on_heap
的好处就是在获取主进程锁成功的情况下,信号数据被直接复制到了进程的主堆上。坏处就是需要获取主进程锁,来防止在这个过程中发生垃圾回收。所以,在非常多的进程同时给一个进程发消息的时候,off_heap
具有更好的扩展性,因为不需要去争抢接收者的主进程锁。
尽管如此,外信号队列锁依旧是一个性能瓶颈。
下面我们可以聊聊如何优化了。
回顾我们之前提到的 erlang 虚拟机对于信号顺序的要求,能看出我们需要的是一条N车道的公路,现在却只有一个收费站(接收者的外信号队列锁),车全堵在这了。优化的方案显然也呼之欲出了,就是增加“收费站“的数量。通过简单地对发送者进程的pid做哈希,将信号分流到64个 slot 队列中。
只有在同时获取外信号队列的进程数量超过一定阈值的时候,此优化才会被触发。
此优化为我们在多核机器上进行 N 对 1 的大量消息传递提供了更好的性能。更多的细节请参见原文。