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

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

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

  • Gin + MySQL

  • 提供一个查询接口

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

初始架构

Client
  ↓
Gin HTTP Server
  ↓
Service Layer
  ↓
MySQL

初始代码(问题版本)

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)

#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

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

go tool pprof http://localhost:8080/debug/pprof/profile

热点函数

queryUserFromDB
database/sql.(*DB).QueryContext

结论很清晰:

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

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

❌ 错误配置(默认)

sql.Open("mysql", dsn)

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

|
参数
|
默认值
|
| — | — |
|
MaxOpenConns
|
0(无限,但受 MySQL 限制)
|
|
MaxIdleConns
|
2
|
|
ConnMaxLifetime
|

|

✅ 正确配置

db.SetMaxOpenConns(200)
db.SetMaxIdleConns(100)
db.SetConnMaxLifetime(time.Minute * 5)

📈 优化后压测

  • QPS:≈ 4500

  • P99:80ms

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

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

这是最关键的一步。

问题本质

  • 大量请求访问 同一用户

  • 每次都打 DB

  • DB QPS 成为硬瓶颈

优化策略

  • 本地缓存(示例)

  • Redis 也可以

缓存版本代码

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 再看一次 👇

encoding/json.(*encodeState).marshal

问题原因

  • Gin 默认使用 encoding/json

  • 高 QPS 下 CPU 消耗巨大

优化方案

  • 使用 jsoniter

  • 或者 sonic(字节系常用)

示例(jsoniter)

var json = jsoniter.ConfigFastest

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

📈 效果

  • QPS:≈ 17000

  • CPU 使用率明显下降


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

1️⃣ Gin 默认 Logger 很重

r := gin.New()
r.Use(gin.Recovery())
// 不使用 gin.Logger()

2️⃣ HTTP Server 参数优化

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君”回复“源码”获取源码


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值