19

关于fasthttp + K8S service负载均衡的一些心得

 4 years ago
source link: https://studygolang.com/articles/27207
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近在做一个项目,项目中用golang 写了一个网关gateway,gateway接受来自外部的请求,并转发到后端的容器中。gateway和应用的容器都部署在同一个K8S集群当中。流程如下图

a6fmq2E.png!web

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+端口的连接。如下图:

yUBvUz6.png!web

图中每一个方框代表一个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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK