linux性能評估-內存案例實戰篇

1.內存泄漏,該如何定位和處理

機器配置: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() 調用就能修復內存泄漏。不過,實際應用程序就複雜多了。好比說,

  • malloc() 和 free() 一般並非成對出現,而是須要你,在每一個異常處理路徑和成功路徑上都釋放內存 。
  • 在多線程程序中,一個線程中分配的內存,可能會在另外一個線程中訪問和釋放。
  • 更復雜的是,在第三方的庫函數中,隱式分配的內存可能須要應用程序顯式釋放。

因此,爲了不內存泄漏,最重要的一點就是養成良好的編程習慣,好比分配內存後,必定要先寫好內存釋放的代碼,再去開發其餘邏輯。

 

 

2.內存中的Buffer 和 Cache 在不一樣場景下的使用狀況

機器配置:2 CPU,4GB 內存。

預先安裝 sysstat 包,如 apt install sysstat。

準備環節的最後一步,爲了減小緩存的影響,記得在第一個終端中,運行下面的命令來清理系統緩存:

# 清理文件頁、目錄項、Inodes 等各類緩存
$ echo 3 > /proc/sys/vm/drop_caches

這裏的 /proc/sys/vm/drop_caches ,就是經過 proc 文件系統修改內核行爲的一個示例,寫入 3 表示清理文件頁、目錄項、Inodes 等各類緩存。

場景 1:磁盤和文件寫案例

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 就是咱們要關注的重點。

  • buff 和 cache 就是咱們前面看到的 Buffers 和 Cache,單位是 KB。
  • bi 和 bo 則分別表示塊設備讀取和寫入的大小,單位爲塊 / 秒。由於 Linux 中塊的大小是 1KB,因此這個單位也就等價於 KB/s。

正常狀況下,空閒系統中,你應該看到的是,這幾個值在屢次結果中一直保持不變。
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 也會緩存寫文件時的數據。

場景 2:磁盤和文件讀案例

瞭解了磁盤和文件寫的狀況,咱們再反過來想,磁盤和文件讀的時候,又是怎樣的呢?
咱們回到第二個終端,運行下面的命令。清理緩存後,從文件 /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 既能夠用做「從文件讀取數據的頁緩存」,也能夠用做「寫文件的頁緩存」。

簡單來講,Buffer 是對磁盤數據的緩存,而 Cache 是文件數據的緩存,它們既會用在讀請求中,也會用在寫請求中
從寫的角度來講,不只能夠優化磁盤和文件的寫入,對應用程序也有好處,應用程序能夠在數據真正落盤前,就返回去作其餘工做。
從讀的角度來講,不只能夠提升那些頻繁訪問數據的讀取速度,也下降了頻繁 I/O 對磁盤的壓力。

相關文章
相關標籤/搜索