国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁 > news >正文

京東商城網(wǎng)官網(wǎng)seo工資待遇 seo工資多少

京東商城網(wǎng)官網(wǎng),seo工資待遇 seo工資多少,做網(wǎng)站主機(jī)電腦,靈動(dòng)網(wǎng)站建設(shè)文章目錄 零、資料一、內(nèi)存映射1.1 TLB1.2 多級(jí)頁表1.3 大頁 二、虛擬內(nèi)存空間分布2.1 用戶空間的段2.2 內(nèi)存分配和回收2.2.1 小對(duì)象2.2.2 釋放 三、查看內(nèi)存使用情況3.1 Buffer 和 Cache3.1.1 proc 文件系統(tǒng)3.1.2 案例3.1.2.1 場(chǎng)景 1:磁盤和文件寫案例3.1.2.2 場(chǎng)景…

文章目錄

  • 零、資料
  • 一、內(nèi)存映射
    • 1.1 TLB
    • 1.2 多級(jí)頁表
    • 1.3 大頁
  • 二、虛擬內(nèi)存空間分布
    • 2.1 用戶空間的段
    • 2.2 內(nèi)存分配和回收
      • 2.2.1 小對(duì)象
      • 2.2.2 釋放
  • 三、查看內(nèi)存使用情況
    • 3.1 Buffer 和 Cache
      • 3.1.1 proc 文件系統(tǒng)
      • 3.1.2 案例
        • 3.1.2.1 場(chǎng)景 1:磁盤和文件寫案例
        • 3.1.2.2 場(chǎng)景 2:磁盤和文件讀案例
        • 3.1.2.3 磁盤和文件的區(qū)別
      • 3.1.4 如何統(tǒng)計(jì)所有進(jìn)程的物理內(nèi)存使用量
    • 3.2 緩存命中率
      • 3.2.1 cachestat、cachetop
      • 3.2.2 pcstat 查看某文件的緩存大小
      • 3.2.3 案例一
      • 3.2.4 案例二
  • 四、內(nèi)存泄露
    • 4.1 內(nèi)存的分配和回收
      • 4.1.1 案例:斐波那契數(shù)列
      • 4.1.2 內(nèi)存回收與 OOM
    • 4.2 Swap
      • 4.2.1 Swap 原理
      • 4.2.2 NUMA 與 Swap
      • 4.2.3 swappiness
      • 4.2.4 swap 升高后如何定位分析
  • 五、定位內(nèi)存問題方法
    • 5.1 內(nèi)存性能指標(biāo)
    • 5.2 內(nèi)存性能工具
    • 5.3 性能指標(biāo)和工具的聯(lián)系
    • 5.4 如何迅速分析內(nèi)存的性能瓶頸
    • 5.5 常見內(nèi)存優(yōu)化方式

零、資料

手把手教你使用程序(C語言)來實(shí)際地去搞清楚虛擬內(nèi)存分布

一、內(nèi)存映射

說到內(nèi)存,你能說出你現(xiàn)在用的這臺(tái)計(jì)算機(jī)內(nèi)存有多大嗎?我估計(jì)你記得很清楚,因?yàn)檫@是我們購(gòu)買時(shí),首先考慮的一個(gè)重要參數(shù),比方說,我的筆記本電腦內(nèi)存就是 8GB 的 。

我們通常所說的內(nèi)存容量,就像我剛剛提到的 8GB,其實(shí)指的是物理內(nèi)存。物理內(nèi)存也稱為主存,大多數(shù)計(jì)算機(jī)用的主存都是動(dòng)態(tài)隨機(jī)訪問內(nèi)存(DRAM)。只有內(nèi)核才可以直接訪問物理內(nèi)存。那么,進(jìn)程要訪問內(nèi)存時(shí),該怎么辦呢?

Linux 內(nèi)核給每個(gè)進(jìn)程都提供了一個(gè)獨(dú)立的虛擬地址空間,并且這個(gè)地址空間是連續(xù)的。這樣,進(jìn)程就可以很方便地訪問內(nèi)存,更確切地說是訪問虛擬內(nèi)存。

虛擬地址空間的內(nèi)部又被分為內(nèi)核空間和用戶空間兩部分,不同字長(zhǎng)(也就是單個(gè) CPU 指令可以處理數(shù)據(jù)的最大長(zhǎng)度)的處理器,地址空間的范圍也不同。比如最常見的 32 位和 64 位系統(tǒng),我畫了兩張圖來分別表示它們的虛擬地址空間,如下所示:

通過這里可以看出,32 位系統(tǒng)的內(nèi)核空間占用 1G,位于最高處,剩下的 3G 是用戶空間。而 64 位系統(tǒng)的內(nèi)核空間和用戶空間都是 128T,分別占據(jù)整個(gè)內(nèi)存空間的最高和最低處,剩下的中間部分是未定義的。

還記得進(jìn)程的用戶態(tài)和內(nèi)核態(tài)嗎?進(jìn)程在用戶態(tài)時(shí),只能訪問用戶空間內(nèi)存;只有進(jìn)入內(nèi)核態(tài)后,才可以訪問內(nèi)核空間內(nèi)存。雖然每個(gè)進(jìn)程的地址空間都包含了內(nèi)核空間,但這些內(nèi)核空間,其實(shí)關(guān)聯(lián)的都是相同的物理內(nèi)存。這樣,進(jìn)程切換到內(nèi)核態(tài)后,就可以很方便地訪問內(nèi)核空間內(nèi)存。

既然每個(gè)進(jìn)程都有一個(gè)這么大的地址空間,那么所有進(jìn)程的虛擬內(nèi)存加起來,自然要比實(shí)際的物理內(nèi)存大得多。所以,并不是所有的虛擬內(nèi)存都會(huì)分配物理內(nèi)存,只有那些實(shí)際使用的虛擬內(nèi)存才分配物理內(nèi)存,并且分配后的物理內(nèi)存,是通過內(nèi)存映射來管理的。

內(nèi)存映射,其實(shí)就是將虛擬內(nèi)存地址映射到物理內(nèi)存地址。為了完成內(nèi)存映射,內(nèi)核為每個(gè)進(jìn)程都維護(hù)了一張頁表,記錄虛擬地址與物理地址的映射關(guān)系,如下圖所示:

頁表實(shí)際上存儲(chǔ)在 CPU 的內(nèi)存管理單元 MMU 中,這樣,正常情況下,處理器就可以直接通過硬件,找出要訪問的內(nèi)存。

而當(dāng)進(jìn)程訪問的虛擬地址在頁表中查不到時(shí),系統(tǒng)會(huì)產(chǎn)生一個(gè)缺頁異常,進(jìn)入內(nèi)核空間分配物理內(nèi)存、更新進(jìn)程頁表,最后再返回用戶空間,恢復(fù)進(jìn)程的運(yùn)行。

1.1 TLB

TLB(Translation Lookaside Buffer,轉(zhuǎn)譯后備緩沖器)會(huì)影響 CPU 的內(nèi)存訪問性能,在這里其實(shí)就可以得到解釋。

TLB 其實(shí)就是 MMU 中頁表的高速緩存。由于進(jìn)程的虛擬地址空間是獨(dú)立的,而 TLB 的訪問速度又比 MMU 快得多,所以,通過減少進(jìn)程的上下文切換,減少 TLB 的刷新次數(shù),就可以提高 TLB 緩存的使用率,進(jìn)而提高 CPU 的內(nèi)存訪問性能。

不過要注意,MMU 并不以字節(jié)為單位來管理內(nèi)存,而是規(guī)定了一個(gè)內(nèi)存映射的最小單位,也就是頁,通常是 4 KB 大小。這樣,每一次內(nèi)存映射,都需要關(guān)聯(lián) 4 KB 或者 4KB 整數(shù)倍的內(nèi)存空間。

頁的大小只有 4 KB ,導(dǎo)致的另一個(gè)問題就是,整個(gè)頁表會(huì)變得非常大。比方說,僅 32 位系統(tǒng)就需要 100 多萬個(gè)頁表項(xiàng)(4GB/4KB),才可以實(shí)現(xiàn)整個(gè)地址空間的映射。為了解決頁表項(xiàng)過多的問題,Linux 提供了兩種機(jī)制,也就是多級(jí)頁表和大頁(HugePage)。

1.2 多級(jí)頁表

多級(jí)頁表就是把內(nèi)存分成區(qū)塊來管理,將原來的映射關(guān)系改成區(qū)塊索引和區(qū)塊內(nèi)的偏移。由于虛擬內(nèi)存空間通常只用了很少一部分,那么,多級(jí)頁表就只保存這些使用中的區(qū)塊,這樣就可以大大地減少頁表的項(xiàng)數(shù)。

Linux 用的正是四級(jí)頁表來管理內(nèi)存頁,如下圖所示,虛擬地址被分為 5 個(gè)部分,前 4 個(gè)表項(xiàng)用于選擇頁,而最后一個(gè)索引表示頁內(nèi)偏移。

1.3 大頁

大頁,顧名思義,就是比普通頁更大的內(nèi)存塊,常見的大小有 2MB 和 1GB。大頁通常用在使用大量?jī)?nèi)存的進(jìn)程上,比如 Oracle、DPDK 等。

二、虛擬內(nèi)存空間分布

通過這些機(jī)制,在頁表的映射下,進(jìn)程就可以通過虛擬地址來訪問物理內(nèi)存了。那么具體到一個(gè) Linux 進(jìn)程中,這些內(nèi)存又是怎么使用的呢?首先,我們需要進(jìn)一步了解虛擬內(nèi)存空間的分布情況。

2.1 用戶空間的段

最上方的內(nèi)核空間不用多講。

下方的用戶空間內(nèi)存,其實(shí)又被分成了多個(gè)不同的段。以 32 位系統(tǒng)為例,我畫了一張圖來表示它們的關(guān)系:通過這張圖你可以看到,用戶空間內(nèi)存,從低到高分別是五種不同的內(nèi)存段。

  • 只讀段,包括代碼和常量等。
  • 數(shù)據(jù)段,包括全局變量等。
  • 堆,包括動(dòng)態(tài)分配的內(nèi)存,從低地址開始向上增長(zhǎng)。
  • 文件映射段,包括動(dòng)態(tài)庫(kù)、共享內(nèi)存等,從高地址開始向下增長(zhǎng)。
  • 棧,包括局部變量和函數(shù)調(diào)用的上下文等。棧的大小是固定的,一般是 8 MB。

在這五個(gè)內(nèi)存段中,堆和文件映射段的內(nèi)存是動(dòng)態(tài)分配的。比如說,使用 C 標(biāo)準(zhǔn)庫(kù)的 malloc() 或者 mmap() ,就可以分別在堆和文件映射段動(dòng)態(tài)分配內(nèi)存。

其實(shí) 64 位系統(tǒng)的內(nèi)存分布也類似,只不過內(nèi)存空間要大得多。那么,更重要的問題來了,內(nèi)存究竟是怎么分配的呢?

2.2 內(nèi)存分配和回收

malloc() 是 C 標(biāo)準(zhǔn)庫(kù)提供的內(nèi)存分配函數(shù),對(duì)應(yīng)到系統(tǒng)調(diào)用上,有兩種實(shí)現(xiàn)方式,即 brk() 和 mmap()。

  • 對(duì)小塊內(nèi)存(小于 128K),C 標(biāo)準(zhǔn)庫(kù)使用 brk() 來分配,也就是通過移動(dòng)堆頂?shù)奈恢脕矸峙鋬?nèi)存。這些內(nèi)存釋放后并不會(huì)立刻歸還系統(tǒng),而是被緩存起來,這樣就可以重復(fù)使用。
  • 而大塊內(nèi)存(大于 128K),則直接使用內(nèi)存映射 mmap() 來分配,也就是在文件映射段找一塊空閑內(nèi)存分配出去。

這兩種方式,自然各有優(yōu)缺點(diǎn)。

  • brk() 方式的緩存,可以減少缺頁異常的發(fā)生,提高內(nèi)存訪問效率。不過,由于這些內(nèi)存沒有歸還系統(tǒng),在內(nèi)存工作繁忙時(shí),頻繁的內(nèi)存分配和釋放會(huì)造成內(nèi)存碎片。
  • 而 mmap() 方式分配的內(nèi)存,會(huì)在釋放時(shí)直接歸還系統(tǒng),所以每次 mmap 都會(huì)發(fā)生缺頁異常。在內(nèi)存工作繁忙時(shí),頻繁的內(nèi)存分配會(huì)導(dǎo)致大量的缺頁異常,使內(nèi)核的管理負(fù)擔(dān)增大。這也是 malloc 只對(duì)大塊內(nèi)存使用 mmap 的原因。

了解這兩種調(diào)用方式后,我們還需要清楚一點(diǎn),那就是,當(dāng)這兩種調(diào)用發(fā)生后,其實(shí)并沒有真正分配內(nèi)存。這些內(nèi)存,都只在首次訪問時(shí)才分配,也就是通過缺頁異常進(jìn)入內(nèi)核中,再由內(nèi)核來分配內(nèi)存。

整體來說,Linux 使用伙伴系統(tǒng)來管理內(nèi)存分配。前面我們提到過,這些內(nèi)存在 MMU 中以頁為單位進(jìn)行管理,伙伴系統(tǒng)也一樣,以頁為單位來管理內(nèi)存,并且會(huì)通過相鄰頁的合并,減少內(nèi)存碎片化(比如 brk 方式造成的內(nèi)存碎片)。

2.2.1 小對(duì)象

你可能會(huì)想到一個(gè)問題,如果遇到比頁更小的對(duì)象,比如不到 1K 的時(shí)候,該怎么分配內(nèi)存呢?

實(shí)際系統(tǒng)運(yùn)行中,確實(shí)有大量比頁還小的對(duì)象,如果為它們也分配單獨(dú)的頁,那就太浪費(fèi)內(nèi)存了。

所以,在用戶空間,malloc 通過 brk() 分配的內(nèi)存,在釋放時(shí)并不立即歸還系統(tǒng),而是緩存起來重復(fù)利用。在內(nèi)核空間,Linux 則通過 slab 分配器來管理小內(nèi)存。你可以把 slab 看成構(gòu)建在伙伴系統(tǒng)上的一個(gè)緩存,主要作用就是分配并釋放內(nèi)核中的小對(duì)象。

2.2.2 釋放

對(duì)內(nèi)存來說,如果只分配而不釋放,就會(huì)造成內(nèi)存泄漏,甚至?xí)谋M系統(tǒng)內(nèi)存。所以,在應(yīng)用程序用完內(nèi)存后,還需要調(diào)用 free() 或 unmap() ,來釋放這些不用的內(nèi)存。

當(dāng)然,系統(tǒng)也不會(huì)任由某個(gè)進(jìn)程用完所有內(nèi)存。在發(fā)現(xiàn)內(nèi)存緊張時(shí),系統(tǒng)就會(huì)通過一系列機(jī)制來回收內(nèi)存,比如下面這三種方式:

  • 回收緩存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的內(nèi)存頁面;
  • 回收不常訪問的內(nèi)存,把不常用的內(nèi)存通過交換分區(qū)直接寫到磁盤中;
  • 殺死進(jìn)程,內(nèi)存緊張時(shí)系統(tǒng)還會(huì)通過 OOM(Out of Memory),直接殺掉占用大量?jī)?nèi)存的進(jìn)程。

其中,第二種方式回收不常訪問的內(nèi)存時(shí),會(huì)用到交換分區(qū)(以下簡(jiǎn)稱 Swap)。Swap 其實(shí)就是把一塊磁盤空間當(dāng)成內(nèi)存來用。它可以把進(jìn)程暫時(shí)不用的數(shù)據(jù)存儲(chǔ)到磁盤中(這個(gè)過程稱為換出),當(dāng)進(jìn)程訪問這些內(nèi)存時(shí),再?gòu)拇疟P讀取這些數(shù)據(jù)到內(nèi)存中(這個(gè)過程稱為換入)。

