十八、Linux效能優化實戰學習筆記- 記憶體漏失了,我該如何定位和處理?

2020-08-11 17:35:23

   

目錄

一、記憶體的分配和回收及存在的泄漏點

1.1 棧上的記憶體分配與回收

1.2 堆上的記憶體分配與回收

1.3 只讀段上記憶體分配與回收

1.4 數據段上記憶體分配與回收

1.5 數據段上記憶體分配與回收

二、記憶體不足帶來的負面影響

三、範例


 

當進程通過 malloc() 申請虛擬記憶體後,系統並不會立即爲其分配實體記憶體,而是在首次存取時,才通過缺頁異常陷入內核中分配記憶體.對應用程式來說,動態記憶體的分配和回收,是既核心又複雜的一個邏輯功能模組。管理記憶體的過程中,也很容易發生各種各樣的「事故」.

一、記憶體的分配和回收及存在的泄漏點

1.1 棧上的記憶體分配與回收

在程式中定義了一個區域性變數,比如一個整數陣列 int data[64] ,就定義了一個可以儲存 64 個整數的記憶體段。由於這是一個區域性變數,它會從記憶體空間的棧中分配記憶體。
棧記憶體由系統自動分配和管理。一旦程式執行超出了這個區域性變數的作用域,棧記憶體就會被系統自動回收,所以不會產生記憶體漏失的問題

1.2 堆上的記憶體分配與回收

用到標準庫函數 malloc()在程式中動態分配記憶體。系統就會從記憶體空間的堆中分配記憶體

什麼時候會用到呢?

定義一個整數陣列 int data[64],這個是事先知道陣列不會超過65個元素,如果事先不知道數據大小,就需要動態開闢記憶體。這部分記憶體是從堆上分配的。

堆記憶體由應用程式自己來分配和管理。除非程式退出,這些堆記憶體並不會被系統自動釋放,而是需要應用程式明確呼叫庫函數 free() 來釋放它們。如果應用程式沒有正確釋放堆記憶體,就會造成記憶體漏失。

1.3 只讀段上記憶體分配與回收

只讀段,包括程式的程式碼和常數,由於是隻讀的,不會再去分配新的記憶體,所以也不會產生記憶體漏失

1.4 數據段上記憶體分配與回收

數據段,包括全域性變數和靜態變數,這些變數在定義時就已經確定了大小,所以也不會產生記憶體漏失

1.5 數據段上記憶體分配與回收

記憶體對映段,包括動態鏈接庫和共用記憶體,其中共用記憶體由程式動態分配和管理。所以,如果程式在分配後忘了回收,就會導致跟堆記憶體類似的泄漏問題。

結合這張圖理解。

 

二、記憶體不足帶來的負面影響

記憶體漏失的危害非常大,這些忘記釋放的記憶體,不僅應用程式自己不能存取,系統也不能把它們再次分配給其他應用。記憶體漏失不斷累積,甚至會耗盡系統記憶體。系統最終可以通過 OOM (Out of Memory)機制 機製殺死進程,但進程在 OOM前,可能已經引發了一連串的反應,導致嚴重的效能問題。

其他需要記憶體的進程,可能無法分配新的記憶體;記憶體不足,又會觸發系統的快取回收以及 SWAP 機制 機製,從而進一步導致 I/O 的效能問題等等

三、範例

用一個計算斐波那契數列的案例,來看看記憶體漏失問題的定位和處理方法

斐波那契數列是一個這樣的數列:0、1、1、2、3、5、8…,也就是除了前兩個數是 0 和1,
其他數都由前面兩數相加得到,
用數學公式來表示就是 F(n)=F(n-1)+F(n-2),(n>=2),F(0)=0, F(1)=1

安裝docker 和bcc軟體 

# 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.l
sudo apt-get update
sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)

 執行案例

$ 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 秒輸出一次
vmstat結果

# 每隔 3 秒輸出一組數據
$ 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
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 0 6601824 97620 1098784 0 0 0 0 62 322 0 0 100 0 0
0 0 0 6601700 97620 1098788 0 0 0 0 57 251 0 0 100 0 0
0 0 0 6601320 97620 1098788 0 0 0 3 52 306 0 0 100 0 0
0 0 0 6601452 97628 1098788 0 0 0 27 63 326 0 0 100 0 0
2 0 0 6601328 97628 1098788 0 0 0 44 52 299 0 0 100 0 0
0 0 0 6601080 97628 1098792 0 0 0 0 56 285 0 0 100 0 0

從輸出中你可以看到,記憶體的 free 列在不停的變化,並且是下降趨勢;而 buffer 和cache 基本保持不變。

這個案例記憶體漏失的比較慢,如果有其他大量分配和回收記憶體的應用,那用 vmstat 就觀察不明顯了。

未使用記憶體在逐漸減小,而 buffer 和 cache 基本不變,這說明,系統中使用的記憶體一直在升高。但這並不能說明有記憶體漏失,因爲應用程式執行中需要的記憶體也可能會增大。比如說,程式中如果用了一個動態增長的陣列來快取計算結果,佔用記憶體自然會增長。
 

檢測記憶體漏失的工具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]

Couldn’t find .text section in /app
所以呼叫棧不能正常輸出,最後的呼叫棧部分只能看到 [unknown] 的標誌
把 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() 函數分配的記憶體沒釋放。

檢視程式碼發現child() 呼叫了 fibonacci() 函數,但並沒有釋放 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;
} v
oid *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() 中加一個釋放函數
 

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);
}
}

修復後執行

# 清理原來的案例應用
$ 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: