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

最近在做一个项目,项目中用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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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

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

posted @   ndsun  阅读(1517)  评论(0编辑  收藏  举报
编辑推荐:
· 35岁程序员的中年求职记:四次碰壁后的深度反思
· 继承的思维:从思维模式到架构设计的深度解析
· 如何在 .NET 中 使用 ANTLR4
· 后端思维之高并发处理方案
· 理解Rust引用及其生命周期标识(下)
阅读排行:
· 35岁程序员的中年求职记:四次碰壁后的深度反思
· ShadowSql之.net sql拼写神器
· 使用MCP C# SDK开发MCP Server + Client
· 感觉程序员要被 AI 淘汰了?学什么才有机会?
· MQTT协议发布和订阅的实现,一步步带你实现发布订阅服务。
点击右上角即可分享
微信分享提示