第 12 章 · 连接池与性能优化
本章目标:调优 GORM/MySQL 连接池;配置 Redis 连接池;设置 HTTP Server 超时;使用 pprof 定位 CPU/内存热点;识别并修复 N+1 查询;建立 api-go-demo 性能清单;对照 go-dev ch10 限流与 spring-boot-web ch17 HikariCP/N+1。
学时建议:4~5 小时(含 2 小时压测)
前置:完成 gin-web ch11;ch09 Redis 缓存已启用。
12.1 场景说明:高 QPS 读商品
api-go-demo 定位高 QPS 商品读服务。单实例瓶颈常在:
| 层级 | 风险 | 本章手段 |
|---|---|---|
| MySQL | 连接耗尽、慢查询、N+1 | 连接池 + 索引 + Preload |
| Redis | 连接数、大 value | PoolSize、按 slug 粒度 |
| HTTP | goroutine 堆积 | Read/Write 超时 |
| JSON | 反射开销 | 复用 DTO |
| 日志 | IO 阻塞 | prod 级别 Warn+ |
优化顺序:度量(hey + pprof)→ 连接池 → 缓存(ch09)→ SQL/N+1 → 微优化。
hey ──► /api/v1/products/go-handbook
│
├── pprof CPU profile
├── GORM logger 慢查询
└── redis hit ratio
12.2 MySQL 连接池(完整)
GORM 底层 database/sql 连接池:
// internal/database/mysql.go
package database
import (
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type PoolConfig struct {
MaxOpen int
MaxIdle int
MaxLifetime time.Duration
MaxIdleTime time.Duration
}
func DefaultPoolConfig() PoolConfig {
return PoolConfig{
MaxOpen: 50,
MaxIdle: 10,
MaxLifetime: time.Hour,
MaxIdleTime: 10 * time.Minute,
}
}
func OpenMySQL(dsn string, env string, pool PoolConfig) (*gorm.DB, error) {
logLevel := logger.Info
if env == "prod" {
logLevel = logger.Warn
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
PrepareStmt: true, // 预编译,高 QPS 下减少解析
})
if err != nil {
return nil, err
}
return db, ConfigurePool(db, pool)
}
func ConfigurePool(db *gorm.DB, pool PoolConfig) error {
sqlDB, err := db.DB()
if err != nil {
return err
}
if pool.MaxOpen > 0 {
sqlDB.SetMaxOpenConns(pool.MaxOpen)
}
if pool.MaxIdle > 0 {
sqlDB.SetMaxIdleConns(pool.MaxIdle)
}
if pool.MaxLifetime > 0 {
sqlDB.SetConnMaxLifetime(pool.MaxLifetime)
}
if pool.MaxIdleTime > 0 {
sqlDB.SetConnMaxIdleTime(pool.MaxIdleTime)
}
return nil
}
| 参数 | 含义 | 起点公式 |
|---|---|---|
| MaxOpenConns | 最大打开连接 | CPU 核数 × 2~4,按压测调 |
| MaxIdleConns | 空闲连接 | MaxOpen 的 20%~50% |
| ConnMaxLifetime | 连接最大存活 | < MySQL wait_timeout |
| ConnMaxIdleTime | 空闲回收 | 5~15 分钟 |
监控:
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
对照 spring-boot-web ch17 HikariCP:maximum-pool-size 等价 MaxOpenConns;max-lifetime 等价 ConnMaxLifetime。
12.3 Redis 客户端池
// internal/cache/redis.go(扩展)
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
PoolSize: 20,
MinIdleConns: 5,
PoolTimeout: 4 * time.Second,
DialTimeout: 3 * time.Second,
ReadTimeout: 2 * time.Second,
WriteTimeout: 2 * time.Second,
})
| 信号 | 调整 |
|---|---|
redis: connection pool timeout | 增大 PoolSize 或查慢命令 |
| 空闲过多 | 减小 MinIdleConns |
12.4 HTTP Server 超时
Gin 默认 Run() 无精细超时;生产改用 http.Server:
// cmd/server/main.go
func runHTTPServer(cfg *config.Config, handler http.Handler) error {
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}
return srv.ListenAndServe()
}
| 超时 | 防什么 |
|---|---|
| ReadTimeout | 慢客户端占满 goroutine |
| WriteTimeout | 大响应/client 不读 |
| IdleTimeout | keep-alive 泄漏 |
Listen 地址用 :8080 而非 127.0.0.1:8080,否则 Docker 健康检查失败(ch15)。
12.5 pprof 剖析
注册 pprof(仅 dev/staging)
import _ "net/http/pprof"
func startPprof(addr string) {
go func() {
log.Println("pprof listening on", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Println("pprof:", err)
}
}()
}
// main: if cfg.Env != "prod" { startPprof("127.0.0.1:6060") }
CPU profile
go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/profile?seconds=30
压测同时采样:
hey -n 10000 -c 50 -m GET http://127.0.0.1:8080/api/v1/products/go-handbook
其他 endpoint
| URL | 用途 |
|---|---|
/debug/pprof/heap | 内存分配 |
/debug/pprof/goroutine | goroutine 栈 |
/debug/pprof/block | 阻塞 |
/debug/pprof/mutex | 锁竞争 |
安全:pprof 禁止暴露公网;K8s 内用 port-forward。