關於fasthttp + K8S service負載均衡的一些心得

最近在作一個項目,項目中用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

這個字段的意思是鏈接持續最大多長時間後就會關閉,用該參數,實現了長短鏈接結合的方式,兼顧了效率和負載均衡的問題。

相關文章
相關標籤/搜索