本文简单记录一次内存问题定位排查方法。
一、问题背景
在进行 40 万 QPS 的极限压力测试时,Master 进程的内存占用从正常的 1.4GB 迅速飙升至 13.3GB,并随着压测持续进一步增长至 25GB 左右。压测停止后,物理内存占用并未显著回落。
二、排查工具安装与环境准备
1. gperftools (TCMalloc/Pprof) 安装
由于系统使用 libtcmalloc 作为内存分配器,直接利用其自带的 pprof 功能。
安装方式(以 CentOS 为例):
1 | yum install gperftools gperftools-devel -y |
1 | # 设置 4MB 采样一个对象,降低对 40w QPS 业务的影响 |
- 导出 Heap 快照
在压测高位期间或结束后,通过 brpc 内置端口远程获取内存账本:
1 | curl -s http://[IP]:9001/pprof/heap > heap.out |
三、定位分析过程
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 | pprof --text /usr/bin/master ./heap.out |
当前存活分析 (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 中通过值传递捕获重型对象。