所以,你可以發(fā)現(xiàn),Swap 把系統(tǒng)的可用內(nèi)存變大了。不過要注意,通常只在內(nèi)存不足時(shí),才會(huì)發(fā)生 Swap 交換。并且由于磁盤讀寫的速度遠(yuǎn)比內(nèi)存慢,Swap 會(huì)導(dǎo)致嚴(yán)重的內(nèi)存性能問題。

第三種方式提到的 OOM(Out of Memory),其實(shí)是內(nèi)核的一種保護(hù)機(jī)制。它監(jiān)控進(jìn)程的內(nèi)存使用情況,并且使用 oom_score 為每個(gè)進(jìn)程的內(nèi)存使用情況進(jìn)行評(píng)分:

  • 一個(gè)進(jìn)程消耗的內(nèi)存越大,oom_score 就越大;
  • 一個(gè)進(jìn)程運(yùn)行占用的 CPU 越多,oom_score 就越小。

這樣,進(jìn)程的 oom_score 越大,代表消耗的內(nèi)存越多,也就越容易被 OOM 殺死,從而可以更好保護(hù)系統(tǒng)。

當(dāng)然,為了實(shí)際工作的需要,管理員可以通過 /proc 文件系統(tǒng),手動(dòng)設(shè)置進(jìn)程的 oom_adj ,從而調(diào)整進(jìn)程的 oom_score。

oom_adj 的范圍是 [-17, 15],數(shù)值越大,表示進(jìn)程越容易被 OOM 殺死;數(shù)值越小,表示進(jìn)程越不容易被 OOM 殺死,其中 -17 表示禁止 OOM。

比如用下面的命令,你就可以把 sshd 進(jìn)程的 oom_adj 調(diào)小為 -16,這樣, sshd 進(jìn)程就不容易被 OOM 殺死。

echo -16 > /proc/$(pidof sshd)/oom_adj

三、查看內(nèi)存使用情況

你可以看到,free 輸出的是一個(gè)表格,其中的數(shù)值都默認(rèn)以字節(jié)為單位。表格總共有兩行六列,這兩行分別是物理內(nèi)存 Mem 和交換分區(qū) Swap 的使用情況,而六列中,每列數(shù)據(jù)的含義分別為:

  • 第一列,total 是總內(nèi)存大小;
  • 第二列,used 是已使用內(nèi)存的大小,包含了共享內(nèi)存;
  • 第三列,free 是未使用內(nèi)存的大小;
  • 第四列,shared 是共享內(nèi)存的大小;
  • 第五列,buff/cache 是緩存和緩沖區(qū)的大小;
  • 最后一列,available 是新進(jìn)程可用內(nèi)存的大小。注意:available 不僅包含未使用內(nèi)存,還包括了可回收的緩存,所以一般會(huì)比未使用內(nèi)存更大。不過,并不是所有緩存都可以回收,因?yàn)橛行┚彺婵赡苷谑褂弥小?/li>
# 注意不同版本的 free 輸出可能會(huì)有所不同
$ freetotal        used        free      shared  buff/cache   available
Mem:        8169348      263524     6875352         668     1030472     7611064
Swap:             0           0           0

如果你想查看進(jìn)程的內(nèi)存使用情況,可以用 top 或者 ps 等工具。比如,下面是 top 的輸出示例:

# 按下 M 切換到內(nèi)存排序
$ top
...
KiB Mem :  8169348 total,  6871440 free,   267096 used,  1030812 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  7607492 avail MemPID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND430 root      19  -1  122360  35588  23748 S   0.0  0.4   0:32.17 systemd-journal1075 root      20   0  771860  22744  11368 S   0.0  0.3   0:38.89 snapd1048 root      20   0  170904  17292   9488 S   0.0  0.2   0:00.24 networkd-dispat1 root      20   0   78020   9156   6644 S   0.0  0.1   0:22.92 systemd
12376 azure     20   0   76632   7456   6420 S   0.0  0.1   0:00.01 systemd
12374 root      20   0  107984   7312   6304 S   0.0  0.1   0:00.00 sshd
...

這些數(shù)據(jù),包含了進(jìn)程最重要的幾個(gè)內(nèi)存使用情況,我們挨個(gè)來看。

  • VIRT 是進(jìn)程虛擬內(nèi)存的大小,只要是進(jìn)程申請(qǐng)過的內(nèi)存,即便還沒有真正分配物理內(nèi)存,也會(huì)計(jì)算在內(nèi)。
  • RES 是常駐內(nèi)存的大小,也就是進(jìn)程實(shí)際使用的物理內(nèi)存大小,但不包括 Swap 和共享內(nèi)存。
  • SHR 是共享內(nèi)存的大小,比如與其他進(jìn)程共同使用的共享內(nèi)存、加載的動(dòng)態(tài)鏈接庫(kù)以及程序的代碼段等。
  • %MEM 是進(jìn)程使用物理內(nèi)存占系統(tǒng)總內(nèi)存的百分比。

除了要認(rèn)識(shí)這些基本信息,在查看 top 輸出時(shí),你還要注意兩點(diǎn)。

  • 第一,虛擬內(nèi)存通常并不會(huì)全部分配物理內(nèi)存。從上面的輸出,你可以發(fā)現(xiàn)每個(gè)進(jìn)程的虛擬內(nèi)存都比常駐內(nèi)存大得多。
  • 第二,共享內(nèi)存 SHR 并不一定是共享的,比方說,程序的代碼段、非共享的動(dòng)態(tài)鏈接庫(kù),也都算在 SHR 里。當(dāng)然,SHR 也包括了進(jìn)程間真正共享的內(nèi)存。所以在計(jì)算多個(gè)進(jìn)程的內(nèi)存使用時(shí),不要把所有進(jìn)程的 SHR 直接相加得出結(jié)果。

linux的內(nèi)存跟windows的很不一樣。類linux的系統(tǒng)會(huì)盡量使用內(nèi)存緩存東西,提供運(yùn)行效率。所以linux/mac顯示的free剩余內(nèi)存通常很小,但實(shí)際上被緩存的cache可能很大,并不代表系統(tǒng)內(nèi)存緊張

3.1 Buffer 和 Cache

man freebuffersMemory used by kernel buffers (Buffers in /proc/meminfo)cache  Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo)buff/cacheSum of buffers and cache

從 free 的手冊(cè)中,你可以看到 buffer 和 cache 的說明。

  • Buffers 是內(nèi)核緩沖區(qū)用到的內(nèi)存,對(duì)應(yīng)的是 /proc/meminfo 中的 Buffers 值。
  • Cache 是內(nèi)核頁緩存和 Slab 用到的內(nèi)存,對(duì)應(yīng)的是 /proc/meminfo 中的 Cached 與 SReclaimable 之和。

這里的說明告訴我們,這些數(shù)值都來自 /proc/meminfo,但更具體的 Buffers、Cached 和 SReclaimable 的含義,還是沒有說清楚。

要弄明白它們到底是什么,我估計(jì)你第一反應(yīng)就是去百度或者 Google 一下。雖然大部分情況下,網(wǎng)絡(luò)搜索能給出一個(gè)答案。但是,且不說篩選信息花費(fèi)的時(shí)間精力,對(duì)你來說,這個(gè)答案的準(zhǔn)確性也是很難保證的。

要注意,網(wǎng)上的結(jié)論可能是對(duì)的,但是很可能跟你的環(huán)境并不匹配。最簡(jiǎn)單來說,同一個(gè)指標(biāo)的具體含義,就可能因?yàn)閮?nèi)核版本、性能工具版本的不同而有挺大差別。這也是為什么,我總在專欄中強(qiáng)調(diào)通用思路和方法,而不是讓你死記結(jié)論。對(duì)于案例實(shí)踐來說,機(jī)器環(huán)境就是我們的最大限制。

那么,有沒有更簡(jiǎn)單、更準(zhǔn)確的方法,來查詢它們的含義呢?

3.1.1 proc 文件系統(tǒng)

我在前面 CPU 性能模塊就曾經(jīng)提到過,/proc 是 Linux 內(nèi)核提供的一種特殊文件系統(tǒng),是用戶跟內(nèi)核交互的接口。比方說,用戶可以從 /proc 中查詢內(nèi)核的運(yùn)行狀態(tài)和配置選項(xiàng),查詢進(jìn)程的運(yùn)行狀態(tài)、統(tǒng)計(jì)數(shù)據(jù)等,當(dāng)然,你也可以通過 /proc 來修改內(nèi)核的配置。

proc 文件系統(tǒng)同時(shí)也是很多性能工具的最終數(shù)據(jù)來源。比如我們剛才看到的 free ,就是通過讀取 /proc/meminfo ,得到內(nèi)存的使用情況。

繼續(xù)說回 /proc/meminfo,既然 Buffers、Cached、SReclaimable 這幾個(gè)指標(biāo)不容易理解,那我們還得繼續(xù)查 proc 文件系統(tǒng),獲取它們的詳細(xì)定義。

執(zhí)行 man proc ,你就可以得到 proc 文件系統(tǒng)的詳細(xì)文檔。

注意這個(gè)文檔比較長(zhǎng),你最好搜索一下(比如搜索 meminfo),以便更快定位到內(nèi)存部分。

Buffers %luRelatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).Cached %luIn-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.
...
SReclaimable %lu (since Linux 2.6.19)Part of Slab, that might be reclaimed, such as caches.SUnreclaim %lu (since Linux 2.6.19)Part of Slab, that cannot be reclaimed on memory pressure.

通過這個(gè)文檔,我們可以看到:

  • Buffers 是對(duì)原始磁盤塊的臨時(shí)存儲(chǔ),也就是用來緩存磁盤的數(shù)據(jù),通常不會(huì)特別大(20MB 左右)。這樣,內(nèi)核就可以把分散的寫集中起來,統(tǒng)一優(yōu)化磁盤的寫入,比如可以把多次小的寫合并成單次大的寫等等。
  • Cached 是從磁盤讀取文件的頁緩存,也就是用來緩存從文件讀取的數(shù)據(jù)。這樣,下次訪問這些文件數(shù)據(jù)時(shí),就可以直接從內(nèi)存中快速獲取,而不需要再次訪問緩慢的磁盤。
  • SReclaimable 是 Slab 的一部分。Slab 包括兩部分,其中的可回收部分,用 SReclaimable 記錄;而不可回收部分,用 SUnreclaim 記錄。

下面實(shí)踐幾個(gè)案例:

3.1.2 案例

首先清理系統(tǒng)緩存

# 清理文件頁、目錄項(xiàng)、Inodes 等各種緩存
$ echo 3 > /proc/sys/vm/drop_caches
3.1.2.1 場(chǎng)景 1:磁盤和文件寫案例

我們先來模擬第一個(gè)場(chǎng)景。首先,在第一個(gè)終端,運(yùn)行下面這個(gè) vmstat 命令:

# 每隔 1 秒輸出 1 組數(shù)據(jù)
$ 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      0 7743608   1112  92168    0    0     0     0   52  152  0  1 100  0  0
0  0      0 7743608   1112  92168    0    0     0     0   36   92  0  0 100  0  0

輸出界面里, 內(nèi)存部分的 buff 和 cache ,以及 io 部分的 bi 和 bo 就是我們要關(guān)注的重點(diǎn)。

  • buff 和 cache 就是我們前面看到的 Buffers 和 Cache,單位是 KB。
  • bi 和 bo 則分別表示塊設(shè)備讀取和寫入的大小,單位為塊 / 秒。因?yàn)?Linux 中塊的大小是 1KB,所以這個(gè)單位也就等價(jià)于 KB/s。

正常情況下,空閑系統(tǒng)中,你應(yīng)該看到的是,這幾個(gè)值在多次結(jié)果中一直保持不變。

接下來,到第二個(gè)終端執(zhí)行 dd 命令,通過讀取隨機(jī)設(shè)備,生成一個(gè) 500MB 大小的文件:

$ dd if=/dev/urandom of=/tmp/file bs=1M count=500

然后再回到第一個(gè)終端,觀察 Buffer 和 Cache 的變化情況:

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 7499460   1344 230484    0    0     0     0   29  145  0  0 100  0  0
1  0      0 7338088   1752 390512    0    0   488     0   39  558  0 47 53  0  0
1  0      0 7158872   1752 568800    0    0     0     4   30  376  1 50 49  0  0
1  0      0 6980308   1752 747860    0    0     0     0   24  360  0 50 50  0  0
0  0      0 6977448   1752 752072    0    0     0     0   29  138  0  0 100  0  0
0  0      0 6977440   1760 752080    0    0     0   152   42  212  0  1 99  1  0
...
0  1      0 6977216   1768 752104    0    0     4 122880   33  234  0  1 51 49  0
0  1      0 6977440   1768 752108    0    0     0 10240   38  196  0  0 50 50  0

通過觀察 vmstat 的輸出,我們發(fā)現(xiàn),在 dd 命令運(yùn)行時(shí), Cache 在不停地增長(zhǎng),而 Buffer 基本保持不變。

再進(jìn)一步觀察 I/O 的情況,你會(huì)看到,

  • 在 Cache 剛開始增長(zhǎng)時(shí),塊設(shè)備 I/O 很少,bi 只出現(xiàn)了一次 488 KB/s,bo 則只有一次 4KB。而過一段時(shí)間后,才會(huì)出現(xiàn)大量的塊設(shè)備寫,比如 bo 變成了 122880。
  • 當(dāng) dd 命令結(jié)束后,Cache 不再增長(zhǎng),但塊設(shè)備寫還會(huì)持續(xù)一段時(shí)間,并且,多次 I/O 寫的結(jié)果加起來,才是 dd 要寫的 500M 的數(shù)據(jù)。

把這個(gè)結(jié)果,跟我們剛剛了解到的 Cache 的定義做個(gè)對(duì)比,你可能會(huì)有點(diǎn)暈乎。為什么前面文檔上說 Cache 是文件讀的頁緩存,怎么現(xiàn)在寫文件也有它的份?

這個(gè)疑問,我們暫且先記下來,接著再來看另一個(gè)磁盤寫的案例。兩個(gè)案例結(jié)束后,我們?cè)俳y(tǒng)一進(jìn)行分析。

不過,對(duì)于接下來的案例,我必須強(qiáng)調(diào)一點(diǎn):

下面的命令對(duì)環(huán)境要求很高,需要你的系統(tǒng)配置多塊磁盤,并且磁盤分區(qū) /dev/sdb1 還要處于未使用狀態(tài)。如果你只有一塊磁盤,千萬不要嘗試,否則將會(huì)對(duì)你的磁盤分區(qū)造成損壞。

如果你的系統(tǒng)符合標(biāo)準(zhǔn),就可以繼續(xù)在第二個(gè)終端中,運(yùn)行下面的命令。清理緩存后,向磁盤分區(qū) /dev/sdb1 寫入 2GB 的隨機(jī)數(shù)據(jù):

# 首先清理緩存
$ echo 3 > /proc/sys/vm/drop_caches
# 然后運(yùn)行 dd 命令向磁盤分區(qū) /dev/sdb1 寫入 2G 數(shù)據(jù)
$ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
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

從這里你會(huì)看到,雖然同是寫數(shù)據(jù),寫磁盤跟寫文件的現(xiàn)象還是不同的。寫磁盤時(shí)(也就是 bo 大于 0 時(shí)),Buffer 和 Cache 都在增長(zhǎng),但顯然 Buffer 的增長(zhǎng)快得多。

