1. 背景

今天遇到了一个客户问题, 用户说我们这边的程序占用的内存越来越多, 我们的程序是通过 windows server 托管的, 使用 golang 编译成 exe 后, 使用安装器安装到用户 windows 服务器上的, 理论上只是一个心跳 + 信息收集用的, 不会占用太多内存.

不过当看到用户 64G 内存马上满了之后, 意识到肯定是哪里出了问题 😱

2. 复现步骤

找了公司里面几台安装的比较久的 exe 看了下, 运行一两个月后, 内存确实到了 1 G 左右. 😂 这个时候只能一步一步排查哪里出问题了

排查第一步

首先想到的是 Pprof 神器, fmt.Println(http.ListenAndServe("0.0.0.0:6063", nil)) 一把梭之后, dump 查看内存占用
image.png
一顿操作猛如虎, 一看内存几百K, 这什么都没有

想到可能是使用了 CGO, 这部分内存是没办法在 pprof 中观察到的, 于是开启了第二步

排查第二步

神器 windbg, 想到可以转储内存, 之后通过分析内存中的内容, 就可以知道都是那些内容占据的内存了
首先转储内存, 直接在任务管理器中操作转储
image.png

下载 windbg, 并打开转储文件
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/ 可以从微软这里下载, 不过这个版本比较新了, 一些命令可能与旧版本不兼容, 我是 google 下载的之前版本

image.png

分析前的 windbg 配置工作

分析前, 需要将 window 的符号配置路径, 可以在 windbg 中输入下面的命令进行加载

1.sympath SRV*c:\mySymbols*http://msdl.microsoft.com/download/symbols
2!sym noisy
3.reload

然后加载转储文件使用如下的命令进行分析

常用的 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 自带性能监视器

image.png
image.png
一般来说, 自带的已经可以了, 但是不足之处是, 无法查看具体的数值, 只有统计值

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 中观察就可以看到指定进程的内存占用情况
image.png

具体的指标项为 windows_process_working_set_private_bytes, 通过 grafana 可以只管查看到具体的内存占用数据