Go语言版本升级带来的挑战

发表时间: 2022-11-25 00:06

背景

前几天,QA 环境的脚本作业平台(类似 ansible: 可以控制机器跑脚本)作业下发失败:没有找到对应机器的长链接

过程

问题出在作业机器和服务端的长链接失效了,一直在重新创建,导致了作业一直下发失败。很明显是网络模块相关的问题,这部分代码比较底层,最近也没有动过,看起来是一个比较棘手的问题。


随后通过版本回滚,问题就修复了。因为最近新加了一个依赖库,需要依赖 Go 1.18 以上的泛型,所以把 Go 的版本从 1.17.2 升级到了 1.19, 导致了这个问题。可以说明这是一个相对底层的问题。

当然,我们需要找到真正的原因,继续深入探索。


尝试抓包试试看,结果发现是服务端主动断开链接的,问题应该是出在服务端。(2026 端口是服务端, 4496客户端


于是从服务端入手,Debug 看看,去掉一些无关紧要的信息,服务端代码大概如下图所示:

package mainimport (  "fmt"  "net"  "net/http"  "time"  "golang.org/x/net/http2")type B struct {}func (b *B) ServeHTTP(resp http.ResponseWriter, req *http.Request) {  resp.Write([]byte("long"))  resp.(http.Flusher).Flush()  time.Sleep(2 * time.Second)  resp.Write([]byte("long end"))  resp.(http.Flusher).Flush()  return}type A struct {}func (a *A) ServeHTTP(resp http.ResponseWriter, req *http.Request) {  conn, err := UpgradeServerTCP(resp, req)  if err != nil {    return  }  fmt.Println(resp)  go func() {    server := &http2.Server{}    server.ServeConn(conn, &http2.ServeConnOpts{      Handler: &B{},    })  }()}func UpgradeServerTCP(w http.ResponseWriter, req *http.Request) (c net.Conn, err error) {  hijacker, ok := w.(http.Hijacker)  if !ok {    return nil, fmt.Errorf("http upgrade failed")  }  h := w.Header()  h.Set("Content-Length", "2")  h.Set("Content-Type", "application/json")  w.WriteHeader(101)  w.Write([]byte("{}"))  conn, _, err := hijacker.Hijack()  if err != nil {    return nil, err  }  return conn, nil}func main() {  err := http.ListenAndServe("127.0.0.1:10001", &A{})  if err != nil {    panic(err)  }}

也对 Go 的版本进行分析:1.18.81.19 之间的变更导致了此次的问题。把这2个版本之间的变更 diff 出来, 只看网络相关的变更,进一步缩小范围。

通过不断的 Debug 分析,我发现了一个细节:1.19 版本的服务端在与客户端建立连接之前,会多发出一个数据帧。

参照着代码看,这应该是在发送 101 Switching Protocol 之后发送的

  h := w.Header()  h.Set("Content-Length", "2")  h.Set("Content-Type", "application/json")  w.WriteHeader(101)  w.Write([]byte("{}"))

刚好,1.19WriteHeader 进行了修改,感觉已经快接近真相了。(左边1.18.8, 右边 1.19)

可以看出来,1.19 的 WriteHeader 会单独发送一个 Frame, 不用我们自己封装一个 Frame,所以我们多 w.Write([]byte("{}")) 这个会导致服务端重新写一个流式的数据包,最终长链接建立不起来。

看来源码以后,发现是我们的代码写的有问题,其实不需要我们自己封装一个frame,只需要写 writeHeader 就可以了, 这是版本兼容的。

h := w.Header()

h.Set("Content-Length", "2")

h.Set("Content-Type", "application/json")

w.WriteHeader(101)

w.Write([]byte("{}"))


记录:

https://github.com/golang/go/issues/56753