1. 背景
今天遇到了一个客户问题, 用户说我们这边的程序占用的内存越来越多, 我们的程序是通过 windows server 托管的, 使用 golang 编译成 exe 后, 使用安装器安装到用户 windows 服务器上的, 理论上只是一个心跳 + 信息收集用的, 不会占用太多内存.
不过当看到用户 64G 内存马上满了之后, 意识到肯定是哪里出了问题 😱
2. 复现步骤
找了公司里面几台安装的比较久的 exe 看了下, 运行一两个月后, 内存确实到了 1 G 左右. 😂 这个时候只能一步一步排查哪里出问题了
排查第一步
首先想到的是 Pprof 神器, fmt.Println(http.ListenAndServe("0.0.0.0:6063", nil))
一把梭之后, dump 查看内存占用
一顿操作猛如虎, 一看内存几百K, 这什么都没有
想到可能是使用了 CGO, 这部分内存是没办法在 pprof 中观察到的, 于是开启了第二步
排查第二步
神器 windbg, 想到可以转储内存, 之后通过分析内存中的内容, 就可以知道都是那些内容占据的内存了
首先转储内存, 直接在任务管理器中操作转储
下载 windbg, 并打开转储文件
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/ 可以从微软这里下载, 不过这个版本比较新了, 一些命令可能与旧版本不兼容, 我是 google 下载的之前版本
分析前的 windbg 配置工作
分析前, 需要将 window 的符号配置路径, 可以在 windbg 中输入下面的命令进行加载
然后加载转储文件使用如下的命令进行分析
常用的 windbg 命令
1!analyse -v 先大致分析下
2
3
4!heap -s 查看内存分布
50:000> !heap -s
6LFH Key : 0x311c1eaa657dc8ce
7Termination on corruption : ENABLED
8 Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
9 (k) (k) (k) (k) length blocks cont. heap
10-------------------------------------------------------------------------------------
110000000001980000 00000002 32552 20420 32552 479 896 6 0 2 LFH
120000000000010000 00008000 64 4 64 2 1 1 0 0
130000000000320000 00001002 7216 3236 7216 37 6 4 0 6 LFH
140000000028670000 00001002 60 16 60 5 2 1 0 0
1500000000287f0000 00001002 60 8 60 5 1 1 0 0
16-------------------------------------------------------------------------------------
17
18
19!heap -stat -h 0000000001980000 参看Heap
20 size #blocks total ( %) (percent of total busy bytes)
21 118 d - e38 (20.12)
22 8e4 1 - 8e4 (12.58)
23 800 1 - 800 (11.32)
24 400 2 - 800 (11.32)
25 782 1 - 782 (10.62)
26 50 f - 4b0 (6.63)
27 410 1 - 410 (5.75)
28 238 1 - 238 (3.14)
29 1e0 1 - 1e0 (2.65)
30 1b0 1 - 1b0 (2.39)
31 20 a - 140 (1.77)
32 100 1 - 100 (1.41)
33 68 2 - d0 (1.15)
34 3e 3 - ba (1.03)
35 98 1 - 98 (0.84)
36 30 3 - 90 (0.80)
37 44 2 - 88 (0.75)
38 42 2 - 84 (0.73)
39 40 2 - 80 (0.71)
40 3c 2 - 78 (0.66)
41
42!heap -flt s 118 查看 118 大小的数据里面有什么, 什么都没看出来
43 _HEAP @ 1980000
44 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
45 00000000019819d0 0012 0000 [00] 00000000019819e0 00118 - (busy)
46 0000000001981b50 0012 0012 [00] 0000000001981b60 00118 - (busy)
47 0000000001981fc0 0012 0012 [00] 0000000001981fd0 00118 - (busy)
48 0000000001982140 0012 0012 [00] 0000000001982150 00118 - (busy)
49 0000000001982380 0012 0012 [00] 0000000001982390 00118 - (busy)
50 0000000001983090 0012 0012 [00] 00000000019830a0 00118 - (busy)
51 00000000019832c0 0012 0012 [00] 00000000019832d0 00118 - (busy)
52 0000000001983520 0012 0012 [00] 0000000001983530 00118 - (busy)
53 0000000001983730 0012 0012 [00] 0000000001983740 00118 - (busy)
54 0000000001984650 0012 0012 [00] 0000000001984660 00118 - (busy)
55 0000000001984cf0 0012 0012 [00] 0000000001984d00 00118 - (busy)
56 0000000001984fa0 0012 0012 [00] 0000000001984fb0 00118 - (busy)
57 0000000001987640 0012 0012 [00] 0000000001987650 00118 - (busy)
58 _HEAP @ 10000
59 _HEAP @ 320000
60 _HEAP @ 28670000
61 _HEAP @ 287f0000
🥲 看了下 分布很均匀, 看不出来到底是那种类型数据, 似乎没有有用的特征信息, 推断可能是内核态的进程了, 太费事了, 于是想到了万能的笨方法
排查第三步 逐步尝试, 查看哪个函数消耗的内存
由于代码量不大, 相关的 cgo 函数就那么几个. 所以可以直接遍历, 定位内存泄露函数
1for i := 0; i < 10000000; i++ {
2 fmt.Printf("TestXXX: %#v\n", getXXX())
3 fmt.Printf("TestXXX: %#v\n", getXXX1())
4 fmt.Printf("TestXXX: %#v\n", getXXX2())
5}
写一个单测, 直接运行, 查看内存变化
最后确实定位到了问题点, 程序中使用了 # DsGetDomainControllerInfoW
函数, 使用了系统调用去获取了 windows server 的信息, 但是信息是 dll 中申请的, golang 这边管理不了, 后续要手动使用 # DsFreeDomainControllerInfoW
系统调用手动释放, 😒 真是 fuck, 不过好在这部分代码不是我写的, 可以甩锅 😁
具体的函数使用说明 https://learn.microsoft.com/zh-cn/windows/win32/api/ntdsapi/nf-ntdsapi-dsgetdomaincontrollerinfoa
3. windows 下进程内存的观测方法
3.1. windows 自带性能监视器
一般来说, 自带的已经可以了, 但是不足之处是, 无法查看具体的数值, 只有统计值
3.2. 使用 windows exporter 查看进程状态
https://github.com/prometheus-community/windows_exporter
直接从 release 下载最新版本, 在 windows 上运行, 注意默认情况下, 是不会监控进程相关的指标的, 需要在启动命令中增加相关参数
启动命令
1.\windows_exporter.exe --collectors.enabled "process" --collector.process.include="firefox.+"
默认的采集端是 9182, 在 prometheus 中配置采集对象
prometheus.yml
1global:
2 scrape_interval: 60s
3 evaluation_interval: 60s
4
5scrape_configs:
6 - job_name: prometheus
7 static_configs:
8 - targets: ['localhost:9182']
9 labels:
10 instance: prometheus
最后在 grafana 中观察就可以看到指定进程的内存占用情况
具体的指标项为 windows_process_working_set_private_bytes
, 通过 grafana 可以只管查看到具体的内存占用数据