共计 1605 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点
在广告投放系统中,ad skill 模块负责实时计算广告候选集的匹配度和权重。随着业务规模增长,我们遇到了明显的性能瓶颈:

- 实时特征计算成为主要延迟源:
- 每个请求需要计算上百个特征(用户画像、上下文特征、广告属性)
-
火焰图显示 40% 的 CPU 时间消耗在特征归一化计算上
-
多策略竞争导致资源争用:
- 质量预测模型、点击率预估、频控策略等同步执行
-
I/ O 等待占比达到 35%(主要来自特征库查询)
-
突发流量下的雪崩效应:
- 高峰期单个服务节点 CPU 利用率突破 90%
- 级联超时导致 p99 延迟突破 300ms
架构演进
同步架构 vs 异步流水线
flowchart LR
A[同步架构] -->| 请求 | B[特征计算]
B --> C[策略执行]
C --> D[结果聚合]
D --> E[响应]
F[异步流水线] -->| 消息 | G[预处理队列]
G --> H[特征计算 Worker]
H --> I[策略决策队列]
I --> J[动态权重分配]
J --> K[结果缓存]
关键改进点:
- 消息队列解耦:
- 使用 Pulsar 替代直接 HTTP 调用
-
吞吐量从 500QPS 提升至 3000QPS
-
动态分级策略:
- Hot skill(高频策略):独占 CPU 核
- Warm skill:共享线程池
-
Cold skill:延迟计算
-
背压 (backpressure) 机制:
- 基于令牌桶控制特征计算速率
- 拒绝超过 200ms 的等待请求
核心代码实现
Goroutine 池优化
// 使用 Go 1.21 的泛型工作池
type WorkerPool[T any] struct {
tasks chan T
sem chan struct{}}
func NewPool[T any](size int) *WorkerPool[T] {return &WorkerPool[T]{tasks: make(chan T, 1000),
sem: make(chan struct{}, size),
}
}
// 带超时控制的任务提交
func (p *WorkerPool[T]) Submit(task T, timeout time.Duration) error {
select {case p.sem <- struct{}{}:
go p.execute(task)
return nil
case <-time.After(timeout):
return fmt.Errorf("pool full")
}
}
Redis 分布式锁优化
-- KEYS[1]锁 key, ARGV[1]随机值, ARGV[2]过期时间(ms)
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
local current = redis.call("GET", KEYS[1])
if current == ARGV[1] then
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1
end
return 0
end
GC 调优技巧
// 特征预加载时禁用 GC
func preloadFeatures() {debug.SetGCPercent(-1) // 暂停 GC
defer debug.SetGCPercent(100)
// 批量预加载代码...
}
性能验证
压测数据对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均延迟 | 200ms | 15ms |
| p99 延迟 | 300ms | 25ms |
| 错误率 | 2.3% | 0.01% |
| CPU 利用率 | 92% | 65% |
关键优化手段:
- 特征计算异步化:节省 40% 的 CPU 时间
- 分布式锁优化:减少 85% 的锁竞争
- 内存池复用:降低 60% 的 GC 压力
避坑指南
- 内存泄漏:
- 未关闭的 channel 会导致 goroutine 泄漏
-
使用
go vet检查发送 / 接收不匹配 -
时钟漂移问题:
- NTP 同步间隔设置为 30 秒
-
在分布式锁中使用单调时钟
-
灰度发布策略:
- 按 5% 流量逐步上线新特征
- 双写对比验证结果一致性
开放思考
如何平衡实时性与特征覆盖率?当需要引入耗时的高价值特征(如深度学习模型)时,是选择:
- 降级部分实时性要求
- 预计算 + 缓存方案
- 动态特征重要性采样
欢迎在评论区分享你的实践经验。
正文完