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

Golang使用JSON-RPC调用偶现unexpected EOF错误求助排查

偶发unexpected EOF错误排查与解决方案

问题背景

客户端通过HAProxy访问两个RPC后端服务,偶发unexpected EOF错误,排查日志未发现其他异常,无明确线索。

客户端代码

func (this *SingleConnRpcClient) Call(method string, args interface{}, reply interface{}) error {
    this.Lock()
    defer this.Unlock()

    err := this.serverConn()
    if err != nil {
        return err
    }

    timeout := time.Duration(10 * time.Second)
    done := make(chan error, 1)

    go func() {
        err := this.rpcClient.Call(method, args, reply)
        done <- err
    }()

    select {
    case <-time.After(timeout):
        this.close()
        tools.LogInfo(fmt.Sprintf("rpc call timeout %s", this.RpcServer))
    case err := <-done:
        if err != nil {
            this.close()
            return err
        }
    }

    return nil
}

服务端代码

addr := g.Config().Rpc.Listen
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
    log.Fatalf("net.ResolveTCPAddr fail: %s", err)
}

listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
    log.Fatalf("listen %s fail: %s", addr, err)
} else {
    log.Println("rpc listening", addr)
}

server := rpc.NewServer()
server.Register(new(Transfer))

for {
    conn, err := listener.Accept()
    if err != nil {
        log.Println("listener.Accept occur error:", err)
        continue
    }
    go server.ServeCodec(jsonrpc.NewServerCodec(conn))
}

排查方向与解决办法

1. HAProxy配置问题

  • 超时参数不匹配:HAProxy的timeout server如果小于客户端调用超时(当前客户端是10s),会主动切断未完成的RPC连接,导致客户端收到EOF。建议将timeout server设为15s以上,同时检查timeout client、timeout connect参数是否合理。
  • 连接回收机制:HAProxy默认会回收空闲连接,如果客户端复用了已被HAProxy回收的连接,会触发EOF。可通过调整timeout http-keep-alive(TCP模式下为timeout tunnel)延长空闲连接保留时间,或启用option tcp-check让HAProxy主动检测后端连接可用性,避免转发请求到已失效的连接。

2. 客户端连接复用缺陷

客户端采用单连接复用模式,但未检测连接有效性:

  • 在serverConn()方法中增加连接健康检查,避免复用已被远端断开的连接:
    func (this *SingleConnRpcClient) serverConn() error {
        if this.rpcClient != nil {
            rawConn, ok := this.rpcClient.Client().Conn.(net.Conn)
            if ok {
                // 尝试通过读超时检测连接是否存活
                rawConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
                var buf [1]byte
                _, err := rawConn.Read(buf[:])
                if err != nil {
                    if err != io.EOF && !errors.Is(err, os.ErrDeadlineExceeded) || err == io.EOF {
                        this.close()
                    }
                }
                rawConn.SetReadDeadline(time.Time{})
            }
        }
        if this.rpcClient == nil {
            conn, err := net.Dial("tcp", this.RpcServer)
            if err != nil {
                return err
            }
            this.rpcClient = jsonrpc.NewClient(conn)
        }
        return nil
    }
    
  • 增加EOF错误重试逻辑,在收到unexpected EOF时尝试重新建立连接并再次调用:
    case err := <-done:
        if err != nil {
            this.close()
            // 针对EOF错误进行一次重试
            if strings.Contains(err.Error(), "unexpected EOF") {
                if reErr := this.serverConn(); reErr == nil {
                    reErr = this.rpcClient.Call(method, args, reply)
                    if reErr == nil {
                        return nil
                    }
                }
            }
            return err
        }
    

3. 服务端连接处理缺失

服务端未捕获连接处理过程中的错误,无法定位异常关闭原因:

  • 修改服务端连接处理逻辑,捕获并记录ServeCodec的错误:
    go func(conn net.Conn) {
        defer conn.Close()
        err := server.ServeCodec(jsonrpc.NewServerCodec(conn))
        if err != nil && !errors.Is(err, io.EOF) {
            log.Printf("rpc serve error: %v, client addr: %v", err, conn.RemoteAddr())
        }
    }(conn)
    
  • 检查服务端是否有主动关闭空闲连接的逻辑,若存在需确保超时时间大于HAProxy和客户端的超时配置。

4. 网络层面异常

偶发网络波动、防火墙主动断开连接也会导致EOF,可通过以下方式缓解:

  • 客户端增加全局重试次数限制(如最多重试2次),避免单次异常导致请求失败。
  • 在网络链路中增加监控,追踪TCP连接的建立、断开状态,定位是否存在网络设备主动切断连接的情况。

内容的提问来源于stack exchange,提问作者Daunmingze

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 07:10:18