ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

1024 给后端工程师的一份验证清单:把“我觉得”变成证据

1024 给后端工程师的一份验证清单:把“我觉得”变成证据

程序员节适合给自己留一个小练习:下次判断“这就是根因”以前,先把证据写出来。

做后端转 AI 应用,最容易带过去的坏习惯,是把一个熟悉现象直接接到一个熟悉结论上。TIME_WAIT 很多,就说连接泄漏;回答变长,就说模型变差;缓存命中高,就说成本下降。每一句都可能对,也都可能漏掉另一半条件。

把一句判断拆成四栏

我建议把排障笔记写成下面这样。它是构造练习,不是某次生产事故的还原:

栏目连接复用练习中的内容
观察连续请求是否使用了同一条连接
假设响应体的读取与关闭方式影响复用
验证同一客户端访问本地测试服务,记录 GotConn 的 Reused
边界本地 HTTP/1.1 结果不能直接证明生产代理、HTTP/2 或端口耗尽根因

这四栏强迫自己承认:观察和解释不是同一件事。保留边界,也让后来接手的人知道还缺什么。

一个能独立运行的观察程序

package main import ( "fmt" "io" "net/http" "net/http/httptest" "net/http/httptrace" ) func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, "ok") })) defer srv.Close() client := srv.Client() defer client.CloseIdleConnections() for i := 0; i < 2; i++ { reused := false req, err := http.NewRequest("GET", srv.URL, nil) if err != nil { panic(err) } trace := &httptrace.ClientTrace{GotConn: func(c httptrace.GotConnInfo) { reused = c.Reused }} req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) resp, err := client.Do(req) if err != nil { panic(err) } _, readErr := io.Copy(io.Discard, resp.Body) closeErr := resp.Body.Close() if readErr != nil { panic(readErr) } if closeErr != nil { panic(closeErr) } fmt.Printf("request=%d reused=%t\n", i+1, reused) } }

保存成 main.go,执行 go run main.go。它只用本地回环网络,不访问任何生产系统。两次请求共用一个客户端,第一条没有现成连接,第二条在连接保持可用时可以复用。程序观察的是这个条件,不是“读取完就永远不会建新连接”的保证。

net/http 文档描述了 Body 生命周期和复用关系。版本也要留下:本地工具链是 Go 1.26.5;当前网上标准库文档可能对应更新版本,不能把新文档行为自动算到旧运行时头上。

当证据和预想不一致时

如果第二条没有复用,别先改连接池参数。检查响应是否读完、服务是否主动关闭、代理路径是否改变、请求是否被取消。每排除一个条件,笔记里就多一条可检查事实。

性能判断也是一样。Go 1.27 发布说明对小对象分配给出了有条件的改进描述,它不等于每个业务都会获得同样收益。真正值得留下的是同样输入、同样环境下的对照,以及结论不适用的范围。

Google SRE 的复盘章节强调从系统条件中学习。写下错误假设并不丢人;把猜测包装成确定事实,才会让下一次排障继续绕路。

这个 1024,我更愿意祝大家多留几份能复跑的记录,少背几句没有条件的结论。本文示例已通过编译,但本机沙箱拒绝回环端口监听,实际运行停在 httptest 建立监听处。因此前述复用输出是预期行为,本轮没有观察到;没有将这个受限结果当作通过的实验。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进