GRPC 連接池的設計與實現

前言

在分佈式高併發服務器中,client 到 server 以及 server 中的多個節點之間的連接往往使用連接池來管理。簡單來說就是將提前創建好的連接保存在池中,當有請求到來時,直接使用連接池中的連接對 server 端訪問,省去了創建連接和銷燬連接的開銷 (TCP 建立連接時的三次握手和釋放連接時的四次揮手),從而提高了性能。

目錄

設計原則

連接池的擴縮容

通常連接池屬性包含最大空閒連接數和最大活躍連接數。

最大空閒連接數:連接池一直保持的連接數,無論這些連接被使用與否都會被保持。如果客戶端對連接池的使用量不大,便會造成服務端連接資源的浪費。

最大活躍連接數:連接池最多保持的連接數,如果客戶端請求超過次數,便要根據池滿的處理機制來處理沒有得到連接的請求。

擴容:當請求到來時,如果連接池中沒有空閒的連接,同時連接數也沒有達到最大活躍連接數,便會按照特定的增長策略創建新的連接服務該請求,同時用完之後歸還到池中,而不是關閉連接。

縮容:當連接池一段時間沒有被使用,同時池中的連接數超過了最大空閒連接數,那麼便會關閉一部分連接,使池中的連接數始終維持在最大空閒連接數。

空閒連接的超時與保活

超時 如果連接沒有被客戶端使用的話,便會成爲空閒連接,在一段時間後,服務端可能會根據自己的超時策略關閉空閒連接,此時空閒連接已經失效,如果客戶端再使用失效的連接,便會通信失敗。爲了避免這種情況發生,通常連接池中的連接設有最大空閒超時時間(最好略小於服務器的空閒連接超時時間),在從池中獲取連接時,判斷是否空閒超時,如果超時則關閉,沒有超時則可以繼續使用。

保活 如果服務器發生重啓,那麼連接池中的連接便會全部失效,如果此時再從池中獲取連接,不論獲取到哪一個,都將通信失敗。因此,連接池必須考慮連接的保活問題,有兩種解決方法:

1、連接池設置一個 Ping 函數,專門用來做連接的保活。在從池中獲取連接的時候,Ping 一下服務器,如果得到響應,則連接依然有效,便可繼續使用,如果超時無響應,則關閉該連接,生成新的連接,由於每次都要 Ping 一下,必然會增加延遲。也可以後臺用一個線程或者協程定期的執行 Ping 函數,進行連接的保活,缺點是感知連接的失效會有一定的延遲,從池中仍然有可能獲取到失效的連接。

2、客戶端加入相應的重試機制。比如重試 3 次,前兩次從池中獲取連接執行,如果報的錯是失效的連接等有關連接問題的錯誤,那麼第 3 次從池中獲取的時候帶上參數,指定獲取新建的連接,同時連接池移除前兩次獲取的失效的連接。

池滿的處理機制

連接池不可能無限的容納連接,當池滿時,有兩種處理機制:

1、池新建連接,並返回給客戶端,當客戶端用完時,如果池滿則關閉連接,否則放入池中。

2、設置一定的超時時間來等待空閒連接。需要客戶端加入重試機制,避免因超時之後獲取不到空閒連接產生的錯誤。

基本原理

  1. 服務啓動時建立連接池。

  2. 初始化連接池,建立最大空閒連接數個連接。

  3. 請求到來時,從池中獲取一個連接。如果沒有空閒連接且連接數沒有達到最大活躍連接數,則新建連接;如果達到最大活躍連接數,設置一定的超時時間,等待獲取空閒連接。

  4. 獲取到連接後進行通信服務。

  5. 釋放連接,此時是將連接放回連接池,如果池滿則關閉連接。

  6. 釋放連接池,關閉所有連接。

GRPC 特性

關於 GRPC 的介紹,不在這裏闡述,可閱讀深入瞭解 GRPC 協議,也可自行 Google。這裏主要簡要說明 GRPC 的兩個特性:多路複用、超時重連。

