背景
前几天,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.8 到 1.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.19 对 WriteHeader 进行了修改,感觉已经快接近真相了。(左边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