作者
Rickard Green <rickard(at)erlang(dot)org>
狀態
最終版/24.0 已在 OTP 版本 24 中實作
類型
標準追蹤
建立日期
2019 年 9 月 1 日
Erlang 版本
OTP-24.0

EEP 53:防止延遲回覆到達客戶端的處理序別名 #

摘要 #

目前,在逾時或連線遺失發生後,沒有輕量級機制可以防止伺服器向客戶端傳送延遲回覆。如今,防止延遲回覆的唯一方法是透過代理處理序發出請求。

建議的處理序別名功能是一種輕量級機制,可解決上述問題。處理序別名類似於暫時使用的已註冊名稱,以處理未完成的請求。如果請求逾時或與伺服器的連線遺失,別名將會停用,這可防止延遲的回覆到達客戶端。

版權 #

本文檔已置於公有領域。

規格 #

別名屬於 Erlang 類型 reference(),當使用 ! 運算子傳送訊息,或當使用 erlang:send()erlang:send_nosuspend() BIF 傳送訊息時,可作為目的地。別名可同時在分散式系統的本機節點和遠端節點上使用。別名識別節點上存在或曾經存在的處理序,其節點名稱由 node(Alias) 傳回。

從現在開始,所有參考都會被接受為上述訊息傳送操作的目的地。如果參考不是別名,也不是已停用的先前別名,訊息將會被無聲地捨棄。

引入下列新的 BIF

  • alias/0alias/1alias() BIF 建立並傳回一個別名,該別名可用於在將訊息傳送給呼叫 alias() BIF 的處理序時使用。alias/1 BIF 將選項清單作為引數,並接受以下選項

    • explicit_unalias - 別名將會保留,直到它被 unalias/1 BIF 停用為止。
    • reply - 當收到使用別名傳送的回覆訊息時,別名將會自動停用。
  • unalias/1unalias(Alias) BIF 會停用識別呼叫處理序的別名。如果別名 Alias 識別呼叫處理序並因此而被停用,則 BIF 會傳回 true;否則,別名狀態沒有任何變更,並且傳回 false

  • monitor/3monitor/3 BIF 是 monitor/2 BIF 的延伸,其中第三個引數是選項清單。在其引入時,它接受兩個選項

    • {alias, UnaliasOpt}。二元組的第一個元素表示我們希望傳回的監視器參考也作為別名。第二個元素決定別名應如何停用

      • explicit_unalias - 別名將會保留,直到它被 unalias/1 BIF 停用為止。
      • demonitor - 當監視器停用時,別名將會停用。也就是說,當在監視器上呼叫 demonitor() BIF 時,或當接收到 'DOWN' 訊息時監視器自動停用時。別名仍可在這之前,透過呼叫 unalias/1 BIF 來停用。
      • reply_demonitor - 當監視器停用時,或接收到使用別名傳遞的訊息時,別名將會停用。如果別名因使用別名傳遞的訊息而停用,則監視器也會停用,如同已呼叫 demonitor() BIF 一樣。
    • {tag, UserDefinedTag}。這會將監視器觸發時傳遞的監視器訊息中的預設 Tag 取代為 UserDefinedTag。例如,當監視處理序時,下行訊息中的 'DOWN' 標籤將被 UserDefinedTag 取代。

spawn_opt()spawn_request() BIF 也已擴充為接受 {monitor, MonitorOpts} 選項,其中 MonitorOpts 對應於 monitor/3 BIF 的選項清單。

這些 BIF 和選項的完整文件可透過 pull request #2735 找到,其中包含參考實作。

無法擷取別名所識別處理序的處理序識別碼,也無法測試參考是否為別名。

動機 #