這說明,寫磁盤用到了大量的 Buffer,這跟我們?cè)谖臋n中查到的定義是一樣的。

對(duì)比兩個(gè)案例,我們發(fā)現(xiàn),寫文件時(shí)會(huì)用到 Cache 緩存數(shù)據(jù),而寫磁盤則會(huì)用到 Buffer 來緩存數(shù)據(jù)。 所以,回到剛剛的問題,雖然文檔上只提到,Cache 是文件讀的緩存,但實(shí)際上,Cache 也會(huì)緩存寫文件時(shí)的數(shù)據(jù)。

3.1.2.2 場(chǎng)景 2:磁盤和文件讀案例

了解了磁盤和文件寫的情況,我們?cè)俜催^來想,磁盤和文件讀的時(shí)候,又是怎樣的呢?

我們回到第二個(gè)終端,運(yùn)行下面的命令。清理緩存后,從文件 /tmp/file 中,讀取數(shù)據(jù)寫入空設(shè)備:

# 首先清理緩存
$ echo 3 > /proc/sys/vm/drop_caches
# 運(yùn)行 dd 命令讀取文件數(shù)據(jù)
$ dd if=/tmp/file of=/dev/null

然后,再回到終端一,觀察內(nèi)存和 I/O 的變化情況:

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st0  1      0 7724164   2380 110844    0    0 16576     0   62  360  2  2 76 21  00  1      0 7691544   2380 143472    0    0 32640     0   46  439  1  3 50 46  00  1      0 7658736   2380 176204    0    0 32640     0   54  407  1  4 50 46  00  1      0 7626052   2380 208908    0    0 32640    40   44  422  2  2 50 46  0

觀察 vmstat 的輸出,你會(huì)發(fā)現(xiàn)讀取文件時(shí)(也就是 bi 大于 0 時(shí)),Buffer 保持不變,而 Cache 則在不停增長(zhǎng)。這跟我們查到的定義“Cache 是對(duì)文件讀的頁緩存”是一致的。

那么,磁盤讀又是什么情況呢?我們?cè)龠\(yùn)行第二個(gè)案例來看看。

首先,回到第二個(gè)終端,運(yùn)行下面的命令。清理緩存后,從磁盤分區(qū) /dev/sda1 中讀取數(shù)據(jù),寫入空設(shè)備:

# 首先清理緩存
$ echo 3 > /proc/sys/vm/drop_caches
# 運(yùn)行 dd 命令讀取文件
$ dd if=/dev/sda1 of=/dev/null bs=1M count=1024

然后,再回到終端一,觀察內(nèi)存和 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
0  0      0 7225880   2716 608184    0    0     0     0   48  159  0  0 100  0  0
0  1      0 7199420  28644 608228    0    0 25928     0   60  252  0  1 65 35  0
0  1      0 7167092  60900 608312    0    0 32256     0   54  269  0  1 50 49  0
0  1      0 7134416  93572 608376    0    0 32672     0   53  253  0  0 51 49  0
0  1      0 7101484 126320 608480    0    0 32748     0   80  414  0  1 50 49  0

觀察 vmstat 的輸出,你會(huì)發(fā)現(xiàn)讀磁盤時(shí)(也就是 bi 大于 0 時(shí)),Buffer 和 Cache 都在增長(zhǎng),但顯然 Buffer 的增長(zhǎng)快很多。這說明讀磁盤時(shí),數(shù)據(jù)緩存到了 Buffer 中。

當(dāng)然,我想,經(jīng)過上一個(gè)場(chǎng)景中兩個(gè)案例的分析,你自己也可以對(duì)比得出這個(gè)結(jié)論:讀文件時(shí)數(shù)據(jù)會(huì)緩存到 Cache 中,而讀磁盤時(shí)數(shù)據(jù)會(huì)緩存到 Buffer 中。

到這里你應(yīng)該發(fā)現(xiàn)了,雖然文檔提供了對(duì) Buffer 和 Cache 的說明,但是仍不能覆蓋到所有的細(xì)節(jié)。比如說,今天我們了解到的這兩點(diǎn):

  • Buffer 既可以用作“將要寫入磁盤數(shù)據(jù)的緩存”,也可以用作“從磁盤讀取數(shù)據(jù)的緩存”。
  • Cache 既可以用作“從文件讀取數(shù)據(jù)的頁緩存”,也可以用作“寫文件的頁緩存”。

簡(jiǎn)單來說,Buffer 是對(duì)磁盤數(shù)據(jù)的緩存,而 Cache 是文件數(shù)據(jù)的緩存,它們既會(huì)用在讀請(qǐng)求中,也會(huì)用在寫請(qǐng)求中。

Buffer 和 Cache 分別緩存磁盤和文件系統(tǒng)的讀寫數(shù)據(jù)。

  • 從寫的角度來說,不僅可以優(yōu)化磁盤和文件的寫入,對(duì)應(yīng)用程序也有好處,應(yīng)用程序可以在數(shù)據(jù)真正落盤前,就返回去做其他工作。
  • 從讀的角度來說,既可以加速讀取那些需要頻繁訪問的數(shù)據(jù),也降低了頻繁 I/O 對(duì)磁盤的壓力。
3.1.2.3 磁盤和文件的區(qū)別

磁盤是一個(gè)存儲(chǔ)設(shè)備(確切地說是塊設(shè)備),可以被劃分為不同的磁盤分區(qū)。而在磁盤或者磁盤分區(qū)上,還可以再創(chuàng)建文件系統(tǒng),并掛載到系統(tǒng)的某個(gè)目錄中。這樣,系統(tǒng)就可以通過這個(gè)掛載目錄,來讀寫文件。

換句話說,磁盤是存儲(chǔ)數(shù)據(jù)的塊設(shè)備,也是文件系統(tǒng)的載體。所以,文件系統(tǒng)確實(shí)還是要通過磁盤,來保證數(shù)據(jù)的持久化存儲(chǔ)。

你在很多地方都會(huì)看到這句話, Linux 中一切皆文件。換句話說,你可以通過相同的文件接口,來訪問磁盤和文件(比如 open、read、write、close 等)。

  • 我們通常說的“文件”,其實(shí)是指普通文件。
  • 而磁盤或者分區(qū),則是指塊設(shè)備文件。

你可以執(zhí)行 “l(fā)s -l < 路徑 >” 查看它們的區(qū)別。如果不懂 ls 輸出的含義,別忘了 man 一下就可以。執(zhí)行 man ls 命令,以及 info ‘(coreutils) ls invocation’ 命令,就可以查到了。

在讀寫普通文件時(shí),I/O 請(qǐng)求會(huì)首先經(jīng)過文件系統(tǒng),然后由文件系統(tǒng)負(fù)責(zé),來與磁盤進(jìn)行交互。而在讀寫塊設(shè)備文件時(shí),會(huì)跳過文件系統(tǒng),直接與磁盤交互,也就是所謂的“裸 I/O”。

這兩種讀寫方式使用的緩存自然不同。文件系統(tǒng)管理的緩存,其實(shí)就是 Cache 的一部分。而裸磁盤的緩存,用的正是 Buffer。

3.1.4 如何統(tǒng)計(jì)所有進(jìn)程的物理內(nèi)存使用量

PSS 表示常駐內(nèi)存,把進(jìn)程用到的共享內(nèi)存也算了進(jìn)去。所以,直接累加各進(jìn)程的 PSS 會(huì)導(dǎo)致共享內(nèi)存被重復(fù)計(jì)算,不能得到準(zhǔn)確的答案。這個(gè)問題的關(guān)鍵在于理解 PSS 的含義。

詳見 https://unix.stackexchange.com/questions/33381/getting-information-about-a-process-memoryusage-from-proc-pid-smaps

你當(dāng)然可以通過 stackexchange 上的鏈接找到答案,不過,我還是更推薦,直接查 proc 文件系統(tǒng)的文檔:

The “proportional set size” (PSS) of a process is the count of pages it has in memory, where each page is divided by the number of processes sharing it. So if a process has 1000 pages all to itself, and 1000 shared with one other process, its PSS will be 1500.

這里我簡(jiǎn)單解釋一下,每個(gè)進(jìn)程的 PSS ,是指把共享內(nèi)存平分到各個(gè)進(jìn)程后,再加上進(jìn)程本身的非共享內(nèi)存大小的和。

就像文檔中的這個(gè)例子,一個(gè)進(jìn)程的非共享內(nèi)存為 1000 頁,它和另一個(gè)進(jìn)程的共享進(jìn)程也是 1000 頁,那么它的 PSS=1000/2+1000=1500 頁。

這樣,你就可以直接累加 PSS ,不用擔(dān)心共享內(nèi)存重復(fù)計(jì)算的問題了。

比如,你可以運(yùn)行下面的命令來計(jì)算:

# 使用 grep 查找 Pss 指標(biāo)后,再用 awk 計(jì)算累加值
$ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
391266 kB

3.2 緩存命中率

緩存命中率,是指直接通過緩存獲取數(shù)據(jù)的請(qǐng)求次數(shù),占所有數(shù)據(jù)請(qǐng)求次數(shù)的百分比。

命中率越高,表示使用緩存帶來的收益越高,應(yīng)用程序的性能也就越好。

實(shí)際上,緩存是現(xiàn)在所有高并發(fā)系統(tǒng)必需的核心模塊,主要作用就是把經(jīng)常訪問的數(shù)據(jù)(也就是熱點(diǎn)數(shù)據(jù)),提前讀入到內(nèi)存中。這樣,下次訪問時(shí)就可以直接從內(nèi)存讀取數(shù)據(jù),而不需要經(jīng)過硬盤,從而加快應(yīng)用程序的響應(yīng)速度。

3.2.1 cachestat、cachetop

這些獨(dú)立的緩存模塊通常會(huì)提供查詢接口,方便我們隨時(shí)查看緩存的命中情況。不過 Linux 系統(tǒng)中并沒有直接提供這些接口,所以這里我要介紹一下,cachestat 和 cachetop ,它們正是查看系統(tǒng)緩存命中情況的工具。

  • cachestat 提供了整個(gè)操作系統(tǒng)緩存的讀寫命中情況。
  • cachetop 提供了每個(gè)進(jìn)程的緩存命中情況。

這兩個(gè)工具都是 bcc 軟件包的一部分,它們基于 Linux 內(nèi)核的 eBPF(extended Berkeley Packet Filters)機(jī)制,來跟蹤內(nèi)核中管理的緩存,并輸出緩存的使用和命中情況。使用 cachestat 和 cachetop 前,我們首先要安裝 bcc 軟件包。比如,在 Ubuntu 系統(tǒng)中,你可以運(yùn)行下面的命令來安裝:(老版本的Linux 沒有 bcc 的話,可以用 valgrind 做同樣的事,畢竟沒有辦法升級(jí)公司服務(wù)器的內(nèi)核。)

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/xenial xenial 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-tools 需要內(nèi)核版本為 4.1 或者更新的版本,如果你用的是 CentOS,那就需要手動(dòng)[升級(jí)內(nèi)核版本后再安裝](https://github.com/iovisor/bcc/issues/462)。操作完這些步驟,bcc 提供的所有工具就都安裝到 /usr/share/bcc/tools 這個(gè)目錄中了。不過這里提醒你,bcc 軟件包默認(rèn)不會(huì)把這些工具配置到系統(tǒng)的 PATH 路徑中,所以你得自己手動(dòng)配置:
$ export PATH=$PATH:/usr/share/bcc/tools
### entos7 安裝 bcc 的方式如下 ###### 第一步,升級(jí)內(nèi)核。你可以運(yùn)行下面的命令來操作:
# 升級(jí)系統(tǒng)
yum update -y# 安裝 ELRepo
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm# 安裝新內(nèi)核
yum remove -y kernel-headers kernel-tools kernel-tools-libs
yum --enablerepo="elrepo-kernel" install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel# 更新 Grub 后重啟
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default 0
reboot# 重啟后確認(rèn)內(nèi)核版本已升級(jí)為 4.20.0-1.el7.elrepo.x86_64
uname -r### 第二步,安裝 bcc-tools:
# 安裝 bcc-tools
yum install -y bcc-tools# 配置 PATH 路徑
export PATH=$PATH:/usr/share/bcc/tools# 驗(yàn)證安裝成功
cachestat

配置完,你就可以運(yùn)行 cachestat 和 cachetop 命令了。比如,下面就是一個(gè) cachestat 的運(yùn)行界面,它以 1 秒的時(shí)間間隔,輸出了 3 組緩存統(tǒng)計(jì)數(shù)據(jù):

$ cachestat 1 3TOTAL   MISSES     HITS  DIRTIES   BUFFERS_MB  CACHED_MB2        0        2        1           17        2792        0        2        1           17        2792        0        2        1           17        279 

你可以看到,cachestat 的輸出其實(shí)是一個(gè)表格。每行代表一組數(shù)據(jù),而每一列代表不同的緩存統(tǒng)計(jì)指標(biāo)。這些指標(biāo)從左到右依次表示:

  • TOTAL ,表示總的 I/O 次數(shù);
  • MISSES ,表示緩存未命中的次數(shù);
  • HITS ,表示緩存命中的次數(shù);
  • DIRTIES, 表示新增到緩存中的臟頁數(shù);
  • BUFFERS_MB 表示 Buffers 的大小,以 MB 為單位;
  • CACHED_MB 表示 Cache 的大小,以 MB 為單位。

接下來我們?cè)賮砜匆粋€(gè) cachetop 的運(yùn)行界面:

$ cachetop
11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%13029 root     python                  1        0        0     100.0%       0.0%

它的輸出跟 top 類似,默認(rèn)按照緩存的命中次數(shù)(HITS)排序,展示了每個(gè)進(jìn)程的緩存命中情況。具體到每一個(gè)指標(biāo),這里的 HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含義一樣,分別代表間隔時(shí)間內(nèi)的緩存命中次數(shù)、未命中次數(shù)以及新增到緩存中的臟頁數(shù)。

而 READ_HIT 和 WRITE_HIT ,分別表示讀和寫的緩存命中率。

3.2.2 pcstat 查看某文件的緩存大小

除了緩存的命中率外,還有一個(gè)指標(biāo)你可能也會(huì)很感興趣,那就是指定文件在內(nèi)存中的緩存大小。你可以使用 pcstat 這個(gè)工具,來查看文件在內(nèi)存中的緩存大小以及緩存比例。

pcstat 是一個(gè)基于 Go 語言開發(fā)的工具,所以安裝它之前,你首先應(yīng)該安裝 Go 語言,你可以點(diǎn)擊這里下載安裝。安裝完 Go 語言,再運(yùn)行下面的命令安裝 pcstat:

$ export GOPATH=~/go
$ export PATH=~/go/bin:$PATH
$ go get golang.org/x/sys/unix
$ go get github.com/tobert/pcstat/pcstat

全部安裝完成后,你就可以運(yùn)行 pcstat 來查看文件的緩存情況了。比如,下面就是一個(gè) pcstat 運(yùn)行的示例,它展示了 /bin/ls 這個(gè)文件的緩存情況:

$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 0         | 000.000 |
+---------+----------------+------------+-----------+---------+

這個(gè)輸出中,Cached 就是 /bin/ls 在緩存中的大小,而 Percent 則是緩存的百分比。你看到它們都是 0,這說明 /bin/ls 并不在緩存中。

接著,如果你執(zhí)行一下 ls 命令,再運(yùn)行相同的命令來查看的話,就會(huì)發(fā)現(xiàn) /bin/ls 都在緩存中了:

$ ls
$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 33        | 100.000 |
+---------+----------------+------------+-----------+---------+

