最近在作一个项目,项目中用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
这个字段的意思是链接持续最大多长时间后就会关闭,用该参数,实现了长短链接结合的方式,兼顾了效率和负载均衡的问题。