Envoy 有狀態會話保持機制設計與實現

語言: CN / TW / HK

1. 問題背景

會話保持是七層負載均衡的核心功能之一。對於同一會話的請求或者連線,通過會話保持機制,負載均衡軟體會將其路由到同一個後端,以利用區域性性原理來提高服務整體的效率。

在傳統的 LB 產品之中,會話保持能力可以說是重中之重,比如 F5 軟體就提供了豐富的會話保持機制供使用者選擇。而在雲原生的場景當中,由於更多面向無狀態的服務,服務例項動態啟用和銷燬,會話保持能力相對而言重要性有所降低。而作為雲原生邊緣網路代理,Envoy 在會話保持方面自然也相對薄弱。

在開源 Envoy 當中,只提供了基於一致性雜湊的會話保持機制。具體來說,Envoy 提供了一個一致性雜湊負載均衡演算法進行流量的負載均衡。對於一組後端服務例項,Envoy 會根據服務例項名稱或者地址來計算出一個雜湊環。之後,對於每一個 HTTP 請求,Envoy 都會根據請求的特徵計算一個雜湊值,然後根據雜湊值在後端雜湊環中選擇一個最合適的後端例項(通過折半查詢)。該會話保持機制不需要儲存額外的狀態,最終結果只和計算所得的雜湊值以及雜湊環有關。此外,天然的,該會話保持機制也可以做到跨例項會話保持,也即是當擁有多個 Envoy 例項時,請求無論是由 Envoy A 例項轉發或者是 Envoy B 例項轉發,最終轉發到的目標服務例項都是一致的。

Envoy 支援配置從不同的請求特徵中計算雜湊值,比如使用特定請求頭,使用源地址 IP,或者使用 cookie 等。只要後端例項保持穩定,這一機制幾乎是完美的。但是,事實上,後端例項並不總能夠保持穩定,而且有一些特殊的場景,基於一致性雜湊的負載均衡機制也無法實現。 因為基於一致性雜湊的會話保持機制本質上是無狀態會話保持。會話保持依賴的不是某個持續的狀態值,而是演算法實時計算的結果。

為了進一步的完善 Envoy 的會話保持能力,拓展 Envoy 的使用場景,比如在傳統分散式系統中作為七層軟負載均衡,輕舟微服務團隊對 Envoy 負載均衡器做了進一步的增強,以支援有狀態的會話保持機制。具體來說,有狀態會話保持是為了解決以下問題:

  • 保證當後端節點集狀態發生變化時(節點被標記為 degrade 或者不健康、新增節點或者移除節點),屬於已有會話的請求仍舊能夠正確的路由到會話對應的節點。現有的基於一致性雜湊的會話保持機制,當後端節點發生變化時,就會重新構造雜湊環,導致會話保持的結果不穩定。
  • 使得最小連線數等演算法也能夠具備會話保持能力。現有會話保持是構建在一致性雜湊負載均衡演算法的基礎之上,但是如最小連線數,輪轉等負載均衡演算法不具備會話保持能力。
  • 提高一致性雜湊負載均衡策略的效能。一致性雜湊演算法通過折半查詢的方法來找到合適的節點。如果使用有狀態會話保持,可以有效的減少在具備大量節點時的節點搜尋的開銷。

目前,團隊已經將整個方案的所有設計與實現均貢獻給了開源 Envoy 並且完成了合入,成為了開源 Envoy 的一部分。

2. 例項管理

要實現各種各樣的負載均衡演算法,自然需要實現對後端例項集合的管理,在 Envoy 中,為了實現後端例項的管理,使用了一個 PrioritySet 的資料型別。其主要的特點三個:

PrioritySet

而要實現有狀態會話保持,首先,必須要提供快速的例項搜尋能力,這是開源 PrioritySet 並不具備的。為此,資料面擴充套件增強了 PrioritySet ,在其中實現了一個以例項地址為 Key 的 HostMap,來提供跨優先順序的快速搜尋能力。

同時,為了儘可能的減小該項增強帶來的額外記憶體開銷,HostMap在各個執行緒的 PrioritySet 中共享。但是需要注意到,Envoy 特色功能之一就是可以動態更新後端例項列表,所以這個 HostMap 也需要更新(更新都在主執行緒中)並且面臨執行緒安全問題。

一般來說,面對此類一寫多讀的資料更新,一個讀寫鎖是一個不錯的選擇。但是在有狀態會話保持中,HostMap 的搜尋是高頻操作,每個請求都是至少操作一次。而例項表更新則是一個相對來說非常低頻的操作。為了一個低頻的例項更新,而在每次請求中都引入一次讀鎖的開銷,對於高效能網路代理來說,是很難讓人接受的。所以為此需要設計一套機制來保證執行緒安全的例項更新的同時,保證搜尋的無鎖和高效能。

圖 1

該機制簡單原理如上所示,當需要更新例項表時,Envoy 會從已有的 HostMap 中建立一份拷貝,所有更新都在新的拷貝進行,而不是直接修改已有 HostMap,這樣避免了已有的 HostMap 在多執行緒上讀寫的衝突。(當然,這並非毫無代價,每次更新都需要拷貝一次 HostMap,以主執行緒的拷貝開銷換取工作現場的無鎖操作)

在更新完成之後,需要將最終更新就緒的 HostMap 同步到工作執行緒中去時,再 commit 拷貝出來的 HostMap,並向各個工作執行緒推送一個更新事件,最終完成整個例項表以及 HostMap 更新。

3. 會話狀態

在完成第 2 小節中的例項管理機制增強之後,就為有狀態會話保持提供了一個基礎。另一個需要解決的問題是會話狀態的儲存。

第一種典型的方案是使用 HTTP Cookie 來儲存會話狀態。具體來說,使用者可以指定一個特殊的 Cookie 名稱。對於每一個請求,如果包含指定的 Cookie,則從 Cookie 中解析出目標例項的地址,並且根據地址檢索目標例項。如果不存在對應的 Cookie 或者 Cookie 中地址不存在對應目標例項,則根據負載均衡演算法選擇一個後端例項,並且將該後端例項的地址通過 Set-Cookie 新增到響應中。

圖 2

該方案簡單有效,而且最重的是 Envoy 將會話狀態儲存的責任轉嫁給了客戶端,保持了自身的無狀態。這樣做不但使得實現上更為簡單,最重要的是,該方案天然的支援跨例項的會話保持。也即是說,當存在多個 Envoy 例項時,請求無論在 Envoy A 或者 Envoy B 上被處理,都不影響會話保持的結果。 不過這個方案,也有它的缺陷。它的靈活性相對較弱。對於每個沒有有效指定 Cookie 的新請求,Envoy 都會為它新增對應的 Cookie 並將之作為一個新的會話處理。它無法更加靈活的使用請求源 IP、請求頭等資訊來作為識別會話的特徵。

為此,資料面提出了另一種基於儲存/快取的方案作為上一方案的補充。該方案會根據請求指定的特徵(如源 IP、請求頭等)計算特徵值,並且儲存特徵值和目標例項的對映(記憶體快取+中心快取)。如此,就可以實現根據任意的請求特徵來作為會話保持的依據。

圖 3

不難發現第二個方案和第一個方案在流程處理上幾乎完全一致。唯一的不同在於“有狀態會話保持”中的狀態由誰來儲存的問題。在第二個方案中,帶來了更好的靈活性,狀態的保持需要 Envoy/資料面自身來處理。

4. 負載互動

在第 2 小節中,接入瞭如何設計一個安全、高效的 HostMap 來管理後端例項表並且實現例項的快速搜尋。在第 3 小節中,介紹瞭如果維護會話狀態,實現會話狀態的儲存和提取。

但是如何在請求路由轉發的過程中,將會話狀態和 Host 搜尋選擇結合到一起,則是第三個問題。在 Envoy 當中,相比於極端的高效能,更加看重模組化、易測試等特點。在具體的實現當中,會話狀態的儲存是在代理協議的 L4 Filter 內部實現的。以 HTTP 協議為例,會話狀態需要靠 HTTP Connection Manager 這個 L4 Filter 來實現(其中內嵌的 HTTP L7 Filter 當作整個 L4 Filter 的一部分)。而後端例項、負載均衡、HostMap 等則是由 Upstrem 模組實現和管理。兩者之間僅僅通過 LoadBalancerContext 介面進行互動。 為此,需要拓展 LoadBalancerContext 介面,允許 L4 Filter 通過 LoadBalancerContext 向 Upstream 傳遞會話狀態。而 Upstream 則根據會話狀態優先選擇目標例項。如果會話狀態無效,則正常的根據負載均衡演算法選擇後端例項。具體的擴充套件介面設計如下:

