跳到主要内容

服务性能调优

问题

Go 服务在高并发下响应变慢,如何系统化地进行性能调优?

答案

性能调优方法论

核心原则

Don't guess, measure. 先量化再优化,避免凭直觉优化不是瓶颈的地方。

第一步:建立基线

压测工具

# wrk - HTTP 压测
wrk -t12 -c400 -d30s http://localhost:8080/api/users

# hey - Go 写的压测工具
hey -n 10000 -c 200 http://localhost:8080/api/users

# vegeta - 支持恒定 QPS 压测
echo "GET http://localhost:8080/api/users" | vegeta attack -rate=1000 -duration=30s | vegeta report

记录基线指标:

  • QPS(每秒请求数)
  • P50 / P95 / P99 延迟
  • CPU / 内存使用率
  • 错误率

第二步:定位瓶颈

排查工具矩阵

瓶颈类型工具
CPU 热点pprof/profile 火焰图
内存分配pprof/heappprof/allocs
锁竞争pprof/mutex
阻塞等待pprof/block
调度问题go tool trace
GC 压力GODEBUG=gctrace=1
数据库慢查询慢查询日志 + EXPLAIN
网络 IO链路追踪 + tcp 抓包

快速 pprof 排查

# 同时抓 CPU 和内存
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30 &
go tool pprof -http=:8082 http://localhost:6060/debug/pprof/heap

# 看 GC 频率
GODEBUG=gctrace=1 ./myapp 2>&1 | head -20
# gc 5 @0.5s 2%: ... → 2% 表示 GC 占 CPU 2%

# 看调度追踪
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

第三步:针对性优化

3.1 减少内存分配

这是 Go 性能优化中最常见也最有效的手段:

// ❌ 频繁分配
func process(items []Item) []string {
var results []string // 每次调用都分配
for _, item := range items {
results = append(results, item.Name) // 多次扩容
}
return results
}

// ✅ 预分配
func process(items []Item) []string {
results := make([]string, 0, len(items)) // 预分配,避免扩容
for _, item := range items {
results = append(results, item.Name)
}
return results
}
// ❌ string 拼接分配大量临时对象
func buildSQL(ids []int) string {
s := "SELECT * FROM users WHERE id IN ("
for i, id := range ids {
if i > 0 {
s += ","
}
s += strconv.Itoa(id)
}
return s + ")"
}

// ✅ strings.Builder 零拷贝
func buildSQL(ids []int) string {
var b strings.Builder
b.WriteString("SELECT * FROM users WHERE id IN (")
for i, id := range ids {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(strconv.Itoa(id))
}
b.WriteByte(')')
return b.String()
}

3.2 sync.Pool 复用对象

var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}

func ProcessRequest(data []byte) string {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()

buf.Write(data)
// ...处理
return buf.String()
}

3.3 JSON 序列化优化

// encoding/json 基于反射,高 QPS 下是瓶颈

// 方案 1: json-iterator(兼容标准库)
import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary

// 方案 2: sonic(字节跳动,SIMD 加速)
import "github.com/bytedance/sonic"

// 方案 3: 代码生成(编译时生成序列化代码,性能最好)
// easyjson、ffjson

3.4 数据库优化

// 批量查询替代 N+1
// ❌ N+1 查询
for _, userID := range userIDs {
db.QueryRow("SELECT name FROM users WHERE id = ?", userID)
}

// ✅ 批量查询
db.Query("SELECT id, name FROM users WHERE id IN (?)", userIDs)

// 使用预编译语句
stmt, _ := db.Prepare("SELECT name FROM users WHERE id = ?")
defer stmt.Close()
for _, id := range ids {
stmt.QueryRow(id) // 复用编译结果
}

3.5 并发优化

// 串行调用→并行调用
func getProfile(ctx context.Context, userID string) (*Profile, error) {
g, ctx := errgroup.WithContext(ctx)

var user *User
var orders []*Order
var posts []*Post

g.Go(func() error {
var err error
user, err = getUser(ctx, userID)
return err
})
g.Go(func() error {
var err error
orders, err = getOrders(ctx, userID)
return err
})
g.Go(func() error {
var err error
posts, err = getPosts(ctx, userID)
return err
})

if err := g.Wait(); err != nil {
return nil, err
}

return &Profile{User: user, Orders: orders, Posts: posts}, nil
}

3.6 缓存优化

// 本地缓存 + Redis 二级缓存
func GetUser(ctx context.Context, id string) (*User, error) {
// L1: 本地缓存
if v, ok := localCache.Get(id); ok {
return v.(*User), nil
}

// L2: Redis
data, err := rdb.Get(ctx, "user:"+id).Bytes()
if err == nil {
var user User
json.Unmarshal(data, &user)
localCache.Set(id, &user, 1*time.Minute)
return &user, nil
}

// L3: DB
user, err := db.GetUser(ctx, id)
if err != nil {
return nil, err
}
data, _ = json.Marshal(user)
rdb.Set(ctx, "user:"+id, data, 5*time.Minute)
localCache.Set(id, user, 1*time.Minute)
return user, nil
}

优化效果量化

// Benchmark 对比(before/after)
func BenchmarkProcess(b *testing.B) {
for i := 0; i < b.N; i++ {
process(testData)
}
}
# benchstat 对比两次结果
go test -bench=BenchmarkProcess -count=10 > old.txt
# 修改代码后
go test -bench=BenchmarkProcess -count=10 > new.txt

benchstat old.txt new.txt
# name old time/op new time/op delta
# Process-8 1.25ms 0.35ms -72.00%

优化优先级

优先级方向效果
1减少网络请求(缓存、批量)★★★★★
2数据库优化(索引、批量查询)★★★★★
3并行化(errgroup)★★★★
4减少内存分配★★★
5JSON 库替换★★★
6锁优化★★
7GC 调优★★

常见面试问题

Q1: Go 服务 QPS 从 1000 提到 10000 你会怎么做?

答案

  1. 压测建立基线,pprof 找瓶颈
  2. 常见优化路径:
    • 加缓存(Redis + 本地缓存)
    • 数据库优化(索引、连接池、读写分离)
    • 并行化下游调用
    • JSON 库替换
  3. 水平扩容(K8s 多副本)
  4. 验证:再次压测确认达标

Q2: 什么时候应该优化,什么时候应该扩容?

答案

  • 扩容优先:业务快速迭代、人力有限、性能差距不大时
  • 优化优先:CPU/内存利用率已很高(扩容成本高)、单节点瓶颈(如 DB 锁)、核心链路延迟敏感
  • 实际中通常先扩容扛住,再迭代优化降成本

Q3: Benchmark 有哪些注意事项?

答案

  • b.ResetTimer() 排除初始化时间
  • b.ReportAllocs() 报告内存分配
  • -count=10 + benchstat 统计显著性
  • 确保编译器不会把结果优化掉(赋值给全局变量)
var result int // 防止编译器优化

func BenchmarkXxx(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result = compute()
}
}

Q4: GOGC 和 GOMEMLIMIT 怎么用于性能调优?

答案

  • GOGC=200:堆增长 200% 才 GC → GC 频率降低、CPU 降低,但内存增加
  • GOMEMLIMIT=1GiB:内存软上限,接近时触发 GC
  • 推荐组合:GOMEMLIMIT=容器内存*0.7 + GOGC=100~200
  • 这样既控制内存不 OOM,又避免 GC 过于频繁

相关链接