3.2.3 案例一

第一個(gè)案例,我們先來看一下上一節(jié)提到的 dd 命令。

dd 作為一個(gè)磁盤和文件的拷貝工具,經(jīng)常被拿來測(cè)試磁盤或者文件系統(tǒng)的讀寫性能。不過,既然緩存會(huì)影響到性能,如果用 dd 對(duì)同一個(gè)文件進(jìn)行多次讀取測(cè)試,測(cè)試的結(jié)果會(huì)怎么樣呢?

我們來動(dòng)手試試。首先,打開兩個(gè)終端,連接到 Ubuntu 機(jī)器上,確保 bcc 已經(jīng)安裝配置成功。

然后,使用 dd 命令生成一個(gè)臨時(shí)文件,用于后面的文件讀取測(cè)試:

# 生成一個(gè) 512MB 的臨時(shí)文件
$ dd if=/dev/sda1 of=file bs=1M count=512
# 清理緩存
$ echo 3 > /proc/sys/vm/drop_caches

繼續(xù)在第一個(gè)終端,運(yùn)行 pcstat 命令,確認(rèn)剛剛生成的文件不在緩存中。如果一切正常,你會(huì)看到 Cached 和 Percent 都是 0:

$ pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 0         | 000.000 |
+-------+----------------+------------+-----------+---------+

還是在第一個(gè)終端中,現(xiàn)在運(yùn)行 cachetop 命令:

# 每隔 5 秒刷新一次數(shù)據(jù)
$ cachetop 5

這次是第二個(gè)終端,運(yùn)行 dd 命令測(cè)試文件的讀取速度:

$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s

從 dd 的結(jié)果可以看出,這個(gè)文件的讀性能是 33.4 MB/s。由于在 dd 命令運(yùn)行前我們已經(jīng)清理了緩存,所以 dd 命令讀取數(shù)據(jù)時(shí),肯定要通過文件系統(tǒng)從磁盤中讀取。

不過,這是不是意味著, dd 所有的讀請(qǐng)求都能直接發(fā)送到磁盤呢?

我們?cè)倩氐降谝粋€(gè)終端, 查看 cachetop 界面的緩存命中情況:

PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
\.\.\.3264 root     dd                  37077    37330        0      49.8%      50.2%

從 cachetop 的結(jié)果可以發(fā)現(xiàn),并不是所有的讀都落到了磁盤上,事實(shí)上讀請(qǐng)求的緩存命中率只有 50% 。

接下來,我們繼續(xù)嘗試相同的測(cè)試命令。先切換到第二個(gè)終端,再次執(zhí)行剛才的 dd 命令:

$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.118415 s, 4.5 GB/s

看到這次的結(jié)果,有沒有點(diǎn)小驚訝?磁盤的讀性能居然變成了 4.5 GB/s,比第一次的結(jié)果明顯高了太多。為什么這次的結(jié)果這么好呢?

不妨再回到第一個(gè)終端,看看 cachetop 的情況:

10:45:22 Buffers MB: 4 / Cached MB: 719 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
\.\.\.32642 root     dd                 131637        0        0     100.0%       0.0%

顯然,cachetop 也有了不小的變化。你可以發(fā)現(xiàn),這次的讀的緩存命中率是 100.0%,也就是說這次的 dd 命令全部命中了緩存,所以才會(huì)看到那么高的性能。

然后,回到第二個(gè)終端,再次執(zhí)行 pcstat 查看文件 file 的緩存情況:

$ pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 131072    | 100.000 |
+-------+----------------+------------+-----------+---------+

從 pcstat 的結(jié)果你可以發(fā)現(xiàn),測(cè)試文件 file 已經(jīng)被全部緩存了起來,這跟剛才觀察到的緩存命中率 100% 是一致的。

這兩次結(jié)果說明,系統(tǒng)緩存對(duì)第二次 dd 操作有明顯的加速效果,可以大大提高文件讀取的性能。

但同時(shí)也要注意,如果我們把 dd 當(dāng)成測(cè)試文件系統(tǒng)性能的工具,由于緩存的存在,就會(huì)導(dǎo)致測(cè)試結(jié)果嚴(yán)重失真。

3.2.4 案例二

接下來,我們?cè)賮砜匆粋€(gè)文件讀寫的案例。這個(gè)案例類似于前面學(xué)過的不可中斷狀態(tài)進(jìn)程的例子。它的基本功能比較簡(jiǎn)單,也就是每秒從磁盤分區(qū) /dev/sda1 中讀取 32MB 的數(shù)據(jù),并打印出讀取數(shù)據(jù)花費(fèi)的時(shí)間。

為了方便你運(yùn)行案例,我把它打包成了一個(gè) Docker 鏡像。 跟前面案例類似,我提供了下面兩個(gè)選項(xiàng),你可以根據(jù)系統(tǒng)配置,自行調(diào)整磁盤分區(qū)的路徑以及 I/O 的大小。

  • -d 選項(xiàng),設(shè)置要讀取的磁盤或分區(qū)路徑,默認(rèn)是查找前綴為 /dev/sd 或者 /dev/xvd 的磁盤。
  • -s 選項(xiàng),設(shè)置每次讀取的數(shù)據(jù)量大小,單位為字節(jié),默認(rèn)為 33554432(也就是 32MB)。

這個(gè)案例同樣需要你開啟兩個(gè)終端。分別 SSH 登錄到機(jī)器上后,先在第一個(gè)終端中運(yùn)行 cachetop 命令:

# 每隔 5 秒刷新一次數(shù)據(jù)
$ cachetop 5 

接著,再到第二個(gè)終端,執(zhí)行下面的命令運(yùn)行案例:

$ docker run --privileged --name=app -itd feisky/app:io-direct

案例運(yùn)行后,我們還需要運(yùn)行下面這個(gè)命令,來確認(rèn)案例已經(jīng)正常啟動(dòng)。如果一切正常,你應(yīng)該可以看到類似下面的輸出:

$ docker logs app
Reading data from disk /dev/sdb1 with buffer size 33554432
Time used: 0.929935 s to read 33554432 bytes
Time used: 0.949625 s to read 33554432 bytes

從這里你可以看到,每讀取 32 MB 的數(shù)據(jù),就需要花 0.9 秒。這個(gè)時(shí)間合理嗎?我想你第一反應(yīng)就是,太慢了吧。那這是不是沒用系統(tǒng)緩存導(dǎo)致的呢?

我們?cè)賮頇z查一下?;氐降谝粋€(gè)終端,先看看 cachetop 的輸出,在這里,我們找到案例進(jìn)程 app 的緩存使用情況:

16:39:18 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%21881 root     app                  1024        0        0     100.0%       0.0% 

這個(gè)輸出似乎有點(diǎn)意思了。1024 次緩存全部命中,讀的命中率是 100%,看起來全部的讀請(qǐng)求都經(jīng)過了系統(tǒng)緩存。但是問題又來了,如果真的都是緩存 I/O,讀取速度不應(yīng)該這么慢。

不過,話說回來,我們似乎忽略了另一個(gè)重要因素,每秒實(shí)際讀取的數(shù)據(jù)大小。HITS 代表緩存的命中次數(shù),那么每次命中能讀取多少數(shù)據(jù)呢?自然是一頁。

前面講過,內(nèi)存以頁為單位進(jìn)行管理,而每個(gè)頁的大小是 4KB。所以,在 5 秒的時(shí)間間隔里,命中的緩存為 1024*4K/1024 = 4MB,再除以 5 秒,可以得到每秒讀的緩存是 0.8MB,顯然跟案例應(yīng)用的 32 MB/s 相差太多。

至于為什么只能看到 0.8 MB 的 HITS,我們后面再解釋,這里你先知道怎么根據(jù)結(jié)果來分析就可以了。

這也進(jìn)一步驗(yàn)證了我們的猜想,這個(gè)案例估計(jì)沒有充分利用系統(tǒng)緩存。其實(shí)前面我們遇到過類似的問題,如果為系統(tǒng)調(diào)用設(shè)置直接 I/O 的標(biāo)志,就可以繞過系統(tǒng)緩存。

那么,要判斷應(yīng)用程序是否用了直接 I/O,最簡(jiǎn)單的方法當(dāng)然是觀察它的系統(tǒng)調(diào)用,查找應(yīng)用程序在調(diào)用它們時(shí)的選項(xiàng)。使用什么工具來觀察系統(tǒng)調(diào)用呢?自然還是 strace。

繼續(xù)在終端二中運(yùn)行下面的 strace 命令,觀察案例應(yīng)用的系統(tǒng)調(diào)用情況。注意,這里使用了 pgrep 命令來查找案例進(jìn)程的 PID 號(hào):

# strace -p $(pgrep app)
strace: Process 4988 attached
restart_syscall(<\.\.\. resuming interrupted nanosleep \.\.\.>) = 0
openat(AT_FDCWD, "/dev/sdb1", O_RDONLY|O_DIRECT) = 4
mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f448d240000
read(4, "8vq\213\314\264u\373\4\336K\224\25@\371\1\252\2\262\252q\221\n0\30\225bD\252\266@J"\.\.\., 33554432) = 33554432
write(1, "Time used: 0.948897 s to read 33"\.\.\., 45) = 45
close(4)                                = 0

從 strace 的結(jié)果可以看到,案例應(yīng)用調(diào)用了 openat 來打開磁盤分區(qū) /dev/sdb1,并且傳入的參數(shù)為 O_RDONLY|O_DIRECT(中間的豎線表示或)。

O_RDONLY 表示以只讀方式打開,而 O_DIRECT 則表示以直接讀取的方式打開,這會(huì)繞過系統(tǒng)的緩存。

驗(yàn)證了這一點(diǎn),就很容易理解為什么讀 32 MB 的數(shù)據(jù)就都要那么久了。直接從磁盤讀寫的速度,自然遠(yuǎn)慢于對(duì)緩存的讀寫。這也是緩存存在的最大意義了。

找出問題后,我們還可以在再看看案例應(yīng)用的源碼,再次驗(yàn)證一下:

int flags = O_RDONLY | O_LARGEFILE | O_DIRECT; 
int fd = open(disk, flags, 0755);

上面的代碼,很清楚地告訴我們:它果然用了直接 I/O。

找出了磁盤讀取緩慢的原因,優(yōu)化磁盤讀的性能自然不在話下。修改源代碼,刪除 O_DIRECT 選項(xiàng),讓應(yīng)用程序使用緩存 I/O ,而不是直接 I/O,就可以加速磁盤讀取速度。

app-cached.c 就是修復(fù)后的源碼,我也把它打包成了一個(gè)容器鏡像。在第二個(gè)終端中,按 Ctrl+C 停止剛才的 strace 命令,運(yùn)行下面的命令,你就可以啟動(dòng)它:

# 刪除上述案例應(yīng)用
$ docker rm -f app# 運(yùn)行修復(fù)后的應(yīng)用
$ docker run --privileged --name=app -itd feisky/app:io-cached

還是第二個(gè)終端,再來運(yùn)行下面的命令查看新應(yīng)用的日志,你應(yīng)該能看到下面這個(gè)輸出:

$ docker logs app
Reading data from disk /dev/sdb1 with buffer size 33554432
Time used: 0.037342 s s to read 33554432 bytes
Time used: 0.029676 s to read 33554432 bytes

現(xiàn)在,每次只需要 0.03 秒,就可以讀取 32MB 數(shù)據(jù),明顯比之前的 0.9 秒快多了。所以,這次應(yīng)該用了系統(tǒng)緩存。

我們?cè)倩氐降谝粋€(gè)終端,查看 cachetop 的輸出來確認(rèn)一下:

16:40:08 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%22106 root     app                 40960        0        0     100.0%       0.0%

果然,讀的命中率還是 100%,HITS (即命中數(shù))卻變成了 40960,同樣的方法計(jì)算一下,換算成每秒字節(jié)數(shù)正好是 32 MB(即 40960*4k/5/1024=32M)。

這個(gè)案例說明,在進(jìn)行 I/O 操作時(shí),充分利用系統(tǒng)緩存可以極大地提升性能。 但在觀察緩存命中率時(shí),還要注意結(jié)合應(yīng)用程序?qū)嶋H的 I/O 大小,綜合分析緩存的使用情況。

案例的最后,再回到開始的問題,為什么優(yōu)化前,通過 cachetop 只能看到很少一部分?jǐn)?shù)據(jù)的全部命中,而沒有觀察到大量數(shù)據(jù)的未命中情況呢?這是因?yàn)?#xff0c;cachetop 工具并不把直接 I/O 算進(jìn)來。這也又一次說明了,了解工具原理的重要。

四、內(nèi)存泄露

對(duì)普通進(jìn)程來說,能看到的其實(shí)是內(nèi)核提供的虛擬內(nèi)存,這些虛擬內(nèi)存還需要通過頁表,由系統(tǒng)映射為物理內(nèi)存。

當(dāng)進(jìn)程通過 malloc() 申請(qǐng)?zhí)摂M內(nèi)存后,系統(tǒng)并不會(huì)立即為其分配物理內(nèi)存,而是在首次訪問時(shí),才通過缺頁異常陷入內(nèi)核中分配內(nèi)存。

為了協(xié)調(diào) CPU 與磁盤間的性能差異,Linux 還會(huì)使用 Cache 和 Buffer ,分別把文件和磁盤讀寫的數(shù)據(jù)緩存到內(nèi)存中。

對(duì)應(yīng)用程序來說,動(dòng)態(tài)內(nèi)存的分配和回收,是既核心又復(fù)雜的一個(gè)邏輯功能模塊。管理內(nèi)存的過程中,也很容易發(fā)生各種各樣的“事故”,比如,

  • 沒正確回收分配后的內(nèi)存,導(dǎo)致了泄漏。
  • 訪問的是已分配內(nèi)存邊界外的地址,導(dǎo)致程序異常退出,等等。

今天我就帶你來看看,內(nèi)存泄漏到底是怎么發(fā)生的,以及發(fā)生內(nèi)存泄漏之后該如何排查和定位。

說起內(nèi)存泄漏,這就要先從內(nèi)存的分配和回收說起了。

4.1 內(nèi)存的分配和回收

先回顧一下,你還記得應(yīng)用程序中,都有哪些方法來分配內(nèi)存嗎?用完后,又該怎么釋放還給系統(tǒng)呢?

前面講進(jìn)程的內(nèi)存空間時(shí),我曾經(jīng)提到過,用戶空間內(nèi)存包括多個(gè)不同的內(nèi)存段,比如只讀段、數(shù)據(jù)段、堆、棧以及文件映射段等。這些內(nèi)存段正是應(yīng)用程序使用內(nèi)存的基本方式。

舉個(gè)例子,你在程序中定義了一個(gè)局部變量,比如一個(gè)整數(shù)數(shù)組 int data[64] ,就定義了一個(gè)可以存儲(chǔ) 64 個(gè)整數(shù)的內(nèi)存段。由于這是一個(gè)局部變量,它會(huì)從內(nèi)存空間的棧中分配內(nèi)存。

棧內(nèi)存由系統(tǒng)自動(dòng)分配和管理。一旦程序運(yùn)行超出了這個(gè)局部變量的作用域,棧內(nèi)存就會(huì)被系統(tǒng)自動(dòng)回收,所以不會(huì)產(chǎn)生內(nèi)存泄漏的問題。

再比如,很多時(shí)候,我們事先并不知道數(shù)據(jù)大小,所以你就要用到標(biāo)準(zhǔn)庫(kù)函數(shù) malloc() 在程序中動(dòng)態(tài)分配內(nèi)存。這時(shí)候,系統(tǒng)就會(huì)從內(nèi)存空間的堆中分配內(nèi)存。

