緩沖區buffer和緩存cache
- 緩存包括兩部分,一部分是磁盤讀取檔案的頁緩存,用來緩存從磁盤讀取的資料,可以加快以後再次通路的速度。另一部分,則是 Slab 配置設定器中的可回收記憶體。
- 緩沖區是對原始磁盤塊的臨時存儲,用來緩存将要寫入磁盤的資料。這樣,核心就可以把分散的寫集中起來,統一優化磁盤寫入。
寫檔案時會用到Cache 緩存資料,而寫磁盤則會用到 Buffer來緩存資料。是以,回到剛剛的問題,雖然文檔上隻提到,Cache是檔案讀的緩存,但實際上,Cache 也會緩存寫檔案時的資料。
觀察buffer cache變化,vmstat指令∶
[root@www ~]# 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 1460 498756 0 106032 0 0 10 36 101 103 1 0 99 0 0
0 0 1460 498740 0 106032 0 0 0 0 99 117 0 1 99 0 0
0 0 1460 498740 0 106032 0 0 0 0 133 133 1 0 99 0 0
輸出界面裡,記憶體部分的 buff和 cache,以及 io 部分的bi和bo 就是我們要關注的重點。
- buff和 cache就是我們前面看到的Buffers和Cache,機關是 KB。
- bi和 bo 則分别表示塊裝置讀取和寫入的大小,機關為塊/秒。因為Linux中塊的大小是1KB,是以這個機關也就等價于 KB/s。
簡單來說,Buffer 是對磁盤資料的緩存,而 Cache 是檔案資料的緩存,它們既會用在讀請求中,也會用在寫請求中。
- 從寫的角度來說,不僅可以優化磁盤和檔案的寫入,對應用程式也有好處,應用程式可以在資料真正落盤前,就傳回去做其他工作。
- 從讀的角度來說,既可以加速讀取那些需要頻繁通路的資料,也降低了頻繁I/O對磁盤的壓力。
緩存命中率
緩存的命中率。所謂緩存命中率,是指直接通過緩存擷取資料的請求次數,占所有資料請求次數的百分比。命中率越高,表示使用緩存帶來的收益越高,應用程式的性能也就越好。
實際上,緩存是現在所有高并發系統必需的核心子產品,主要作用就是把經常通路的資料(也就是熱點資料),提前讀入到記憶體中。這樣,下次通路時就可以直接從記憶體讀取資料,而不需要經過硬碟,進而加快應用程式的響應速度。
cachestat 提供了整個作業系統緩存的讀寫命中情況
[root@docker ~]# cachestat 1 3
HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
0 0 0 0.00% 16 394
1 0 0 100.00% 16 394
0 0 0 0.00% 16 394
你可以看到,cachestat的輸出其實是一個表格。每行代表一組資料,而每一列代表不同的緩存統計名額。這些名額從左到右依次表示∶
- MSSES,表示緩存未命中的次數
- HITS,表示緩存命中的次數
- DIRTIES,表示新增到緩存中的髒頁數
- BUFFERS_MB表示 Buffers的大小,以 MB為機關
- CACHED_MB表示 Cache的大小,以 MB為機關
cachetop 提供了每個程序的緩存命中情況
接下來我們再來看一個 cachetop的運作界面∶
$ 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類似,預設按照緩存的命中次數(HITS)排序,展示了每個程序的緩存命中情況。具體到每一個名額,這裡的HITS、MISSES和 DIRTIES,跟 cachestat 裡的含義一樣,分别代表間隔時間内的緩存命中次數、未命中次數以及新增到緩存中的髒頁數。
而READ_HIT和WRITE_HIT,分别表示讀和寫的緩存命中率。
檢視某個容器狀态,檢視是什麼原因退出
[root@docker ~]# docker inspect a6fb3d53a55b | grep -i status -A 10
"Status": "exited",
"Running": false,
"Paused": false,
"Restarting": false,
"OOMKilled": true,
"Dead": false,
"Pid": 0,
"ExitCode": 137,
"Error": "",
"StartedAt": "2021-11-11T00:48:00.806908787Z",
"FinishedAt": "2021-11-11T00:48:39.15824301Z"
2.vm.overcommit_memory
Redis在啟動時可能會出現這樣的日志:
WARNING overcommit_memory is set to 0! Background save may fail under low memory condition.
To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf
and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
在分析這個問題之前, 首先要弄清楚什麼是overcommit? Linux作業系統對大部分申請記憶體的請求都回複yes, 以便能運作更多的程式。 因為申請記憶體後, 并不會馬上使用記憶體, 這種技術叫做overcommit。 如果Redis在啟動時有上面的日志, 說明vm.overcommit_memory=0, Redis提示把它設定為1。
vm.overcommit_memory用來設定記憶體配置設定政策, 有三個可選值, 如表:可用記憶體代表實體記憶體與swap之和

