去年有一段时间,我被一个看起来很普通的故障折磨得不轻:服务在压力测试里跑了两天,内存从400M一路涨到1.6G,曲线斜率稳定得像故意画出来的。我一开始还在用最原始的办法——翻代码、加日志、注释掉可疑模块再跑一轮,折腾了三个晚上,最终才在一条异常分支里找到漏掉的delete。那时候我就在想:如果这次泄漏不是一处,而是分散在七八个模块里,我是不是要查一个月?
查完之后我把整个过程拆开看了一遍,发现真正需要的不只是某个调试技巧,而是一套能把"发现内存泄漏"到"定位到分配堆栈"这件事自动化的系统。所以这篇文章把我从原理、工具选型到落地流程的完整做法写出来,重点回答一个很多人都会问的问题:像Windbg(也有人拼成windebug)这种调试器,到底能不能检测内存泄漏?如果能,它在自动化体系里应该放在哪一步;如果不能,那真正承担"检测"职责的又是什么。
1. 先厘清一个问题:自动检测系统到底要解决什么
1.1 内存泄漏最让人头痛的不是"泄漏"本身
在C/C++这类需要手动管理内存的语言里,内存泄漏的本质是:一段内存被分配出来后,程序失去了对它的引用,或者它的所有者永远不会执行释放逻辑。它不像空指针崩溃那样当场暴露,也不会打印一条"我泄漏了"的日志。它的表现非常隐蔽——内存稳步增长、系统响应变慢、缓存命中率下降,然后在某个深夜被OOM杀掉,留下一堆不知道从哪开始的线索。
但真正折磨人的不是泄漏本身,而是查找链路太长。一次典型的排查过程是这样的:先用性能计数器确认内存确实在涨,再靠代码审查猜可能是哪个模块,然后为怀疑对象加日志重新跑测试,跑完发现不是,再换下一个。每一轮循环都消耗大把时间,而且高度依赖排查者的经验。我对"自动检测系统"的需求定义,最开始就是想把这个循环压缩掉。
1.2 检测这件事,其实分成四个层级
| 层级 | 要回答的问题 | 自动化能做到吗 |
|---|---|---|
| 发现 | 内存是不是在持续增长? | 能,性能计数器+趋势判断 |
| 定位 | 增长的量来自哪个堆、哪条调用路径? | 能,配合栈回溯和快照对比 |
| 根因 | 为什么这段内存没有被释放?引用关系哪里断了? | 半自动,需要结合业务逻辑判断 |
| 修复 | 代码应该怎么改? | 基本靠人 |
想清楚这个边界特别重要。我见过不少团队的自动检测项目,一开始就想让工具直接指出"这里少了个delete",结果做出来一堆伪智能,反而不可用。一个务实的目标是:让系统稳定地把前两层跑完,输出一份带调用栈的泄漏候选清单,把根因分析交给熟悉业务的人。这样自动化能覆盖80%的重复劳动,又不会因为试图理解业务语义而陷入泥潭。
1.3 我给系统划定的输入输出边界
所以这个系统的输入很清晰:一个被测进程,以及一段可以重复执行的测试场景。输出就三样东西:
- 疑似泄漏堆栈清单,按置信度排序;
- 每条堆栈对应的dump文件或堆快照;
- 内存趋势数据和触发判定时的关键指标。
系统不负责自动改代码,也不负责区分"这是泄漏还是业务缓存"——它只负责把证据摆到桌面上,让人来判断。我觉得"自动检测"这四个字的价值,恰恰是省掉人肉抓证据的时间,而不是替代人的判断。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 检测原理是源头:Windbg凭什么能/不能定位泄漏
2.1 Windows生态里常见的三种泄漏检测原理
理解了原理,才能理解工具之间为什么长得完全不一样。我在Windows上常用的检测手段大致分三类:
第一类是分配点追踪。在每次内存分配时记录调用栈,进程退出或周期性地把"还活着的堆块"连同栈打出来。典型代表是Debug CRT的泄漏报告、VLD(Visual Leak Detector)、App Verifier的Leak检测。这种方案的定位能力很强,能直接看到分配栈,但需要被测代码配合,而且一般不适合长时间在线上开启。
第二类是水位快照对比。周期性记录进程堆的分配状态,把两个时间点的快照做差,看哪些调用栈对应的内存增量最大。UMDH就是典型工具。它不需要改被测代码,适合服务端长时间抓取,但它看到的是"分配差",没法判断多出来的对象是不是真的失去引用。
第三类是运行期水位监控,也就是用性能计数器长时间观察Private Bytes之类指标,判断是否存在持续上升趋势。它只能告诉你有问题,不能告诉你在哪,所以通常作为前两类工具的触发器。
2.2 Windbg的真实定位:分析器,不是自动检测器
回到那个高频问题:Windbg能检测内存泄漏吗?准确回答是:Windbg不是检测器,它是在你已经怀疑某处泄漏之后,用来把证据钉死的分析器。
Windbg本身不会主动扫描"哪些块是泄漏的"。它只能做几件事:看当前进程堆的整体统计、遍历堆块、在开启栈回溯后显示某个堆块的分配调用栈。如果进程是一台刚跑完压力测试的机器,你用Windbg打开dump,它能告诉你某个堆里确实有大量同尺寸的块、分配栈指向同一个函数——但"这是泄漏"这个结论,仍然是人根据业务逻辑做的判断。
要让Windbg能看到分配栈,前提是提前开启GFlags的UST(用户态栈回溯)选项,或者对进程开启页堆。没有这个前提,Windbg就只能看到一块裸内存,什么都定位不了。这个前提条件非常关键,后面我会专门展开。
2.3 一个常见误解:!heap -s里“堆大小增长”不等于泄漏
很多人第一次打开!heap -s,看到堆的总大小从100M涨到300M,就下结论"泄漏了"。这是不严谨的。堆大小增长可能来自三种情况:
- 真的泄漏:对象失去引用,永远不会释放;
- 正常缓存:程序故意缓存一些东西来提升性能,比如连接池、对象池;
- 堆碎片化:虽然总占用涨了,但实际活跃对象并不多,只是堆不再还给操作系统。
在自动检测系统里,我一般把!heap -s当成证据链的一环,而不是判定依据。判定依据必须加上时间维度——同一个堆栈在多轮采样里持续增长,或分配次数与被释放次数长期不成比例,才值得进入候选清单。
2.4 常用检测工具横向对比
| 工具 | 检测原理 | 适合阶段 | 主要限制 |
|---|---|---|---|
| Debug CRT | 分配点追踪 | 本地开发、单元测试 | 仅Debug构建有效 |
| VLD | 分配点追踪+栈回溯 | 本地联调 | 对第三方库和COM泄漏基本无效 |
| App Verifier | 分配点追踪+句柄/COM检查 | 测试环境回归 | 性能开销大,需要重启应用 |
| UMDH | 水位快照对比 | 服务端长时间诊断 | 只能看虚拟内存差,区分不了缓存 |
| Windbg | 事后堆分析 | 事故dump定位 | 依赖+ust和符号,否则只能看总量 |
| 自研水位监控 | 性能计数器趋势判断 | 持续运行期 | 只发现不定位 |
表格看完就能明白一件事:自动检测系统不应该只押宝在单一工具上,而是要把"发现、定位、取证"三件事分别交给最合适的工具。
3. 在Windows上把检测环境配到够用
3.1 开发期先动手:Debug CRT和VLD把家常泄漏扫干净
如果你的项目还在开发阶段,我建议第一步先开启Debug CRT的泄漏检测。这几乎是零成本的做法,只需要在入口处加上几行:
cpp复制#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
// 业务逻辑
_CrtDumpMemoryLeaks();
return 0;
}
跑一遍测试后,Visual Studio的输出窗口会列出所有没释放的堆块,双击能定位到分配代码行。如果泄漏发生在特定次数的分配上,可以用_CrtSetBreakAlloc(864)在分配序号864处直接下断点,这是定位"前一万次没事、第一万零一次开始泄漏"这类问题的利器。
但Debug CRT只在Debug构建下有效,而且它输出的东西可读性一般。所以在实际项目里我更推荐VLD作为开发期主力。VLD本质是Hook了分配函数,在进程退出时打印出完整的未释放堆栈。它不用改业务代码,只需要在包含入口的源文件里加:
cpp复制#include <vld.h>
然后在vld.ini里配置输出方式:
ini复制ReportTo = file
MaxTraceFrames = 16
VLD的栈可读性比CRT好很多,能直接看到调用链,配置成本也低。我踩过的坑是:VLD对第三方库内部的分配经常无能为力,它只能看到跨过库边界的上层调用。所以它适合扫自家代码,不能当作唯一防线。
3.2 运行期打底:GFlags的+ust是Windbg能看到栈的前提
现在谈运行期诊断。要让Windbg或者UMDH能看到每个堆块的分配栈,必须先在系统里开启用户态栈回溯:
code复制gflags /i MyService.exe +ust
注意这个操作是往注册表里写Image File Execution Options,只对指定进程名生效,不会影响整个系统。开启之后,每次堆分配都会额外记录一份栈回溯信息,代价是每块分配的内存占用会高出几十字节。内存小的机器上,光这个开销就可能让进程内存翻倍,所以只建议在测试环境、预发环境开。
查当前状态:
code复制gflags /i MyService.exe
关闭:
code复制gflags /i MyService.exe -ust
3.3 Windbg里真正有用的堆命令组合
开启+ust之后,Windbg的堆分析才真正可用。我常用的命令组合是固定的,基本就四步:
先看整体:
code复制0:000> !heap -s
如果发现某类尺寸的块特别多,筛一下:
code复制0:000> !heap -flt s 512
对筛出来的某个块,看它的分配栈:
code复制0:000> !heap -p -a 0x0000023a0012a870
!heap -p -a配合+ust会打印出这个堆块的完整分配调用栈,这是定位的黄金信息。如果没开启+ust,这个命令只会告诉你"无法获取栈"。
我一般还会用!heap -stat -h按堆句柄看具体某个堆的大小分布,这在对多堆进程做分析时很有用。命令本身不难,难的是知道什么时候该用。每次接到内存增长的dump,我的思路都是:!heap -s先确认问题是不是出在默认进程堆,再决定要不要深入。
3.4 UMDH快照对比:不停进程也能抓增长曲线
UMDH的全称是User-Mode Dump Heap,它和Windbg配合能实现"无侵入长时间观察"。前提同样是先开启+ust。然后按这几个步骤走:
code复制umdh -p:<PID> -f:before.txt
跑一段业务场景,比如一轮压力测试,之后再抓一次:
code复制umdh -p:<PID> -f:after.txt
把两次快照做差:
code复制umdh before.txt after.txt -d > diff.txt
diff.txt里会按增长量排序列出调用栈,排在前面的就是嫌疑最大的分配路径。这个工具的优点是全程不需要停进程,也不需要在代码里加任何日志;缺点是只能反映虚拟内存分配差,如果程序里有大量高频分配和释放,差值的噪声会很大。所以我会连续抓多轮,每轮只跑同样的场景,看同一组栈是不是稳定出现在增长榜前列。
3.5 别忘了App Verifier:它能覆盖到普通堆工具够不着的地方
App Verifier是Windows SDK里一个经常被忽略但非常强力的工具。它不只是做内存泄漏检测,还能检测句柄泄漏、COM引用计数问题、堆损坏等。启用方式很简单:
code复制appverif -enable Leak -for MyApp.exe
它会在应用退出时,把所有疑似未释放的堆块连同分配栈报告出来。我为什么说它重要?因为C++的普通分配泄漏,Debug CRT和VLD就够用了;但COM对象泄漏、GDI句柄泄漏这些,普通堆工具是看不见的,App Verifier的专项检查能补上这块短板。
代价就是性能开销比较大,开启后进程运行速度会明显变慢,所以它不是常驻选项,而是"怀疑有问题时专门跑一轮"的武器。
4. 自动检测系统的整体架构与落地流程
4.1 系统目标与模块划分
工具准备好之后,我开始搭建自动化系统。整个系统的逻辑可以拆成五个模块:
- 采集器:定时采集进程的性能计数器,包括Private Bytes、Handle Count、线程数等;
- 判定器:对采集到的时间序列做趋势分析,判断内存是否在持续增长;
- 取证器:判定触发后自动抓取dump,保存进程信息和符号路径;
- 分析器:调用Windbg的命令行版本cdb,批量执行堆分析命令,解析文本输出;
- 聚合器:把多次分析结果里的堆栈做归一化和聚合,输出带置信度的报告。
每个模块只做一件事,模块之间通过文件或消息队列解耦。这样即使分析器卡住了,采集和判定还能继续工作,不至于整个链路因为一环出错而崩溃。
4.2 采集指标怎么选:Private Bytes、Working Set还是Heap Size
一开始我以为只需要看一个内存指标就行了,后来发现不行。每个指标都有自己的脾气:
- Private Bytes:进程私有的虚拟内存总量,最直观地反映"这进程吃了多少内存",我把它当主指标;
- Working Set:物理内存占用,但这个值会受系统内存压力的影响,别人占多了它就被挤出去,不适合用来判断泄漏;
- Heap Size:只反映进程堆,但很多程序自己用HeapCreate创建额外堆,而且像VirtualAlloc这种直接分配的内存根本不在堆里,单看它容易漏判。
实际系统的做法是:主指标用Private Bytes,辅助指标用Handle Count和GDI Object,采样间隔设在30到60秒。太密集会影响被测进程性能,太稀疏又会漏掉短时间内的快速膨胀。
4.3 判定逻辑:别等进程爆掉,要在趋势上发现问题
判定器的逻辑是整个系统里最关键的部分。我踩过最大的坑是试图用固定阈值,比如"内存超过2G就报警"——但不同版本、不同场景的启动基线都不一样,固定阈值要么误报满天飞,要么完全漏报。
后来换成了趋势判断。最简单的实现是最小二乘线性回归,对最近N个采样点算斜率:
python复制def detect_leak(samples, threshold=0.01):
# samples: [(timestamp, private_bytes), ...]
# 返回斜率是否超过阈值
x_mean = sum(t for t, _ in samples) / len(samples)
y_mean = sum(v for _, v in samples) / len(samples)
numerator = sum((t - x_mean) * (v - y_mean) for t, v in samples)
denominator = sum((t - x_mean) ** 2 for t, _ in samples)
if denominator == 0:
return False
slope = numerator / denominator
return slope > threshold
判定逻辑是:Private Bytes在最近30分钟内单调上升,且线性拟合的斜率超过某个人为设定的值,就认为存在泄漏嫌疑。这个值需要针对不同服务调,压测服务和低流量服务差别很大。我会先跑一套"正常版本"采集数据,把基线斜率算出来,再设置成基线的3到5倍作为告警阈值。
4.4 触发后的自动取证流程
判定器一旦触发,取证器就要在最短时间内把证据留下来。顺序很重要,因为有些信息只有进程还活着的时候才能拿到,dump则只能在进程挂掉之前抓。
第一步,保存进程当前版本信息,包括可执行文件版本、PDB路径、模块列表。这一步通常直接读取进程的环境变量和工作目录即可。
第二步,用procdump抓取完整dump:
code复制procdump -ma -accepteula -t MyService.exe
如果要指定PID:
code复制procdump -ma 12345
-ma表示抓完整内存,虽然文件很大,但信息全;-t表示在进程终止时触发,适合抢救性抓取。
第三步,调用cdb批量分析。cdb是Windbg的命令行版本,可以跑在无人值守环境里:
code复制cdb -y "SRV*C:\Symbols*https://msdl.microsoft.com/download/symbols" -z dump.dmp -logo analyze.txt -c "!analyze -v; !heap -s; q"
-z表示分析崩溃转储文件,-logo把输出写到文件,-c后面跟要执行的命令序列。分析完得到的文本再交给解析器去处理。
4.5 报告聚合与堆栈归一化的工程实现
分析器输出的是几千行文本,直接丢给人看是灾难。聚合器要做的事情是:把每条泄漏堆栈从"带地址偏移量的原始文本"转换成"去掉偏移的函数签名",然后按签名归类、计数、排序。
比如这两条栈本质上是同一个泄漏点:
code复制myapp.dll!LoadConfig+0x1a4
myapp.dll!LoadConfig+0x1b2
归一化后都是:
code复制myapp.dll!LoadConfig
虽然在底层函数不同时会丢失精度,但对于自动检测系统来说,"哪条路径在持续分配没释放"这个粒度已经足够让人工介入。聚合后的报告我会设计成这样一张表:
| 排名 | 分配栈签名 | 出现次数 | 增长量占比 | 关联dump |
|---|---|---|---|---|
| 1 | myapp.dll!ProcessTask + worker | 128 | 63% | dump_0331_1442.dmp |
| 2 | libcore.dll!CreateBuffer | 41 | 17% | dump_0331_1442.dmp |
| 3 | myapp.dll!CacheManager::Insert | 23 | 9% | dump_0331_1450.dmp |
到这里,系统的价值就出来了:人工不需要从头开始翻代码,而是直接看排名表,从最可疑的堆栈开始验证。
5. 自动化中的三个工程细节:符号、去重和误报
5.1 符号服务器和源索引是自动化的地基
如果构建机上没有把PDB发布到统一的符号服务器,上面所有自动化分析都会变成废纸。因为Windbg在没有符号文件时只能输出十六进制偏移,解析器看到的是一堆无法归类的地址,聚合根本做不起来。
所以第一步是让构建流水线在编译后把PDB上传到内部符号服务器,然后在所有分析机上配置统一的符号路径:
code复制SRV*C:\Symbols*\\symstore\symbols
有条件的团队还可以配置源索引,把PDB里的调试信息关联到版本库的源码目录,这样堆栈不仅能定位到函数,还能直接跳到源码行。这个功能在Windbg里叫srcsrv,配置好以后配合扩展命令能自动拉取源码。
我在搭建初期就在这上面吃过亏:本地分析一切正常,一上自动分析机就全是问号,折腾了半天发现是符号路径没配,机器上读不到内网符号服务器。这个问题必须在系统上线前就堵死,否则后续分析全部白搭。
5.2 堆栈去重与相似度聚合的工程实现
堆栈归一化第一步是去掉偏移,保留模块名和函数名。但真正难的是"相似但不完全相同"的栈怎么聚合。
比如同一条泄漏路径,在不同线程里调用,线程启动时的栈底必然不同,但栈顶的分配路径是一样的。如果只做整条栈的字符串比较,这两条会被当成不同记录。
我的做法是:对每条栈计算它和已有栈的公共前缀长度,如果公共前缀超过总帧数的一半,就把它们分到同一组。组内的代表栈取公共前缀部分,组内其他栈记录线程差异和出现次数。实现上就是维护一个分组列表,每条新栈进来时做一次前缀匹配。
这个过程看似简单,但对系统可用性的提升是决定性的。没有这一步,报告会散成一地鸡毛,同一个泄漏点被分成几十条记录,人工根本没法看。
5.3 误报治理:基线动态化与白名单机制
趋势检测天然会产生误报,我见过最典型的是:某些服务启动后会有一段缓存预热期,内存先涨一波然后稳住;如果判定器正好在预热期触发,就会把正常缓存当成泄漏。
所以系统里我加了"预热期"概念:进程启动后的前30分钟,采集器照常工作,但判定器不报警,只记录基线;等数据攒够了,再用动态基线和后续数据做对比。不同业务场景的预热期可以配置,压测服务我一般设成10分钟,长稳服务设成1小时。
另一个是白名单机制。对于已经确认是"有意缓存"的模块,可以把它加入白名单,后续分析时直接过滤。但白名单不能是一删了之的规则,而要在报告里保留一条"被过滤的记录数",让团队知道有多少数据被有意忽略了。我还会把每条误报都记录下来,定期回看,防止误报堆积成沉默的数据。
6. 实测中的坑与一条完整的排查链路
6.1 诊断工具本身引起的"假泄漏":32位进程与+ust的代价
我们曾在一个32位的旧服务上做检测,开启+ust之后,进程的虚拟内存一个小时就飙到上限,看起来像是泄漏,实际是诊断工具把地址空间撑爆了。
原因很简单:+ust让每个堆块额外记录栈回溯,大约每块多几十字节。32位进程默认只有2GB虚拟地址空间,本来高压力下堆就很紧,再加上这些栈信息,地址空间迅速耗尽。这种情况下,任何趋势分析都会误判为泄漏。
解决办法是把测试尽量迁移到64位版本;如果必须保留32位,可以给链接器加/LARGEADDRESSAWARE让进程支持3GB地址空间,或者放弃+ust,改用VLD这样的开发期工具来定位。我最后选择的是:32位服务只做开发期检测,运行期自动检测统一跑在64位环境里。这个决策直接让误报率降了一半。
6.2 Debug CRT报告和Release行为不一致的"假泄漏"
还有一次,我在一个组件库里用Debug CRT扫出一大堆泄漏,一度以为是第三方库的问题。后来发现是混合模式导致的假象:主程序用Debug CRT,第三方库用Release CRT,两边对堆的管理方式不同,CRT退出时的扫描自然报告了一堆"跨CRT未释放"的块。
这个坑提醒我:Debug CRT和VLD扫出来的泄漏,在Release构建下不一定复现。所以我把它们定位成"开发期的辅助工具",用来在早期发现问题;而真正拍板"是不是泄漏",必须用Release构建跑UMDH或App Verifier,用运行期数据说话。否则就会在一个假问题上浪费掉一整天。
6.3 一次典型的COM引用计数泄漏排查链路
下面这条链路可以完整复现我当时排查COM泄漏的过程。
现象是:某后台服务每处理一批批量任务后,Private Bytes会稳定增长30M,多跑几轮后进程响应明显变慢。我用自己搭的系统做了以下操作:
第一步,确认趋势:
powershell复制Get-Counter -Counter "\Process(svc32)\Private Bytes" -SampleInterval 5 -MaxSamples 120
输出曲线确实在走高,排除了一次性分配的可能。
第二步,抓dump并打开Windbg的!heap -s,发现默认进程堆的统计没有明显变化。这说明增长的不是普通CRT堆分配,而是别的机制——COM、系统API或直接VirtualAlloc的内存。
第三步,用!address -summary查看内存分布,发现MEM_COMMIT区域出现大量话术各异的私有区域,这和典型的COM分配模式吻合。
第四步,用App Verifier开启Leak检测,在同一场景下重跑,退出时拿到了两条带分配栈的记录,指向代码里一个异常分支——对象创建成功后,在某个错误判断条件下直接return了,忘记调用Release。
修复就是在return前补上释放逻辑。之后我用同样的场景再跑三轮回归,Private Bytes曲线恢复了平直。
这条链路里有意思的是:每一步换一个工具,不是为了炫技,而是因为上一层的工具确实回答不了下一层的问题。普通堆检测看不到COM,内存分布分析只能给方向,最终把栈钉死的是App Verifier。这也印证了我在第2章的观点:检测和定位需要组合拳。
6.4 端到端跑通之后的效果变化
这套系统上线后,我把原先需要人工两到三天完成的"发现+定位"过程压缩到了约二十分钟。触发流程是:压测脚本跑起来,采集器每30秒记录一次,判定器在趋势满足条件后自动抓dump、自动跑cdb分析、自动出报告。整个过程没有人工介入。
实际使用中最大的感触是:以前排查泄漏是个很吃经验的事情,现在系统把证据链完整呈现出来,一个不太熟悉内存管理的开发也能根据堆栈排名快速找到嫌疑人对应的代码。团队里新同学在处理类似问题时,不再是从零开始猜,而是先看系统生成的报告。
7. 从能跑到好用:我的几点扩展体会
7.1 第一版别追求完美,先把链路串起来
我一开始想把系统做成"开箱即用"的通用平台,结果花了很多时间在设计配置项上。后来发现正确的做法是先挑一个项目、一个场景,用最短路径把采集、判定、取证、分析、聚合这五个环节跑通,哪怕逻辑粗糙一点也比没跑通强。一旦端到端跑起来,后续优化就有基准了。
比如判定器第一版完全可以只是"最近30分钟Private Bytes增长超过20%就触发",不必一上来就上线性回归。跑一段时间之后,再根据误报和漏报的数据来调整算法。自动化系统的价值本来就在运行中积累,而不是一开始就设计完美。
7.2 同一套机制可以迁移到其他问题上
内存泄漏只是"运行期资源异常增长"里的一个子类。我把这套"采样-判定-取证-分析-聚合"的思路迁移到句柄泄漏上,只需要把采集指标从Private Bytes换成Handle Count,把取证方式从抓完整dump换成抓句柄快照,其他环节几乎不用动。CPU漂移、线程增长类似的问题,理论上都可以套这个框架。
这也是为什么我把各模块之间的耦合降到最低:采集器不知道判定器怎么算趋势,判定器不知道分析器怎么解析堆栈。换一个指标、换一个工具,只改对应模块内部实现,不需要动整条链。
7.3 上线前必须想清楚的两件事
第一,不要在正式生产环境常开+ust。栈回溯的内存和性能开销会导致"检测系统本身引起的问题"比漏检更麻烦。我的做法是把这套检测跑在测试环境和压测环境,生产环境只保留性能计数器的水位监控,发现问题后把当时的数据导出到测试环境复现。
第二,报告的质量比报告的数量重要。一个每天发几十条低质告警的系统,很快就会被团队当成噪音忽略,然后在真正出问题时没人看。宁可阈值调紧一点、告警少一点,也要保证每一条告警都能被某个开发者认真看一眼。
我个人的经验是:内存泄漏自动检测系统的核心不是某个高深算法,而是把一堆成熟的工具,按照正确的顺序串起来,让证据链尽可能完整地呈现在人面前。如果你也在为"内存又涨了但不知道是谁分配的心"发愁,不妨先从一条最简单的链路开始搭——采集Private Bytes,写一个斜率判断,触发时调procdump,再用cdb看一次!heap -s和!heap -p -a。等这条链路走通了,再慢慢往里面加自动化细节。
