每日一Go-78、Go服务从 2k QPS 到 2w QPS 的优化方法

一、背景:一个"看起来没问题"的 Go 服务

这是一个非常常见的业务场景:

  • Gin + MySQL

  • 提供一个查询接口

  • QPS 上不去,但 CPU、内存看着也不算满

初始架构

nginx 复制代码
Client
  ↓
Gin HTTP Server
  ↓
Service Layer
  ↓
MySQL

初始代码(问题版本)

go 复制代码
func GetUser(c *gin.Context) {
    id := c.Query("id")

    user, err := queryUserFromDB(id)
    if err != nil {
        c.JSON(500, gin.H{"err": err.Error()})
        return
    }

    c.JSON(200, user)
}

压测结果(wrk)

bash 复制代码
#wrk -t线程数 -c连接数 -d压测时间 URL
wrk -t8 -c200 -d30s http://localhost:8080/user?id=1

wrk

是一款 高性能 HTTP 压测工具 ,常用于压测 Web / API 服务(Go、Nginx、Spring Boot、Gin 等),特点是 轻量、并发高、支持 Lua 脚本 ,非常适合做 Go 高并发服务 / 云原生 场景 👍

📉 结果

  • QPS:≈ 2000

  • P99 延迟:120ms

  • CPU:40% 左右

  • DB 连接数:经常打满

问题来了:

资源没跑满,QPS 却上不去。

二、第一步:pprof 定位性能瓶颈

1️⃣ 开启 pprof

go 复制代码
package main

import (
"github.com/gin-gonic/gin"
"github.com/gin-contrib/pprof"
)

func main() {
	r := gin.New()

// 业务路由
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "pong"})
	})

// 注册 pprof(默认路径 /debug/pprof)
	pprof.Register(r)

// 启动 Gin
	r.Run(":8080")
}

2️⃣ CPU Profiling

bash 复制代码
go tool pprof http://localhost:8080/debug/pprof/profile

热点函数

bash 复制代码
queryUserFromDB
database/sql.(*DB).QueryContext

结论很清晰:

瓶颈在数据库 & 每次请求都直连 DB

三、第二步:数据库连接池参数错误(经典坑)

❌ 错误配置(默认)

javascript 复制代码
sql.Open("mysql", dsn)

Go 的 database/sql默认连接池非常保守

|

参数

|

默认值

|

| --- | --- |

|

MaxOpenConns

|

0(无限,但受 MySQL 限制)

|

|

MaxIdleConns

|

2

|

|

ConnMaxLifetime

|

|

✅ 正确配置

apache 复制代码
db.SetMaxOpenConns(200)
db.SetMaxIdleConns(100)
db.SetConnMaxLifetime(time.Minute * 5)

📈 优化后压测

  • QPS:≈ 4500

  • P99:80ms

👉 第一条经验:Go 性能问题,80% 从连接池开始

四、第三步:热点数据不该打 DB(引入缓存)

这是最关键的一步。

问题本质

  • 大量请求访问 同一用户

  • 每次都打 DB

  • DB QPS 成为硬瓶颈

优化策略

  • 本地缓存(示例)

  • Redis 也可以

缓存版本代码

go 复制代码
var userCache sync.Map

func GetUser(c *gin.Context) {
    id := c.Query("id")

    if v, ok := userCache.Load(id); ok {
        c.JSON(200, v)
        return
    }

    user, err := queryUserFromDB(id)
    if err != nil {
        c.JSON(500, gin.H{"err": err.Error()})
        return
    }

    userCache.Store(id, user)
    c.JSON(200, user)
}

📈 压测结果

  • QPS:≈ 12000

  • P99:25ms

  • DB QPS:骤降

五、第四步:JSON 编码成为新瓶颈

pprof 再看一次 👇

bash 复制代码
encoding/json.(*encodeState).marshal

问题原因

  • Gin 默认使用 encoding/json

  • 高 QPS 下 CPU 消耗巨大

优化方案

  • 使用 jsoniter

  • 或者 sonic(字节系常用)

示例(jsoniter)

javascript 复制代码
var json = jsoniter.ConfigFastest

c.Data(200, "application/json", json.MarshalToString(user))

📈 效果

  • QPS:≈ 17000

  • CPU 使用率明显下降


六、第五步:减少系统调用 & 上下文切换

1️⃣ Gin 默认 Logger 很重

css 复制代码
r := gin.New()
r.Use(gin.Recovery())
// 不使用 gin.Logger()

2️⃣ HTTP Server 参数优化

css 复制代码
server := &http.Server{
    Addr:           ":8080",
    Handler:        r,
    ReadTimeout:    5 * time.Second,
    WriteTimeout:   5 * time.Second,
    MaxHeaderBytes: 1 << 20,
}

📈 最终压测

  • QPS:≈ 21000

  • P99:18ms

  • CPU:≈ 75%

  • 服务稳定运行

七、最终对比总结

|

阶段

|

QPS

|

| --- | --- |

|

初始版本

|

2,000

|

|

连接池优化

|

4,500

|

|

加缓存

|

12,000

|

|

JSON 优化

|

17,000

|

|

Server & Gin 优化

| 21,000+ |

八、Go 性能优化的「黄金法则」

永远先 pprof,再优化连接池 > 缓存 > 编码 > 框架细节没有 DB 扛得住无脑高并发QPS 上不去,多半不是 CPU 的问题


加班费计算器:(vx小程序 "加班计")

*源码地址*

1、vx"Codee君"回复"源码"获取源码


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!