一次内存问题排查与定位技术报告

本文简单记录一次内存问题定位排查方法。

一、问题背景

在进行 40 万 QPS 的极限压力测试时,Master 进程的内存占用从正常的 1.4GB 迅速飙升至 13.3GB,并随着压测持续进一步增长至 25GB 左右。压测停止后,物理内存占用并未显著回落。

二、排查工具安装与环境准备

1. gperftools (TCMalloc/Pprof) 安装

由于系统使用 libtcmalloc 作为内存分配器,直接利用其自带的 pprof 功能。

安装方式(以 CentOS 为例):

1
2
yum install gperftools gperftools-devel -y
开启采样:通过环境变量控制采样频率和导出路径。
1
2
# 设置 4MB 采样一个对象,降低对 40w QPS 业务的影响
export TCMALLOC_SAMPLE_PARAMETER=4194304
  1. 导出 Heap 快照
    在压测高位期间或结束后,通过 brpc 内置端口远程获取内存账本:
1
2
curl -s http://[IP]:9001/pprof/heap > heap.out
注:若返回文件大小为 0,说明 Master 因高压处于假死边缘,需配合长超时参数 --max-time 300 执行。

三、定位分析过程

1. 静态统计定位(GetStats)

通过 http://[IP]:9001/vars/tcmalloc_release_free_bytes 等接口获取内部统计。

发现:In use by application 达到 12.4GB 以上,而 page heap freelist 较小。

初步判断:内存并非被逻辑泄露(即不是完全丢失),而是被大量活跃对象占用,或因碎片化无法被 TCMalloc 归还给内核。

2. 动态采样定位(pprof)

使用 pprof 对导出的 heap.out 进行多维度分析。

累计全量分析 (alloc_space):

1
2
pprof --text /usr/bin/master ./heap.out
结果显示总计分配 25.4GB,其中 95.2% 的路径集中在 brpc::policy::ProcessRpcRequest。

当前存活分析 (inuse_space):

1
pprof --text --inuse_space /usr/bin/master ./heap.out

关键数据解读:

63.8% (16.1GB):std::_Function_base::_M_init_functor
这是异步任务闭包(std::function) 占用的核心空间。

31.2% (7.9GB):cstor::lava::common::threadpool::Pool::Submit
这是线程池任务提交队列的积压占用。

4.8% (1.2GB):::do_malloc_pages
这是底层页堆频繁申请物理页的系统开销。

四、根因总结

任务积压(Backpressure)

40 万 QPS 远超线程池的处理速率,导致 6 万长度 的任务队列被迅速填满。

闭包内存放大

每个积压在队列中的 std::function 任务都携带了复杂的请求/响应对象。由于对象未处理完,无法析构,产生了 16GB 以上的“账面占用”。

内存页“钉子户”碎片

在极高频的分配/释放中,由于 TCMalloc 以页(64KB)为单位管理,只要页内有一个小对象(闭包或字符串)未释放,整个页就无法归还内核,导致 RSS 持续高位。

五、解决方案建议

1. 策略调优(立即见效)

  • 缩短队列:将线程池队列从 6 万降低至 2000~5000。当系统处理不过来时,通过“快速失败”保护内存不被撑爆。

  • 加速回收:设置环境变量 TCMALLOC_RELEASE_RATE=5.0,强制分配器更激进地释放空闲页。

2. 代码优化(彻底根治)

引入 Protobuf Arena:将 GetAllSlotsView 中的 Response 内存分配由“零散堆申请”改为“内存池批量批发”,消除页内碎片。

精简闭包:优化线程池提交逻辑,避免在 Lambda 中通过值传递捕获重型对象。

如果你觉得本文对你有帮助,欢迎打赏