You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

1600个gRPC客户端并发请求失败,报connection closed before server preface错误

问题分析与解决:并发1600个gRPC客户端请求失败,报错connection closed before server preface received

问题原因

  1. 连接复用缺失:每个goroutine都创建独立的gRPC连接,瞬间发起1600个TCP连接,远超系统默认文件描述符限制(Linux默认一般为1024),导致大量连接无法完成建立,服务器直接关闭未完成握手的连接,触发该错误。
  2. 懒连接机制放大问题:grpc.Dial默认采用懒连接策略,即第一次调用RPC时才真正建立连接。1600个请求同时触发连接建立,服务器和系统资源瞬间耗尽,无法处理所有握手请求。
  3. 错误处理缺失:客户端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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 14:33:21