[elixir! #0080] 讀 erlang 開發團隊部落格 之 N 對 1 並行訊息的效能優化

Ljzn發表於2021-11-12

自從 erlang OTP 團隊開設技術部落格以來,很多高質量的文章讓我們有機會能夠了解 erlang 內部的各種機制。 譬如最近的這篇 https://www.erlang.org/blog/p... ,就講述了在 erlang 虛擬機器中是如何對 “N對1” 的程式訊息傳遞進行效能優化的。

本文只是站在筆者的角度對文章內容進行轉述,如有理解錯誤或者不到位的地方,敬請在評論中指出。

image.png

上面這張圖很直觀地表現了優化的效果,這是在多核機器上,很多程式同時向一個程式傳送短訊息的效能對比。其中橫軸是程式數量,縱軸是每秒運算元。可以看到在優化後,已經實現了水平擴充套件,即程式數量越多,每秒運算元越多。而在優化之前,程式數越多,效能越低。

在深入瞭解這個優化是如何做到的之前,先來了解一下 erlang 虛擬機器中的訊號(signal)機制。

在 erlang 虛擬機器中,實體(entity)代表所有併發執行的東西,包括程式、Port 等等。普通的程式訊息也是一種訊號。訊號的順序遵循以下規則:

如果實體 A 先傳送訊號 S1 給 B, 然後傳送 S2 給 B。那麼 S1 保證不會在 S2 之後到達。 

通俗地講,想象一條N個車道的公路,不允許超車,那麼在同一條車道上,汽車的順序是一定的;而不同車道之間,汽車的前後總是在變化。

下圖是在優化之前,一個程式內簡略結構。

image.png

程式傳送訊息的步驟是這樣的:

  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 佇列中。

image.png

只有在同時獲取外訊號佇列的程式數量超過一定閾值的時候,此優化才會被觸發。

此優化為我們在多核機器上進行 N 對 1 的大量訊息傳遞提供了更好的效能。更多的細節請參見原文。

相關文章