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
相关产品推荐
相关产品推荐