堆內(nèi)存由應(yīng)用程序自己來分配和管理。除非程序退出,這些堆內(nèi)存并不會(huì)被系統(tǒng)自動(dòng)釋放,而是需要應(yīng)用程序明確調(diào)用庫(kù)函數(shù) free() 來釋放它們。如果應(yīng)用程序沒有正確釋放堆內(nèi)存,就會(huì)造成內(nèi)存泄漏。

這是兩個(gè)棧和堆的例子,那么,其他內(nèi)存段是否也會(huì)導(dǎo)致內(nèi)存泄漏呢?經(jīng)過我們前面的學(xué)習(xí),這個(gè)問題并不難回答。

  • 只讀段,包括程序的代碼和常量,由于是只讀的,不會(huì)再去分配新的內(nèi)存,所以也不會(huì)產(chǎn)生內(nèi)存泄漏。
  • 數(shù)據(jù)段,包括全局變量和靜態(tài)變量,這些變量在定義時(shí)就已經(jīng)確定了大小,所以也不會(huì)產(chǎn)生內(nèi)存泄漏。
  • 最后一個(gè)內(nèi)存映射段,包括動(dòng)態(tài)鏈接庫(kù)和共享內(nèi)存,其中共享內(nèi)存由程序動(dòng)態(tài)分配和管理。所以,如果程序在分配后忘了回收,就會(huì)導(dǎo)致跟堆內(nèi)存類似的泄漏問題。

內(nèi)存泄漏的危害非常大,這些忘記釋放的內(nèi)存,不僅應(yīng)用程序自己不能訪問,系統(tǒng)也不能把它們?cè)俅畏峙浣o其他應(yīng)用。內(nèi)存泄漏不斷累積,甚至?xí)谋M系統(tǒng)內(nèi)存。

雖然,系統(tǒng)最終可以通過 OOM (Out of Memory)機(jī)制殺死進(jìn)程,但進(jìn)程在 OOM 前,可能已經(jīng)引發(fā)了一連串的反應(yīng),導(dǎo)致嚴(yán)重的性能問題。

比如,其他需要內(nèi)存的進(jìn)程,可能無法分配新的內(nèi)存;內(nèi)存不足,又會(huì)觸發(fā)系統(tǒng)的緩存回收以及 SWAP 機(jī)制,從而進(jìn)一步導(dǎo)致 I/O 的性能問題等等。

4.1.1 案例:斐波那契數(shù)列

接下來,我們就用一個(gè)計(jì)算斐波那契數(shù)列的案例,來看看內(nèi)存泄漏問題的定位和處理方法。

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

今天的案例基于 Ubuntu 18.04,當(dāng)然,同樣適用其他的 Linux 系統(tǒng)。

  • 機(jī)器配置:2 CPU,8GB 內(nèi)存
  • 預(yù)先安裝 sysstat、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.list.d/iovisor.list
sudo apt-get update
sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)上面步驟安裝完后,它提供的所有工具都位于 /usr/share/bcc/tools 這個(gè)目錄中
$ docker run --name=app -itd feisky/app:mem-leak案例成功運(yùn)行后,你需要輸入下面的命令,確認(rèn)案例應(yīng)用已經(jīng)正常啟動(dòng)。如果一切正常,你應(yīng)該可以看到下面這個(gè)界面:
docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13

在這里插入圖片描述

從輸出中,我們可以發(fā)現(xiàn),這個(gè)案例會(huì)輸出斐波那契數(shù)列的一系列數(shù)值。實(shí)際上,這些數(shù)值每隔 1 秒輸出一次。

知道了這些,我們應(yīng)該怎么檢查內(nèi)存情況,判斷有沒有泄漏發(fā)生呢?你首先想到的可能是 top 工具,不過,top 雖然能觀察系統(tǒng)和進(jìn)程的內(nèi)存占用情況,但今天的案例并不適合。內(nèi)存泄漏問題,我們更應(yīng)該關(guān)注內(nèi)存使用的變化趨勢(shì)。

所以,開頭我也提到了,今天推薦的是另一個(gè)老熟人, vmstat 工具。

# 每隔 3 秒輸出一組數(shù)據(jù)
$ 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      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 

從輸出中你可以看到,內(nèi)存的 free 列在不停的變化,并且是下降趨勢(shì);而 buffer 和 cache 基本保持不變。

未使用內(nèi)存在逐漸減小,而 buffer 和 cache 基本不變,這說明,系統(tǒng)中使用的內(nèi)存一直在升高。但這并不能說明有內(nèi)存泄漏,因?yàn)閼?yīng)用程序運(yùn)行中需要的內(nèi)存也可能會(huì)增大。比如說,程序中如果用了一個(gè)動(dòng)態(tài)增長(zhǎng)的數(shù)組來緩存計(jì)算結(jié)果,占用內(nèi)存自然會(huì)增長(zhǎng)。

那怎么確定是不是內(nèi)存泄漏呢?或者換句話說,有沒有簡(jiǎn)單方法找出讓內(nèi)存增長(zhǎng)的進(jìn)程,并定位增長(zhǎng)內(nèi)存用在哪兒呢?

根據(jù)前面內(nèi)容,你應(yīng)該想到了用 top 或 ps 來觀察進(jìn)程的內(nèi)存使用情況,然后找出內(nèi)存使用一直增長(zhǎng)的進(jìn)程,最后再通過 pmap 查看進(jìn)程的內(nèi)存分布。

但這種方法并不太好用,因?yàn)橐袛鄡?nèi)存的變化情況,還需要你寫一個(gè)腳本,來處理 top 或者 ps 的輸出。

這里,我介紹一個(gè)專門用來檢測(cè)內(nèi)存泄漏的工具,memleak。memleak 可以跟蹤系統(tǒng)或指定進(jìn)程的內(nèi)存分配、釋放請(qǐng)求,然后定期輸出一個(gè)未釋放內(nèi)存和相應(yīng)調(diào)用棧的匯總情況(默認(rèn) 5 秒)。

當(dāng)然,memleak 是 bcc 軟件包中的一個(gè)工具,我們一開始就裝好了,執(zhí)行 /usr/share/bcc/tools/memleak 就可以運(yùn)行它。比如,我們運(yùn)行下面的命令:

# -a 表示顯示每個(gè)內(nèi)存分配請(qǐng)求的大小以及地址
# -p 指定案例應(yīng)用的 PID 號(hào)
$ /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 /appaddr = 7f8f704732b0 size = 8192addr = 7f8f704772d0 size = 8192addr = 7f8f704712a0 size = 8192addr = 7f8f704752c0 size = 819232768 bytes in 4 allocations from stack[unknown] [app][unknown] [app]start_thread+0xdb [libpthread-2.27.so] 

從 memleak 的輸出可以看到,案例應(yīng)用在不停地分配內(nèi)存,并且這些分配的地址沒有被回收。

這里有一個(gè)問題,Couldn’t find .text section in /app,所以調(diào)用棧不能正常輸出,最后的調(diào)用棧部分只能看到 [unknown] 的標(biāo)志。

為什么會(huì)有這個(gè)錯(cuò)誤呢?實(shí)際上,這是由于案例應(yīng)用運(yùn)行在容器中導(dǎo)致的。memleak 工具運(yùn)行在容器之外,并不能直接訪問進(jìn)程路徑 /app。

比方說,在終端中直接運(yùn)行 ls 命令,你會(huì)發(fā)現(xiàn),這個(gè)路徑的確不存在:

$ ls /app
ls: cannot access '/app': No such file or directory

類似的問題,我在 CPU 模塊中的 perf 使用方法 中已經(jīng)提到好幾個(gè)解決思路。最簡(jiǎn)單的方法,就是在容器外部構(gòu)建相同路徑的文件以及依賴庫(kù)。這個(gè)案例只有一個(gè)二進(jìn)制文件,所以只要把案例應(yīng)用的二進(jìn)制文件放到 /app 路徑中,就可以修復(fù)這個(gè)問題。

比如,你可以運(yùn)行下面的命令,把 app 二進(jìn)制文件從容器中復(fù)制出來,然后重新運(yùn)行 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 = 8192addr = 7f8f70861210 size = 8192addr = 7f8f7085b1e0 size = 8192addr = 7f8f7085f200 size = 8192addr = 7f8f7085d1f0 size = 819240960 bytes in 5 allocations from stackfibonacci+0x1f [app]child+0x4f [app]start_thread+0xdb [libpthread-2.27.so] 

這一次,我們終于看到了內(nèi)存分配的調(diào)用棧,原來是 fibonacci() 函數(shù)分配的內(nèi)存沒釋放。

定位了內(nèi)存泄漏的來源,下一步自然就應(yīng)該查看源碼,想辦法修復(fù)它。我們一起來看案例應(yīng)用的源代碼 app.c:

$ docker exec app cat /app.c
...
long long *fibonacci(long long *n0, long long *n1)
{// 分配 1024 個(gè)長(zhǎng)整數(shù)空間方便觀測(cè)內(nèi)存的變化情況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);}
}
... 

你會(huì)發(fā)現(xiàn), child() 調(diào)用了 fibonacci() 函數(shù),但并沒有釋放 fibonacci() 返回的內(nèi)存。所以,想要修復(fù)泄漏問題,在 child() 中加一個(gè)釋放函數(shù)就可以了,比如:

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);    // 釋放內(nèi)存sleep(1);}
} 

我把修復(fù)后的代碼放到了 app-fix.c,也打包成了一個(gè) Docker 鏡像。你可以運(yùn)行下面的命令,驗(yàn)證一下內(nèi)存泄漏是否修復(fù):

# 清理原來的案例應(yīng)用
$ docker rm -f app# 運(yùn)行修復(fù)后的應(yīng)用
$ docker run --name=app -itd feisky/app:mem-leak-fix# 重新執(zhí)行 memleak 工具檢查內(nèi)存泄漏情況
$ /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:

今天的案例比較簡(jiǎn)單,只用加一個(gè) free() 調(diào)用就能修復(fù)內(nèi)存泄漏。不過,實(shí)際應(yīng)用程序就復(fù)雜多了。比如說,

  • malloc() 和 free() 通常并不是成對(duì)出現(xiàn),而是需要你,在每個(gè)異常處理路徑和成功路徑上都釋放內(nèi)存 。
  • 在多線程程序中,一個(gè)線程中分配的內(nèi)存,可能會(huì)在另一個(gè)線程中訪問和釋放。
  • 更復(fù)雜的是,在第三方的庫(kù)函數(shù)中,隱式分配的內(nèi)存可能需要應(yīng)用程序顯式釋放。

4.1.2 內(nèi)存回收與 OOM

怎么理解 LRU 內(nèi)存回收?

回收后的內(nèi)存又到哪里去了?

OOM 是按照虛擬內(nèi)存還是實(shí)際內(nèi)存來打分?

怎么估計(jì)應(yīng)用程序的最小內(nèi)存?

其實(shí)在 Linux 內(nèi)存的原理篇和 Swap 原理篇中我曾經(jīng)講到,一旦發(fā)現(xiàn)內(nèi)存緊張,系統(tǒng)會(huì)通過三種方式回收內(nèi)存。我們來復(fù)習(xí)一下,這三種方式分別是 :

  • 基于 LRU(Least Recently Used)算法,回收緩存;
  • 基于 Swap 機(jī)制,回收不常訪問的匿名頁;
  • 基于 OOM(Out of Memory)機(jī)制,殺掉占用大量?jī)?nèi)存的進(jìn)程。

前兩種方式,緩存回收和 Swap 回收,實(shí)際上都是基于 LRU 算法,也就是優(yōu)先回收不常訪問的內(nèi)存。LRU 回收算法,實(shí)際上維護(hù)著 active 和 inactive 兩個(gè)雙向鏈表,其中:

  • active 記錄活躍的內(nèi)存頁;
  • inactive 記錄非活躍的內(nèi)存頁。

越接近鏈表尾部,就表示內(nèi)存頁越不常訪問。這樣,在回收內(nèi)存時(shí),系統(tǒng)就可以根據(jù)活躍程度,優(yōu)先回收不活躍的內(nèi)存。

活躍和非活躍的內(nèi)存頁,按照類型的不同,又分別分為文件頁和匿名頁,對(duì)應(yīng)著緩存回收和 Swap 回收。

當(dāng)然,你可以從 /proc/meminfo 中,查詢它們的大小,比如:

# grep 表示只保留包含 active 的指標(biāo)(忽略大小寫)
# sort 表示按照字母順序排序
$ cat /proc/meminfo | grep -i active | sort
Active(anon):     167976 kB
Active(file):     971488 kB
Active:          1139464 kB
Inactive(anon):      720 kB
Inactive(file):  2109536 kB
Inactive:        2110256 kB

第三種方式,OOM 機(jī)制按照 oom_score 給進(jìn)程排序。oom_score 越大,進(jìn)程就越容易被系統(tǒng)殺死。

當(dāng)系統(tǒng)發(fā)現(xiàn)內(nèi)存不足以分配新的內(nèi)存請(qǐng)求時(shí),就會(huì)嘗試 「直接內(nèi)存回收」。這種情況下,如果回收完文件頁和匿名頁后,內(nèi)存夠用了,當(dāng)然皆大歡喜,把回收回來的內(nèi)存分配給進(jìn)程就可以了。但如果內(nèi)存還是不足,OOM 就要登場(chǎng)了。

OOM 發(fā)生時(shí),你可以在 dmesg 中看到 Out of memory 的信息,從而知道是哪些進(jìn)程被 OOM 殺死了。比如,你可以執(zhí)行下面的命令,查詢 OOM 日志:

$ dmesg | grep -i "Out of memory"
Out of memory: Kill process 9329 (java) score 321 or sacrifice child

當(dāng)然了,如果你不希望應(yīng)用程序被 OOM 殺死,可以調(diào)整進(jìn)程的 oom_score_adj,減小 OOM 分值,進(jìn)而降低被殺死的概率?;蛘?#xff0c;你還可以開啟內(nèi)存的 overcommit,允許進(jìn)程申請(qǐng)超過物理內(nèi)存的虛擬內(nèi)存(這兒實(shí)際上假設(shè)的是,進(jìn)程不會(huì)用光申請(qǐng)到的虛擬內(nèi)存)。

這三種方式,我們就復(fù)習(xí)完了。接下來,我們回到開始的四個(gè)問題,相信你自己已經(jīng)有了答案。

  • LRU 算法的原理剛才已經(jīng)提到了,這里不再重復(fù)。
  • 內(nèi)存回收后,會(huì)被重新放到未使用內(nèi)存中。這樣,新的進(jìn)程就可以請(qǐng)求、使用它們。
  • OOM 觸發(fā)的時(shí)機(jī)基于虛擬內(nèi)存。換句話說,進(jìn)程在申請(qǐng)內(nèi)存時(shí),如果申請(qǐng)的虛擬內(nèi)存加上服務(wù)器實(shí)際已用的內(nèi)存之和,比總的物理內(nèi)存還大,就會(huì)觸發(fā) OOM。
  • 要確定一個(gè)進(jìn)程或者容器的最小內(nèi)存,最簡(jiǎn)單的方法就是讓它運(yùn)行起來,再通過 ps 或者 smap ,查看它的內(nèi)存使用情況。不過要注意,進(jìn)程剛啟動(dòng)時(shí),可能還沒開始處理實(shí)際業(yè)務(wù),一旦開始處理實(shí)際業(yè)務(wù),就會(huì)占用更多內(nèi)存。所以,要記得給內(nèi)存留一定的余量。

4.2 Swap

