用 subsetting 限制连接池中的连接数量
內網使用服務發現后,服務與其它服務的實例之間使用一條 TCP 長連接進行通信。這種情況下常見的做法是按照 registry 下發的 host:port 列表來直接建連。
簡單來說就是下圖這樣:
每一個服務實例都需要和它依賴的服務的每一個實例都把連接給建上。如果各個服務的規模不大,這樣沒什么問題。互聯網公司的核心服務規模都比較大,幾千/萬臺機器(或幾千/萬個實例)的單一服務并不少見,這時候 client 要和所有 server 實例建連,會導致 client 端的 conn pool 里有大量連接,當然,server 端自然也少不了,這么多連接可能會產生一些問題:
- 活躍的連接管理需要使用連接池,依賴 5~6 個大服務就得建出幾萬條連接來,如果是在 Go 里,那我們就得有一堆 goroutine 了 
- 同理,client 端的連接和 server 端都是對應的,server 端也好不到哪里去 
- 連接?;钚枰瞻l應用層心跳以應對網絡的異常情況,這也是有成本的,極端情況下可能服務沒有請求的前提下,心跳請求就消耗了 40% 的 CPU 
如果讓我們自己去設計一個新的方案,挑選一些實例來建連的話,還是有點難的,這里面要考慮:
- 連接分布應該均衡,不能這個實例 10 條,那個 199 條 
- server 和 client 上下線,不能造成大量的連接重建和遷移 
- 連接要夠用,不能影響客戶端 
Google 的 subset 算法
好在 Google 爸爸給我們提供了一個解決方案:subsetting。當然,SRE 書里是先說了隨機的 subset 不可以,這里的算法被稱為 deterministic subsetting:
func?Subset(backends?[]string,?clientID,?subsetSize?int)?[]string?{subsetCount?:=?len(backends)?/?subsetSize//?Group?clients?into?rounds;?each?round?uses?the?same?shuffled?list:round?:=?clientID?/?subsetCountr?:=?rand.New(rand.NewSource(int64(round)))r.Shuffle(len(backends),?func(i,?j?int)?{?backends[i],?backends[j]?=?backends[j],?backends[i]?})//?The?subset?id?corresponding?to?the?current?client:subsetID?:=?clientID?%?subsetCountstart?:=?subsetID?*?subsetSizereturn?backends[start?:?start+subsetSize] }抄作業有點丟人,我們還是管這個叫借鑒業界先進經驗好了。算法非常的短,不過還是需要解釋清楚的。
入參:backends 指的是你的 server 端列表,client_id 我們可以給 client 分配一個 id,subsetSize 其實就是你的 client 端的連接量需求,也就是我一個 client 端對應的一個外部依賴,建立多少條連接合適,那么最終也就會從這個大 backends 列表中挑出 subsetSize 個項來。
為什么是均勻的
首先,shuffle 算法保證在 round 一致的情況下,backend 的排列一定是一致的。
因為每個實例擁有從 0 開始的連續唯一的自增 id,且計算過程能夠保證每個 round 內所有實例拿到的服務列表的排列一致,因此在同一個 round 內的 client 會分別 backend 排列的不同部分的切片作為選中的后端服務來建連。
Round?0:?[0,?6,?3,?5,?1,?7,?11,?9,?2,?4,?8,?10]Round?1:?[8,?11,?4,?0,?5,?6,?10,?3,?2,?7,?9,?1]Round?2:?[8,?3,?7,?2,?1,?4,?9,?10,?6,?5,?0,?11]上面是不同 round 的 backends 排列,如果我的 client id 計算出的 round 是 0,且這個 client 需要 4 條連接,則說明每個 round 可以分為 3 組,client id % 3 = 0,那么這個 client 則應該選擇 [0, 6, 3, 5] 這 4 條連接。如果 client id % 3 = 2,則應該選擇 [2, 4, 8, 10] 這 4 條連接。
Round?0:?[0,?6,?3,?5,?1,?7,?11,?9,?2,?4,?8,?10]第?0?組??????第?1?組??????第?2?組比較好理解,只要我的 client id 能保證連續,那么 client 打到后端的連接則一定是均勻的。
上下線的情況
client 上下線
client 上下線用滾動更新的方式,并不會影響其它 client 的連接分布,所以每個 client 下線時,只是對應的后端少了一些連接,暫時會導致某些 backend 的連接比其它 backend 少 1。
上線 client 從尾部開始,client id 依然是遞增的,按照該算法,這些 client 會繼續排在其它 client 后面,一個 round 一個 round 地將連接分布在后端服務上,也必然是均勻的。
server 上下線
與 client 上下線類似,server 的滾動升級和上下線也是不會有大影響的,因為每個 server 會隨機地分布在不同 client 的子集中,不會因為該 server 上下線,導致計算結果有大變化。
這個算法的問題
這個算法看上去比較完美,但是問題在于它需要一些前提。
每個服務都能被分配從 0 到 N 的連續唯一 id,這一點在沒有外部依賴的情況下比較難做到。綁定了外部基礎設施的方案又可能比較難推廣。比如 k8s 的 statefulset,也沒辦法強制所有服務都使用?
服務下線時,并不一定能保證下線的服務的 client id 是連續的,這樣就總是可以構造出一些極端情況,在拿到一些 client 之后,讓某臺 backend 的連接數變為 0。
現在大規模的服務節點很多,有些批量發布一次性發布幾百個節點,Google 的這個算法說一般 100 條連接(We typically use a subset size of 20 to 100 backend tasks)就夠了?如果正好批量發布的后端都被同一個 client 選中了,那這個 client 就廢掉了。
client 服務是需要知道 backends 的 id 的,否則當 backend 發生下線時,會導致 client 端的連接重新排布。
因此想要應用該算法,我們還是要進行一些特殊情況的考量和處理。
參考資料:
[1] https://sre.google/sre-book/load-balancing-datacenter/
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的用 subsetting 限制连接池中的连接数量的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 无人值守的自动 dump(一)
- 下一篇: 启用读者群
