golang GRPC连接池的设计与实现

goper

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

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的重连机制可以完美的解决连接池设计原则中的空闲连接的超时与保活问题。


推荐开源项目: https://github.com/grpc-ecosy...
gRPC-Gateway是protoc的插件.它读取gRPC服务定义并生成反向代理服务器,将 RESTful JSON API转换为gRPC.此服务器是根据你的gRPC定义中的自定义选项生成的.
gRPC-Gateway可帮助你同时提供gRPC和RESTful风格的API.


单例的 grpc client 是否会引起性能问题?
按说,grpc使用http2,而http2是支持多路复用的,这样说来或许不需要额外的连接池。
真的是这样吗?
通过压测,单个连接的client可以支持最大到9w的qps,并发量再大, cpu占用率会上去,但吞吐量会下来。

为什么会这样的?
猜测大概是由于tcp头阻塞和client锁竞争引起的。
应用层的头阻塞:在http1.1时各个request和response需要排队进行,在没有收到response报文之前,该tcp不能被复用。

虽然http2避免了应用层的头阻塞,但依然存在tcp层的头阻塞。
tcp头阻塞:tcp在并发发包时,接收方可能会先收到后面的包,那么这个包就不能放到socket buf上,只能先放到内核协议栈上,这就是tcp头阻塞。

那么怎么解决呢?
一个成熟的方案是,池化。
通过使用自建连接池,经测qps可以达到40w左右。

总结,如果服务的并发量不高,使用单个client既可以避免大量的tcp TIME_WAIT问题,也可以支撑数万的qps。

当并发量远远超过这个数量级时,可以考虑使用池化方案进行优化。


//使用https://github.com/shimingyah... 这个开源项目实现多个grpc 连接池

package main

import (
    "fmt"
    "github.com/shimingyah/pool"
    "google.golang.org/grpc"
    "log"
    "sync"
)

var (
    grpcOnce sync.Once
    poolConn  pool.Pool
)

func main() {

}

//参考: https://github.com/shimingyah/pool
func GetGrpcClient() *grpc.ClientConn {
    grpcOnce.Do(func() {
        var err error
        //grpc 服务器的地址或者 服务注册中心的地址
        address :=""
        poolConn, err = pool.New(address, pool.DefaultOptions)
        if err != nil {
            log.Fatalf("failed to new pool: %v", err)
        }
    })
    conn, err := poolConn.Get()
    if err != nil {
        log.Fatalf("failed to get conn: %v", err)
    }
    return conn.Value()
}

参考文章:

使用grpc client pool复用连接

grpc复用client连接

[golang grpc网关使用连接池提吞吐量
](https://xiaorui.cc/archives/6001)

gRPC 应用篇之客户端 Connection Pool

Golang连接池的几种实现案例

大量的time_wait状态tcp连接导致性能问题的解决方案,及grpc client请求优化


github 项目:
GRPC连接池实现思路:

  1. 参考 mysql 连接池实现 (https://github.com/go-sql-dri...)
  2. 参考 go-redis 连接池实现
  3. https://github.com/go-basic/pool
  4. https://github.com/shimingyah...
    (https://blog.didiyun.com/inde...)

https://github.com/flyaways/pool
https://github.com/silenceper...
https://github.com/processout...

阅读 2.3k

go 后端开发

374 声望
15 粉丝
0 条评论

go 后端开发

374 声望
15 粉丝
文章目录
宣传栏