最近在做一個項目,項目中用golang 寫了一個網關gateway,gateway接受來自外部的請求,並轉發到后端的容器中。gateway和應用的容器都部署在同一個K8S集群當中。流程如下圖
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實現,並且為了提高效率,采用的是長連接的形式。
我們現在為了實現自動化擴縮容,引入了HPA擴縮容機制,也就是說service對應的pod會根據訪問量和CPU的變化進行自動的擴縮容。現在的問題是,這種方案能否在擴容之后實現負載均衡嗎?答案是不能,或者說負載均衡的效果並不好(如果采用RoundRobin的負載均衡策略,多個pod並不能均勻的接受到請求),下面說一下我的分析:
我們知道,使用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)]
這一行代碼就是關鍵。大概解釋一下,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+端口的連接。如下圖:
圖中每一個方框代表一個hostclient的連接池,框1指的就是本例中的情況,而框2和框3指的是通過ip+端口建立連接的情況。在K8S中,service的負載均衡指的是建立連接時,會均衡的和pod建立連接,但是,由於我們pod的創建順序有先后區別(初始的時候只有一個pod,后面通過hpa擴容起來),導致框1中的連接肯定無法做到均勻分配,因此擴容起來之后的pod,無法做到真正意義的嚴格的負載均衡。
那么有什么辦法改進呢:
1.gateway到后端的請求是通過host(K8S的域名)通過service進行請求的,如果改成直接通過podIP進行訪問,那么就可以自己實現負載均衡方案,但是這樣的復雜度在於必須要自己做服務發現機制,即不能依賴K8S的service服務發現。
2.采用短連接,短連接顯然沒有任何問題,完全取決於service的負載均衡。但是短連接必然會影響轉發效率,所以,可以采用一種長短連接結合的方式,即每個連接設置最大的請求次數或連接持續時間。這樣能在一定程度上解決負載分配不均勻的問題。
以上是個人的一些理解和看法,因筆者水平有限,難免有理解錯誤或不足的地方,歡迎大家指出,也歡迎大家留言討論。
------------------------------------------------------------------------------------2019.11.11更新------------------------------------------------------------------------------------------
以前的faasthttp的client里,沒有 MaxConnDuration 字段,我也在github上提出了一個issue,希望作者能加上該字段,很高興,作者已經更新,加上了該字段。參考https://github.com/valyala/fasthttp/issues/692
這個字段的意思是連接持續最大多長時間后就會關閉,用該參數,實現了長短連接結合的方式,兼顧了效率和負載均衡的問題。