1600个gRPC客户端并发请求失败,报connection closed before server preface错误
问题分析与解决:并发1600个gRPC客户端请求失败,报错
connection closed before server preface received 问题原因
- 连接复用缺失:每个goroutine都创建独立的gRPC连接,瞬间发起1600个TCP连接,远超系统默认文件描述符限制(Linux默认一般为1024),导致大量连接无法完成建立,服务器直接关闭未完成握手的连接,触发该错误。
- 懒连接机制放大问题:
grpc.Dial默认采用懒连接策略,即第一次调用RPC时才真正建立连接。1600个请求同时触发连接建立,服务器和系统资源瞬间耗尽,无法处理所有握手请求。 - 错误处理缺失:客户端
init方法返回false时,代码仍继续执行RPC调用,未提前终止,加剧了无效请求的数量。
解决方法
1. 复用gRPC连接
gRPC连接基于HTTP/2,天生支持多路复用,单个连接可承载大量并发RPC请求,完全不需要为每个请求新建连接。修改代码如下:
var ( threads = 1600 reqlength = 122 port = 8011 ) var globalClient pb.TestQPSClient var globalConn *grpc.ClientConn func initGlobalClient(addr string) error { var err error globalConn, err = grpc.Dial(addr, grpc.WithInsecure()) if err != nil { return fmt.Errorf("dial server failed: %v", err) } globalClient = pb.NewTestQPSClient(globalConn) return nil } func main() { fmt.Printf("start client with %d threads size %d\n", threads, reqlength) // 初始化全局共享连接 if err := initGlobalClient(fmt.Sprintf("127.0.0.1:%d", port)); err != nil { fmt.Println(err) return } defer globalConn.Close() wg := sync.WaitGroup{} for j := 0; j < threads; j++ { wg.Add(1) go func(x int) { defer wg.Done() buf := make([]byte, reqlength) req := pb.Req{ReqData: buf} _, err := globalClient.LoadData(context.Background(), &req) if err != nil { fmt.Println(err.Error()) return } }(j) } wg.Wait() }
2. 控制并发请求数
即使复用连接,瞬间发起1600个请求也可能压垮服务器,建议通过带缓冲的channel控制并发数:
func main() { fmt.Printf("start client with %d threads size %d\n", threads, reqlength) if err := initGlobalClient(fmt.Sprintf("127.0.0.1:%d", port)); err != nil { fmt.Println(err) return } defer globalConn.Close() // 设置最大并发数,可根据服务器性能调整 sem := make(chan struct{}, 200) wg := sync.WaitGroup{} for j := 0; j < threads; j++ { wg.Add(1) sem <- struct{}{} go func(x int) { defer func() { wg.Done() <-sem }() buf := make([]byte, reqlength) req := pb.Req{ReqData: buf} _, err := globalClient.LoadData(context.Background(), &req) if err != nil { fmt.Println(err.Error()) return } }(j) } wg.Wait() }
3. 完善错误处理
在客户端初始化时,必须检查返回值,避免无效的RPC调用:
// 原goroutine内的初始化逻辑修改为: _cli := RpcClient{} if !_cli.init(fmt.Sprintf("127.0.0.1:%d", port)) { fmt.Println("client init failed") return }
4. 调整系统资源限制(可选)
如果确实需要维持大量并发连接,可以调整系统的文件描述符限制:
- Linux临时调整:执行
ulimit -n 65535 - 永久调整:修改
/etc/security/limits.conf,添加:* soft nofile 65535 * hard nofile 65535
同时服务器端也需调整gRPC配置,比如设置最大连接数、握手超时时间等。
内容的提问来源于stack exchange,提问作者ou lee
相关产品推荐
相关产品推荐

