7 月性能调优清单:10 个配置改动带来的延迟下降
7 月性能调优清单:10 个配置改动带来的延迟下降
一、性能调优不需要大动作,有时候改个参数就够了
七月团队对平台上的 4 个推理服务和 6 个微服务做了一轮系统性的性能调优。最终带来延迟下降的改动有 10 项,其中 7 项是配置层面的调整,不需要改一行业务代码。这个事实值得关注:很多性能问题的根源不在代码逻辑,而在运行环境和服务配置的"默认值"与真实负载不匹配。
下面这 10 项改动按延迟下降幅度降序排列,每项都附调优前的指标、调优后的数据和配置变更。
二、10 项配置改动的全景视图
改动一:vLLM max_num_seqs 从默认 256 调至 128(P99 ↓35%)
vLLM 的max_num_seqs控制了单次推理可同时处理的序列数。默认值 256 在并发 100+ 时会导致显存带宽竞争,P99 延迟飙升到 680ms。
将max_num_seqs从 256 降到 128 后,P99 延迟降至 440ms,下降 35%。根因是减少了显存带宽的并发争抢——每个序列的 KV Cache 都需要读写显存,序列数越多带宽竞争越激烈。128 是在当前 A100-80G 上测出的最优值:高于它延迟开始加速上升,低于它吞吐浪费。
但这个值不具有普适性。如果上下文平均长度增加,需要进一步降低。调优方法是在固定 QPS 下扫描 64-256 区间,找到延迟和吞吐的交点。
改动二:GPU 显存预分配从 0.9 调至 0.85(P99 ↓28%)
vLLM 默认使用 GPU 总显存的 90% 作为 KV Cache 池。实测 90% 时,CUDA Graph 占用、驱动开销和碎片空间不足,频繁触发 OOM Killer 导致 Pod 重启。
降到 85% 后不仅 OOM 次数从日均 3 次降到零,还因为 GC 压力减小,P99 延迟下降了 28%。这 5% 的显存留给 CUDA 运行时做临时分配,性价比很高。
改动三:启用 KV Cache FP8 量化(P99 ↓22%)
对于 Qwen-72B 这类大模型,KV Cache 容量是吞吐瓶颈。FP8 量化将 KV Cache 精度从 FP16 降到 FP8,显存占用减半。效果立竿见影:相同显存下可支持的 batch_size 翻倍,在同等并发下 P99 延迟下降了 22%。精度损失在内部评测中 BLEU 和 ROUGE 差异 <0.5%,生产可接受。
改动四:批处理超时从 100ms 调至 30ms(P99 ↓18%)
自适应批处理引擎的默认等待窗口是 100ms——当队列中请求不足 batch_size 时,最多等 100ms 再执行。在高并发场景下,100ms 的等待时间直接叠加到端到端延迟上。
降到 30ms 后,低负载场景下可能会有少量 GPU 算力浪费(batch 没填满),但 P99 延迟下降了 18%。对于延迟敏感的生产服务,这个 trade-off 值得。
改动五:启用 HTTP/2 多路复用(P99 ↓15%)
推理网关和下游服务之间的 HTTP/1.1 连接每个请求独立握手,当并发超过 500 时,TCP 连接数爆炸导致 TIME_WAIT 堆积。
切换到 HTTP/2 后连接数从 500 降到 8(多路复用),不仅降低了 CPU 开销(TLS 握手次数减少),P99 延迟也下降了 15%。配置改动非常简单——在 HTTP Client 初始化时设置http2.WithTransport。
改动六:gRPC Keepalive 参数调整(P99 ↓12%)
内部微服务之间通过 gRPC 通信。默认的 Keepalive 配置(Time: infinity, Timeout: 20s)意味着连接空闲后不会主动探测,如果对端崩溃,客户端需要等到 TCP 超时(通常 15 分钟)才能发现。
加上合理的 Keepalive 参数后:
grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 30 * time.Second, // 每 30s 发送 ping Timeout: 10 * time.Second, // ping 响应超时 PermitWithoutStream: true, // 空闲连接也发 ping })这避免了向已死连接发送请求导致的 15 分钟超时等待。P99 在节点故障场景下下降了 12%。
改动七:数据库连接池从无限制改为有界(P99 ↓10%)
Pod 启动时数据库连接池无界创建,高峰期 6 个 Pod 各自建立 120 个连接,数据库侧连接数 720。超过 RDS 实例的max_connections: 500限制后,新连接拒绝,超时时间覆盖 P99。
db.SetMaxOpenConns(50) // 6 Pod × 50 = 300,留 200 余量 db.SetMaxIdleConns(20) // 空闲连接保留,减少握手 db.SetConnMaxLifetime(30 * time.Minute) // 防止长连接导致的不均衡设置后的效果是两个维度:P99 下降 10%,数据库连接数从 720 降到 300。连接池大小公式:MaxOpenConns = (DB max_connections × 0.6) / Pod 数量。
改动八:GOMAXPROCS 与容器 CPU Limit 对齐(P99 ↓8%)
Go 运行时默认 GOMAXPROCS = 宿主机 CPU 核数。但 K8s 容器被限制为 2 核时,GOMAXPROCS 仍为宿主机的 64 核。这导致 GC 时过度并行(64 个 GC Worker 争抢 2 个核),STW 时间从正常 2ms 延长到 15ms。
使用uber-go/automaxprocs自动对齐后,P99 下降 8%。这个改动一行代码:
import _ "go.uber.org/automaxprocs"改动九:IO 调度器从 mq-deadline 切换到 kyber(P99 ↓6%)
节点 NVMe 盘的 IO 调度器默认 mq-deadline,在大量随机读(推理服务加载模型权重)场景下延迟抖动大。切换到 kyber 调度器后,IO 延迟的 P99 抖动从 120ms 降到 80ms,反映到端到端延迟下降 6%。
改动十:文件描述符上限从 1024 调至 65536(OOM 次数 ↓90%)
默认 ulimit 1024 在生产环境严重不够——一个 Go 服务的 goroutine 网络连接、文件日志、metrics 端点等轻松超过 1024。七月有两次因为 fd 耗尽导致新连接失败(表现为诡异的 EOF 错误)。
将 fs.file-max 调到 65536 后,由 fd 耗尽导致的 OOM 风险消失,服务稳定性显著提升。
三、调优方法论:先找瓶颈再动手
七月调优最重要的经验是"不要凭直觉猜测瓶颈在哪"。调优前的标准做法是:
- 用火焰图找 CPU 热点:如果是 CPU bound,优先调算法。
- 用
perf stat看 IPC 和 cache miss:如果 IPC 低且 cache miss 高,瓶颈可能在内存访问模式而非配置。 - 用 eBPF 看 IO 延迟分布:区分是 IO bound 还是锁竞争。
- 确认瓶颈类型后才决定是改配置还是改代码。
七月 10 个改动中 7 个是配置调整,说明团队在之前的部署中留下了大量默认值,而这些默认值并不是为生产环境设计的。
四、配置改动也有副作用
需要明确的是:每个配置调整都是 trade-off,不是"免费午餐"。
max_num_seqs降到 128 提升了延迟,但吞吐上限也相应降低了约 15%。在 QPS 继续增长时需要重新评估。- KV Cache FP8 量化以精度换速度,对一些精度敏感的 Fine-tuning 场景(如数学推理)可能不适用。
- 批处理超时降到 30ms 会导致 GPU 空转增加约 5%-8%,成本会轻微上升。
五、总结
七月 10 项性能调优改动覆盖了推理引擎参数、网络协议、连接池、Go 运行时和操作系统五个层面。P99 延迟累计下降明显,其中最大的收益来自 vLLM 的配置优化(三项合计下降 50%+)和 Go 运行时对齐(GOMAXPROCS)。
两条最值得推广的经验:第一,默认值从来不是为了生产环境设计的,应该把"检查所有默认配置"作为服务上线的标准步骤;第二,性能调优要先定位瓶颈再动手,盲目调参可能越调越差。基础设施需要持续迭代的优化,每一次小小的改动都是对系统稳定性的一寸寸夯实。