如先前所述,可透過使用將回覆轉發給客戶端的代理處理序來防止延遲回覆。透過產生代理處理序,並將其處理序識別碼傳送至伺服器,而不是客戶端自己的處理序識別碼,代理可以在操作逾時或連線遺失時終止。由於代理處理序未運作,因此回覆將會被無聲地捨棄,且沒有任何異常訊息會到達先前的請求客戶端。但是,這不僅使程式碼更複雜,而且效率也比需要低。效率低落的原因來自於需要建立、排程、執行和終止代理處理序,以及透過代理處理序額外複製資料。

當客戶端程式碼的作者完全控制客戶端處理序時,可以無需使用代理來處理此類延遲回覆,因為程式碼可以知道這些潛在的異常訊息,並在收到時捨棄它們。但是,在實作程式庫程式碼時,這是不可行的。然後,您要么需要使用代理處理序(如 gen_statem 行為所做的那樣),要么接受客戶端處理序可能會在呼叫後取得異常訊息(如 gen_server 行為所做的那樣)。

處理序別名透過非常小的額外負荷解決了這些問題。

基本原理 #

為什麼要使用參考資料類型作為別名?#

這或多或少是參考資料類型的用途。一種可識別大量不同實體的資料類型。參考是唯一的,並包含一個識別其來源節點的節點識別碼。這使其容易識別特定節點上的特定處理序,同時也識別相同處理序建立的不同別名。內嵌的節點識別碼使其易於提供分配透明度。

為什麼不將別名設為不透明資料類型?#

預期的最常見的使用案例是在客戶端伺服器請求中。例如 gen_server:call()。Erlang 中的客戶端伺服器請求通常是在從客戶端監視伺服器時發出的。為了盡量減少請求中產生和傳送的資料,我們希望重複使用為監視器識別建立的參考,使其也作為別名。由於監視器識別碼被記錄為參考,而不是不透明(可以說這是在引入監視器時的設計錯誤),因此很難不將別名的類型也記錄為參考。

為什麼不允許參考作為現有 API 中的已註冊名稱?#

有兩個原因。分配透明度和可擴展性。

由於使用者可以以相同的方式使用此功能,無論它是本機節點操作還是遠端節點操作,分配透明度非常理想。名稱註冊 API 不是分配透明的。

關於可擴展性。由於名稱註冊 API 的設計方式,我們需要某種表格才能實作 API。並行執行的處理序將會讀取和寫入此表格。在我們關注的使用案例中,名稱 (別名) 預計是暫時的,並且會大量建立。也就是說,從不同處理器上執行的處理序會對此表格進行大量的修改。這將使實作可良好擴展的此類表格具有挑戰性。

在建議的解決方案中,將訊息路由至正確位置所需資訊儲存在別名本身(即參考)中。判斷透過別名傳遞的訊息是否應該捨棄或傳遞所需資訊,則會儲存在別名所識別的處理序中。也就是說,所有需要的資訊都會分發到需要的地方,而不是集中在節點全域表格中。這種分散式資訊方法在完全實作後(以下會詳細介紹),不會引入任何新的同步點,這將可以非常良好地擴展。基於節點全域表格的實作在可擴展性方面永遠無法與其競爭。

現有的已註冊名稱功能無法使用此分散式資訊方法實作,但需要集中儲存這些名稱。也就是說,無法使用現有的 API。

除了節點識別碼之外,參考現在還包含三個 32 位元的資料字組,換句話說,就是 96 位元的資料。在這 96 位元中,只有 82 位元允許透過分散式傳遞至另一個節點。這是出於歷史原因。雖然參考存在於本機,但它或多或少可以包含無限量的資料。82 位元不足以在本機節點上產生唯一的參考,同時也能唯一識別本機節點處理序。為了能夠將所有需要的資訊儲存在別名中,需要擴充參考資料類型。

在建議的解決方案中,用作別名的參考會擴充為在 64 位元架構上使用五個 32 位元的字組,在 32 位元架構上使用四個 32 位元的字組。由於參考中如此多的資料無法透過分散式傳遞,因此參考實作將作用中的別名儲存在節點全域表格中。當本機節點別名透過分散式進入本機節點時,需要在此表格中查找它,才能將其還原為實際值。當別名在本機傳遞時,無需在此表格中查找。