class LoadBalancerContext {
public:
  // ......
  using OverrideHost = absl::string_view;
  /**
   * Returns the host the load balancer should select directly. If the expected host exists and
   * the host can be selected directly, the load balancer can bypass the load balancing algorithm
   * and return the corresponding host directly.
   */
  virtual absl::optional<OverrideHost> overrideHostToSelect() const PURE;
};

此外,輕舟還對 HTTP Connection Manager 暴露給 L7 Filter 的 API 做了增強,允許開發通過編寫簡單的 HTTP Filter 來設定 override host,讓開發者可以根據自身的需要對負載均衡結果做更靈活的控制。

class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks {
public:
  // ......

  /**
  * Set override host to be used by the upstream load balancing. If the target host
  * exists in the host list of the routed cluster, the host should be selected
  * first.
  * @param host The override host address.
  */
  virtual void setUpstreamOverrideHost(absl::string_view host) PURE;

  /**
  * @return absl::optional<absl::string_view> optional override host for the
  * upstream load balancing.
  */
  virtual absl::optional<absl::string_view> upstreamOverrideHost() const PURE;
};

5. API 設計

最後一部分是資料面 Envoy 向上層控制面暴露的 API,使得控制面可以對有狀態會話保持功能進行控制。它包括 HTTP 擴充套件和服務級別負載均衡配置兩部分。

在 HTTP 擴充套件中,添加了一個額外的 HTTP Filter,來根據具體的有狀態會話保持實現來提取目標例項,比如 cookie 實現就會從指定 cookie 中獲取目標例項地址(當然,因為 HTTP Connection Manager 本身也已經做了增強,完全從頭開發新的 HTTP 擴充套件想來也是輕而易舉之事)。

message StatefulSession {
  // Specific implementation of session state. This session state will be used to store and
  // get address of the upstream host to which the session is assigned.
  //
  // [#extension-category: envoy.http.stateful_session]
  config.core.v3.TypedExtensionConfig session_state = 1;
}

message CookieBasedSessionState {
  // The cookie configuration used to track session state.
  type.http.v3.Cookie cookie = 1 [(validate.rules).message = {required: true}];
}

在服務均衡配置當中,提供了額外的狀態限制,確保只有指定狀態的示例可以通過前述的 override host 選擇,假設目標例項建康狀態符合預期,則認為該會話狀態為有效狀態,否則根據負載均衡演算法重新選擇目標例項並更新會話狀態。如此就完成了整個方案的最後一環。

// This controls what hosts are considered valid when using
// :ref:`host overrides <arch_overview_load_balancing_override_host>`, which is used by some
// filters to modify the load balancing decision.
//
// If this is unset then [UNKNOWN, HEALTHY, DEGRADED] will be applied by default. If this is
// set with an empty set of statuses then host overrides will be ignored by the load balancing.
core.v3.HealthStatusSet override_host_status = 8;

6. 全文小結

整個有狀態會話保持機制主要分為三個部分,分別是:在 Upstream 模組中實現執行緒安全的高效能 HostMap 以便於快速的例項搜尋以及例項狀態限制;在 L4/L7 Filter 中實現會話狀態的記錄、提取,且提供了非常良好的擴充套件性,且支援多種有狀態會話實現;擴充套件 LoadBalancerContext 以實現兩個模組互動上下游模組互動,允許 Filter 指定目標例項覆蓋負載均衡結果。

Envoy 有狀態會話保持機制彌補了 Envoy 在會話保持機制方面相對薄弱的缺陷。解決了在部分傳統場景下 Envoy 的短板。而且,目前輕舟微服務團隊已經將整個有狀態會話保持機制都貢獻給了 Envoy 開源社群,並持續維護。有興趣或者有這方面需求的同學可以直接使用,如果其中有不足之處,也可以直接向社群反饋 :)。

9 月 23 日,2022 網易數字+大會將在杭州康萊德酒店召開,屆時我們團隊和中國信通院、中泰證券、吉利集團、安信證券的相關負責人會分享更多雲原生技術與實踐。歡迎感興趣的朋友 提前關注