當(dāng)發(fā)生了內(nèi)存泄漏時(shí),或者運(yùn)行了大內(nèi)存的應(yīng)用程序,導(dǎo)致系統(tǒng)的內(nèi)存資源緊張時(shí),系統(tǒng)又會(huì)如何應(yīng)對(duì)呢?

在內(nèi)存基礎(chǔ)篇我們已經(jīng)學(xué)過,這其實(shí)會(huì)導(dǎo)致兩種可能結(jié)果,內(nèi)存回收和 OOM 殺死進(jìn)程。

我們先來看后一個(gè)可能結(jié)果,內(nèi)存資源緊張導(dǎo)致的 OOM(Out Of Memory),相對(duì)容易理解,指的是系統(tǒng)殺死占用大量?jī)?nèi)存的進(jìn)程,釋放這些內(nèi)存,再分配給其他更需要的進(jìn)程。

這一點(diǎn)我們前面詳細(xì)講過,這里就不再重復(fù)了。

接下來再看第一個(gè)可能的結(jié)果,內(nèi)存回收,也就是系統(tǒng)釋放掉可以回收的內(nèi)存,比如我前面講過的緩存和緩沖區(qū),就屬于可回收內(nèi)存。它們?cè)趦?nèi)存管理中,通常被叫做文件頁(File-backed Page)。

大部分文件頁,都可以直接回收,以后有需要時(shí),再?gòu)拇疟P重新讀取就可以了。而那些被應(yīng)用程序修改過,并且暫時(shí)還沒寫入磁盤的數(shù)據(jù)(也就是臟頁),就得先寫入磁盤,然后才能進(jìn)行內(nèi)存釋放。這些臟頁,一般可以通過兩種方式寫入磁盤。

  • 可以在應(yīng)用程序中,通過系統(tǒng)調(diào)用 fsync ,把臟頁同步到磁盤中;
  • 也可以交給系統(tǒng),由內(nèi)核線程 pdflush 負(fù)責(zé)這些臟頁的刷新。

除了緩存和緩沖區(qū),通過內(nèi)存映射獲取的文件映射頁,也是一種常見的文件頁。它也可以被釋放掉,下次再訪問的時(shí)候,從文件重新讀取。

除了文件頁外,還有沒有其他的內(nèi)存可以回收呢?比如,應(yīng)用程序動(dòng)態(tài)分配的堆內(nèi)存,也就是我們?cè)趦?nèi)存管理中說到的匿名頁(Anonymous Page),是不是也可以回收呢?

我想,你肯定會(huì)說,它們很可能還要再次被訪問啊,當(dāng)然不能直接回收了。非常正確,這些內(nèi)存自然不能直接釋放。

但是,如果這些內(nèi)存在分配后很少被訪問,似乎也是一種資源浪費(fèi)。是不是可以把它們暫時(shí)先存在磁盤里,釋放內(nèi)存給其他更需要的進(jìn)程?

其實(shí),這正是 Linux 的 Swap 機(jī)制。Swap 把這些不常訪問的內(nèi)存先寫到磁盤中,然后釋放這些內(nèi)存,給其他更需要的進(jìn)程使用。再次訪問這些內(nèi)存時(shí),重新從磁盤讀入內(nèi)存就可以了。

4.2.1 Swap 原理

前面提到,Swap 說白了就是把一塊磁盤空間或者一個(gè)本地文件(以下講解以磁盤為例),當(dāng)成內(nèi)存來使用。它包括換出和換入兩個(gè)過程。

  • 所謂換出,就是把進(jìn)程暫時(shí)不用的內(nèi)存數(shù)據(jù)存儲(chǔ)到磁盤中,并釋放這些數(shù)據(jù)占用的內(nèi)存。
  • 而換入,則是在進(jìn)程再次訪問這些內(nèi)存的時(shí)候,把它們從磁盤讀到內(nèi)存中來。

所以你看,Swap 其實(shí)是把系統(tǒng)的可用內(nèi)存變大了。這樣,即使服務(wù)器的內(nèi)存不足,也可以運(yùn)行大內(nèi)存的應(yīng)用程序。

還記得我最早學(xué)習(xí) Linux 操作系統(tǒng)時(shí),內(nèi)存實(shí)在太貴了,一個(gè)普通學(xué)生根本就用不起大的內(nèi)存,那會(huì)兒我就是開啟了 Swap 來運(yùn)行 Linux 桌面。當(dāng)然,現(xiàn)在的內(nèi)存便宜多了,服務(wù)器一般也會(huì)配置很大的內(nèi)存,那是不是說 Swap 就沒有用武之地了呢?

當(dāng)然不是。事實(shí)上,內(nèi)存再大,對(duì)應(yīng)用程序來說,也有不夠用的時(shí)候。

一個(gè)很典型的場(chǎng)景就是,即使內(nèi)存不足時(shí),有些應(yīng)用程序也并不想被 OOM 殺死,而是希望能緩一段時(shí)間,等待人工介入,或者等系統(tǒng)自動(dòng)釋放其他進(jìn)程的內(nèi)存,再分配給它。

除此之外,我們常見的筆記本電腦的休眠和快速開機(jī)的功能,也基于 Swap 。休眠時(shí),把系統(tǒng)的內(nèi)存存入磁盤,這樣等到再次開機(jī)時(shí),只要從磁盤中加載內(nèi)存就可以。這樣就省去了很多應(yīng)用程序的初始化過程,加快了開機(jī)速度。

話說回來,既然 Swap 是為了回收內(nèi)存,那么 Linux 到底在什么時(shí)候需要回收內(nèi)存呢?前面一直在說內(nèi)存資源緊張,又該怎么來衡量?jī)?nèi)存是不是緊張呢?

一個(gè)最容易想到的場(chǎng)景就是,有新的大塊內(nèi)存分配請(qǐng)求,但是剩余內(nèi)存不足。這個(gè)時(shí)候系統(tǒng)就需要回收一部分內(nèi)存(比如前面提到的緩存),進(jìn)而盡可能地滿足新內(nèi)存請(qǐng)求。這個(gè)過程通常被稱為直接內(nèi)存回收。

除了直接內(nèi)存回收,還有一個(gè)專門的內(nèi)核線程用來定期回收內(nèi)存,也就是kswapd0。為了衡量?jī)?nèi)存的使用情況,kswapd0 定義了三個(gè)內(nèi)存閾值(watermark,也稱為水位),分別是頁最小閾值(pages_min)、頁低閾值(pages_low)和頁高閾值(pages_high)。剩余內(nèi)存,則使用 pages_free 表示。其關(guān)系如下圖:

kswapd0 定期掃描內(nèi)存的使用情況,并根據(jù)剩余內(nèi)存落在這三個(gè)閾值的空間位置,進(jìn)行內(nèi)存的回收操作。

  • 剩余內(nèi)存小于頁最小閾值,說明進(jìn)程可用內(nèi)存都耗盡了,只有內(nèi)核才可以分配內(nèi)存。
  • 剩余內(nèi)存落在頁最小閾值和頁低閾值中間,說明內(nèi)存壓力比較大,剩余內(nèi)存不多了。這時(shí) kswapd0 會(huì)執(zhí)行內(nèi)存回收,直到剩余內(nèi)存大于高閾值為止。
  • 剩余內(nèi)存落在頁低閾值和頁高閾值中間,說明內(nèi)存有一定壓力,但還可以滿足新內(nèi)存請(qǐng)求。
  • 剩余內(nèi)存大于頁高閾值,說明剩余內(nèi)存比較多,沒有內(nèi)存壓力。

我們可以看到,一旦剩余內(nèi)存小于頁低閾值,就會(huì)觸發(fā)內(nèi)存的回收。這個(gè)頁低閾值,其實(shí)可以通過內(nèi)核選項(xiàng) /proc/sys/vm/min_free_kbytes 來間接設(shè)置。min_free_kbytes 設(shè)置了頁最小閾值,而其他兩個(gè)閾值,都是根據(jù)頁最小閾值計(jì)算生成的,計(jì)算方法如下 :

pages_low = pages_min*5/4
pages_high = pages_min*3/2

4.2.2 NUMA 與 Swap

很多情況下,你明明發(fā)現(xiàn)了 Swap 升高,可是在分析系統(tǒng)的內(nèi)存使用時(shí),卻很可能發(fā)現(xiàn),系統(tǒng)剩余內(nèi)存還多著呢。為什么剩余內(nèi)存很多的情況下,也會(huì)發(fā)生 Swap 呢?這正是處理器的 NUMA (Non-Uniform Memory Access)架構(gòu)導(dǎo)致的。

關(guān)于 NUMA,我在 CPU 模塊中曾簡(jiǎn)單提到過。在 NUMA 架構(gòu)下,多個(gè)處理器被劃分到不同 Node 上,且每個(gè) Node 都擁有自己的本地內(nèi)存空間。

而同一個(gè) Node 內(nèi)部的內(nèi)存空間,實(shí)際上又可以進(jìn)一步分為不同的內(nèi)存域(Zone),比如直接內(nèi)存訪問區(qū)(DMA)、普通內(nèi)存區(qū)(NORMAL)、偽內(nèi)存區(qū)(MOVABLE)等,如下圖所示:先不用特別關(guān)注這些內(nèi)存域的具體含義,我們只要會(huì)查看閾值的配置,以及緩存、匿名頁的實(shí)際使用情況就夠了。

既然 NUMA 架構(gòu)下的每個(gè) Node 都有自己的本地內(nèi)存空間,那么,在分析內(nèi)存的使用時(shí),我們也應(yīng)該針對(duì)每個(gè) Node 單獨(dú)分析。

你可以通過 numactl 命令,來查看處理器在 Node 的分布情況,以及每個(gè) Node 的內(nèi)存使用情況。比如,下面就是一個(gè) numactl 輸出的示例:

$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...

這個(gè)界面顯示,我的系統(tǒng)中只有一個(gè) Node,也就是 Node 0 ,而且編號(hào)為 0 和 1 的兩個(gè) CPU, 都位于 Node 0 上。另外,Node 0 的內(nèi)存大小為 7977 MB,剩余內(nèi)存為 4416 MB。

了解了 NUNA 的架構(gòu)和 NUMA 內(nèi)存的查看方法后,你可能就要問了這跟 Swap 有什么關(guān)系呢?

實(shí)際上,前面提到的三個(gè)內(nèi)存閾值(頁最小閾值、頁低閾值和頁高閾值),都可以通過內(nèi)存域在 proc 文件系統(tǒng)中的接口 /proc/zoneinfo 來查看。

比如,下面就是一個(gè) /proc/zoneinfo 文件的內(nèi)容示例:

$ cat /proc/zoneinfo
...
Node 0, zone   Normalpages free     227894min      14896low      18620high     22344
...nr_free_pages 227894nr_zone_inactive_anon 11082nr_zone_active_anon 14024nr_zone_inactive_file 539024nr_zone_active_file 923986
...

這個(gè)輸出中有大量指標(biāo),我來解釋一下比較重要的幾個(gè)。

  • pages 處的 min、low、high,就是上面提到的三個(gè)內(nèi)存閾值,而 free 是剩余內(nèi)存頁數(shù),它跟后面的 nr_free_pages 相同。
  • nr_zone_active_anon 和 nr_zone_inactive_anon,分別是活躍和非活躍的匿名頁數(shù)。
  • nr_zone_active_file 和 nr_zone_inactive_file,分別是活躍和非活躍的文件頁數(shù)。

從這個(gè)輸出結(jié)果可以發(fā)現(xiàn),剩余內(nèi)存(free)遠(yuǎn)大于頁高閾值(low),所以此時(shí)的 kswapd0 不會(huì)回收內(nèi)存。

當(dāng)然,某個(gè) Node 內(nèi)存不足時(shí),系統(tǒng)可以從其他 Node 尋找空閑內(nèi)存,也可以從本地內(nèi)存中回收內(nèi)存。具體選哪種模式,你可以通過 /proc/sys/vm/zone_reclaim_mode 來調(diào)整。它支持以下幾個(gè)選項(xiàng):

  • 默認(rèn)的 0 ,也就是剛剛提到的模式,表示既可以從其他 Node 尋找空閑內(nèi)存,也可以從本地回收內(nèi)存。
  • 1、2、4 都表示只回收本地內(nèi)存,2 表示可以回寫臟數(shù)據(jù)回收內(nèi)存,4 表示可以用 Swap 方式回收內(nèi)存。

4.2.3 swappiness

到這里,我們就可以理解內(nèi)存回收的機(jī)制了。這些回收的內(nèi)存既包括了文件頁,又包括了匿名頁。

  • 對(duì)文件頁的回收,當(dāng)然就是直接回收緩存,或者把臟頁寫回磁盤后再回收。
  • 而對(duì)匿名頁的回收,其實(shí)就是通過 Swap 機(jī)制,把它們寫入磁盤后再釋放內(nèi)存。

不過,你可能還有一個(gè)問題。既然有兩種不同的內(nèi)存回收機(jī)制,那么在實(shí)際回收內(nèi)存時(shí),到底該先回收哪一種呢?

其實(shí),Linux 提供了一個(gè) /proc/sys/vm/swappiness 選項(xiàng),用來調(diào)整使用 Swap 的積極程度。

# cat /proc/sys/vm/swappiness
60

swappiness 的范圍是 0-100,數(shù)值越大,越積極使用 Swap,也就是更傾向于回收匿名頁;數(shù)值越小,越消極使用 Swap,也就是更傾向于回收文件頁。

雖然 swappiness 的范圍是 0-100,不過要注意,這并不是內(nèi)存的百分比,而是調(diào)整 Swap 積極程度的權(quán)重,即使你把它設(shè)置成 0,當(dāng)剩余內(nèi)存 + 文件頁小于頁高閾值時(shí),還是會(huì)發(fā)生 Swap。

清楚了 Swap 原理后,當(dāng)遇到 Swap 使用變高時(shí),又該怎么定位、分析呢?別急,下一節(jié),我們將用一個(gè)案例來探索實(shí)踐。

總結(jié):

  • 可以設(shè)置 /proc/sys/vm/min_free_kbytes,來調(diào)整系統(tǒng)定期回收內(nèi)存的閾值(也就是頁低閾值),還可以設(shè)置 /proc/sys/vm/swappiness,來調(diào)整文件頁和匿名頁的回收傾向。
  • 在 NUMA 架構(gòu)下,每個(gè) Node 都有自己的本地內(nèi)存空間,而當(dāng)本地內(nèi)存不足時(shí),默認(rèn)既可以從其他 Node 尋找空閑內(nèi)存,也可以從本地內(nèi)存回收。
  • 你可以設(shè)置 /proc/sys/vm/zone_reclaim_mode ,來調(diào)整 NUMA 本地內(nèi)存的回收策略。

4.2.4 swap 升高后如何定位分析

當(dāng) Swap 使用升高時(shí),要如何定位和分析呢?下面,我們就來看一個(gè)磁盤 I/O 的案例,實(shí)戰(zhàn)分析和演練。

下面案例基于 Ubuntu 18.04,同樣適用于其他的 Linux 系統(tǒng)。

  • 機(jī)器配置:2 CPU,8GB 內(nèi)存
  • 你需要預(yù)先安裝 sysstat 等工具,如 apt install sysstat

首先,我們打開兩個(gè)終端,分別 SSH 登錄到兩臺(tái)機(jī)器上,并安裝上面提到的這些工具。

然后,在終端中運(yùn)行 free 命令,查看 Swap 的使用情況。比如,在我的機(jī)器中,輸出如下:

$ freetotal        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:             0           0           0

從這個(gè) free 輸出你可以看到,Swap 的大小是 0,這說明我的機(jī)器沒有配置 Swap。