多路複用GRPC 使用 HTTP/2 作爲應用層的傳輸協議,HTTP/2 會複用底層的 TCP 連接。每一次 RPC 調用會產生一個新的 Stream,每個 Stream 包含多個 Frame,Frame 是 HTTP/2 裏面最小的數據傳輸單位。同時每個 Stream 有唯一的 ID 標識,如果是客戶端創建的則 ID 是奇數,服務端創建的 ID 則是偶數。如果一條連接上的 ID 使用完了,Client 會新建一條連接,Server 也會給 Client 發送一個 GOAWAY Frame 強制讓 Client 新建一條連接。一條 GRPC 連接允許併發的發送和接收多個 Stream,而控制的參數便是MaxConcurrentStreams,Golang 的服務端默認是 100。

超時重連我們在通過調用 Dial 或者 DialContext 函數創建連接時,默認只是返回 ClientConn 結構體指針,同時會啓動一個 Goroutine 異步的去建立連接。如果想要等連接建立完再返回,可以指定 grpc.WithBlock() 傳入 Options 來實現。超時機制很簡單,在調用的時候傳入一個 timeout 的 context 就可以了。重連機制通過啓動一個 Goroutine 異步的去建立連接實現的,可以避免服務器因爲連接空閒時間過長關閉連接、服務器重啓等造成的客戶端連接失效問題。也就是說通過 GRPC 的重連機制可以完美的解決連接池設計原則中的空閒連接的超時與保活問題。

以 Golang 的 GRPC 客戶端爲例:

GRPC 調優

GRPC 默認的參數對於傳輸大數據塊來說不夠友好,我們需要進行特定參數的調優。

MaxSendMsgSizeGRPC 最大允許發送的字節數,默認 4MiB,如果超過了 GRPC 會報錯。Client 和 Server 我們都調到 4GiB。

MaxRecvMsgSizeGRPC 最大允許接收的字節數,默認 4MiB,如果超過了 GRPC 會報錯。Client 和 Server 我們都調到 4GiB。

InitialWindowSize基於 Stream 的滑動窗口,類似於 TCP 的滑動窗口,用來做流控,默認 64KiB,吞吐量上不去,Client 和 Server 我們調到 1GiB。

InitialConnWindowSize基於 Connection 的滑動窗口,默認 16 * 64KiB,吞吐量上不去,Client 和 Server 我們也都調到 1GiB。

KeepAliveTime每隔 KeepAliveTime 時間,發送 PING 幀測量最小往返時間,確定空閒連接是否仍然有效,我們設置爲 10S。

KeepAliveTimeout超過 KeepAliveTimeout,關閉連接,我們設置爲 3S。

PermitWithoutStream如果爲 true,當連接空閒時仍然發送 PING 幀監測,如果爲 false,則不發送忽略。我們設置爲 true。

實現細則

代碼:https://github.com/shimingyah/pool

基於 GRPC 的多路複用、超時重連特性,我們很容易實現 GRPC 連接池。

接口設計

提供簡潔的 Pool 和 Conn 的接口設計。

連接複用

GRPC 是支持多路複用的,所以在設計 GRPC 池的時候和其他連接池區別之一是支持連接複用,通過MaxConcurrentStreams控制,默認 64。我們稱單個的 GRPC 爲物理連接,複用的連接爲邏輯連接。池的實際有效連接邏輯連接=物理連接 * MaxConcurrentStreams

擴縮容

擴容初始化池的有效連接數 (邏輯連接) 爲:最大空閒連接數 * MaxConcurrentStreams,每一次請求都會對池的引用計數原子 ++,同時 hash 求取選取連接,當引用計數超過邏輯連接數時,就需要進行擴容了,如果最大空閒連接沒有達到最大活躍連接數,則按照 double 的方式擴容,如果達到了最大活躍連接數,我們會根據 Reuse 參數的值來做進一步操作:如果爲 true,則繼續使用池中的連接,即使用的是物理連接的邏輯連接,關閉連接時,對引用計數原子 -- 即可,如果爲 false,則新建連接,關閉連接時還需要對連接進行真正的 Close。

縮容如果池的引用計數爲 0 時,便會觸發縮容操作,是連接維持到最大空閒連接數。

超時保活

基於 GRPC 的 Keepalived 特性,我們不需要自己實現保活機制,也無需關注連接池中的連接是否有效,因爲就算失效,GRPC 會自動重連的,此時只不過耗時會略微增加,即認爲除了服務器一直處於 down 狀態等原因,連接池中的連接是始終有效的。

Tips

轉自:

zhuanlan.zhihu.com/p/104887882

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DCJMUQAsUk7353AAEHkllg