关于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

这个字段的意思是链接持续最大多长时间后就会关闭,用该参数,实现了长短链接结合的方式,兼顾了效率和负载均衡的问题。

相关文章
相关标签/搜索