參考實作也修改了分散式通訊協定,以允許最多有五個 32 位元值的參考。由於向後相容性原因,在引入別名時,無法立即使用對分散式通訊協定的此修改。因為我們需要能夠在一段時間內與先前版本中的舊節點進行通訊。當此功能在系統中存在足夠長的時間後(預計為 OTP 26),我們可以開始傳送最多有五個 32 位元值的參考,並移除使用表格將參考透過分散式對應至別名的做法。也就是說,直到發生這種情況,別名實作才算完全完成。

為什麼無法取得別名所參照處理序的 PID?#

最重要的是,無需知道別名所參照處理序的 PID,即可解決別名旨在解決的問題。使用者應在知道參考是否為別名的通訊協定中使用別名,並且不需要知道其所參照的處理序的 PID。

除了上述問題之外,此類功能還存在其他問題。參考的內容只是一個很大的整數。為了保持分發透明性,必須指定如何解釋此整數,或者需要與識別進程所在的節點進行同步信號傳輸。同步信號傳輸的成本會非常高昂。如果指定如何解釋參考整數,將會阻止未來對參考整數的解釋方式進行變更,這可能會妨礙未來的最佳化、改進和新功能。在可以使用五個 32 位元字組傳遞大型參考之前,同步通訊也是實現此類功能的唯一選項。

如果我們應該模仿已註冊名稱 API 的 whereis() 函數,您也可以查看名稱是否已註冊,除了與別名識別的進程進行同步信號傳輸之外,沒有其他選項。

為什麼無法測試參考是否為別名? #

原因與為什麼無法取得別名所指的進程 PID 相同。

為什麼不允許註冊任意 Erlang 項? #

此功能可以解決別名旨在解決的相同問題,但這種方法存在一些問題。

除了 PID、埠口和參考之外的項,其資料類型中沒有嵌入節點識別碼。對於此類資料類型,您需要其他方式來識別名稱註冊所在的節點。在目前將原子作為註冊名稱的情況下,這是透過將名稱包裝在包含節點名稱的二元組中來完成的。對於除了純 PID、埠口和參考之外的所有其他項,都需要類似的東西。這也產生了一個問題。二元組只是名稱還是名稱加上節點識別碼?

是否應該允許將 PID 註冊為另一個進程的名稱?這會強制所有傳送操作先在已註冊名稱的表格中查找 PID,然後再執行操作。這將會影響所有傳送操作的效能。埠口也是如此。

我們認為由於會出現問題,不應實作任意項的註冊。然而,當您需要為同一服務註冊多個進程時,目前僅允許原子的註冊功能可能會有點過於限制。一種選項是允許註冊包含原子和整數的二元組。或許也應該允許其他項,例如字串,但不應該允許任意項。

允許參考作為已註冊名稱意味著存在別名 API 中不存在的可擴展性瓶頸。也就是說,這將是我們著手解決的問題的較差解決方案。

可能需要擴展名稱註冊,使其允許比原子更多項,但這是為了解決別名旨在解決的問題以外的其他問題。名稱註冊 API 不適合別名,因此我們認為別名不應與此類名稱註冊 API 的擴展相結合。別名解決方案解決了我們著手解決的問題,因此此 EEP 僅限於此。

為什麼引入 monitor/3 的標籤選項? #

spawn_request() 呼叫中使用監控選項 alias 時,由於您必須取得子進程的進程識別碼,才能與子進程共用別名,因此會產生不必要的延遲。您通常會想要在 spawn_request() 呼叫之前明確建立別名,並將其作為引數傳遞給子進程。

在一般情況下,您會想要接收操作的回應或錯誤。然而,如果您在 spawn_request() 操作之前明確建立別名,監控參考和別名將會是不同的參考。這將會阻止編譯器最佳化接收(以跳過參考建立時訊息佇列中存在的訊息),因為並非所有接收子句都會比對相同的參考。

