固定链接 GRPC连接池的设计与实现

GRPC连接池的设计与实现

GRPC连接池的设计与实现

GRPC连接池的设计与实现

前言

在分布式高并发服务器中,client到server以及server中的多个节点之间的连接往往使用连接池来管理。简单来说就是将提前创建好的连接保存在池中,当有请求到来时,直接使用连接池中的连接对server端访问,省去了创建连接和销毁连接的开销(TCP建立连接时的三次握手和释放连接时的四次挥手),从而提高了性能。

目录

设计原则

  • 连接池的扩缩容
  • 空闲连接的超时与保活
  • 池满的处理机制

连接池的扩缩容

通常连接池属性包含最大空闲连接数和最大活跃连接数。

最大空闲连接数:连接池一直保持的连接数,无论这些连接被使用与否都会被保持。如果客户端对连接池的使用量不大,便会造成服务端连接资源的浪费。

最大活跃连接数:连接池最多保持的连接数,如果客户端请求超过次数,便要根据池满的处理机制来处理没有得到连接的请求。

扩容:当请求到来时,如果连接池中没有空闲的连接,同时连接数也没有达到最大活跃连接数,便会按照特定的增长策略创建新的连接服务该请求,同时用完之后归还到池中,而不是关闭连接。

缩容:当连接池一段时间没有被使用,同时池中的连接数超过了最大空闲连接数,那么便会关闭一部分连接,使池中的连接数始终维持在最大空闲连接数。

空闲连接的超时与保活

超时
如果连接没有被客户端使用的话,便会成为空闲连接,在一段时间后,服务端可能会根据自己的超时策略关闭空闲连接,此时空闲连接已经失效,如果客户端再使用失效的连接,便会通信失败。为了避免这种情况发生,通常连接池中的连接设有最大空闲超时时间(最好略小于服务器的空闲连接超时时间),在从池中获取连接时,判断是否空闲超时,如果超时则关闭,没有超时则可以继续使用。

保活
如果服务器发生重启,那么连接池中的连接便会全部失效,如果此时再从池中获取连接,不论获取到哪一个,都将通信失败。因此,连接池必须考虑连接的保活问题,有两种解决方法:

1、连接池设置一个Ping函数,专门用来做连接的保活。在从池中获取连接的时候,Ping一下服务器,如果得到响应,则连接依然有效,便可继续使用,如果超时无响应,则关闭该连接,生成新的连接,由于每次都要Ping一下,必然会增加延迟。也可以后台用一个线程或者协程定期的执行Ping函数,进行连接的保活,缺点是感知连接的失效会有一定的延迟,从池中仍然有可能获取到失效的连接。

2、客户端加入相应的重试机制。比如重试3次,前两次从池中获取连接执行,如果报的错是失效的连接等有关连接问题的错误,那么第3次从池中获取的时候带上参数,指定获取新建的连接,同时连接池移除前两次获取的失效的连接。

池满的处理机制

连接池不可能无限的容纳连接,当池满时,有两种处理机制:

1、池新建连接,并返回给客户端,当客户端用完时,如果池满则关闭连接,否则放入池中。

2、设置一定的超时时间来等待空闲连接。需要客户端加入重试机制,避免因超时之后获取不到空闲连接产生的错误。

基本原理

  1. 服务启动时建立连接池。
  2. 初始化连接池,建立最大空闲连接数个连接。
  3. 请求到来时,从池中获取一个连接。如果没有空闲连接且连接数没有达到最大活跃连接数,则新建连接;如果达到最大活跃连接数,设置一定的超时时间,等待获取空闲连接。
  4. 获取到连接后进行通信服务。
  5. 释放连接,此时是将连接放回连接池,如果池满则关闭连接。
  6. 释放连接池,关闭所有连接。

GRPC特性

关于GRPC的介绍,不在这里阐述,可阅读深入了解GRPC协议,也可自行Google。这里主要简要说明GRPC的两个特性:多路复用、超时重连。