為了繼續(xù) Swap 的案例, 就需要先配置、開啟 Swap。如果你的環(huán)境中已經(jīng)開啟了 Swap,那你可以略過下面的開啟步驟,繼續(xù)往后走。

要開啟 Swap,我們首先要清楚,Linux 本身支持兩種類型的 Swap,即 Swap 分區(qū)和 Swap 文件。以 Swap 文件為例,在第一個(gè)終端中運(yùn)行下面的命令開啟 Swap,我這里配置 Swap 文件的大小為 8GB:

# 創(chuàng)建 Swap 文件
$ fallocate -l 8G /mnt/swapfile
# 修改權(quán)限只有根用戶可以訪問
$ chmod 600 /mnt/swapfile
# 配置 Swap 文件
$ mkswap /mnt/swapfile
# 開啟 Swap
$ swapon /mnt/swapfile

然后,再執(zhí)行 free 命令,確認(rèn) Swap 配置成功:

$ freetotal        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:       8388604           0     8388604

現(xiàn)在,free 輸出中,Swap 空間以及剩余空間都從 0 變成了 8GB,說明 Swap 已經(jīng)正常開啟。

接下來,我們?cè)诘谝粋€(gè)終端中,運(yùn)行下面的 dd 命令,模擬大文件的讀取:

# 寫入空設(shè)備,實(shí)際上只有磁盤的讀請(qǐng)求
$ dd if=/dev/sda1 of=/dev/null bs=1G count=2048

接著,在第二個(gè)終端中運(yùn)行 sar 命令,查看內(nèi)存各個(gè)指標(biāo)的變化情況。你可以多觀察一會(huì)兒,查看這些指標(biāo)的變化情況。

# 間隔 1 秒輸出一組數(shù)據(jù)
# -r 表示顯示內(nèi)存使用情況,-S 表示顯示 Swap 使用情況
$ sar -r -S 1
04:39:56    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:57      6249676   6839824   1919632     23.50    740512     67316   1691736     10.22    815156    841868         404:39:56    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:57      8388604         0      0.00         0      0.0004:39:57    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:58      6184472   6807064   1984836     24.30    772768     67380   1691736     10.22    847932    874224        2004:39:57    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:58      8388604         0      0.00         0      0.00…04:44:06    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:44:07       152780   6525716   8016528     98.13   6530440     51316   1691736     10.22    867124   6869332         004:44:06    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:44:07      8384508      4096      0.05        52      1.27

我們可以看到,sar 的輸出結(jié)果是兩個(gè)表格,第一個(gè)表格表示內(nèi)存的使用情況,第二個(gè)表格表示 Swap 的使用情況。其中,各個(gè)指標(biāo)名稱前面的 kb 前綴,表示這些指標(biāo)的單位是 KB。去掉前綴后,你會(huì)發(fā)現(xiàn),大部分指標(biāo)我們都已經(jīng)見過了,剩下的幾個(gè)新出現(xiàn)的指標(biāo),我來簡(jiǎn)單介紹一下。

  • kbcommit,表示當(dāng)前系統(tǒng)負(fù)載需要的內(nèi)存。它實(shí)際上是為了保證系統(tǒng)內(nèi)存不溢出,對(duì)需要內(nèi)存的估計(jì)值。%commit,就是這個(gè)值相對(duì)總內(nèi)存的百分比。
  • kbactive,表示活躍內(nèi)存,也就是最近使用過的內(nèi)存,一般不會(huì)被系統(tǒng)回收。
  • kbinact,表示非活躍內(nèi)存,也就是不常訪問的內(nèi)存,有可能會(huì)被系統(tǒng)回收。

清楚了界面指標(biāo)的含義后,我們?cè)俳Y(jié)合具體數(shù)值,來分析相關(guān)的現(xiàn)象。你可以清楚地看到,總的內(nèi)存使用率(%memused)在不斷增長(zhǎng),從開始的 23% 一直長(zhǎng)到了 98%,并且主要內(nèi)存都被緩沖區(qū)(kbbuffers)占用。具體來說:

  • 剛開始,剩余內(nèi)存(kbmemfree)不斷減少,而緩沖區(qū)(kbbuffers)則不斷增大,由此可知,剩余內(nèi)存不斷分配給了緩沖區(qū)。
  • 一段時(shí)間后,剩余內(nèi)存已經(jīng)很小,而緩沖區(qū)占用了大部分內(nèi)存。這時(shí)候,Swap 的使用開始逐漸增大,緩沖區(qū)和剩余內(nèi)存則只在小范圍內(nèi)波動(dòng)。

你可能困惑了,為什么緩沖區(qū)在不停增大?這又是哪些進(jìn)程導(dǎo)致的呢?

顯然,我們還得看看進(jìn)程緩存的情況。在前面緩存的案例中我們學(xué)過, cachetop 正好能滿足這一點(diǎn)。那我們就來 cachetop 一下。

在第二個(gè)終端中,按下 Ctrl+C 停止 sar 命令,然后運(yùn)行下面的 cachetop 命令,觀察緩存的使用情況:

$ cachetop 5
12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%18280 root     python                 22        0        0     100.0%       0.0%18279 root     dd                  41088    41022        0      50.0%      50.0%

通過 cachetop 的輸出,我們看到,dd 進(jìn)程的讀寫請(qǐng)求只有 50% 的命中率,并且未命中的緩存頁數(shù)(MISSES)為 41022(單位是頁)。這說明,正是案例開始時(shí)運(yùn)行的 dd,導(dǎo)致了緩沖區(qū)使用升高。

你可能接著會(huì)問,為什么 Swap 也跟著升高了呢?直觀來說,緩沖區(qū)占了系統(tǒng)絕大部分內(nèi)存,還屬于可回收內(nèi)存,內(nèi)存不夠用時(shí),不應(yīng)該先回收緩沖區(qū)嗎?

這種情況,我們還得進(jìn)一步通過 /proc/zoneinfo ,觀察剩余內(nèi)存、內(nèi)存閾值以及匿名頁和文件頁的活躍情況。

你可以在第二個(gè)終端中,按下 Ctrl+C,停止 cachetop 命令。然后運(yùn)行下面的命令,觀察 /proc/zoneinfo 中這幾個(gè)指標(biāo)的變化情況:

# -d 表示高亮變化的字段
# -A 表示僅顯示 Normal 行以及之后的 15 行輸出
$ watch -d grep -A 15 'Normal' /proc/zoneinfo
Node 0, zone   Normalpages free     21328min      14896low      18620high     22344spanned  1835008present  1835008managed  1796710protection: (0, 0, 0, 0, 0)nr_free_pages 21328nr_zone_inactive_anon 79776nr_zone_active_anon 206854nr_zone_inactive_file 918561nr_zone_active_file 496695nr_zone_unevictable 2251nr_zone_write_pending 0

你可以發(fā)現(xiàn),剩余內(nèi)存(pages_free)在一個(gè)小范圍內(nèi)不停地波動(dòng)。當(dāng)它小于頁低閾值(pages_low) 時(shí),又會(huì)突然增大到一個(gè)大于頁高閾值(pages_high)的值。

再結(jié)合剛剛用 sar 看到的剩余內(nèi)存和緩沖區(qū)的變化情況,我們可以推導(dǎo)出,剩余內(nèi)存和緩沖區(qū)的波動(dòng)變化,正是由于內(nèi)存回收和緩存再次分配的循環(huán)往復(fù)。

  • 當(dāng)剩余內(nèi)存小于頁低閾值時(shí),系統(tǒng)會(huì)回收一些緩存和匿名內(nèi)存,使剩余內(nèi)存增大。其中,緩存的回收導(dǎo)致 sar 中的緩沖區(qū)減小,而匿名內(nèi)存的回收導(dǎo)致了 Swap 的使用增大。
  • 緊接著,由于 dd 還在繼續(xù),剩余內(nèi)存又會(huì)重新分配給緩存,導(dǎo)致剩余內(nèi)存減少,緩沖區(qū)增大。

其實(shí)還有一個(gè)有趣的現(xiàn)象,如果多次運(yùn)行 dd 和 sar,你可能會(huì)發(fā)現(xiàn),在多次的循環(huán)重復(fù)中,有時(shí)候是 Swap 用得比較多,有時(shí)候 Swap 很少,反而緩沖區(qū)的波動(dòng)更大。

換句話說,系統(tǒng)回收內(nèi)存時(shí),有時(shí)候會(huì)回收更多的文件頁,有時(shí)候又回收了更多的匿名頁。

顯然,系統(tǒng)回收不同類型內(nèi)存的傾向,似乎不那么明顯。你應(yīng)該想到了上節(jié)課提到的 swappiness,正是調(diào)整不同類型內(nèi)存回收的配置選項(xiàng)。

還是在第二個(gè)終端中,按下 Ctrl+C 停止 watch 命令,然后運(yùn)行下面的命令,查看 swappiness 的配置:

$ cat /proc/sys/vm/swappiness
60

swappiness 顯示的是默認(rèn)值 60,這是一個(gè)相對(duì)中和的配置,所以系統(tǒng)會(huì)根據(jù)實(shí)際運(yùn)行情況,選擇合適的回收類型,比如回收不活躍的匿名頁,或者不活躍的文件頁。

到這里,我們已經(jīng)找出了 Swap 發(fā)生的根源。另一個(gè)問題就是,剛才的 Swap 到底影響了哪些應(yīng)用程序呢?換句話說,Swap 換出的是哪些進(jìn)程的內(nèi)存?

這里我還是推薦 proc 文件系統(tǒng),用來查看進(jìn)程 Swap 換出的虛擬內(nèi)存大小,它保存在 /proc/pid/status 中的 VmSwap 中(推薦你執(zhí)行 man proc 來查詢其他字段的含義)。

在第二個(gè)終端中運(yùn)行下面的命令,就可以查看使用 Swap 最多的進(jìn)程。注意 for、awk、sort 都是最常用的 Linux 命令,如果你還不熟悉,可以用 man 來查詢它們的手冊(cè),或上網(wǎng)搜索教程來學(xué)習(xí)。

# 按 VmSwap 使用量對(duì)進(jìn)程排序,輸出進(jìn)程名稱、進(jìn)程 ID 以及 SWAP 用量
$ for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head
dockerd 2226 10728 kB
docker-containe 2251 8516 kB
snapd 936 4020 kB
networkd-dispat 911 836 kB
polkitd 1004 44 kB

或者用smem --sort swap命令可以直接將進(jìn)程按照swap使用量排序顯示。

從這里你可以看到,使用 Swap 比較多的是 dockerd 和 docker-containe 進(jìn)程,所以,當(dāng) dockerd 再次訪問這些換出到磁盤的內(nèi)存時(shí),也會(huì)比較慢。

這也說明了一點(diǎn),雖然緩存屬于可回收內(nèi)存,但在類似大文件拷貝這類場(chǎng)景下,系統(tǒng)還是會(huì)用 Swap 機(jī)制來回收匿名內(nèi)存,而不僅僅是回收占用絕大部分內(nèi)存的文件頁。

最后,如果你在一開始配置了 Swap,不要忘記在案例結(jié)束后關(guān)閉。你可以運(yùn)行下面的命令,關(guān)閉 Swap:

$ swapoff -a

實(shí)際上,關(guān)閉 Swap 后再重新打開,也是一種常用的 Swap 空間清理方法,比如:

$ swapoff -a && swapon -a 

總結(jié):
在內(nèi)存資源緊張時(shí),Linux 會(huì)通過 Swap ,把不常訪問的匿名頁換出到磁盤中,下次訪問的時(shí)候再?gòu)拇疟P換入到內(nèi)存中來。你可以設(shè)置 /proc/sys/vm/min_free_kbytes,來調(diào)整系統(tǒng)定期回收內(nèi)存的閾值;也可以設(shè)置 /proc/sys/vm/swappiness,來調(diào)整文件頁和匿名頁的回收傾向。

當(dāng) Swap 變高時(shí),你可以用 sar、/proc/zoneinfo、/proc/pid/status 等方法,查看系統(tǒng)和進(jìn)程的內(nèi)存使用情況,進(jìn)而找出 Swap 升高的根源和受影響的進(jìn)程。

反過來說,通常,降低 Swap 的使用,可以提高系統(tǒng)的整體性能。要怎么做呢?這里,我也總結(jié)了幾種常見的降低方法。

  • 禁止 Swap,現(xiàn)在服務(wù)器的內(nèi)存足夠大,所以除非有必要,禁用 Swap 就可以了。隨著云計(jì)算的普及,大部分云平臺(tái)中的虛擬機(jī)都默認(rèn)禁止 Swap。
  • 如果實(shí)在需要用到 Swap,可以嘗試降低 swappiness 的值,減少內(nèi)存回收時(shí) Swap 的使用傾向。
  • 響應(yīng)延遲敏感的應(yīng)用,如果它們可能在開啟 Swap 的服務(wù)器中運(yùn)行,你還可以用庫(kù)函數(shù) mlock() 或者 mlockall() 鎖定內(nèi)存,阻止它們的內(nèi)存換出。

五、定位內(nèi)存問題方法

有沒有迅速定位內(nèi)存問題的方法?當(dāng)定位出內(nèi)存的瓶頸后,又有哪些優(yōu)化內(nèi)存的思路呢?

5.1 內(nèi)存性能指標(biāo)

為了分析內(nèi)存的性能瓶頸,首先你要知道,怎樣衡量?jī)?nèi)存的性能,也就是性能指標(biāo)問題。我們先來回顧一下,前幾節(jié)學(xué)過的內(nèi)存性能指標(biāo)。

你可以自己先找張紙,憑著記憶寫一寫;或者打開前面的文章,自己總結(jié)一下。

首先,你最容易想到的是系統(tǒng)內(nèi)存使用情況,比如已用內(nèi)存、剩余內(nèi)存、共享內(nèi)存、可用內(nèi)存、緩存和緩沖區(qū)的用量等。

  • 已用內(nèi)存和剩余內(nèi)存很容易理解,就是已經(jīng)使用和還未使用的內(nèi)存。
  • 共享內(nèi)存是通過 tmpfs 實(shí)現(xiàn)的,所以它的大小也就是 tmpfs 使用的內(nèi)存大小。tmpfs 其實(shí)也是一種特殊的緩存。
  • 可用內(nèi)存是新進(jìn)程可以使用的最大內(nèi)存,它包括剩余內(nèi)存和可回收緩存。
  • 緩存包括兩部分,一部分是磁盤讀取文件的頁緩存,用來緩存從磁盤讀取的數(shù)據(jù),可以加快以后再次訪問的速度。另一部分,則是 Slab 分配器中的可回收內(nèi)存。
  • 緩沖區(qū)是對(duì)原始磁盤塊的臨時(shí)存儲(chǔ),用來緩存將要寫入磁盤的數(shù)據(jù)。這樣,內(nèi)核就可以把分散的寫集中起來,統(tǒng)一優(yōu)化磁盤寫入。

第二類很容易想到的,應(yīng)該是進(jìn)程內(nèi)存使用情況,比如進(jìn)程的虛擬內(nèi)存、常駐內(nèi)存、共享內(nèi)存以及 Swap 內(nèi)存等。

  • 虛擬內(nèi)存,包括了進(jìn)程代碼段、數(shù)據(jù)段、共享內(nèi)存、已經(jīng)申請(qǐng)的堆內(nèi)存和已經(jīng)換出的內(nèi)存等。這里要注意,已經(jīng)申請(qǐng)的內(nèi)存,即使還沒有分配物理內(nèi)存,也算作虛擬內(nèi)存。
  • 常駐內(nèi)存是進(jìn)程實(shí)際使用的物理內(nèi)存,不過,它不包括 Swap 和共享內(nèi)存。
  • 共享內(nèi)存,既包括與其他進(jìn)程共同使用的真實(shí)的共享內(nèi)存,還包括了加載的動(dòng)態(tài)鏈接庫(kù)以及程序的代碼段等。
  • Swap 內(nèi)存,是指通過 Swap 換出到磁盤的內(nèi)存。

