根本原因不是dns问题,而是http.defaulttransport连接池过小导致请求排队、dns查询被阻塞:maxidleconnsperhost默认仅2、maxidleconns默认100、idleconntimeout默认30秒,高并发下连接耗尽并触发大量time_wait,最终文件描述符耗尽使dial失败。

为什么直接用 http.DefaultClient 在压测时频繁报 dial tcp: lookup 失败
根本不是 DNS 问题,而是连接池太小导致请求排队、DNS 查询被阻塞在队列里。http.DefaultTransport 的 MaxIdleConnsPerHost 默认只有 2,意味着对同一个域名最多复用 2 个空闲连接;MaxIdleConns 默认 100,但只要并发稍高(比如每秒 30+ 请求打同一接口),就不断新建连接 → 触发大量 TIME_WAIT → 文件描述符耗尽 → 后续 dial 直接失败,连 DNS 查询都发不出去。
实操建议:
- 永远不要在生产或压测中直接用
http.DefaultClient,它只适合低频调试 - 把连接池配置封装成函数,用可变参数支持按需覆盖,比如:
NewHTTPClient(WithMaxIdleConnsPerHost(50), WithIdleConnTimeout(30*time.Second)) - 关键参数必须显式设:至少覆盖
MaxIdleConns、MaxIdleConnsPerHost、IdleConnTimeout,缺一不可
如何用可变参数函数统一管理 HTTP 和 DB 连接池配置
硬编码 transport 或调用一堆 db.SetXXX 会导致配置散落、难以复用。用函数式选项(Functional Options)模式,既能保持类型安全,又避免构造函数参数爆炸。
示例结构:
type HTTPClientOption func(*http.Client)
<p>func WithMaxIdleConnsPerHost(n int) HTTPClientOption {
return func(c <em>http.Client) {
if t, ok := c.Transport.(</em>http.Transport); ok {
t.MaxIdleConnsPerHost = n
}
}
}</p><p>func WithIdleConnTimeout(d time.Duration) HTTPClientOption {
return func(c <em>http.Client) {
if t, ok := c.Transport.(</em>http.Transport); ok {
t.IdleConnTimeout = d
}
}
}</p><p>func NewHTTPClient(opts ...HTTPClientOption) <em>http.Client {
tr := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 25,
IdleConnTimeout: 90 </em> time.Second,
}
c := &http.Client{Transport: tr}
for _, opt := range opts {
opt(c)
}
return c
}</p>
好处:
- 调用方只传真正需要改的参数,其余走合理默认值
- HTTP 和 DB 可共用一套 Option 模式(比如
WithMaxOpenConns、WithConnMaxLifetime),语义一致 - 测试时能快速切配置:
NewHTTPClient(WithMaxIdleConnsPerHost(2))模拟默认行为,验证降级逻辑
DB 连接池里 SetMaxOpenConns 设太高反而让 P99 延迟飙升
不是连接越多越快。MySQL/PostgreSQL 每个连接对应服务端一个线程或进程,SetMaxOpenConns 设到 200,但数据库 max_connections 只有 151,结果就是新连接被拒绝,后续请求全卡在 WaitCount 上——你看到的是“慢”,实际是“等”。
实操建议:
- 先查数据库真实上限:
SHOW VARIABLES LIKE 'max_connections';(MySQL)或SELECT setting FROM pg_settings WHERE name = 'max_connections';(PostgreSQL) -
SetMaxOpenConns推荐设为该值的 60%~80%,例如 MySQL 默认 151 → 设 100 是安全起点 - 必须配对设置
SetMaxIdleConns(建议为 Open 值的 1/2~1/3)和SetConnMaxIdleTime(推荐 5~10 分钟),否则空闲连接堆积却不释放,NAT 或 LB 会静默断连 - 上线后盯紧
db.Stats().WaitCount:持续上涨说明不够用;InUse长期等于MaxOpenConns且不回落,大概率是事务没 commit 或 rows 没 close
连接复用是否生效,不能只看 QPS,得看这三个指标
很多人以为 QPS 上去了就代表连接复用成功,其实不然。真正要验证复用效果,得看底层连接是否真的被反复使用,而不是反复新建销毁。
最轻量可靠的验证方式:
- 启动时加一行日志:
log.Printf("pool stats: %+v", db.Stats()),观察Idle和OpenConnections是否稳定波动,而非单向增长 - 用
netstat -an | grep :443 | wc -l(HTTPS)或对应端口,在压测前后对比 ESTABLISHED 连接数变化幅度 —— 复用到位的话,这个数字应远小于请求数 - 抓包看 TCP 握手次数:用
tcpdump -i any port 443 -c 100抓 100 个包,如果 SYN 包占比超过 5%,说明复用率低
最容易被忽略的一点:HTTP/2 下连接复用粒度更粗(一个连接复用多个 stream),但前提是服务端真支持;别信 ForceAttemptHTTP2 = true,先用 curl -v --http2 https://your-api/ 看响应头有没有 h2,再决定是否启用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











