最近在作一個項目,項目中用golang 寫了一個網關gateway,gateway接受來自外部的請求,並轉發到後端的容器中。gateway和應用的容器都部署在同一個K8S集羣當中。流程以下圖git
gateway到pod的請求,是經過K8S的dns機制來訪問service,使用的是service的endpoint的負載均衡機制。當gateway獲得一個請求以後,經過解析對應的參數,而後能夠判斷須要轉發到哪一個host,例如:請求轉發到service.namespace.svc.cluster.local:8080,而後DNS解析會解析出對應service的clusterIp,經過service轉發請求到後端的pod上(具體轉發原理能夠了解一下kube-proxy的原理),gateway到service的請求經過golang的 fasthttp實現,而且爲了提升效率,採用的是長鏈接的形式。github
咱們如今爲了實現自動化擴縮容,引入了HPA擴縮容機制,也就是說service對應的pod會根據訪問量和CPU的變化進行自動的擴縮容。如今的問題是,這種方案可否在擴容以後實現負載均衡嗎?答案是不能,或者說負載均衡的效果並很差(若是採用RoundRobin的負載均衡策略,多個pod並不能均勻的接受到請求),下面說一下個人分析:golang
咱們知道,使用fasthttp做爲客戶端並採用長鏈接的時候,TPC的鏈接存在一個鏈接池,而這個鏈接池是如何管理的相當重要。看代碼: client.go後端
func (c *Client) Do(req *Request, resp *Response) error { uri := req.URI() host := uri.Host() isTLS := false scheme := uri.Scheme() if bytes.Equal(scheme, strHTTPS) { isTLS = true } else if !bytes.Equal(scheme, strHTTP) { return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme) } startCleaner := false c.mLock.Lock() m := c.m if isTLS { m = c.ms } if m == nil { m = make(map[string]*HostClient) if isTLS { c.ms = m } else { c.m = m } } hc := m[string(host)] if hc == nil { hc = &HostClient{ Addr: addMissingPort(string(host), isTLS), Name: c.Name, NoDefaultUserAgentHeader: c.NoDefaultUserAgentHeader, Dial: c.Dial, DialDualStack: c.DialDualStack, IsTLS: isTLS, TLSConfig: c.TLSConfig, MaxConns: c.MaxConnsPerHost, MaxIdleConnDuration: c.MaxIdleConnDuration, MaxIdemponentCallAttempts: c.MaxIdemponentCallAttempts, ReadBufferSize: c.ReadBufferSize, WriteBufferSize: c.WriteBufferSize, ReadTimeout: c.ReadTimeout, WriteTimeout: c.WriteTimeout, MaxResponseBodySize: c.MaxResponseBodySize, DisableHeaderNamesNormalizing: c.DisableHeaderNamesNormalizing, } m[string(host)] = hc if len(m) == 1 { startCleaner = true } } c.mLock.Unlock() if startCleaner { go c.mCleaner(m) } return hc.Do(req, resp) }
其中負載均衡
hc := m[string(host)]spa
這一行代碼就是關鍵。大概解釋一下,httpclient當中維護了一個 map[string]*HostClient ,其中key即爲host,value爲hostClient對象。那這個host,即爲咱們請求的host。在本例中就是service.namespace.svc.cluster.local:8080,而每個hostClient,又維護了一個TCP的鏈接池,這個鏈接池中,真正維護着TCP鏈接。每次進行http請求時,先經過請求的host找到對應的hostClient,再從hostClient的鏈接池中取一個鏈接來發送http請求。問題的關鍵就在於,map中的key,用的是域名+端口仍是ip+端口的形式。若是是域名+端口,那麼對應的hostClient中的鏈接,就會可能包含到該域名對應的各個ip的鏈接,而這些鏈接的數量沒法保證均勻。但若是key是ip+端口,那麼對應hostClient中的鏈接池只有到該ip+端口的鏈接。以下圖:code
圖中每個方框表明一個hostclient的鏈接池,框1指的就是本例中的狀況,而框2和框3指的是經過ip+端口創建鏈接的狀況。在K8S中,service的負載均衡指的是創建鏈接時,會均衡的和pod創建鏈接,可是,因爲咱們pod的建立順序有前後區別(初始的時候只有一個pod,後面經過hpa擴容起來),致使框1中的鏈接確定沒法作到均勻分配,所以擴容起來以後的pod,沒法作到真正意義的嚴格的負載均衡。orm
那麼有什麼辦法改進呢:對象
1.gateway到後端的請求是經過host(K8S的域名)經過service進行請求的,若是改爲直接經過podIP進行訪問,那麼就能夠本身實現負載均衡方案,可是這樣的複雜度在於必需要本身作服務發現機制,即不能依賴K8S的service服務發現。blog
2.採用短鏈接,短鏈接顯然沒有任何問題,徹底取決於service的負載均衡。可是短鏈接必然會影響轉發效率,因此,能夠採用一種長短鏈接結合的方式,即每一個鏈接設置最大的請求次數或鏈接持續時間。這樣能在必定程度上解決負載分配不均勻的問題。
以上是我的的一些理解和見解,因筆者水平有限,不免有理解錯誤或不足的地方,歡迎你們指出,也歡迎你們留言討論。
------------------------------------------------------------------------------------2019.11.11更新------------------------------------------------------------------------------------------
之前的faasthttp的client裏,沒有 MaxConnDuration 字段,我也在github上提出了一個issue,但願做者能加上該字段,很高興,做者已經更新,加上了該字段。參考https://github.com/valyala/fasthttp/issues/692
這個字段的意思是鏈接持續最大多長時間後就會關閉,用該參數,實現了長短鏈接結合的方式,兼顧了效率和負載均衡的問題。