<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>HTTP on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/http/</link>
        <description>Recent content in HTTP on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sun, 14 Jun 2026 21:48:47 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/http/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Go HTTP 请求慢在哪里？</title>
            <link>https://www.wujiachen.com.cn/posts/go-httptrace-tracing/</link>
            <pubDate>Sun, 14 Jun 2026 21:48:46 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/go-httptrace-tracing/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/cover.png&#34; alt=&#34;Featured image of post Go HTTP 请求慢在哪里？&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;一个 HTTP 请求耗时 500ms。DNS？TCP？TLS？服务端？没有 httptrace，你猜不出来。&lt;/p&gt;&#xA;&lt;p&gt;因为 &lt;code&gt;http.Client.Do&lt;/code&gt; 只给你一个最终结果，不告诉你中间发生了什么。&lt;/p&gt;&#xA;&lt;p&gt;Go 标准库里的 &lt;code&gt;net/http/httptrace&lt;/code&gt; 就是来解决这个问题的。它能把一次 HTTP 请求拆成 5 个独立阶段，每个阶段都能精确测量耗时。更好的是，它不需要第三方库，不需要额外的代理或中间件，一行 import 就能用。&lt;/p&gt;&#xA;&lt;p&gt;这不是一个新工具了——&lt;code&gt;httptrace&lt;/code&gt; 从 Go 1.7 开始就有了，至今快 10 年。但在我的观察里，很多 Go 开发者听过但没用过。原因不是它不好用，而是很少有人告诉你&amp;quot;什么时候该用它&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章会带你从头搭建一个 httptrace 诊断工具，用实测数据看看&amp;quot;慢&amp;quot;到底在哪，最后给你一个完整的定位框架——遇到慢请求时，你知道从哪入手。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文基于 Go 1.26.4 实测，但 &lt;code&gt;httptrace&lt;/code&gt; 的核心行为从 Go 1.7 以来基本一致。不同版本间 &lt;code&gt;ClientTrace&lt;/code&gt; 的字段数和 Transport 默认值可能有细微差异。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;一http-请求的-5-个阶段&#34;&gt;&lt;a href=&#34;#%e4%b8%80http-%e8%af%b7%e6%b1%82%e7%9a%84-5-%e4%b8%aa%e9%98%b6%e6%ae%b5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、HTTP 请求的 5 个阶段&#xA;&lt;/h2&gt;&lt;p&gt;一次 HTTPS 请求从发出到收到响应，在底层要经历这些步骤：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;DNS 解析&lt;/strong&gt;：把域名解析成 IP 地址&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;TCP 连接&lt;/strong&gt;：三次握手建立连接&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;TLS 握手&lt;/strong&gt;：协商加密参数（仅 HTTPS）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;等待响应（TTFB）&lt;/strong&gt;：请求发出去后等服务端返回第一个字节&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Body 传输&lt;/strong&gt;：读取响应体的内容&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;每个阶段都可能成为瓶颈，但常规手段看不到它们各自花了多少时间。&lt;code&gt;httptrace&lt;/code&gt; 做的事情很简单：它在这 5 个阶段各插了一个钩子，你往钩子上挂一个记录时间戳的函数，就能拿到每个阶段的起止时间。&lt;/p&gt;&#xA;&lt;p&gt;使用方式很特别。它没有在 &lt;code&gt;http.Client&lt;/code&gt; 上加一个 &lt;code&gt;Tracer&lt;/code&gt; 字段，而是把 trace 塞进了 &lt;code&gt;context.Context&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;trace&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSStartInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;DNS start: %s\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSDone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSDoneInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;DNS done: %v\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Addrs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;WithClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Background&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;trace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NewRequestWithContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;MethodGet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;https://example.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DefaultClient&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Do&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么用 context 而不是接口？这个设计选择绕开了两个常见问题。&lt;/p&gt;&#xA;&lt;p&gt;假设在 &lt;code&gt;http.Client&lt;/code&gt; 上加一个 &lt;code&gt;Tracer&lt;/code&gt; 接口字段会怎样：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Client&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Tracer&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Tracer&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 所有请求共享同一个 Tracer&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样会有一个问题：你没法给单次请求单独配置 trace——要么所有请求都有，要么都没有。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;httptrace&lt;/code&gt; 选择把 trace 绑在 context 上，trace 的作用域天然绑定在单次请求。同一个 &lt;code&gt;http.Client&lt;/code&gt; 可以被多个 goroutine 复用，每个请求都可以带自己的 trace，不需要在 client 上改共享状态。如果你的中间件会转发 context，trace 也跟着自动传播——零侵入。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch1-context-vs-interface.png&#34; alt=&#34;context vs 接口设计对比&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ClientTrace&lt;/code&gt; 本身是一个结构体，里面是一组可选的函数字段。你关心 DNS，就填 &lt;code&gt;DNSStart&lt;/code&gt; 和 &lt;code&gt;DNSDone&lt;/code&gt;；关心首字节，就填 &lt;code&gt;GotFirstResponseByte&lt;/code&gt;。没有设置的字段是 nil，Transport 会跳过。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;官方文档&lt;/strong&gt;说 hook 可能从不同 goroutine 调用，有些 hook 甚至可能在请求完成或失败之后才触发。所以在 hook 里写共享变量时，要么只服务于单次请求的局部记录，要么自己处理并发访问。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;code&gt;httptrace&lt;/code&gt; 提供的 hooks 远不止上面提到的几个。完整的 &lt;code&gt;ClientTrace&lt;/code&gt; 在 Go 1.26 中有 16 个可选字段——除了 DNS 和连接的 hooks，还有 &lt;code&gt;GetConn&lt;/code&gt;（获取连接前）、&lt;code&gt;WroteHeaders&lt;/code&gt;（请求头发送完）、&lt;code&gt;WroteRequest&lt;/code&gt;（请求体写完）、&lt;code&gt;PutIdleConn&lt;/code&gt;（连接放回空闲池）等。用得最多的是 DNS/TCP/TLS/首字节这 4 类，但 &lt;code&gt;GotConn&lt;/code&gt; 的连接复用信息在实际诊断中价值很高，后面会单独展开。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch1-http-phases-timeline.png&#34; alt=&#34;HTTP 请求 5 阶段时序图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;二写一个工具看看时间花在哪&#34;&gt;&lt;a href=&#34;#%e4%ba%8c%e5%86%99%e4%b8%80%e4%b8%aa%e5%b7%a5%e5%85%b7%e7%9c%8b%e7%9c%8b%e6%97%b6%e9%97%b4%e8%8a%b1%e5%9c%a8%e5%93%aa&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、写一个工具，看看时间花在哪&#xA;&lt;/h2&gt;&lt;p&gt;理论说完了，写一个真正的排查工具。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch2-cli-tool-running.png&#34; alt=&#34;CLI 工具运行场景&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;定义一个结构体来记录每个阶段的时间点：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;phaseTimes&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;begin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;     &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dialBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dialEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;connReady&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;wroteReq&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;firstByte&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;finished&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后在 &lt;code&gt;ClientTrace&lt;/code&gt; 里填上 hook——每个 hook 做的事就是记录当前时间：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;buildClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;phaseTimes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ClientTrace&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSStartInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSDone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DNSDoneInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ConnectStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dialBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ConnectDone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dialEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TLSHandshakeStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsBegin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TLSHandshakeDone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tls&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ConnectionState&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsEnd&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;GotConn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;GotConnInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;connReady&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;WroteRequest&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;WroteRequestInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;wroteReq&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;GotFirstResponseByte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;firstByte&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;组合起来就是一个完整的命令行工具（完整代码见 &lt;code&gt;evidence/code/httptrace-cli/main.go&lt;/code&gt;）。它对几个常用站点跑一组实测，就能告诉你&amp;quot;慢在哪&amp;quot;。我对 github.com、baidu.com 和 pkg.go.dev 各跑了一次，结果如下。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;实测数据 [实测 Go 1.26.4 darwin/arm64，家庭宽带]：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;站点&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;DNS 解析&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;TCP 连接&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;TLS 握手&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;TTFB&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;Body 传输&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;总耗时&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;github.com&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;7.26ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0.45ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;399.47ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;76.73ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;98.81ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;583.98ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;baidu.com&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;53.23ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0.63ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;102.58ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;29.35ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;2.00ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;370.58ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;pkg.go.dev&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;13.60ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0.28ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;298.51ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;254.62ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;153.96ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;721.32ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;数据里有几个值得注意的发现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;TLS 握手是本次测试中最耗时的阶段&lt;/strong&gt;。github.com 的 TLS 花了将近 400ms，占整个请求的 68%。baidu.com 好一些，但也用了 102ms。但这跟证书链长度、OCSP 验证、物理距离有关，不代表所有 HTTPS 请求都如此。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;TCP 连接本身非常快&lt;/strong&gt;。三次握手只花了不到 1ms——你平时觉得&amp;quot;连接慢&amp;quot;，其实慢的是 TLS，不是 TCP。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;DNS 解析差异大&lt;/strong&gt;。github.com 的 DNS 只要 7ms，baidu.com 却要 53ms。baidu.com 用了智能 DNS 调度，不同地域的解析结果可能指向不同的 CDN 节点——这会导致 DNS 耗时差异大。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;TTFB 差异反映服务端处理速度&lt;/strong&gt;。baidu.com 只用了 29ms 就返回了第一个字节，pkg.go.dev 却用了 254ms——这反映的是服务端响应速度，不是网络问题。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;大多数 HTTP 慢请求，不是慢在传输，而是慢在握手。这次测试里，三个站点的 TLS 分别占了总耗时的 28% 到 68%。如果你遇到一个 HTTPS 请求比预期慢很多，第一个要问的问题不是&amp;quot;网络是不是有问题&amp;quot;，而是&amp;quot;TLS 握手花了多久&amp;quot;。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;关于 TCP 耗时&lt;/strong&gt;：&amp;lt; 1ms 的 TCP 连接在互联网环境下并不常见——三次握手通常需要 20-100ms（取决于 RTT）。这里的异常低值是因为系统可能有连接缓存或 Keep-Alive 介入，&lt;code&gt;ConnectStart&lt;/code&gt;/&lt;code&gt;ConnectDone&lt;/code&gt; 记录的不是完整的三次握手时间。后面 §四 会更详细讨论连接复用的影响。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;还有一个细节：&lt;code&gt;DNSStart&lt;/code&gt; 和 &lt;code&gt;DNSDone&lt;/code&gt; 只有在真正触发了 DNS 查询时才有值。如果你传的是 IP 地址，或者自定义了 DialContext 直接连接 IP，这两个 hook 就不会触发。但要注意，即使系统 DNS 缓存命中了，hook 仍然会触发，只是返回速度非常快。同样，&lt;code&gt;TLSHandshakeStart&lt;/code&gt; 和 &lt;code&gt;TLSHandshakeDone&lt;/code&gt; 只在 HTTPS 请求中出现，HTTP 请求不会有 TLS 阶段。&lt;/p&gt;&#xA;&lt;p&gt;实际使用时，需要对这些情况做判空处理。比如打印 DNS 耗时之前先检查 &lt;code&gt;dnsBegin&lt;/code&gt; 和 &lt;code&gt;dnsEnd&lt;/code&gt; 是否非零：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;IsZero&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsEnd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;IsZero&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;DNS 解析: %v\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsEnd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sub&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;否则在连接复用的场景下，你的日志会输出一堆零值，容易误导判断。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch2-multi-site-benchmark.png&#34; alt=&#34;多站点阶段耗时对比柱状图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;三把-trace-嵌入项目roundtripper-封装&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e6%8a%8a-trace-%e5%b5%8c%e5%85%a5%e9%a1%b9%e7%9b%aeroundtripper-%e5%b0%81%e8%a3%85&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、把 trace 嵌入项目——RoundTripper 封装&#xA;&lt;/h2&gt;&lt;p&gt;上面那个 CLI 工具对排查单个请求有用。但在实际项目中，你希望所有通过某个 &lt;code&gt;http.Client&lt;/code&gt; 发出的请求都自动记录耗时，不需要手动为每个请求创建 trace。&lt;/p&gt;&#xA;&lt;p&gt;不是所有项目都需要用 RoundTripper 包装。我建议的场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;你在开发一个 API 客户端&lt;/strong&gt;，需要了解每次请求的性能特征&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;线上出现间歇性慢请求&lt;/strong&gt;，你想在不改业务代码的前提下增加可观测性&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;你想做长周期的性能基线采集&lt;/strong&gt;，看看请求耗时是否在逐渐劣化&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果只是临时排查一个请求慢，直接用上面那个 CLI 工具就够了，不需要封装到项目里。&lt;/p&gt;&#xA;&lt;p&gt;需要封装的话，Go 的 &lt;code&gt;http.RoundTripper&lt;/code&gt; 接口就是做这个的。它是一个中间件模式——你可以包装默认的 Transport，在请求前后插入自己的逻辑：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TracingTransport&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Base&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;RoundTripper&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Log&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;phaseTimes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tt&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TracingTransport&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;RoundTrip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Response&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;base&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Base&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;base&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;base&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;DefaultTransport&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;phaseTimes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;begin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;trace&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;buildClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;WithContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;WithClientTrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;trace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;base&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;RoundTrip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;finished&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Log&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用时：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Transport&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TracingTransport&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;phaseTimes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;%s %s dns=%v tls=%v ttfb=%v total=%v&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Method&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;URL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsEnd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sub&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dnsBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsEnd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sub&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tlsBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;firstByte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sub&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;wroteReq&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;finished&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sub&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;begin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样每次请求都会自动输出一条带各阶段耗时的日志，不需要为每个请求手动写 trace 代码。&lt;/p&gt;&#xA;&lt;p&gt;这里有两个容易被忽略的细节。&lt;/p&gt;&#xA;&lt;p&gt;先说一个实现细节，基于当前 Go 实现，&lt;code&gt;httptrace.WithClientTrace&lt;/code&gt; 会把新 hook 和旧 hook 合并，新注册的先调用。这意味着调用方已有的 trace 不会被覆盖。比如你的框架已经加了 trace 记录请求耗时，你在业务代码里再加一个 trace 关注特定阶段——两个都会生效。不过这是内部实现细节，未来版本可能变化。&lt;/p&gt;&#xA;&lt;p&gt;另一个容易被忽略的点：&lt;code&gt;RoundTrip&lt;/code&gt; 返回时，响应头已经读到了，但响应 body 通常还没读完。上面代码里的 &lt;code&gt;pt.finished&lt;/code&gt; 更接近&amp;quot;拿到响应头的时间&amp;quot;，不是包括 body 下载在内的完整耗时。如果需要更精确的完整耗时，可以包装 &lt;code&gt;res.Body&lt;/code&gt;，在 &lt;code&gt;Close&lt;/code&gt; 时记录最终时间。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;进阶&lt;/strong&gt;：包装 &lt;code&gt;res.Body&lt;/code&gt; 的思路是通过 &lt;code&gt;onClose&lt;/code&gt; 闭包捕获 &lt;code&gt;req&lt;/code&gt; 和 &lt;code&gt;pt&lt;/code&gt;。每次 &lt;code&gt;RoundTrip&lt;/code&gt; 会创建新的 &lt;code&gt;pt&lt;/code&gt; 实例，因此被多 goroutine 复用时不会产生竞争。代码实现见 &lt;code&gt;evidence/code/httptrace-cli/main.go&lt;/code&gt;。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch3-transport-architecture.png&#34; alt=&#34;TracingTransport 调用链架构图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;四连接复用最容易被忽略的性能杀手&#34;&gt;&lt;a href=&#34;#%e5%9b%9b%e8%bf%9e%e6%8e%a5%e5%a4%8d%e7%94%a8%e6%9c%80%e5%ae%b9%e6%98%93%e8%a2%ab%e5%bf%bd%e7%95%a5%e7%9a%84%e6%80%a7%e8%83%bd%e6%9d%80%e6%89%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、连接复用：最容易被忽略的性能杀手&#xA;&lt;/h2&gt;&lt;p&gt;很多人遇到过这种情况：同一个 &lt;code&gt;http.Client&lt;/code&gt; 反复请求同一 URL，每次耗时都差不多，没觉得有缓存效果。很可能连接根本没有被复用。&lt;/p&gt;&#xA;&lt;p&gt;Go 的 &lt;code&gt;http.Transport&lt;/code&gt; 默认维护一个连接池。复用连接的好处很明显——省去了 DNS 解析、TCP 连接和 TLS 握手。TLS 握手在 HTTPS 请求中通常是最贵的阶段，一次握手 100ms-1s 不等。如果每次请求都要重新握手，性能损失是巨大的。&lt;/p&gt;&#xA;&lt;p&gt;连接池默认是这样的：每个 host 最多保持 2 个空闲连接（&lt;code&gt;MaxIdleConnsPerHost&lt;/code&gt; 默认值 2），空闲连接默认超时 90 秒（&lt;code&gt;IdleConnTimeout&lt;/code&gt; 默认值 90s）。&lt;code&gt;MaxIdleConns&lt;/code&gt;（总空闲连接数）默认是 0（无限制），所以主要瓶颈在 per-host 的限制。这个配置对大多数场景够用，不过如果你的服务需要跟同一后端建立大量连接，可能需要调整这些参数。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;httptrace.GotConnInfo&lt;/code&gt; 能直接告诉你连接是否被复用了。它的 &lt;code&gt;Reused&lt;/code&gt; 字段表示这条连接之前是否服务过其他请求，&lt;code&gt;WasIdle&lt;/code&gt; 表示它是否从空闲池取出来的，&lt;code&gt;IdleTime&lt;/code&gt; 表示空闲了多久。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;GotConn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;httptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;GotConnInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;connReady&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Reused&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;连接复用，空闲了 %v&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;IdleTime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;新建连接到 %s&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Conn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;RemoteAddr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我做了个简单的测试验证连接复用的效果。连续对同一 URL 发 3 次请求，看看耗时变化：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;实测数据 [实测 Go 1.26.4 darwin/arm64，2026-06-14]：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;请求序号&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;连接复用&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;总耗时&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;第1次（新建连接）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;false&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1598ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;第2次（复用连接）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;true&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;253ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;第3次（复用连接）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;true&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;363ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;第一次请求花了 1.6 秒，其中 TLS 握手占了 1.25 秒。第二次请求只用了 253 毫秒——快了 6 倍。这就是连接复用的威力。&lt;/p&gt;&#xA;&lt;p&gt;连接复用的第一课：让连接活到被复用。&lt;/p&gt;&#xA;&lt;p&gt;第2、3次请求中，&lt;code&gt;TLSHandshakeStart&lt;/code&gt;/&lt;code&gt;Done&lt;/code&gt; 均未触发，确认 TLS 握手被完全跳过。第3次比第2次慢了 110ms——这个波动可能是瞬时网络拥塞或服务端负载变化导致的。单次测试的读数仅供参考，建议连续跑 10 次取 P50/P95 来评估真实性能。&lt;/p&gt;&#xA;&lt;p&gt;但注意，连接复用不是万能的。请求频率太低时，连接池可能因为超时回收连接（默认 &lt;code&gt;IdleConnTimeout&lt;/code&gt; 为 90s，但操作系统或中间网络设备可能主动关闭空闲连接），下次请求还是得重新建。另外需要注意协议差异——HTTP/2 的多路复用跟 HTTP/1.1 的连接池机制不一样：HTTP/2 的一条连接可以同时处理多个请求，不需要排队。测试中使用的就是 HTTP/2（ALPN 协商），HTTP/2 的 SETTINGS 帧交换可能在首次连接时额外增加开销。&lt;/p&gt;&#xA;&lt;p&gt;如果你连续访问同一个 host，却发现每次 &lt;code&gt;Reused&lt;/code&gt; 都是 &lt;code&gt;false&lt;/code&gt;，优先检查两件事。&lt;/p&gt;&#xA;&lt;p&gt;一是&lt;strong&gt;代码是否每次请求都创建新的 &lt;code&gt;http.Client&lt;/code&gt; 或新的 &lt;code&gt;Transport&lt;/code&gt;&lt;/strong&gt;。&lt;code&gt;Transport&lt;/code&gt; 负责连接池，频繁新建会让复用很难发生。正确的做法是复用同一个 &lt;code&gt;http.Client&lt;/code&gt; 实例。&lt;/p&gt;&#xA;&lt;p&gt;二是&lt;strong&gt;响应体是否被读完并关闭&lt;/strong&gt;。&lt;code&gt;net/http&lt;/code&gt; 的连接复用依赖调用方正确处理 &lt;code&gt;res.Body&lt;/code&gt;。只拿到 response 就返回，或者错误路径里漏掉 &lt;code&gt;Close&lt;/code&gt;，后续请求就只能重新建连接。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 错误写法——不关闭 Body&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 连接不会被放回池子&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 正确写法&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;io&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;io&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Discard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Close&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;很多人在业务代码里写 &lt;code&gt;defer res.Body.Close()&lt;/code&gt;，看起来没问题。但如果请求返回的不是 200，有人可能直接 &lt;code&gt;return&lt;/code&gt; 了，&lt;code&gt;defer&lt;/code&gt; 不会执行。或者有人只调了 &lt;code&gt;res.Body.Close()&lt;/code&gt; 但没有先读取完 Body——这样连接虽然被放回池子，但池子可能会认为这条连接还有未读数据，下次复用时会先读残留数据，导致奇怪的延迟。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch4-connection-pool-states.png&#34; alt=&#34;连接池状态示意图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;五排查框架看到结果以后怎么办&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e6%8e%92%e6%9f%a5%e6%a1%86%e6%9e%b6%e7%9c%8b%e5%88%b0%e7%bb%93%e6%9e%9c%e4%bb%a5%e5%90%8e%e6%80%8e%e4%b9%88%e5%8a%9e&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、排查框架：看到结果以后怎么办&#xA;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;httptrace&lt;/code&gt; 帮你定位到&amp;quot;慢在哪一步&amp;quot;，但定位之后呢？&lt;/p&gt;&#xA;&lt;p&gt;每个阶段慢的原因不同，排查手段也不同。当你用 httptrace 发现某个阶段异常时，按对应方案排查：&lt;/p&gt;&#xA;&lt;h3 id=&#34;dns-慢-100ms&#34;&gt;&lt;a href=&#34;#dns-%e6%85%a2-100ms&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;DNS 慢（&amp;gt; 100ms）&#xA;&lt;/h3&gt;&lt;p&gt;httptrace 能告诉你精确到毫秒的 DNS 耗时——这比 &lt;code&gt;dig&lt;/code&gt; 更真实，因为 &lt;code&gt;dig&lt;/code&gt; 测的是你机器到 DNS 服务器的延迟，httptrace 测的是 Go 实际走的解析路径。用 &lt;code&gt;dig @8.8.8.8 &amp;lt;域名&amp;gt;&lt;/code&gt; 或 &lt;code&gt;nslookup&lt;/code&gt; 对比。如果差异大，换 114DNS（国内公共 DNS）试试。频繁切换域名会导致 DNS 缓存无法生效——这是 httptrace 的 DNS hook 能直接证实的。&lt;/p&gt;&#xA;&lt;h3 id=&#34;tcp-连接慢&#34;&gt;&lt;a href=&#34;#tcp-%e8%bf%9e%e6%8e%a5%e6%85%a2&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;TCP 连接慢&#xA;&lt;/h3&gt;&lt;p&gt;TCP 连接本身很快（&amp;lt; 1ms）。如果 httptrace 显示 TCP 阶段耗时异常，通常不是三次握手本身的问题，而是&lt;strong&gt;网络延迟大&lt;/strong&gt;。用 &lt;code&gt;ping&lt;/code&gt; 看 RTT——如果 ping 也慢，是网络问题；如果 ping 快但连接慢，可能是中间有防火墙或代理。阈值参考：同区域访问 &amp;lt; 50ms，跨洲访问 150-300ms。&lt;/p&gt;&#xA;&lt;h3 id=&#34;tls-握手慢-500ms&#34;&gt;&lt;a href=&#34;#tls-%e6%8f%a1%e6%89%8b%e6%85%a2-500ms&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;TLS 握手慢（&amp;gt; 500ms）&#xA;&lt;/h3&gt;&lt;p&gt;这是 HTTPS 请求中最常见的瓶颈。httptrace 的 &lt;code&gt;TLSHandshakeStart&lt;/code&gt;/&lt;code&gt;Done&lt;/code&gt; 能精确到毫秒——结合连接复用状态判断。如果第一次请求 TLS 花了 500ms，是正常的；如果非首次请求 TLS 仍然很慢，说明连接没有被复用。TLS 1.2 需要 2 次往返（2-RTT），TLS 1.3 只需要 1 次（1-RTT）。Go 从 1.13 开始默认支持 TLS 1.3，但如果你的代理或反向代理强制了 TLS 1.2，握手时间会显著增加。用以下命令查看握手细节：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -w &lt;span class=&#34;s2&#34;&gt;&amp;#34;TCP: %{time_connect}s, TLS: %{time_appconnect}s, TTFB: %{time_starttransfer}s\n&amp;#34;&lt;/span&gt; -o /dev/null -s https://example.com&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;ttfb-慢-200ms&#34;&gt;&lt;a href=&#34;#ttfb-%e6%85%a2-200ms&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;TTFB 慢（&amp;gt; 200ms）&#xA;&lt;/h3&gt;&lt;p&gt;TTFB 反映的是&lt;strong&gt;服务端处理速度&lt;/strong&gt;，不是网络问题。httptrace 帮不了你进一步定位了——得用 pprof 或慢查询日志去查服务端慢在哪。如果你发现 TTFB 很慢但其他阶段都正常，问题大概率在服务端。&lt;/p&gt;&#xA;&lt;h3 id=&#34;连接复用异常每次都是新连接&#34;&gt;&lt;a href=&#34;#%e8%bf%9e%e6%8e%a5%e5%a4%8d%e7%94%a8%e5%bc%82%e5%b8%b8%e6%af%8f%e6%ac%a1%e9%83%bd%e6%98%af%e6%96%b0%e8%bf%9e%e6%8e%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;连接复用异常（每次都是新连接）&#xA;&lt;/h3&gt;&lt;p&gt;在开发环境给你的 TracingTransport 加一行 &lt;code&gt;GotConn&lt;/code&gt; 日志，打出来每次请求的连接复用状态。如果发现频繁建新连接，优先检查两件事：一是代码是否每次请求都 &lt;code&gt;new http.Client()&lt;/code&gt;——复用同一个实例；二是错误路径是否漏掉了 &lt;code&gt;res.Body.Close()&lt;/code&gt;。调整 &lt;code&gt;MaxIdleConnsPerHost&lt;/code&gt; 和 &lt;code&gt;IdleConnTimeout&lt;/code&gt; 是最后的优化手段，先确认前两项没问题再动配置。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;慢的阶段&lt;/th&gt;&#xA;          &lt;th&gt;常见原因&lt;/th&gt;&#xA;          &lt;th&gt;首选排查手段&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;DNS&lt;/td&gt;&#xA;          &lt;td&gt;DNS 服务器慢、缓存未命中&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;dig&lt;/code&gt; + 换 DNS&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;TCP&lt;/td&gt;&#xA;          &lt;td&gt;网络延迟大、物理距离远&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ping&lt;/code&gt; RTT&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;TLS&lt;/td&gt;&#xA;          &lt;td&gt;证书链长、OCSP 验证&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;openssl s_client&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;TTFB&lt;/td&gt;&#xA;          &lt;td&gt;服务端处理慢&lt;/td&gt;&#xA;          &lt;td&gt;pprof + 慢查询&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;连接复用&lt;/td&gt;&#xA;          &lt;td&gt;未关闭 Body、新建 Client&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;GotConnInfo.Reused&lt;/code&gt; 日志&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch5-decision-tree.png&#34; alt=&#34;排查决策树流程图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;六总结&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六、总结&#xA;&lt;/h2&gt;&lt;p&gt;回到开头的问题：当 HTTP 请求变慢时，你至少要知道慢在哪一步。&lt;/p&gt;&#xA;&lt;p&gt;这个问题看起来简单，但没有 httptrace 之前，答案是靠猜的。你只能看到总耗时，然后凭经验判断&amp;quot;可能是网络问题&amp;quot;或&amp;quot;可能是服务端问题&amp;quot;。有了 httptrace，猜测变成了测量。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;net/http/httptrace&lt;/code&gt; 帮你看清了这个&amp;quot;黑箱&amp;quot;——DNS 解析、TCP 连接、TLS 握手、首字节时间、Body 传输，每个阶段都可以独立测量。它不需要第三方库、不需要改代码架构，只要在请求的 context 里塞一个 &lt;code&gt;ClientTrace&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;这就是一个完整的诊断链——httptrace 告诉你哪一环慢，系统知识告诉你为什么慢，排查手段告诉你怎么修。三环缺一不可。&lt;/p&gt;&#xA;&lt;p&gt;看到数据只是第一步，真正有价值的是怎么判断它：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TLS 握手花了 400ms？如果是第一次请求，这是正常的。但如果非首次请求 TLS 仍然很慢——说明连接没有被复用。&lt;/li&gt;&#xA;&lt;li&gt;TTFB 用了 200ms+？服务端需要优化。&lt;/li&gt;&#xA;&lt;li&gt;每次都是新建连接？看看 Body 有没有关闭。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;把 httptrace 作为你排查链的第一环。它不解决所有问题，但帮你精确定位问题在哪一环——然后你才知道该往哪个方向使劲。&lt;/p&gt;&#xA;&lt;p&gt;如果你之前没用过 &lt;code&gt;httptrace&lt;/code&gt;，现在就可以试一下：把上面的 CLI 工具保存为 &lt;code&gt;httptrace.go&lt;/code&gt;，&lt;code&gt;go run httptrace.go https://example.com&lt;/code&gt; 就能看到每个阶段的耗时。对你日常访问的几个 URL 做一次分析，结果可能会让你意外——也许你一直以为慢在服务端的问题，其实是 TLS 握手拖了后腿。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文所有实测代码和完整源码可在 &lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;github.com/wujiachen0727/zhiyulab-evidence&lt;/a&gt; 查看（近期将整理上传）。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/go-httptrace-tracing/ch9-diagnosis-chain-summary.png&#34; alt=&#34;诊断链总结概念图&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/go-httptrace-tracing&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item><item>
            <title>为什么大厂还在用 RPC？不是因为快，是因为不崩</title>
            <link>https://www.wujiachen.com.cn/posts/rpc-vs-http/</link>
            <pubDate>Tue, 02 Jun 2026 22:52:31 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/rpc-vs-http/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/cover.png&#34; alt=&#34;Featured image of post 为什么大厂还在用 RPC？不是因为快，是因为不崩&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;所有&amp;quot;RPC 比 HTTP 快&amp;quot;的对比文章，多数拿 HTTP/1.1+JSON 当对手。换成 HTTP/2 之后呢？我跑了实验，协议层的差距在你大部分接口里是个位数百分比。那大厂为什么还在用 RPC？答案不在协议层。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;一那天凌晨三点我们的服务挂了&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e9%82%a3%e5%a4%a9%e5%87%8c%e6%99%a8%e4%b8%89%e7%82%b9%e6%88%91%e4%bb%ac%e7%9a%84%e6%9c%8d%e5%8a%a1%e6%8c%82%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、那天凌晨三点，我们的服务挂了&#xA;&lt;/h2&gt;&lt;p&gt;凌晨三点二十一分，电话把我吵醒。&lt;/p&gt;&#xA;&lt;p&gt;值班同学的声音在那头发抖：&amp;ldquo;订单接口全挂了，下游全在超时，连查询都打不开。&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;我抓起电脑接上 VPN，打开监控。一整列接口的 P99 延迟都飙到 30 秒——也就是 Nginx 的网关超时。底下用户正在双十二大促，每秒几千单。我盯着监控大屏，心里只有一个问题：&lt;strong&gt;到底是哪个环节先崩的？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;那时候我们的架构很简单：所有服务之间用 HTTP/1.1 + JSON 互相调用。Spring Boot + Nginx + Eureka，在内网里跑。说实话，我之前以为这套挺好用的——直到那一晚。&lt;/p&gt;&#xA;&lt;p&gt;后面复盘，事故链是这样的：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;一个不重要的&amp;quot;积分计算&amp;quot;接口突然变慢（下游 Redis 集群有抖动）&lt;/li&gt;&#xA;&lt;li&gt;上游的&amp;quot;下单&amp;quot;服务每次调用都等到超时——但&lt;strong&gt;没有超时上限&lt;/strong&gt;，连接池一格一格被卡住&lt;/li&gt;&#xA;&lt;li&gt;卡住之后，&amp;ldquo;下单&amp;quot;服务的连接池打满，新请求开始排队&lt;/li&gt;&#xA;&lt;li&gt;排队太久，Nginx 那一层的健康检查超时，开始把&amp;quot;下单&amp;quot;服务标成不可用&lt;/li&gt;&#xA;&lt;li&gt;流量被甩到剩下的实例上，那些实例也因为同样的下游问题，几秒后步入同样的命运&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;整条调用链垮了&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;我们花了 47 分钟把它拉回来——靠的不是什么聪明的工程手段，是手动重启每一个被卡死的服务，把&amp;quot;积分计算&amp;quot;这个不重要的功能直接降级成&amp;quot;返回 0&amp;rdquo;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch1-avalanche-flow.png&#34; alt=&#34;雪崩传播路径示意图&#34; loading=&#34;lazy&#34;&gt; [16:9]&lt;/p&gt;&#xA;&lt;p&gt;那一晚我没再睡。坐在工位上想了很多事。第二天复盘会上有人提出来：要不要换 RPC 框架？&lt;/p&gt;&#xA;&lt;p&gt;我当时的第一反应是抗拒——RPC 不就是为了&amp;quot;更快&amp;quot;吗？我们这点流量，&amp;ldquo;快不快&amp;quot;根本不是瓶颈。&lt;strong&gt;改框架的成本谁来承担？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;但接下来三个月发生的事，让我把这句话原封不动地咽了回去。&lt;/p&gt;&#xA;&lt;p&gt;先记住这个画面：&lt;strong&gt;接口崩的时候，崩的不是协议，是没有刹车的调用链。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;二所有rpc-更快的文章都选错了对手&#34;&gt;&lt;a href=&#34;#%e4%ba%8c%e6%89%80%e6%9c%89rpc-%e6%9b%b4%e5%bf%ab%e7%9a%84%e6%96%87%e7%ab%a0%e9%83%bd%e9%80%89%e9%94%99%e4%ba%86%e5%af%b9%e6%89%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、所有&amp;quot;RPC 更快&amp;quot;的文章都选错了对手&#xA;&lt;/h2&gt;&lt;p&gt;雪崩之后，我第一件事是研究 RPC。我打开搜索引擎，搜&amp;quot;RPC 比 HTTP 快多少&amp;rdquo;——好家伙，铺天盖地的文章告诉我&amp;quot;RPC 快 5 倍 / 10 倍 / 20 倍&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;我一篇一篇看。看到第六篇我笑了。&lt;/p&gt;&#xA;&lt;p&gt;我翻了一圈，绝大多数文章，对手都是 HTTP/1.1 + JSON。&lt;/p&gt;&#xA;&lt;p&gt;这就像两个人比谁跑得快，一个穿钉鞋，另一个穿拖鞋。然后告诉你：钉鞋跑得真快。&lt;/p&gt;&#xA;&lt;p&gt;让我把现有的&amp;quot;RPC 更快&amp;quot;论证拆给你看。它们的套路高度一致，就这三招：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第一招：HTTP 头巨大。&lt;/strong&gt;&#xA;他们说，HTTP/1.1 每次请求要发好几百字节的纯文本头部，又是 &lt;code&gt;Content-Type&lt;/code&gt; 又是 &lt;code&gt;Accept&lt;/code&gt;，开销巨大。&#xA;听起来很合理对吧？但你打开 HTTP/2 的 RFC 看看——HPACK 头部压缩在 2015 年就已经标准化。同样的头部，HTTP/2 走二进制 + 静态字典，重复头部通过动态表索引压缩后通常只需 1-2 字节。这一招在 HTTP/2 面前直接报废。&lt;/p&gt;&#xA;&lt;p&gt;而且你想想，这个时代谁还在生产环境用纯 HTTP/1.1？除了一些老遗留系统和外部 API，几乎所有现代微服务网关——Envoy、Nginx 1.9.5+、ALB（对外入口侧默认 HTTP/2，到后端 upstream 需显式配置）、API Gateway——基本都在用 HTTP/2。你拿 HTTP/1.1 的头部开销出来吓人，约等于拿 32 位系统说事——理论上没错，现实里离我们已经很远。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第二招：JSON 序列化慢。&lt;/strong&gt;&#xA;他们说，文本协议比二进制协议解析慢一个数量级。&#xA;这话本身没错。但 JSON 的对手不是 HTTP，是序列化方案。HTTP 完全可以走 Protobuf、可以走 MessagePack、可以走任何二进制协议——只是大多数人懒得换罢了。把&amp;quot;序列化方案&amp;quot;算到&amp;quot;协议&amp;quot;头上，本身就是偷换概念。&lt;/p&gt;&#xA;&lt;p&gt;打个比方。你说&amp;quot;开高铁比开汽车快&amp;quot;——这成立。但你说&amp;quot;开高铁比开汽车快，因为高铁烧的是电&amp;quot;——这就不对了。烧电是动力来源，不是速度差距的根本原因；快是因为它跑在专线轨道上。把&amp;quot;序列化方式&amp;quot;算到&amp;quot;协议&amp;quot;头上，犯的就是这种错。HTTP 完全可以载二进制，gRPC 跑在 HTTP/2 上就是最好的证明。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第三招：连接握手开销大。&lt;/strong&gt;&#xA;他们说，HTTP 每次请求要重新握手。&#xA;这一招最离谱。Keep-Alive 是 1997 年的 HTTP/1.1 RFC 写进去的，连接复用早就是默认行为。任何一个稍微正经一点的 client 都不会每次重新建连。&lt;/p&gt;&#xA;&lt;p&gt;你打开 Java 的 Apache HttpClient、Go 的 &lt;code&gt;http.Transport&lt;/code&gt;、Python 的 &lt;code&gt;requests.Session&lt;/code&gt;——它们底层全都是连接池，全都是 Keep-Alive。会让你&amp;quot;每次重新握手&amp;quot;的写法，是 PHP 早期那种 &lt;code&gt;file_get_contents&lt;/code&gt; 短连接调用——但那已经是十几年前的故事了。&lt;/p&gt;&#xA;&lt;p&gt;三招都打偏。那为什么这些文章看起来还挺有道理？&lt;/p&gt;&#xA;&lt;p&gt;因为他们对比的是 2010 年的 HTTP，对比的是没人用心配过的客户端，对比的是教科书里那张&amp;quot;OSI 七层&amp;quot;的简化图。他们没在拿你今天用的东西做对比。&lt;/p&gt;&#xA;&lt;p&gt;而你今天用的东西是什么？&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;gRPC = HTTP/2 + Protobuf + 一套 IDL&lt;/li&gt;&#xA;&lt;li&gt;Dubbo = TCP + Hessian/Protobuf + 注册中心（Dubbo 3.x 起主流版本默认已改用基于 HTTP/2 的 Triple 协议，兼容 gRPC）&lt;/li&gt;&#xA;&lt;li&gt;现代 HTTP 服务 = HTTP/2 + Protobuf 或 JSON + Keep-Alive&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;你看，&lt;strong&gt;gRPC 本身就是 HTTP/2&lt;/strong&gt;。这两个东西在协议层面已经是一家人了。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch2-wrong-opponent.png&#34; alt=&#34;“选错对手&amp;#34;对比图&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;p&gt;那 RPC 和 HTTP 的边界到底在哪？&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;不在协议层。在框架层。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;但要论证这一点，光说不够。我得让数据说话。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;三实测协议优化的真实收益只有-5&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e5%ae%9e%e6%b5%8b%e5%8d%8f%e8%ae%ae%e4%bc%98%e5%8c%96%e7%9a%84%e7%9c%9f%e5%ae%9e%e6%94%b6%e7%9b%8a%e5%8f%aa%e6%9c%89-5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、实测：协议优化的真实收益只有 5%&#xA;&lt;/h2&gt;&lt;p&gt;我写了两组实验。第一组对比三种协议组合的吞吐和延迟，第二组拆解一次真实调用的各环节耗时。代码全部开源，你可以自己跑。&lt;/p&gt;&#xA;&lt;h3 id=&#34;实验一三种协议组合谁更快&#34;&gt;&lt;a href=&#34;#%e5%ae%9e%e9%aa%8c%e4%b8%80%e4%b8%89%e7%a7%8d%e5%8d%8f%e8%ae%ae%e7%bb%84%e5%90%88%e8%b0%81%e6%9b%b4%e5%bf%ab&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;实验一：三种协议组合，谁更快？&#xA;&lt;/h3&gt;&lt;p&gt;我用 Go 起了三个服务，跑同一个接口（返回一个用户对象），用 32 个并发 worker 持续打 5 秒：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;方案&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;QPS&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;P50 延迟&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;P99 延迟&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;HTTP/1.1 + JSON&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;104,841&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;276µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;919µs&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;HTTP/2 + JSON&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;77,007&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;404µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;848µs&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;HTTP/2 + 二进制&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;77,804&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;403µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;837µs&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;实验配置：macOS / Go 1.26.2 / 同主机 loopback / 32 worker / 5s。响应大小：HTTP/1.1+JSON 179 B，HTTP/2+二进制 84 B。代码在 &lt;code&gt;evidence/code/e1-protocol-bench/&lt;/code&gt;，可复现。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;HTTP/1.1 + JSON 的 QPS 居然最高——这不是我特意挑的反差数据。同主机 loopback 消除了网络往返延迟，HTTP/2 多路复用的核心价值（减少 RTT 等待）在这个场景根本无法体现；叠加 HTTP/2 帧处理的额外 CPU 开销，导致同机测试 QPS 反不如 HTTP/1.1。换到真实跨机网络环境，HTTP/2 会反超回来，但反超的幅度也就 10~20%，不是数量级。这组数据更重要的结论是实验本身的局限——loopback 场景不是 HTTP/2 的主场，别拿这组数字去反推生产决策。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch3-e1-protocol-table.png&#34; alt=&#34;E1 协议对比表&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;p&gt;更有趣的是 HTTP/2 + JSON vs HTTP/2 + 二进制的对比：P50 差距只有 &lt;strong&gt;1µs&lt;/strong&gt;——百万分之一秒。把 JSON 换成二进制压缩到 47% 体积，端到端延迟几乎看不出来。&lt;/p&gt;&#xA;&lt;p&gt;为什么？因为响应本身才 179 字节。你省下 95 字节，在万兆网卡上根本不够看。&lt;/p&gt;&#xA;&lt;p&gt;那序列化本身呢？我专门跑了一组：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;操作&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;JSON&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;二进制&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;编码 50000 次平均&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;253ns&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;115ns&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;解码 50000 次平均&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;1.367µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;217ns&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;二进制确实更快——快了一个数量级。但&lt;strong&gt;单位是纳秒&lt;/strong&gt;。换算到端到端，这点差距完全被网络抖动淹没。&lt;/p&gt;&#xA;&lt;p&gt;到这里你应该有个直觉：&lt;strong&gt;协议层的优化空间，比你想象的小得多。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;但直觉不算证据。我得给你看真正的拆解。&lt;/p&gt;&#xA;&lt;h3 id=&#34;实验二一次调用的时间都花在哪了&#34;&gt;&lt;a href=&#34;#%e5%ae%9e%e9%aa%8c%e4%ba%8c%e4%b8%80%e6%ac%a1%e8%b0%83%e7%94%a8%e7%9a%84%e6%97%b6%e9%97%b4%e9%83%bd%e8%8a%b1%e5%9c%a8%e5%93%aa%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;实验二：一次调用的时间都花在哪了？&#xA;&lt;/h3&gt;&lt;p&gt;第二个实验把端到端调用拆成可测量的几段。业务逻辑用 &lt;code&gt;sleep 10ms&lt;/code&gt; 模拟，代表轻量级场景（这是本实验的基准假设，代表轻量级 DB 查询；真实生产 P99 常见 50-200ms，见下方敏感性分析）。&lt;/p&gt;&#xA;&lt;p&gt;跑 1000 次取中位数：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;环节&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;中位耗时&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;占总耗时&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;请求序列化（JSON Marshal）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;2.79µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;0.02%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;网络栈 + 协议处理&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;1.91ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;15.96%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;业务逻辑（DB / 下游调用）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;10ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;83.64%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;响应反序列化（JSON Unmarshal）&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;24.25µs&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;0.20%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;端到端 P50&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;11.96ms&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;100%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;实验代码：&lt;code&gt;evidence/code/e2-latency-breakdown/&lt;/code&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;序列化加反序列化加起来，&lt;strong&gt;0.22%&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;那协议层（网络栈+协议处理）的 16% 看起来挺可观——但别急。这个 16% 是建立在&amp;quot;业务只要 10ms&amp;quot;这种轻量场景下的。真实业务有多重？我做了敏感性分析：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;业务逻辑耗时&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;协议+序列化占比&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;10ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;16.19%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;20ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;8.82%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;50ms&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;3.73%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;100ms&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1.90%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;业务越重，协议层占比越小。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch3-latency-breakdown.png&#34; alt=&#34;端到端延迟堆叠条形图&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;p&gt;而典型微服务的业务逻辑（不算下游调用）通常 20~50ms。&lt;strong&gt;在你大部分接口里，协议层优化的天花板是个位数百分比。&lt;/strong&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;你花一周时间把所有接口从 JSON 切到 Protobuf——也许能省下 5% 的端到端延迟。&#xA;而你的下游服务慢了 50ms——直接把整条链路打崩。&lt;/p&gt;&#xA;&lt;p&gt;哪个更值得你关心？&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;这就是为什么我看到那些&amp;quot;RPC 比 HTTP 快 10 倍&amp;quot;的标题想笑。不是说他们撒谎——他们确实测出了协议层的差距——而是他们测对了一个没人在乎的指标。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;这一章不是说协议优化没价值。10w QPS 量级以上的核心链路，省 5% 是真金白银。问题是，绝大多数业务系统连 1w QPS 都到不了——你优化协议，是给空气省时间。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;四真正的胜负手是出问题的那一刻&#34;&gt;&lt;a href=&#34;#%e5%9b%9b%e7%9c%9f%e6%ad%a3%e7%9a%84%e8%83%9c%e8%b4%9f%e6%89%8b%e6%98%af%e5%87%ba%e9%97%ae%e9%a2%98%e7%9a%84%e9%82%a3%e4%b8%80%e5%88%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、真正的胜负手是出问题的那一刻&#xA;&lt;/h2&gt;&lt;p&gt;回到我开头讲的那个雪崩之夜。&lt;/p&gt;&#xA;&lt;p&gt;复盘那天我画了一张图。把整个事故的传播路径画出来之后，我盯着它发呆——&lt;strong&gt;没有一个环节是被&amp;quot;协议慢&amp;quot;杀死的&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;杀死系统的是这几个东西：&lt;/p&gt;&#xA;&lt;p&gt;超时没有上限——下游一慢，上游连接池就成了无底洞，慢慢被卡死。熔断器不存在，&amp;ldquo;积分计算&amp;quot;连续失败几百次，调用方还在继续发。一个非核心接口出了问题，没有任何机制把它从主链路隔离出去。下游越慢，上游排队越积越多，雪上加霜。&lt;/p&gt;&#xA;&lt;p&gt;这四个东西是什么？是&lt;strong&gt;服务治理&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;而 RPC 框架——我说的是接入公司内部治理 SDK（或 Service Mesh 层）之后的真实 RPC 框架，gRPC、Dubbo（Triple）、Tars（腾讯生态）、Thrift——给你的是什么？&lt;/p&gt;&#xA;&lt;p&gt;这些能力，在接入治理 SDK 后都有。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;注：gRPC 裸框架本身提供 deadline、拦截器、客户端侧负载均衡，不内置熔断/限流/降级。这些治理能力来自：①公司内部封装的治理 SDK（如 tRPC-Go 的内置治理能力）②Sentinel/Resilience4j 独立集成③Istio/Envoy Service Mesh。以下讨论基于公司内部封装的 gRPC 治理 SDK 场景。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;让我把同样的雪崩场景，搬到接入治理 SDK 的 RPC 框架下推演一遍：&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;以下是基于工程常识的场景推演，不是某次真实事故的实测复盘——但雪崩演进的典型路径就是这样。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;纯 HTTP（无治理）的故障演进：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+0s   下游 Redis 抖动，「积分计算」开始变慢&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+5s   上游连接池开始堆积，未完成请求 1000 → 5000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+15s  连接池打满，新请求直接报错 &amp;#34;no available connection&amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+30s  Nginx 健康检查失败，把上游实例标记下线&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+45s  剩余实例承接全部流量，依次进入相同状态&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+60s  整条调用链不可用&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意第二步——&amp;ldquo;连接池开始堆积&amp;rdquo;。这是雪崩的起点。HTTP/1.1 的默认 client 行为，在大多数语言里都没有强制的&amp;quot;等待上限&amp;rdquo;。你可以加 &lt;code&gt;timeout&lt;/code&gt;，但要有人记得加。你的整个团队，里里外外几十个调用点，每一个都加对了吗？这是个赌局，赌输的代价就是某天凌晨三点的电话。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;RPC 框架（接入治理 SDK）的故障演进：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+0s   下游 Redis 抖动，「积分计算」开始变慢&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+2s   SDK 强制的超时上限触发，调用方快速失败&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+5s   服务治理层识别连续失败，「积分计算」被熔断&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+5s   后续请求直接走 fallback 逻辑（返回默认积分 0）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+10s  下游恢复，熔断器进入 half-open，自动恢复&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;T+15s  全链路恢复正常&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;差别在&lt;strong&gt;有没有刹车&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更深一层：差别在&lt;strong&gt;默认行为&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;HTTP 的默认行为是&amp;quot;无脑等到死&amp;rdquo;——除非你主动设超时、主动加重试、主动配熔断、主动接监控。这些动作每一个都不难，但加起来就是一套需要长期维护的体系。问题是，你团队里新来的同学知道为什么这么写吗？三年后业务长出十倍体量的时候，这套手工拼装的治理还能跟上吗？&lt;/p&gt;&#xA;&lt;p&gt;接入内部治理 SDK 的 RPC 框架，它的设计哲学是&amp;quot;假设下游会出问题&amp;quot;——SDK 要求超时显式设置（没有全局默认值兜底），服务治理层识别连续失败后触发熔断，限流和降级开箱即用。它把&amp;quot;治理&amp;quot;从&amp;quot;工程师的自觉&amp;quot;，变成了&amp;quot;框架的契约&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch4-default-behavior.png&#34; alt=&#34;HTTP 默认行为 vs RPC 框架默认行为&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;p&gt;HTTP 这套不是不能加刹车——你可以装 Spring Cloud，可以接 Sentinel，可以自己写中间件。但你得知道你需要刹车，得有人去配，得有人去维护。&lt;/p&gt;&#xA;&lt;p&gt;而内置治理 SDK 的 RPC 框架默认就给你装好了。&lt;strong&gt;你的下游一定会出问题，所以它提前帮你假设它会出问题。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这才是大厂选 RPC 的真正原因。不是因为它&amp;quot;快 5ms&amp;quot;——是因为它在出事的时候给你一根救命稻草。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch4-avalanche-comparison.png&#34; alt=&#34;雪崩演进双场景对比&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;h3 id=&#34;那次选型我们最后选了什么&#34;&gt;&lt;a href=&#34;#%e9%82%a3%e6%ac%a1%e9%80%89%e5%9e%8b%e6%88%91%e4%bb%ac%e6%9c%80%e5%90%8e%e9%80%89%e4%ba%86%e4%bb%80%e4%b9%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;那次选型，我们最后选了什么？&#xA;&lt;/h3&gt;&lt;p&gt;雪崩之后，我们组开了三次会讨论框架。会上有人主张直接上 Dubbo，有人主张换 gRPC，还有人主张就在 HTTP 上接 Sentinel。&lt;/p&gt;&#xA;&lt;p&gt;最后我们的决策不是技术比拼，是这两个判断：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;判断一：我们需要的不是「快」，是「可控」。&lt;/strong&gt;&#xA;我们的业务量根本不到协议优化能体现差距的水平。但我们需要熔断、需要限流、需要降级、需要服务发现、需要负载均衡——这些东西自己写一遍，不仅麻烦，还容易写错。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;判断二：内置 vs 自建，成本差几个数量级。&lt;/strong&gt;&#xA;Spring Cloud 看起来&amp;quot;也能做&amp;quot;——但你试过自己接一遍 Eureka + Hystrix（现已停止维护，社区主推 Resilience4j）+ Ribbon + Zuul + Sleuth + Zipkin 吗？光把这套调通就要两周，跑稳定要半年，每次升级又是一波踩坑。&#xA;gRPC + 公司内部治理 SDK，接入文档走完，接入周期从两周压缩到两天——大部分踩坑的坑，中间件团队替你踩完了。&lt;/p&gt;&#xA;&lt;p&gt;我们选了 gRPC。整个治理栈都在 SDK 里，不需要自己拼装。&lt;/p&gt;&#xA;&lt;p&gt;那之后没再发生过类似的雪崩。不是因为系统变快了——P50 延迟其实差不多——而是因为任何一个下游开始有问题，链路会自己止血。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;五决策框架你到底该不该选-rpc&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e5%86%b3%e7%ad%96%e6%a1%86%e6%9e%b6%e4%bd%a0%e5%88%b0%e5%ba%95%e8%af%a5%e4%b8%8d%e8%af%a5%e9%80%89-rpc&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、决策框架：你到底该不该选 RPC&#xA;&lt;/h2&gt;&lt;p&gt;讲到这里，应该可以给你一个落地的判断标准了。我把这两年看过的、做过的选型总结成一张决策表。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;先回答这四个问题，再决定要不要换 RPC：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;问题&lt;/th&gt;&#xA;          &lt;th&gt;建议方案&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;你的服务调用关系是网状的，超过 5 个内部服务互相调？&lt;/td&gt;&#xA;          &lt;td&gt;✅ RPC&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;你担心某个下游慢/挂会拖垮整个链路？&lt;/td&gt;&#xA;          &lt;td&gt;✅ RPC&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;你需要在服务实例增减时自动发现/负载均衡？&lt;/td&gt;&#xA;          &lt;td&gt;✅ RPC&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;你的接口需要给非内部团队（前端/外部合作方）调用？&lt;/td&gt;&#xA;          &lt;td&gt;✅ HTTP&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;如果前三个全是 RPC，第四个是 HTTP——上 RPC，省心。&#xA;如果只有第四个——继续用 HTTP，REST 风格反而更直观。&#xA;两边都有？典型的&lt;strong&gt;双协议并存&lt;/strong&gt;架构：内部用 gRPC，对外暴露 HTTP/REST 网关层。这是大多数中型公司的最终形态。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch5-decision-flow.png&#34; alt=&#34;RPC/HTTP 决策流程图&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;几个常见的判断误区，顺便也说一下：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;误区一：&amp;ldquo;我们流量小，用不上 RPC。&amp;quot;——错。RPC 的核心价值不是承载大流量，是控制小故障不要演变成大故障。哪怕你只有几百 QPS，只要你的服务调用链路超过三跳，就有雪崩风险。&lt;/p&gt;&#xA;&lt;p&gt;误区二：&amp;ldquo;切 RPC 要花两个月，我们没时间。&amp;quot;——切框架是有成本，但对比的是&amp;quot;出一次大事故的代价&amp;rdquo;。我们那次 47 分钟的故障，业务损失够养一个 5 人小组工作半年。这套治理能力你可以自建，但半年踩坑加持续维护的代价，往往超过直接接入框架的成本。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch5-misconceptions.png&#34; alt=&#34;误区澄清&#34; loading=&#34;lazy&#34;&gt; [1:1]&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;六回到那一晚&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e5%9b%9e%e5%88%b0%e9%82%a3%e4%b8%80%e6%99%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六、回到那一晚&#xA;&lt;/h2&gt;&lt;p&gt;回到我开头那个凌晨三点。&lt;/p&gt;&#xA;&lt;p&gt;那一晚之后我学到的最重要的一件事是：&lt;strong&gt;工程师对&amp;quot;性能&amp;quot;的迷恋，常常掩盖了真正的工程问题。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;我们花了好几年优化接口、压榨 SQL、研究序列化协议——所有这些加起来，可能还不如一行 &lt;code&gt;WithTimeout(3 * time.Second)&lt;/code&gt; 的价值大。&lt;/p&gt;&#xA;&lt;p&gt;不是说优化没意义。是说&lt;strong&gt;优先级搞反了&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;让你的服务跑得更快，是锦上添花。让你的服务在出问题的时候不要一起死掉——这是命。&lt;/p&gt;&#xA;&lt;p&gt;这两年带过几个新同学，每次让他们做服务设计的复盘，问题都集中在一个地方：他们对&amp;quot;快&amp;quot;特别敏感，对&amp;quot;崩&amp;quot;特别迟钝。看到 P99 慢了 10ms 就着急去调，看到没设超时却觉得&amp;quot;反正大概率不会触发&amp;rdquo;。&lt;/p&gt;&#xA;&lt;p&gt;可工程的现实是反过来的——大概率不会出事的事，一旦出事就是大事。&lt;/p&gt;&#xA;&lt;p&gt;下次再看到&amp;quot;RPC 比 HTTP 快多少&amp;quot;的标题，先问一句：它在衡量什么。答案往往已经说明了作者在不在乎你真正的问题。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;选 RPC 不是因为它跑得快，是因为它在你最需要的时候，告诉你：&amp;ldquo;出问题了，我替你刹车。&amp;rdquo;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/rpc-vs-http/ch6-relief.png&#34; alt=&#34;收尾情绪图&#34; loading=&#34;lazy&#34;&gt; [3:2]&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;附录实验代码和原始数据&#34;&gt;&lt;a href=&#34;#%e9%99%84%e5%bd%95%e5%ae%9e%e9%aa%8c%e4%bb%a3%e7%a0%81%e5%92%8c%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;附录：实验代码和原始数据&#xA;&lt;/h2&gt;&lt;p&gt;本文 2 组实验的代码已开源：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub：&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/rpc-vs-http&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/rpc-vs-http&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;e1-protocol-bench/&lt;/code&gt;&lt;/strong&gt;：E1 协议层吞吐与延迟对比（HTTP/1.1+JSON vs HTTP/2+JSON vs HTTP/2+二进制），可复现 46.9% 体积压缩和 1µs P50 差距&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;e2-latency-breakdown/&lt;/code&gt;&lt;/strong&gt;：E2 端到端调用延迟分解（序列化/网络+协议/业务逻辑各环节占比），可复现 83.64% 业务逻辑占比和敏感性分析数据&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每个子目录都有独立说明，二进制编译产物不入库，跑实验前自己 &lt;code&gt;go build&lt;/code&gt;。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/rpc-vs-http&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
