引言

最近我们把一个基于 OpenClaw 的推荐服务从 800 QPS 优化到了 4200 QPS(相同 4 核 8G 机器)。本文复盘整个调优过程,把可复用的方法论沉淀下来。

一、调优前的性能基线

压测脚本(k6):

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  vus: 100,
  duration: '60s',
};

export default function () {
  const res = http.get('http://localhost:8080/api/recommend?user_id=12345');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'latency < 200ms': (r) => r.timings.duration < 200,
  });
}

基线结果:

指标数值
QPS812
P50 延迟95ms
P95 延迟380ms
P99 延迟920ms
CPU 使用率95%
内存占用4.2G

二、第一步:开启 pprof

import _ "net/http/pprof"

go func() {
    http.ListenAndServe("localhost:6060", nil)
}()

启动后访问 http://localhost:6060/debug/pprof/,可以看到 30s CPU profile:

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

在 pprof 交互界面输入 top,看到 CPU 热点:

Showing nodes accounting for 21.4s, 78.2% of 27.4s total
      flat  flat%   sum%        cum   cum%
     5.6s 20.4% 20.4%      7.2s 26.3%  runtime.mallocgc
     3.8s 13.9% 34.3%      3.8s 13.9%  encoding/json.Unmarshal
     2.9s 10.6% 44.9%      4.1s 15.0%  github.com/opencLaw/.../FeatureEngine.Score
     2.4s  8.8% 53.7%      2.4s  8.8%  github.com/opencLaw/.../UserProfile.Load

Top 3 热点

  1. runtime.mallocgc —— 频繁分配内存
  2. encoding/json.Unmarshal —— JSON 反序列化
  3. FeatureEngine.Score —— 推荐打分逻辑

三、调优动作 1:sync.Pool 减少内存分配

JSON 反序列化会大量分配临时对象,用 sync.Pool 复用:

var decoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewDecoder(nil)
    },
}

func parseRequest(r io.Reader, v interface{}) error {
    dec := decoderPool.Get().(*json.Decoder)
    defer decoderPool.Put(dec)
    dec.Reset(r)
    return dec.Decode(v)
}

效果:CPU 占用下降 18%

四、调优动作 2:缓存热点用户画像

UserProfile.Load 是远程调用,每次都查 DB 会很慢。增加本地缓存:

type profileCache struct {
    mu    sync.RWMutex
    data  map[int64]*UserProfile
    ttl   time.Duration
}

func (c *profileCache) Get(userID int64) (*UserProfile, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    p, ok := c.data[userID]
    return p, ok
}

配合 LRU 淘汰策略,命中率 93%

五、调优动作 3:算法向量化

FeatureEngine.Score 原本是循环逐个特征计算,改用矩阵运算(gonum 库):

// 优化前:循环
for i := 0; i < len(features); i++ {
    score += weights[i] * features[i]
}

// 优化后:向量化
scoreVec := mat.NewVecDense(len(features), features)
weightVec := mat.NewVecDense(len(weights), weights)
scoreVec.MulElem(weightVec, scoreVec)
score := mat.Sum(scoreVec)

P95 延迟下降 62%

六、调优动作 4:GOGC 与 GOMEMLIMIT

GOGC=200 GOMEMLIMIT=6GiB ./opencLaw
  • GOGC=200:GC 触发阈值翻倍,减少 GC 频率
  • GOMEMLIMIT:软限制内存,避免 OOM

实测 GC 时间从平均 18ms 降到 4ms

七、调优后效果

指标调优前调优后提升
QPS81242185.2x
P50 延迟95ms22ms4.3x
P95 延迟380ms78ms4.9x
P99 延迟920ms195ms4.7x
CPU 使用率95%78%↓ 17%
内存占用4.2G5.1G↑ 21%

⚠️ 内存略升,是因为缓存了用户画像数据。属合理 trade-off。

八、调优方法论总结

  1. 先量后优:始终从 pprof / trace 出发,找到真正的瓶颈
  2. 由易到难:先参数调优(GOGC、连接池),后代码重构
  3. 分步验证:每次只改一处,验证后再继续
  4. 关注 trade-off:吞吐 vs 延迟 vs 内存,没有银弹

延伸阅读