機器配置:2 CPU,4GB 內存
java
預先安裝 sysstat、Docker 以及 bcc 軟件包,好比:node
# install sysstat docker
sudo apt-get install -y sysstat docker.io
# Install bcc
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo
"deb https://repo.iovisor.org/apt/bionic bionic main"
| sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt-get update
sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
|
軟件包bcc,它提供了一系列的 Linux 性能分析工具,經常使用來動態追蹤進程和內核的行爲。更多工做原理你先不用深究,後面學習咱們會逐步接觸。這裏你只須要記住,按照上面步驟安裝完後,它提供的全部工具都位於 /usr/share/bcc/tools 這個目錄中。linux
注意:bcc-tools 須要內核版本爲 4.1 或者更高,若是你使用的是 CentOS7,或者其餘內核版本比較舊的系統,那麼你須要手動升級內核版本後再安裝。docker
同之前的案例同樣,下面的全部命令都默認以 root 用戶運行,若是你是用普通用戶身份登錄系統,請運行 sudo su root 命令切換到 root 用戶。編程
安裝完成後,再執行下面的命令來運行案例:ubuntu
$ docker run --name=app -itd feisky/app:mem-leak
|
案例成功運行後,你須要輸入下面的命令,確認案例應用已經正常啓動。若是一切正常,你應該能夠看到下面這個界面:數組
$ docker logs app
2th =>
1
3th =>
2
4th =>
3
5th =>
5
6th =>
8
7th =>
13
|
從輸出中,咱們能夠發現,這個案例會輸出斐波那契數列的一系列數值。實際上,這些數值每隔 1 秒輸出一次。緩存
知道了這些,咱們應該怎麼檢查內存狀況,判斷有沒有泄漏發生呢?你首先想到的多是 top 工具,不過,top 雖然能觀察系統和進程的內存佔用狀況,但今天的案例並不適合。內存泄漏問題,咱們更應該關注內存使用的變化趨勢。多線程
運行下面的 vmstat ,等待一段時間,觀察內存的變化狀況。若是忘了 vmstat 裏各指標的含義,記得複習前面內容,或者執行 man vmstat 查詢。app
root
@ubuntu
:/home/xhong# vmstat
3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0
0
392324
2100404
23480
791260
13
41
19871
104
615
619
4
23
72
1
0
0
0
392324
2100272
23480
791296
0
0
0
52
207
386
1
1
99
0
0
0
0
392324
2100148
23488
791296
0
0
0
5
186
371
0
0
99
0
0
0
0
392324
2100180
23520
791332
0
0
0
373
297
456
1
1
99
0
0
0
0
392324
2100056
23520
791332
0
0
0
0
150
342
1
0
99
0
0
|
從輸出中你能夠看到,內存的 free 列在不停的變化,而且是降低趨勢;而 buffer 和 cache 基本保持不變。
未使用內存在逐漸減少,而 buffer 和 cache 基本不變,這說明,系統中使用的內存一直在升高。但這並不能說明有內存泄漏,由於應用程序運行中須要的內存也可能會增大。好比說,程序中若是用了一個動態增加的數組來緩存計算結果,佔用內存天然會增加。
那怎麼肯定是否是內存泄漏呢?或者換句話說,有沒有簡單方法找出讓內存增加的進程,並定位增加內存用在哪兒呢?
根據前面內容,你應該想到了用 top 或 ps 來觀察進程的內存使用狀況,而後找出內存使用一直增加的進程,最後再經過 pmap 查看進程的內存分佈。
但這種方法並不太好用,由於要判斷內存的變化狀況,還須要你寫一個腳本,來處理 top 或者 ps 的輸出。
這裏,我介紹一個專門用來檢測內存泄漏的工具,memleak。memleak 能夠跟蹤系統或指定進程的內存分配、釋放請求,而後按期輸出一個未釋放內存和相應調用棧的彙總狀況(默認 5 秒)。
固然,memleak 是 bcc 軟件包中的一個工具,咱們一開始就裝好了,執行 /usr/share/bcc/tools/memleak 就能夠運行它。好比,咱們運行下面的命令:
# -a 表示顯示每一個內存分配請求的大小以及地址
# -p 指定案例應用的 PID 號
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
WARNING: Couldn't find .text section in /app
WARNING: BCC can't handle sym look ups
for
/app
addr = 7f8f704732b0 size =
8192
addr = 7f8f704772d0 size =
8192
addr = 7f8f704712a0 size =
8192
addr = 7f8f704752c0 size =
8192
32768
bytes in
4
allocations from stack
[unknown] [app]
[unknown] [app]
start_thread+
0xdb
[libpthread-
2.27
.so]
|
從 memleak 的輸出能夠看到,案例應用在不停地分配內存,而且這些分配的地址沒有被回收。
這裏有一個問題,Couldn’t find .text section in /app,因此調用棧不能正常輸出,最後的調用棧部分只能看到 [unknown] 的標誌。
爲何會有這個錯誤呢?實際上,這是因爲案例應用運行在容器中致使的。memleak 工具運行在容器以外,並不能直接訪問進程路徑 /app。
比方說,在終端中直接運行 ls 命令,你會發現,這個路徑的確不存在:
$ ls /app
ls: cannot access
'/app'
: No such file or directory
|
相似的問題,我在 CPU 模塊中的perf 使用方法中已經提到好幾個解決思路。最簡單的方法,就是在容器外部構建相同路徑的文件以及依賴庫。這個案例只有一個二進制文件,因此只要把案例應用的二進制文件放到 /app 路徑中,就能夠修復這個問題。
好比,你能夠運行下面的命令,把 app 二進制文件從容器中複製出來,而後從新運行 memleak 工具:
$ docker cp app:/app /app
$ /usr/share/bcc/tools/memleak -p $(pidof app) -a
Attaching to pid
12512
, Ctrl+C to quit.
[
03
:
00
:
41
] Top
10
stacks with outstanding allocations:
addr = 7f8f70863220 size =
8192
addr = 7f8f70861210 size =
8192
addr = 7f8f7085b1e0 size =
8192
addr = 7f8f7085f200 size =
8192
addr = 7f8f7085d1f0 size =
8192
40960
bytes in
5
allocations from stack
fibonacci+
0x1f
[app]
child+
0x4f
[app]
start_thread+
0xdb
[libpthread-
2.27
.so]
|
這一次,咱們終於看到了內存分配的調用棧,原來是 fibonacci() 函數分配的內存沒釋放。
定位了內存泄漏的來源,下一步天然就應該查看源碼,想辦法修復它。咱們一塊兒來看案例應用的源代碼:
$ docker exec app cat /app.c
...
long
long
*fibonacci(
long
long
*n0,
long
long
*n1)
{
// 分配 1024 個長整數空間方便觀測內存的變化狀況
long
long
*v = (
long
long
*) calloc(
1024
, sizeof(
long
long
));
*v = *n0 + *n1;
return
v;
}
void
*child(
void
*arg)
{
long
long
n0 =
0
;
long
long
n1 =
1
;
long
long
*v = NULL;
for
(
int
n =
2
; n >
0
; n++) {
v = fibonacci(&n0, &n1);
n0 = n1;
n1 = *v;
printf(
"%dth => %lld\n"
, n, *v);
sleep(
1
);
}
}
...
|
你會發現, child() 調用了 fibonacci() 函數,但並無釋放 fibonacci() 返回的內存。因此,想要修復泄漏問題,在 child() 中加一個釋放函數就能夠了,好比:
void
*child(
void
*arg)
{
...
for
(
int
n =
2
; n >
0
; n++) {
v = fibonacci(&n0, &n1);
n0 = n1;
n1 = *v;
printf(
"%dth => %lld\n"
, n, *v);
free(v);
// 釋放內存
sleep(
1
);
}
}
|
修復後的代碼放到了 app-fix.c,也打包成了一個 Docker 鏡像。你能夠運行下面的命令,驗證一下內存泄漏是否修復:
# 清理原來的案例應用
$ docker rm -f app
# 運行修復後的應用
$ docker run --name=app -itd feisky/app:mem-leak-fix
# 從新執行 memleak 工具檢查內存泄漏狀況
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
Attaching to pid
18808
, Ctrl+C to quit.
[
10
:
23
:
18
] Top
10
stacks with outstanding allocations:
[
10
:
23
:
23
] Top
10
stacks with outstanding allocations:
|
如今,咱們看到,案例應用已經沒有遺留內存,證實咱們的修復工做成功完成。
小結:
應用程序能夠訪問的用戶內存空間,由只讀段、數據段、堆、棧以及文件映射段等組成。其中,堆內存和內存映射,須要應用程序來動態管理內存段,因此咱們必須當心處理。不只要會用標準庫函數malloc() 來動態分配內存,還要記得在用完內存後,調用庫函數 _free() 來 _ 釋放它們。
今天的案例比較簡單,只用加一個 free() 調用就能修復內存泄漏。不過,實際應用程序就複雜多了。好比說,
因此,爲了不內存泄漏,最重要的一點就是養成良好的編程習慣,好比分配內存後,必定要先寫好內存釋放的代碼,再去開發其餘邏輯。
機器配置:2 CPU,4GB 內存。
預先安裝 sysstat 包,如 apt install sysstat。
準備環節的最後一步,爲了減小緩存的影響,記得在第一個終端中,運行下面的命令來清理系統緩存:
# 清理文件頁、目錄項、Inodes 等各類緩存
$ echo
3
> /proc/sys/vm/drop_caches
|
這裏的 /proc/sys/vm/drop_caches ,就是經過 proc 文件系統修改內核行爲的一個示例,寫入 3 表示清理文件頁、目錄項、Inodes 等各類緩存。
1.咱們先來模擬第一個場景。首先,在第一個終端,運行下面這個 vmstat 命令:
寫文件:
root
@ubuntu
:/home/xhong# vmstat
1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2
0
792276
760464
3900
178292
7
16
1067
35
203
429
4
2
93
0
0
0
0
792276
760216
3900
178328
0
0
0
0
303
571
2
1
97
0
0
0
0
792276
760216
3900
178328
0
0
0
0
155
342
2
0
98
0
0
|
輸出界面裏, 內存部分的 buff 和 cache ,以及 io 部分的 bi 和 bo 就是咱們要關注的重點。
正常狀況下,空閒系統中,你應該看到的是,這幾個值在屢次結果中一直保持不變。
2.接下來,到第二個終端執行 dd 命令,經過讀取隨機設備,生成一個 500MB 大小的文件:
$ dd
if
=/dev/urandom of=/tmp/file bs=1M count=
500
|
3.而後再回到第一個終端,觀察 Buffer 和 Cache 的變化狀況:
root
@ubuntu
:/home/xhong# vmstat
1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0
0
792276
761200
2560
177500
7
16
1062
41
202
428
4
2
93
0
0
0
0
792276
761200
2560
177520
0
0
0
0
115
304
1
0
99
0
0
0
0
792276
760952
2704
177512
0
0
136
24
161
353
1
1
99
0
0
0
0
792276
761324
2704
177520
0
0
0
0
570
1065
3
3
94
0
0
0
0
792276
760944
2988
177520
4
0
288
0
507
803
4
2
95
0
0
0
0
792276
760952
2992
177556
0
0
4
0
338
789
3
1
96
0
0
0
0
792276
760952
2992
177556
0
0
0
0
174
325
1
0
99
0
0
1
0
792276
611028
3024
326092
0
0
108
65748
680
743
1
45
54
1
0
1
0
792276
447596
3576
489016
0
0
592
159744
1015
1085
4
53
44
0
0
1
0
792276
285868
3580
650696
0
0
4
176128
863
861
1
54
44
1
0
0
0
792276
234968
3588
702384
0
0
12
110592
527
471
2
37
58
4
0
0
0
792276
234968
3588
702384
0
0
0
0
184
340
2
1
98
0
0
0
0
792276
234968
3600
702388
0
0
0
92
200
386
2
0
99
0
0
|
經過觀察 vmstat 的輸出,咱們發現,在 dd 命令運行時, Cache 在不停地增加,而 Buffer 基本保持不變。
再進一步觀察 I/O 的狀況,你會看到,
在 Cache 剛開始增加時,塊設備 I/O 不多,bi 只出現了一次 488 KB/s,bo 則只有一次 4KB。而過一段時間後,纔會出現大量的塊設備寫,好比 bo 變成了 159744。
當 dd 命令結束後,Cache 再也不增加,但塊設備寫還會持續一段時間,而且,屢次 I/O 寫的結果加起來,纔是 dd 要寫的 500M 的數據。
把這個結果,跟咱們剛剛瞭解到的 Cache 的定義作個對比,你可能會有點暈乎。爲何前面文檔上說 Cache 是文件讀的頁緩存,怎麼如今寫文件也有它的份?
這個疑問,咱們暫且先記下來,接着再來看另外一個磁盤寫的案例。兩個案例結束後,咱們再統一進行分析。
不過,對於接下來的案例,必須強調一點:
下面的命令對環境要求很高,須要你的系統配置多塊磁盤,而且磁盤分區 /dev/sdb1 還要處於未使用狀態。若是你只有一塊磁盤,千萬不要嘗試,不然將會對你的磁盤分區形成損壞。
若是你的系統符合標準,就能夠繼續在第二個終端中,運行下面的命令。清理緩存後,向磁盤分區 /dev/sdb1 寫入 2GB 的隨機數據:
寫磁盤:
# 首先清理緩存
$ echo
3
> /proc/sys/vm/drop_caches
# 而後運行 dd 命令向磁盤分區 /dev/sdb1 寫入 2G 數據
$ dd
if
=/dev/urandom of=/dev/sdb1 bs=1M count=
2048
|
而後,再回到終端一,觀察內存和 I/O 的變化狀況:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1
0
0
7584780
153592
97436
0
0
684
0
31
423
1
48
50
2
0
1
0
0
7418580
315384
101668
0
0
0
0
32
144
0
50
50
0
0
1
0
0
7253664
475844
106208
0
0
0
0
20
137
0
50
50
0
0
1
0
0
7093352
631800
110520
0
0
0
0
23
223
0
50
50
0
0
1
1
0
6930056
790520
114980
0
0
0
12804
23
168
0
50
42
9
0
1
0
0
6757204
949240
119396
0
0
0
183804
24
191
0
53
26
21
0
1
1
0
6591516
1107960
123840
0
0
0
77316
22
232
0
52
16
33
0
|
從這裏你會看到,雖然同是寫數據,寫磁盤跟寫文件的現象仍是不一樣的。寫磁盤時(也就是 bo 大於 0 時),Buffer 和 Cache 都在增加,但顯然 Buffer 的增加快得多。
這說明,寫磁盤用到了大量的 Buffer,這跟咱們在文檔中查到的定義是同樣的。
對比兩個案例,咱們發現,寫文件時會用到 Cache 緩存數據,而寫磁盤則會用到 Buffer 來緩存數據。因此,回到剛剛的問題,雖然文檔上只提到,Cache 是文件讀的緩存,但實際上,Cache 也會緩存寫文件時的數據。
瞭解了磁盤和文件寫的狀況,咱們再反過來想,磁盤和文件讀的時候,又是怎樣的呢?
咱們回到第二個終端,運行下面的命令。清理緩存後,從文件 /tmp/file 中,讀取數據寫入空設備:
# 首先清理緩存
$ echo
3
> /proc/sys/vm/drop_caches
# 運行 dd 命令讀取文件數據
$ dd
if
=/tmp/file of=/dev/
null
|
而後,再回到終端一,觀察內存和 I/O 的變化狀況:
root
@ubuntu
:/home/xhong# vmstat
1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0
0
789972
696996
11596
223964
6
15
1029
45
198
418
4
2
93
0
0
2
0
789972
696988
11604
223964
0
0
0
0
175
479
1
1
99
0
0
1
0
789972
696988
11604
223964
0
0
0
0
162
388
1
1
99
0
0
0
0
789972
696988
11604
223964
0
0
0
0
167
460
2
0
98
0
0
1
0
789972
554504
11712
366208
0
0
142504
0
2511
2571
4
21
74
1
0
1
0
789972
323364
11712
597380
0
0
231040
0
4501
5313
10
47
43
1
0
0
0
789972
184452
11712
736464
0
0
138644
0
2585
2771
6
29
61
4
0
0
0
789972
184452
11712
736464
0
0
0
48
111
281
1
1
99
0
0
0
0
789972
184452
11712
736464
0
0
0
0
460
885
2
2
96
0
0
|
觀察 vmstat 的輸出,你會發現讀取文件時(也就是 bi 大於 0 時),Buffer 保持不變,而 Cache 則在不停增加。這跟咱們查到的定義「Cache 是對文件讀的頁緩存」是一致的。
那麼,磁盤讀又是什麼狀況呢?咱們再運行第二個案例來看看。
首先,回到第二個終端,運行下面的命令。清理緩存後,從磁盤分區 /dev/sda1 中讀取數據,寫入空設備:
# 首先清理緩存
$ echo
3
> /proc/sys/vm/drop_caches
# 運行 dd 命令讀取文件
$ dd
if
=/dev/sda1 of=/dev/
null
bs=1M count=
1024
|
而後,再回到終端一,觀察內存和 I/O 的變化狀況:
root
@ubuntu
:/home/xhong# vmstat
1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2
0
789972
753200
4720
178428
6
15
1030
45
198
418
4
2
93
0
0
0
0
789972
752836
4860
178424
16
0
172
0
566
998
3
1
96
1
0
0
0
789972
752836
4860
178420
0
0
0
0
416
773
3
2
95
0
0
1
0
789972
752836
4860
178420
0
0
0
0
155
307
0
1
99
0
0
0
1
789972
576872
178924
178432
0
0
174140
0
1062
1428
4
42
48
6
0
1
0
789972
227564
527604
178460
0
0
348672
48
1179
1656
3
26
47
24
0
2
0
790028
81060
687904
171188
0
72
390144
72
1275
1519
1
37
32
29
0
0
0
790564
78148
694584
171224
0
332
136204
332
571
840
1
14
82
4
0
0
0
790564
78148
694584
171224
0
0
0
0
151
305
1
1
98
0
0
0
0
790564
78148
694584
171224
0
0
0
0
181
382
1
1
99
0
0
0
0
790564
78148
694584
171224
0
0
0
0
166
360
1
0
98
0
0
|
觀察 vmstat 的輸出,你會發現讀磁盤時(也就是 bi 大於 0 時),Buffer 和 Cache 都在增加,但顯然 Buffer 的增加快不少。這說明讀磁盤時,數據緩存到了 Buffer 中。
得出這個結論:讀文件時數據會緩存到 Cache 中,而讀磁盤時數據會緩存到 Buffer 中。
到這裏你應該發現了,雖然文檔提供了對 Buffer 和 Cache 的說明,可是仍不能覆蓋到全部的細節。好比說,今天咱們瞭解到的這兩點:
簡單來講,Buffer 是對磁盤數據的緩存,而 Cache 是文件數據的緩存,它們既會用在讀請求中,也會用在寫請求中
從寫的角度來講,不只能夠優化磁盤和文件的寫入,對應用程序也有好處,應用程序能夠在數據真正落盤前,就返回去作其餘工做。
從讀的角度來講,不只能夠提升那些頻繁訪問數據的讀取速度,也下降了頻繁 I/O 對磁盤的壓力。