长连接负载均衡的问题

在分布式的系统中,如果服务 A 要调用服务 B,并且两个服务都部署了多个实例的话,就要解决负载均衡的问题。即,我们希望到达 B 的 QPS 在 B 的所有实例中都是均衡的。

以前的类似 HTTP/1.1 的实现中,服务 A 每发起一次请求,都需要跟 B 建立一个 TCP 连接。所以负载均衡的实现方式一般都是基于连接数的。但是每次都建立一个新的连接,性能会很低。所以就有了后来长连接的实现方式:建立一个 TCP 连接,在这上面发送很多请求,复用这个 TCP 连接。gRPC 就是基于 HTTP/2 实现的,用了这种长连接的方式。

用长连接会提高性能,因为不用每次都去重新建立一个 TCP 连接。但是也有一些问题。

第一个问题就是负载均衡。Kubernetes 这篇博客讲了为什么 gRPC 需要特殊的负载均衡。很显然,HTTP/1.1 的方式,每次随机选择一个实例去调用,负载是均衡的。但是 HTTP/2 这种一直用一个连接的方式,一旦连接上了就会一直用,使用哪一个实例就取决于最开始选择的谁。

即使是一开始有办法让它连接均衡,但是有一些情况会打破这种均衡。比如说一台一台重启 service instance。

在每重启了一个 instance 之后,原本连接这个 instance 的 client 就会与其断开连接,转而去连接其他的可用 instance。所以,第一台被重启的 instance 重启完成之后是不会有连接的。其他的 instance 会增加:(1/n)/(n-1) * total connections 的连接。n 是总实例数。

因为每一个 instance 重启之后都会增加其他 instance 的连接数,就有两个问题:

  1. 第一个重启的 instance 到头来会有最多的连接数,最后一个重启的,不会有连接,非常不均衡
  2. 最后一个重启的 instance 在重启的时候会造成大量的 client 去重连

第二个问题就是增加服务端 instance 的时候,不会有 client 去连接它。即服务端迁移/上线下线的问题。因为所有的 client 都使用原来已经建立好的连接,不会知道有新的 instance 可用了。其实说到底和第一个问题差不多。

解决的方法,想到 3 个。

第一个就是如同上文博客中提到的那样,在 client 和 server 之间加一个 load balancer,来维护到后端的连接。可以完美解决上面两个问题。缺点是资源会比较高,架构增加复杂性。

第二个方法是从服务端解决:服务端可以不定时给客户端发送 GOAWAY 指令,示意客户端去连接别的 server instance。api-server 有一个选项是可以指定用多少的概率去给 client 发送这个指令:–goaway-chance float.

To prevent HTTP/2 clients from getting stuck on a single apiserver, randomly close a connection (GOAWAY). The client’s other in-flight requests won’t be affected, and the client will reconnect, likely landing on a different apiserver after going through the load balancer again. This argument sets the fraction of requests that will be sent a GOAWAY. Clusters with single apiservers, or which don’t use a load balancer, should NOT enable this. Min is 0 (off), Max is .02 (1/50 requests); .001 (1/1000) is a recommended starting point.

这样还有一个好处,就是下线的时候,不是粗暴地退出,而是可以对自己当前所有的连接都发送 GOAWAY 指令。然后无损地退出。

第三个方法就是从客户端解决:客户端不使用单一连接去连接服务端,而是使用一个连接池:

  1. 客户端每次要发送请求的时候,需要先向自己的连接池请求一个可用连接:
    1. 这时候,如果有,就返回一个连接
    2. 如果没有,就发起建立连接
  2. 使用完成之后,将连接放回连接池
  3. 连接池支持设置一些参数,比如:
    1. 如果 idle 一定的时间,就关闭连接
    2. 一个连接 serve 了多少个 request 之后,或者被使用了多少次之后,就关闭它,不再使用。

这样,一来可以解决一个连接被无限使用的问题,而来关闭连接也是无损的,因为连接池里面的连接没有给任何人使用,由连接池自己管理。其实,像数据库客户端,比如 jdbc,以及 Redis 客户端,都是这么实现的。



长连接负载均衡的问题”已经有6条评论

    • 二者并不冲突,并不是一个 connection 上只能有一个请求,可以拿同一个 connection 来发送多个请求吧。这里用连接池主要是为了维护连接生命周期,以及可以分散请求到多个不同的连接上。

      有多路复用不意味着应用只能使用一个连接来发送所有请求。

  1. 你这里的描述
    “客户端每次要发送请求的时候,需要先向自己的连接池请求一个可用连接:
    这时候,如果有,就返回一个连接
    如果没有,就发起建立连接”
    这种明显就是一个request 一个connect。我不是来抬杠。我知道连接池有它的好处,只是在想这种方式下,技能负载均衡,也能使用上grpc协议本身的多路复用。
    比如说这个连接池get的时候是不是记录下每个连接正在使用的次数。使用一个参数来控制下一个连接最多的使用次数比如100。

    • 也可以。

      但是不知道gRPC 100个请求放一个连接上效率高,还是100个请求放在100个连接上效率高。

      如果是后者的话,可以优先返回空闲连接,或者优先返回复用程度最低的连接来使用。

      谢谢你的评论

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注