自從 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 的大量訊息傳遞提供了更好的效能。更多的細節請參見原文。