多路复用GRPC使用HTTP/2作为应用层的传输协议,HTTP/2会复用底层的TCP连接。每一次RPC调用会产生一个新的Stream,每个Stream包含多个Frame,Frame是HTTP/2里面最小的数据传输单位。同时每个Stream有唯一的ID标识,如果是客户端创建的则ID是奇数,服务端创建的ID则是偶数。如果一条连接上的ID使用完了,Client会新建一条连接,Server也会给Client发送一个GOAWAY Frame强制让Client新建一条连接。一条GRPC连接允许并发的发送和接收多个Stream,而控制的参数便是MaxConcurrentStreams,Golang的服务端默认是100。

超时重连我们在通过调用Dial或者DialContext函数创建连接时,默认只是返回ClientConn结构体指针,同时会启动一个Goroutine异步的去建立连接。如果想要等连接建立完再返回,可以指定grpc.WithBlock()传入Options来实现。超时机制很简单,在调用的时候传入一个timeout的context就可以了。重连机制通过启动一个Goroutine异步的去建立连接实现的,可以避免服务器因为连接空闲时间过长关闭连接、服务器重启等造成的客户端连接失效问题。也就是说通过GRPC的重连机制可以完美的解决连接池设计原则中的空闲连接的超时与保活问题。

以Golang的GRPC客户端为例:
grpc重连

GRPC调优

GRPC默认的参数对于传输大数据块来说不够友好,我们需要进行特定参数的调优。

MaxSendMsgSizeGRPC最大允许发送的字节数,默认4MiB,如果超过了GRPC会报错。Client和Server我们都调到4GiB。

MaxRecvMsgSizeGRPC最大允许接收的字节数,默认4MiB,如果超过了GRPC会报错。Client和Server我们都调到4GiB。

InitialWindowSize基于Stream的滑动窗口,类似于TCP的滑动窗口,用来做流控,默认64KiB,吞吐量上不去,Client和Server我们调到1GiB。

InitialConnWindowSize基于Connection的滑动窗口,默认16 * 64KiB,吞吐量上不去,Client和Server我们也都调到1GiB。

KeepAliveTime每隔KeepAliveTime时间,发送PING帧测量最小往返时间,确定空闲连接是否仍然有效,我们设置为10S。

KeepAliveTimeout超过KeepAliveTimeout,关闭连接,我们设置为3S。

PermitWithoutStream如果为true,当连接空闲时仍然发送PING帧监测,如果为false,则不发送忽略。我们设置为true。

实现细则

代码:https://github.com/shimingyah/pool

基于GRPC的多路复用、超时重连特性,我们很容易实现GRPC连接池。

接口设计

提供简洁的Pool和Conn的接口设计。

连接复用

GRPC是支持多路复用的,所以在设计GRPC池的时候和其他连接池区别之一是支持连接复用,通过MaxConcurrentStreams控制,默认64。我们称单个的GRPC为物理连接,复用的连接为逻辑连接。池的实际有效连接逻辑连接=物理连接 * MaxConcurrentStreams

扩缩容

扩容初始化池的有效连接数(逻辑连接)为:最大空闲连接数 * MaxConcurrentStreams,每一次请求都会对池的引用计数原子++,同时hash求取选取连接,当引用计数超过逻辑连接数时,就需要进行扩容了,如果最大空闲连接没有达到最大活跃连接数,则按照double的方式扩容,如果达到了最大活跃连接数,我们会根据Reuse参数的值来做进一步操作:如果为true,则继续使用池中的连接,即使用的是物理连接的逻辑连接,关闭连接时,对引用计数原子–即可,如果为false,则新建连接,关闭连接时还需要对连接进行真正的Close。

缩容如果池的引用计数为0时,便会触发缩容操作,是连接维持到最大空闲连接数。

超时保活

基于GRPC的Keepalived特性,我们不需要自己实现保活机制,也无需关注连接池中的连接是否有效,因为就算失效,GRPC会自动重连的,此时只不过耗时会略微增加,即认为除了服务器一直处于down状态等原因,连接池中的连接是始终有效的。

Tips

  • 由于使用hash求余,每个GRPC上并发的Stream可能会超过MaxConcurrentStreams。
  • 不同场景对应的连接池配置也不一样,需要根据自己的场景压测得出连接池的最佳参数配置。

作者:史明亚

您的留言将激励我们越做越好