我们如何衡量 HTTP 时延:DNS、TCP、TLS、TTFB 细分
Source: Dev.to

当你检查网站的响应时间时,“847 ms”并不能告诉你太多。是 DNS 查询慢吗?网络延迟?还是服务器本身?你需要一个细分。
在 upsonar.io 我们会精确展示时间的去向。下面是我们使用 Go 的 net/http/httptrace 包进行测量的方法。
The problem
start := time.Now()
resp, _ := http.Get("https://example.com")
fmt.Println(time.Since(start)) // 847ms
847 毫秒——但为什么?它可能是请求生命周期中的任何部分。
解决方案
httptrace 钩住 HTTP 请求的每个阶段。您可以附加回调函数,它们会在每个阶段完成时触发:
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
fmt.Printf("DNS: %v\n", time.Since(dnsStart))
},
// same pattern for TCP, TLS, etc.
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
每个指标的含义
- DNS Lookup – 将域名解析为 IP 地址。> 100 ms 通常表明 DNS 服务器问题或缓存未命中。
- TCP Connect – 三次握手。主要受与服务器的物理距离影响。
- TLS Handshake – 加密协商。典型值为 50–150 ms;> 300 ms 暗示服务器或证书链问题。
- TTFB (Time To First Byte) – 包括 DNS、TCP、TLS 以及服务器处理时间。衡量响应速度的标准指标。
- Transfer – 下载响应体。
工作示例
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"os"
"time"
)
func main() {
url := "https://httpbin.org/delay/2"
if len(os.Args) > 1 {
url = os.Args[1]
}
var dnsStart, tcpStart, tlsStart time.Time
totalStart := time.Now()
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) { fmt.Printf("DNS: %v\n", time.Since(dnsStart)) },
ConnectStart: func(_, _ string) { tcpStart = time.Now() },
ConnectDone: func(_, _ string, _ error) { fmt.Printf("TCP: %v\n", time.Since(tcpStart)) },
TLSHandshakeStart: func() { tlsStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { fmt.Printf("TLS: %v\n", time.Since(tlsStart)) },
GotFirstResponseByte: func() { fmt.Printf("TTFB: %v\n", time.Since(totalStart)) },
}
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
transferStart := time.Now()
io.ReadAll(resp.Body)
fmt.Printf("Transfer: %v\n", time.Since(transferStart))
fmt.Printf("Total: %v\n", time.Since(totalStart))
}
运行程序:
go run main.go https://httpbin.org/delay/2
示例输出
DNS: 148.00525ms
TCP: 142.56925ms
TLS: 289.685ms
TTFB: 2.956339583s
Transfer: 72.208µs
Total: 2.95670575s
TTFB(首字节时间)包括从发起请求到收到第一个字节的所有时间。Transfer 是下载响应体的时间——在这里响应体很小,所以总时间≈TTFB。
大响应示例
go run main.go https://proof.ovh.net/files/10Mb.dat
示例输出
DNS: 3.621333ms
TCP: 54.364208ms
TLS: 116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s
对于 10 MB 的文件,Transfer 在整体延迟中占主导。
注意连接复用
Go 的 http.Client 默认会复用连接。对同一主机的后续请求会因为连接已经建立而显示 DNS、TCP 和 TLS 为 0 ms。
为了获得准确的测量,请禁用 keep‑alives:
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
什么是慢速数字告诉你的
- DNS > 100 ms → 尝试更快的 DNS 提供商(例如 Cloudflare、Google)。
- TCP > 100 ms → 服务器离用户较远;考虑使用 CDN。
- TLS > 200 ms → 检查证书链;启用会话恢复。
- TTFB > 500 ms → 后端瓶颈:数据库慢、冷启动、重处理。
- Transfer high → 负载过大;启用 gzip/Brotli 压缩。
200 ms 规则
用户会注意到约 200 ms 以上的延迟。如果你的 TTFB 单独就超过了这个阈值,无论前端如何优化,页面都会感觉很慢。
使用免费、无需注册的工具在你自己的网站上试一试:upsonar.io/tools/diagnose.