我們透過使用 tag 監控選項以及 reply_tag 產生請求來解決這個問題。以下是在具有別名原型實作的系統上,使用此方法的功能完善的 RPC 實作。

rpc(Node, M, F, A) ->
    Alias = alias([reply]),
    ReqId = spawn_request(Node,
                          fun () ->
                                  Result = apply(M, F, A),
                                  Alias ! {{result, Alias}, Result}
                          end,
                          [{monitor, [{tag, {'DOWN', Alias}}]},
                           {reply_tag, {spawn_reply, Alias}},
                           {reply, error_only}]),
    receive
        {{result, Alias}, Result} ->
            demonitor(ReqId, [flush]),
            Result;
        {{'DOWN', Alias}, ReqId, process, _, Error} ->
            rpc_error_cleanup(Alias, Error);
        {{spawn_reply, Alias}, ReqId, error, Error} ->
            rpc_error_cleanup(Alias, Error)
    end.

rpc_error_cleanup(Alias, Error) ->
    case unalias(Alias) of
        true ->
            %% No flush needed since we used the 'reply' option
            %% to alias(), and the alias was still active...
            error({rpc_error, Error});
        false ->
            %% Flush a possible result message...
            receive {{result, Alias}, Result} -> Result
            after 0 -> error({rpc_error, Error})
            end
    end.

tag 監控選項也可以用於其他情況,以便取得在來自一組進程的所有類型回應中都存在的單一參考。這些進程可能是預先存在的,也可能不是。然後,可以使用此參考來判斷訊息是否對應於對特定進程組執行的特定操作。

目前有計劃擴展接收最佳化,以便所有子句中比對相同參考的多個接收可以利用最佳化。這也將會改善此類實作的效能,使其可以接收多個比對相同參考的訊息。

在設定監控的進程中,本地儲存監控訊息中要使用的標籤,並且不必在進程之間進行通訊。最重要的是,在分散式案例中,它不必透過網路傳送。這也表示,它也可以在監控不支援此功能的舊節點上的進程時使用。

回溯相容性 #

別名功能是純擴展,因此沒有真正的回溯相容性問題。

為了能夠透過先前版本的 Erlang 節點傳輸別名,我們無法透過分散式傳輸大型參考,因此需要在節點全域表格中保留有關別名的資訊。實作可以透過分散式傳輸較大型參考而受益,但直到我們強制規定能夠處理此類大型參考時才會這樣做。OTP 24 和 OTP 25 都能夠透過分散式處理大型參考,並且由於我們只保證與最近兩個版本的向後和向前分散式相容性,因此我們可以在 OTP 26 中強制規定使用大型參考。

與使用進程的 PID 傳送相比,此別名節點全域表格在利用別名時會產生額外負擔。這是由於表格結構的分配和操作所致。與現有利用 Proxy 進程來防止雜散訊息的解決方案相比,此別名節點全域表格的額外負擔很小。幸運的是,此節點全域表格也僅需要暫時存在,並且可以在 OTP 26 中移除。

參考實作 #

參考實作由 提取要求 #2735 提供。

除了實作別名功能之外。提取要求還包含在 gen 行為(例如 gen_server)中使用別名。因此,現在也可以實作類似於 erpc:receive_response()receive_response() 功能,該功能也已實作。

  • gen_server:receive_response/2
  • gen_statem:receive_response/2
  • gen_event:receive_response/2

變更記錄 #

  • 2020-10-29:引入了 tag 監控選項。
  • 2020-11-12:alias/1once 選項變更為 replyalias/1unalias 選項變更為 explicit_unalias。在 alias 監控選項的 UnaliasOpt 部分中,unalias 變更為 explicit_unalias
  • 2020-11-12:EEP 的狀態變更為 Accepted。