必须手动注入并双路透传x-test-flag:1,http用req.header.set,grpc用metadata.appendtooutgoingcontext,同时检查header长度防nginx截断,并据此动态路由db/redis连接池,配合pprof?debug=2和block profile定位连接瓶颈。

压测前必须注入且透传 X-Test-Flag
不加压测标识,所有请求都会打到生产连接池,根本看不出底层连接是否扛得住。Golang 微服务里没有中间件自动打标这回事,必须在入口手动塞 X-Test-Flag: 1,并在 HTTP header 和 gRPC metadata 双路透传。
常见错误是只往 header 写、忘了 gRPC 的 metadata.AppendToOutgoingContext,导致下游服务收不到标识,继续走真实连接池;更隐蔽的坑是 Nginx 默认 header 大小限制 4KB,如果塞了过长 trace 上下文,X-Test-Flag 可能被截断——压测流量悄无声息变成生产流量。
- HTTP handler 中用
req.Header.Set("X-Test-Flag", "1"),不是Add - gRPC unary interceptor 里用
metadata.FromIncomingCtx(ctx)提取,再用metadata.AppendToOutgoingContext回写 - 透传前检查
len(req.Header.Get("X-Test-Flag")) > 0,别依赖空字符串判断
DB/Redis 连接池必须按 context 动态路由
压测时如果还共用生产连接池,你看到的“连接超时”或“too many connections”全是假象——它反映的是 MySQL max_connections 被压穿,而不是你的 Go 模块连接管理有问题。
正确做法是在 DAO 层根据 ctx.Value(testKey) 切换连接池,而不是靠配置开关临时改 sql.Open("mysql://prod") ——后者会导致 ORM(如 GORM)生成错误 SQL,比如把 SELECT * FROM users 变成 SELECT * FROM stress_users 这种非法语句。
- DB 初始化时创建两个连接池:
prodDB和stressDB,前者复用原有配置,后者显式设MaxOpenConns = 20(模拟受限环境) - Redis 封装
Get方法:若检测到压测上下文,自动拼接前缀"stress:" + key,而非硬编码"stress_user_1001" - 千万别在 SQL 字符串里拼库名,GORM 等 ORM 无法识别,会报
ERROR 1049 (42000): Unknown database 'stress'
pprof 必须抓 goroutine?debug=2 + block profile
连接耗尽的典型表现是大量 goroutine 卡在 net.Conn.Read 或 database/sql.(*DB).conn,但默认 /debug/pprof/goroutine 只显示摘要,看不到阻塞点。必须加 ?debug=2 才能看到完整栈,否则你以为是业务逻辑慢,实际是等连接等了 5 秒。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
block profile 更关键:它记录 goroutine 因锁、channel、网络 I/O 等等待的时间。如果 sync.runtime_SemacquireMutex 占比高,说明连接池锁争用严重;如果 net.(*netFD).Read 出现在 top,说明底层连接确实不够,而非 Go 代码问题。
- 采集命令:
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2(看谁卡在 connect/waiting) - 同时跑:
go tool pprof http://localhost:6060/debug/pprof/block?seconds=30(看平均阻塞时长) - 重点关注状态为
select、chan receive、semacquire的 goroutine 数量是否随 QPS 线性增长
客户端压测脚本要禁用 keep-alive
启用 keep-alive 会让连接复用,掩盖服务端 accept 队列积压和连接池不足的真实瓶颈。你看到 QPS 很高,其实只是客户端在复用旧连接,没触发新建连接逻辑。
手写脚本时,要么设 Transport.DisableKeepAlives = true,要么每次请求手动关掉:req.Close = true。vegeta 用 -keepalive=false 参数,否则默认开启。
- 自定义 client 示例:
client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}} - vegeta 命令加
-keepalive=false -timeout=3s,避免因连接复用导致 timeout 不生效 - 观察服务端
netstat -an | grep :8080 | wc -l,压测中 ESTABLISHED 连接数应接近你设的并发数,否则就是 keep-alive 在捣鬼
真实瓶颈往往藏在连接建立阶段,而不是业务逻辑里。很多团队花两周优化 JSON 解析,结果发现真正卡点是 database/sql 默认连接池最大只开 0(即无上限),但在容器里被 cgroup 限制死,连 50 个连接都分不到——这种问题,不靠透传压测标识 + 动态连接池 + debug=2 goroutine profile,根本定位不到。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