當(dāng)然,這些指標(biāo)中,常駐內(nèi)存一般會(huì)換算成占系統(tǒng)總內(nèi)存的百分比,也就是進(jìn)程的內(nèi)存使用率。

除了這些很容易想到的指標(biāo)外,我還想再?gòu)?qiáng)調(diào)一下,缺頁異常。

在內(nèi)存分配的原理中,我曾經(jīng)講到過,系統(tǒng)調(diào)用內(nèi)存分配請(qǐng)求后,并不會(huì)立刻為其分配物理內(nèi)存,而是在請(qǐng)求首次訪問時(shí),通過缺頁異常來分配。缺頁異常又分為下面兩種場(chǎng)景。

  • 可以直接從物理內(nèi)存中分配時(shí),被稱為次缺頁異常。
  • 需要磁盤 I/O 介入(比如 Swap)時(shí),被稱為主缺頁異常。顯然,主缺頁異常升高,就意味著需要磁盤 I/O,那么內(nèi)存訪問也會(huì)慢很多。

除了系統(tǒng)內(nèi)存和進(jìn)程內(nèi)存,第三類重要指標(biāo)就是 Swap 的使用情況,比如 Swap 的已用空間、剩余空間、換入速度和換出速度等。

  • 已用空間和剩余空間很好理解,就是字面上的意思,已經(jīng)使用和沒有使用的內(nèi)存空間。
  • 換入和換出速度,則表示每秒鐘換入和換出內(nèi)存的大小。

這些內(nèi)存的性能指標(biāo)都需要我們熟記并且會(huì)用,我把它們匯總成了一個(gè)思維導(dǎo)圖,你可以保存打印出來,或者自己仿照著總結(jié)一份。

5.2 內(nèi)存性能工具

所有的案例中都用到了 free。這是個(gè)最常用的內(nèi)存工具,可以查看系統(tǒng)的整體內(nèi)存和 Swap 使用情況。相對(duì)應(yīng)的,你可以用 top 或 ps,查看進(jìn)程的內(nèi)存使用情況。

然后,在緩存和緩沖區(qū)的原理篇中,我們通過 proc 文件系統(tǒng),找到了內(nèi)存指標(biāo)的來源;并通過 vmstat,動(dòng)態(tài)觀察了內(nèi)存的變化情況。與 free 相比,vmstat 除了可以動(dòng)態(tài)查看內(nèi)存變化,還可以區(qū)分緩存和緩沖區(qū)、Swap 換入和換出的內(nèi)存大小。

接著,在緩存和緩沖區(qū)的案例篇中,為了弄清楚緩存的命中情況,我們又用了 cachestat ,查看整個(gè)系統(tǒng)緩存的讀寫命中情況,并用 cachetop 來觀察每個(gè)進(jìn)程緩存的讀寫命中情況。

再接著,在內(nèi)存泄漏的案例中,我們用 vmstat,發(fā)現(xiàn)了內(nèi)存使用在不斷增長(zhǎng),又用 memleak,確認(rèn)發(fā)生了內(nèi)存泄漏。通過 memleak 給出的內(nèi)存分配棧,我們找到了內(nèi)存泄漏的可疑位置。

最后,在 Swap 的案例中,我們用 sar 發(fā)現(xiàn)了緩沖區(qū)和 Swap 升高的問題。通過 cachetop,我們找到了緩沖區(qū)升高的根源;通過對(duì)比剩余內(nèi)存跟 /proc/zoneinfo 的內(nèi)存閾,我們發(fā)現(xiàn) Swap 升高是內(nèi)存回收導(dǎo)致的。案例最后,我們還通過 /proc 文件系統(tǒng),找出了 Swap 所影響的進(jìn)程。

到這里,你是不是再次感覺到了來自性能世界的“惡意”。性能工具怎么那么多呀?其實(shí),還是那句話,理解內(nèi)存的工作原理,結(jié)合性能指標(biāo)來記憶,拿下工具的使用方法并不難。

5.3 性能指標(biāo)和工具的聯(lián)系

同 CPU 性能分析一樣,我的經(jīng)驗(yàn)是兩個(gè)不同維度出發(fā),整理和記憶。

  • 從內(nèi)存指標(biāo)出發(fā),更容易把工具和內(nèi)存的工作原理關(guān)聯(lián)起來。
  • 從性能工具出發(fā),可以更快地利用工具,找出我們想觀察的性能指標(biāo)。特別是在工具有限的情況下,我們更得充分利用手頭的每一個(gè)工具,挖掘出更多的問題。

同樣的,根據(jù)內(nèi)存性能指標(biāo)和工具的對(duì)應(yīng)關(guān)系,我做了兩個(gè)表格,方便你梳理關(guān)系和理解記憶。當(dāng)然,你也可以當(dāng)成“指標(biāo)工具”和“工具指標(biāo)”指南來用,在需要時(shí)直接查找。

第一個(gè)表格,從內(nèi)存指標(biāo)出發(fā),列舉了哪些性能工具可以提供這些指標(biāo)。這樣,在實(shí)際排查性能問題時(shí),你就可以清楚知道,究竟要用什么工具來輔助分析,提供你想要的指標(biāo)。

第二個(gè)表格,從性能工具出發(fā),整理了這些常見工具能提供的內(nèi)存指標(biāo)。掌握了這個(gè)表格,你可以最大化利用已有的工具,盡可能多地找到你要的指標(biāo)。

這些工具的具體使用方法并不用背,你只要知道有哪些可用的工具,以及這些工具提供的基本指標(biāo)。真正用到時(shí), man 一下查它們的使用手冊(cè)就可以了。

5.4 如何迅速分析內(nèi)存的性能瓶頸

我相信到這一步,你對(duì)內(nèi)存的性能指標(biāo)已經(jīng)非常熟悉,也清楚每種性能指標(biāo)分別能用什么工具來獲取。

那是不是說,每次碰到內(nèi)存性能問題,你都要把上面這些工具全跑一遍,然后再把所有內(nèi)存性能指標(biāo)全分析一遍呢?

自然不是。前面的 CPU 性能篇我們就說過,簡(jiǎn)單查找法,雖然是有用的,也很可能找到某些系統(tǒng)潛在瓶頸。但是這種方法的低效率和大工作量,讓我們首先拒絕了這種方法。

還是那句話,在實(shí)際生產(chǎn)環(huán)境中,我們希望的是,盡可能快地定位系統(tǒng)瓶頸,然后盡可能快地優(yōu)化性能,也就是要又快又準(zhǔn)地解決性能問題。

那有沒有什么方法,可以又快又準(zhǔn)地分析出系統(tǒng)的內(nèi)存問題呢?

方法當(dāng)然有。還是那個(gè)關(guān)鍵詞,找關(guān)聯(lián)。其實(shí),雖然內(nèi)存的性能指標(biāo)很多,但都是為了描述內(nèi)存的原理,指標(biāo)間自然不會(huì)完全孤立,一般都會(huì)有關(guān)聯(lián)。當(dāng)然,反過來說,這些關(guān)聯(lián)也正是源于系統(tǒng)的內(nèi)存原理,這也是我總強(qiáng)調(diào)基礎(chǔ)原理的重要性,并在文章中穿插講解。

舉個(gè)最簡(jiǎn)單的例子,當(dāng)你看到系統(tǒng)的剩余內(nèi)存很低時(shí),是不是就說明,進(jìn)程一定不能申請(qǐng)分配新內(nèi)存了呢?當(dāng)然不是,因?yàn)檫M(jìn)程可以使用的內(nèi)存,除了剩余內(nèi)存,還包括了可回收的緩存和緩沖區(qū)。


所以,為了迅速定位內(nèi)存問題,我通常會(huì)先運(yùn)行幾個(gè)覆蓋面比較大的性能工具,比如 free、top、vmstat、pidstat 等。具體的分析思路主要有這幾步。

  • 先用 free 和 top,查看系統(tǒng)整體的內(nèi)存使用情況。
  • 再用 vmstat 和 pidstat,查看一段時(shí)間的趨勢(shì),從而判斷出內(nèi)存問題的類型。
  • 最后進(jìn)行詳細(xì)分析,比如內(nèi)存分配分析、緩存 / 緩沖區(qū)分析、具體進(jìn)程的內(nèi)存使用分析等。
  • 同時(shí),我也把這個(gè)分析過程畫成了一張流程圖,你可以保存并打印出來使用。

圖中列出了最常用的幾個(gè)內(nèi)存工具,和相關(guān)的分析流程。其中,箭頭表示分析的方向,舉幾個(gè)例子你可能會(huì)更容易理解。

第一個(gè)例子,當(dāng)你通過 free,發(fā)現(xiàn)大部分內(nèi)存都被緩存占用后,可以使用 vmstat 或者 sar 觀察一下緩存的變化趨勢(shì),確認(rèn)緩存的使用是否還在繼續(xù)增大。

如果繼續(xù)增大,則說明導(dǎo)致緩存升高的進(jìn)程還在運(yùn)行,那你就能用緩存 / 緩沖區(qū)分析工具(比如 cachetop、slabtop 等),分析這些緩存到底被哪里占用。

第二個(gè)例子,當(dāng)你 free 一下,發(fā)現(xiàn)系統(tǒng)可用內(nèi)存不足時(shí),首先要確認(rèn)內(nèi)存是否被緩存 / 緩沖區(qū)占用。排除緩存 / 緩沖區(qū)后,你可以繼續(xù)用 pidstat 或者 top,定位占用內(nèi)存最多的進(jìn)程。

找出進(jìn)程后,再通過進(jìn)程內(nèi)存空間工具(比如 pmap),分析進(jìn)程地址空間中內(nèi)存的使用情況就可以了。

第三個(gè)例子,當(dāng)你通過 vmstat 或者 sar 發(fā)現(xiàn)內(nèi)存在不斷增長(zhǎng)后,可以分析中是否存在內(nèi)存泄漏的問題。

比如你可以使用內(nèi)存分配分析工具 memleak ,檢查是否存在內(nèi)存泄漏。如果存在內(nèi)存泄漏問題,memleak 會(huì)為你輸出內(nèi)存泄漏的進(jìn)程以及調(diào)用堆棧。

注意,這個(gè)圖里我沒有列出所有性能工具,只給出了最核心的幾個(gè)。這么做,一方面,確實(shí)不想讓大量的工具列表嚇到你。

另一方面,希望你能把重心先放在核心工具上,通過我提供的案例和真實(shí)環(huán)境的實(shí)踐,掌握使用方法和分析思路。 畢竟熟練掌握它們,你就可以解決大多數(shù)的內(nèi)存問題。

5.5 常見內(nèi)存優(yōu)化方式

雖然內(nèi)存的性能指標(biāo)和性能工具都挺多,但理解了內(nèi)存管理的基本原理后,你會(huì)發(fā)現(xiàn)它們其實(shí)都有一定的關(guān)聯(lián)。梳理出它們的關(guān)系,掌握內(nèi)存分析的套路并不難。

找到內(nèi)存問題的來源后,下一步就是相應(yīng)的優(yōu)化工作了。在我看來,內(nèi)存調(diào)優(yōu)最重要的就是,保證應(yīng)用程序的熱點(diǎn)數(shù)據(jù)放到內(nèi)存中,并盡量減少換頁和交換。常見的優(yōu)化思路有這么幾種。

  • 最好禁止 Swap。如果必須開啟 Swap,降低 swappiness 的值,減少內(nèi)存回收時(shí) Swap 的使用傾向。
  • 減少內(nèi)存的動(dòng)態(tài)分配。比如,可以使用內(nèi)存池、大頁(HugePage)等。
  • 盡量使用緩存和緩沖區(qū)來訪問數(shù)據(jù)。比如,可以使用堆棧明確聲明內(nèi)存空間,來存儲(chǔ)需要緩存的數(shù)據(jù);或者用 Redis 這類的外部緩存組件,優(yōu)化數(shù)據(jù)的訪問。
  • 使用 cgroups 等方式限制進(jìn)程的內(nèi)存使用情況。這樣,可以確保系統(tǒng)內(nèi)存不會(huì)被異常進(jìn)程耗盡。
  • 通過 /proc/pid/oom_adj ,調(diào)整核心應(yīng)用的 oom_score。這樣,可以保證即使內(nèi)存緊張,核心應(yīng)用也不會(huì)被 OOM 殺死。
http://m.aloenet.com.cn/news/41896.html

相關(guān)文章:

  • 網(wǎng)站建設(shè)要如何選擇成都百度推廣電話
  • 做寵物網(wǎng)站心得谷歌seo怎么優(yōu)化
  • 域名注冊(cè) 網(wǎng)站建設(shè) 好做嗎網(wǎng)站頁面分析
  • 有什么網(wǎng)站可以做微信吳江網(wǎng)站制作
  • 長(zhǎng)春做網(wǎng)站網(wǎng)站搜狐財(cái)經(jīng)峰會(huì)
  • 重慶經(jīng)典論壇新聞評(píng)論windows優(yōu)化大師值得買嗎
  • 做的網(wǎng)站響應(yīng)速度慢免費(fèi)二級(jí)域名注冊(cè)網(wǎng)站有哪些
  • 建新建設(shè)集團(tuán)有限公司網(wǎng)站萬物識(shí)別掃一掃
  • 寫微信小程序需要什么軟件怎么優(yōu)化一個(gè)網(wǎng)站關(guān)鍵詞
  • win服務(wù)器做網(wǎng)站今日重大新聞?lì)^條財(cái)經(jīng)
  • 海南哪家公司做網(wǎng)站信息流廣告有哪些投放平臺(tái)
  • 廣東省建設(shè)工程金匠獎(jiǎng)公布網(wǎng)站優(yōu)化網(wǎng)址
  • 嘉興微信網(wǎng)站百度極簡(jiǎn)網(wǎng)址
  • 四川鴻業(yè)建設(shè)集團(tuán)網(wǎng)站廣告關(guān)鍵詞
  • 金融集團(tuán)網(wǎng)站模板最新新聞
  • 網(wǎng)站跟app的區(qū)別是什么公司網(wǎng)站設(shè)計(jì)需要多少錢
  • 簡(jiǎn)單的企業(yè)網(wǎng)站模板中國(guó)職業(yè)培訓(xùn)在線官方網(wǎng)站
  • 做跨境的網(wǎng)站有哪些內(nèi)容關(guān)鍵詞指數(shù)查詢工具
  • 咋建網(wǎng)站圖片識(shí)別 在線百度識(shí)圖
  • 叫別人做網(wǎng)站需要注意什么品牌策劃方案模板
  • 收企業(yè)做網(wǎng)站備案西安網(wǎng)約車
  • 在哪個(gè)網(wǎng)站做淘寶水印資源
  • 承德網(wǎng)站制作青島優(yōu)化網(wǎng)站關(guān)鍵詞
  • 網(wǎng)站開發(fā)費(fèi)用清單淘寶指數(shù)官網(wǎng)
  • 頁面模板怎么修改鄭州seo排名優(yōu)化公司
  • 威海哪家網(wǎng)站做的好北京seo招聘信息
  • 襄陽哪里有做網(wǎng)站的手機(jī)app開發(fā)
  • 做兼職最靠譜的網(wǎng)站淘寶網(wǎng)站的推廣與優(yōu)化
  • 學(xué)做網(wǎng)站開發(fā)seo排名策略
  • 滄州做網(wǎng)站公司百度指數(shù)app下載