日志中的Background save代表的是bgsave和bgrewriteaof, 如果目前可用記憶體不足, 作業系統應該如何處理fork操作。 如果
vm.overcommit_memory=0, 代表如果沒有可用記憶體, 就申請記憶體失敗, 對應到Redis就是執行fork失敗, 在Redis的日志會出現:
Cannot allocate memory
Redis建議把這個值設定為1, 是為了讓fork操作能夠在低記憶體下也執行成功。
3.oom_badness() 函數
在發生 OOM 的時候,Linux 到底是根據什麼标準來選擇被殺的程序呢?這就要提到一個在 Linux 核心裡有一個 oom_badness() 函數,就是它定義了選擇程序的标準。其實這裡的判斷标準也很簡單,函數中涉及兩個條件:
- 第一,程序已經使用的實體記憶體頁面數。
- 第二,每個程序的 OOM 校準值 oom_score_adj。在 /proc 檔案系統中,每個程序都有一個 /proc/<pid>/oom_score_adj 的接口檔案。我們可以在這個檔案中輸入 -1000 到 1000 之間的任意一個數值,調整程序被 OOM Kill 的幾率。
adj = (long)p->signal->oom_score_adj;
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +mm_pgtables_bytes(p->mm) / PAGE_SIZE;
adj *= totalpages / 1000;
points += adj;
結合前面說的兩個條件,函數 oom_badness() 裡的最終計算方法是這樣的:用系統總的可用頁面數,去乘以 OOM 校準值 oom_score_adj,再加上程序已經使用的實體頁面數,計算出來的值越大,那麼這個程序被 OOM Kill 的幾率也就越大。
每個程序的權值存放在/proc/{progress_id}/oom_score中,這個值受/proc/{progress_id}/oom_adj的控制,oom_adj在不同的Linux版本中最小值不同,可以參考Linux源碼中oom.h(從-15到-17)。當oom_adj設定為最小值時,該程序将不會被OOM killer殺掉,設定方法如下。
echo {value} > /proc/${process_id}/oom_adj
4.Memory Cgroup?
Memory Cgroup 也是 Linux Cgroups 子系統之一,它的作用是對一組程序的 Memory 使用做限制。
第一個參數,叫作 memory.limit_in_bytes。請你注意,這個 memory.limit_in_bytes 是每個控制組裡最重要的一個參數了。這是因為一個控制組裡所有程序可使用記憶體的最大值,就是由這個參數的值來直接限制的。
第二個參數 memory.oom_control 了。這個 memory.oom_control 又是幹啥的呢?當控制組中的程序記憶體使用達到上限值時,這個參數能夠決定會不會觸發 OOM Killer。
如果沒有人為設定的話,memory.oom_control 的預設值就會觸發 OOM Killer。這是一個控制組内的 OOM Killer,和整個系統的 OOM Killer 的功能差不多,差别隻是被殺程序的選擇範圍:控制組内的 OOM Killer 當然隻能殺死控制組内的程序,而不能選節點上的其他程序。
第三個參數,也就是 memory.usage_in_bytes。這個參數是隻讀的,它裡面的數值是目前控制組裡所有程序實際使用的記憶體總和。我們可以檢視這個值,然後把它和 memory.limit_in_bytes 裡的值做比較,根據接近程度來可以做個預判。這兩個值越接近,OOM 的風險越高。通過這個方法,我們就可以得知,目前控制組内使用總的記憶體量有沒有 OOM 的風險了。
控制組
控制組之間也同樣是樹狀的層級結構,在這個結構中,父節點的控制組裡memory.limit_in_bytes 值,就可以限制它的子節點中所有程序的記憶體使用。
我用一個具體例子來說明,比如像下面圖裡展示的那樣,group1 裡的 memory.limit_in_bytes 設定的值是 200MB,它的子控制組 group3 裡 memory.limit_in_bytes 值是 500MB。那麼,我們在 group3 裡所有程序使用的記憶體總值就不能超過 200MB,而不是 500MB。
好了,我們這裡介紹了 Memory Cgroup 最基本的概念,簡單總結一下:
第一,Memory Cgroup 中每一個控制組可以為一組程序限制記憶體使用量,一旦所有程序使用記憶體的總量達到限制值,預設情況下,就會觸發 OOM Killer。這樣一來,控制組裡的“某個程序”就會被殺死。
第二,這裡殺死“某個程序”的選擇标準是,控制組中總的可用頁面乘以程序的 oom_score_adj,加上程序已經使用的實體記憶體頁面,所得值最大的程序,就會被系統選中殺死。
Linux 系統有那些記憶體類型?
隻有知道了記憶體的類型,才能明白每一種類型的記憶體,容器分别使用了多少。而且,對于不同類型的記憶體,一旦總記憶體增高到容器裡記憶體最高限制的數值,相應的處理方式也不同。
Linux 的各個子產品都需要記憶體,比如核心需要配置設定記憶體給頁表,核心棧,還有 slab,也就是核心各種資料結構的 Cache Pool;使用者态程序裡的堆記憶體和棧的記憶體,共享庫的記憶體,還有檔案讀寫的 Page Cache。
Memory Cgroup 裡都不會對核心的記憶體做限制(比如頁表,slab 等)。是以我們今天主要讨論與使用者态相關的兩個記憶體類型,RSS 和 Page Cache。
RSS
先看什麼是 RSS。RSS 是 Resident Set Size 的縮寫,簡單來說它就是指程序真正申請到實體頁面的記憶體大小。這是什麼意思呢?
應用程式在申請記憶體的時候,比如說,調用 malloc() 來申請 100MB 的記憶體大小,malloc() 傳回成功了,這時候系統其實隻是把 100MB 的虛拟位址空間配置設定給了程序,但是并沒有把實際的實體記憶體頁面配置設定給程序。
上一講中,我給你講過,當程序對這塊記憶體位址開始做真正讀寫操作的時候,系統才會把實際需要的實體記憶體配置設定給程序。而這個過程中,程序真正得到的實體記憶體,就是這個 RSS 了。
比如下面的這段代碼,我們先用 malloc 申請 100MB 的記憶體。
p = malloc(100 * MB);
if (p == NULL)
return 0;
然後,我們運作 top 指令檢視這個程式在運作了 malloc() 之後的記憶體,我們可以看到這個程式的虛拟位址空間(VIRT)已經有了 106728KB(~100MB),但是實際的實體記憶體 RSS(top 指令裡顯示的是 RES,就是 Resident 的簡寫,和 RSS 是一個意思)在這裡隻有 688KB。
接着我們在程式裡等待 30 秒之後,我們再對這塊申請的空間裡寫入 20MB 的資料。
sleep(30);
memset(p, 0x00, 20 * MB)
當我們用 memset() 函數對這塊位址空間寫入 20MB 的資料之後,我們再用 top 檢視,這時候可以看到虛拟位址空間(VIRT)還是 106728,不過實體記憶體 RSS(RES)的值變成了 21432(大小約為 20MB), 這裡的機關都是 KB。
[root@8ddcdff501a3 /]# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 1132 4 ? Ss 08:58 0:00 /sbin/docker-init -- /read_file /mnt/test.file 100
root 7 0.0 0.0 5352 2240 ? S 08:58 0:00 /read_file /mnt/test.file 100
root 8 0.0 0.0 12024 3364 pts/0 Ss 09:45 0:00 bash
root 24 0.0 0.0 46352 3428 pts/0 R+ 10:11 0:00 ps -aux
[root@docker ~]# ps -ef | grep mnt
root 7767 7739 0 16:58 ? 00:00:00 /sbin/docker-init -- /read_file /mnt/test.file 100
root 7797 7767 0 16:58 ? 00:00:00 /read_file /mnt/test.file 100
root 11994 10493 0 18:11 pts/4 00:00:00 grep --color=auto mnt
[root@docker ~]# cat /proc/7797/smaps | grep -i RSS
Rss: 4 kB
Rss: 4 kB
Rss: 4 kB
Rss: 1032 kB
Rss: 1176 kB
Rss: 0 kB
Rss: 16 kB
Rss: 8 kB
Rss: 12 kB
Rss: 160 kB
Rss: 8 kB
Rss: 4 kB
Rss: 4 kB
Rss: 4 kB
Rss: 8 kB
Rss: 0 kB
Rss: 4 kB
Rss: 0 kB
[root@docker ~]# echo $((4+4+4+1032+1176+16+8+12+160+8+4+4+4+8+4))
2448