压力测试核心是测handler及依赖链,非gin框架本身;wrk压测虚高因空路由、内存暴涨、中间件冗余、日志同步锁等三大硬伤,需pprof定位真实瓶颈。

压力测试不是测 Gin,是测你写的 handler 和依赖链;用 wrk 测出的 QPS 高,不等于线上扛得住。
wrk 压测结果虚高,线上却崩得快的三个硬伤
基准测试里 wrk -t12 -c400 -d30s http://localhost:8080/echo 只压空路由,掩盖了真实瓶颈:
- 真实请求带
multipart/form-data或 5MB+ JSON:Gin 默认c.ShouldBindJSON会把整个 body 读进内存再解析,400 并发 ≈ 2GB 内存瞬时占用,GC 直接卡住 - 中间件没做 early-return:比如鉴权失败后,仍往下走日志、统计、trace 等中间件,白白消耗 goroutine 生命周期
- 日志中间件用
log.Printf同步写:它底层是全局锁,在 5K+ QPS 下,90% 的 goroutine 在等锁,不是在干活
pprof 抓真瓶颈,别信日志时间戳
Gin 日志里看到两个请求间隔 2 秒,不代表框架串行——每个请求本就在独立 goroutine 执行。所谓“延迟”大概率来自业务层阻塞:
- HTTP 客户端或 DB 查询没设超时:
http.Get卡住、db.QueryRow没配context.WithTimeout,goroutine 挂起等待 I/O - 连接池耗尽:
*sql.DB没调db.SetMaxOpenConns(50),后续请求排队等连接 - 客户端复用 keep-alive 连接:浏览器或 curl 按序发请求,服务端仍是并发处理,但日志输出被顺序化误导
验证方法:在 handler 开头打 time.Now().UnixNano(),结尾再打一次,对比差值与日志间隔——如果前者远小于后者,瓶颈就不在你的代码执行上。
中间件和绑定方式选错,吞吐直接掉 30%
高频接口上,一个中间件、一个绑定函数的选型差异,能拉出明显性能落差:
- 别用
gin.Default():它默认带Logger()和Recovery(),对核心支付或查询路径属于冗余开销;改用gin.New(),按需Use() -
c.ShouldBindJSON→ 改成c.BindJSON(&v):前者强制读全量 body 到内存,后者内部复用标准库,配合结构体字段预分配可减少逃逸 - 超大 payload(如文件上传 + 元数据 JSON):用
json.NewDecoder(c.Request.Body)边流式读边解,禁用decoder.DisallowUnknownFields()防止未知字段触发 panic 堆栈打印(那玩意儿吃内存) - 日志中间件必须异步:用 channel + 固定 worker goroutine 处理,别让每个请求都同步锁写
sync.RWMutex 加在 handler 里,基本等于自废武功
在 handler 函数内声明局部 sync.RWMutex 并加锁,不仅没保护任何共享状态,还引入调度开销和 false sharing 风险:
- 局部变量生命周期只在当前请求内,根本不存在跨 goroutine 竞争
- 为每次请求 new 一个带锁结构体(如
type ReqState { mu sync.RWMutex; data map[string]interface{} }),等于高频创建锁对象,GC 压力翻倍 -
RUnlock()必须严格匹配RLock():defer 里写mu.RUnlock(),但 handler 中提前c.AbortWithStatusJSON()就会导致锁永远不释放,后续所有读请求 hang 住
真正该用 sync.RWMutex 的地方只有三类:全局缓存映射(map[string]*Item)、运行时热更新配置、连接池活跃数统计——它们都满足:跨请求生命周期 + 多 goroutine 访问 + 读远多于写。











