1. 程式人生 > 程式設計 >深入理解 Linux Cgroup 系列(三):記憶體

深入理解 Linux Cgroup 系列(三):記憶體

原文連結:深入理解 Linux Cgroup 系列(三):記憶體

通過上篇文章的學習,我們學會瞭如何檢視當前 cgroup 的資訊,如何通過操作 /sys/fs/cgroup 目錄來動態設定 cgroup,也學會瞭如何設定 CPU shares 和 CPU quota 來控制 slice 內部以及不同 slice 之間的 CPU 使用時間。本文將把重心轉移到記憶體上,通過具體的示例來演示如何通過 cgroup 來限制記憶體的使用。

1. 尋找走失記憶體

上篇文章告訴我們,CPU controller 提供了兩種方法來限制 CPU 使用時間,其中 CPUShares 用來設定相對權重,CPUQuota

用來限制 user、service 或 VM 的 CPU 使用時間百分比。例如:如果一個 user 同時設定了 CPUShares 和 CPUQuota,假設 CPUQuota 設定成 50%,那麼在該 user 的 CPU 使用量達到 50% 之前,可以一直按照 CPUShares 的設定來使用 CPU。

對於記憶體而言,在 CentOS 7 中,systemd 已經幫我們將 memory 繫結到了 /sys/fs/cgroup/memory。systemd 只提供了一個引數 MemoryLimit 來對其進行控制,該引數表示某個 user 或 service 所能使用的實體記憶體總量。拿之前的使用者 tom 舉例, 它的 UID 是 1000,可以通過以下命令來設定:

$ systemctl set-property user-1000.slice MemoryLimit=200M
複製程式碼

現在使用使用者 tom 登入該系統,通過 stress 命令產生 8 個子程式,每個程式分配 256M 記憶體:

$ stress --vm 8 --vm-bytes 256M
複製程式碼

按照預想,stress 程式的記憶體使用量已經超出了限制,此時應該會觸發 oom-killer,但實際上程式仍在執行,這是為什麼呢?我們來看一下目前佔用的記憶體:

$ cd /sys/fs/cgroup/memory/user.slice/user-1000.slice

$ cat memory.usage_in_bytes
209661952
複製程式碼

奇怪,佔用的記憶體還不到 200M,剩下的記憶體都跑哪去了呢?別慌,你是否還記得 linux 系統中的記憶體使用除了包括實體記憶體,還包括交換分割槽,也就是 swap,我們來看看是不是 swap 搞的鬼。先停止剛剛的 stress 程式,稍等 30 秒,觀察一下 swap 空間的佔用情況:

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           3.7G        180M        3.2G        8.9M        318M        3.3G
Swap:          3.9G        512K        3.9G
複製程式碼

重新執行 stress 程式:

$ stress --vm 8 --vm-bytes 256M
複製程式碼

檢視記憶體使用情況:

$ cat memory.usage_in_bytes
209637376
複製程式碼

發現記憶體佔用剛好在 200M 以內。再看 swap 空間佔用情況:

$ free
              total        used        free      shared  buff/cache   available
Mem:        3880876      407464     3145260        9164      328152     3220164
Swap:       4063228     2031360     2031868
複製程式碼

和剛剛相比,多了 2031360-512=2030848k,現在基本上可以確定當程式的使用量達到限制時,核心會嘗試將實體記憶體中的資料移動到 swap 空間中,從而讓記憶體分配成功。我們可以精確計算出 tom 使用者使用的實體記憶體+交換空間總量,首先需要分別檢視 tom 使用者的實體記憶體和交換空間使用量:

$ egrep "swap|rss" memory.stat
rss 209637376
rss_huge 0
swap 1938804736
total_rss 209637376
total_rss_huge 0
total_swap 1938804736
複製程式碼

可以看到實體記憶體使用量為 209637376 位元組,swap 空間使用量為 1938804736 位元組,總量為 (209637376+1938804736)/1024/1024=2048 M。而 stress 程式需要的記憶體總量為 256*8=2048 M,兩者相等。

這個時候如果你每隔幾秒就檢視一次 memory.failcnt 檔案,就會發現這個檔案裡面的數值一直在增長:

$ cat memory.failcnt
59390293
複製程式碼

從上面的結果可以看出,當實體記憶體不夠時,就會觸發 memory.failcnt 裡面的數量加 1,但此時程式不一定會被殺死,核心會盡量將實體記憶體中的資料移動到 swap 空間中。

2. 關閉 swap

為了更好地觀察 cgroup 對記憶體的控制,我們可以使用者 tom 不使用 swap 空間,實現方法有以下幾種:

  1. memory.swappiness 檔案的值修改為 0:

    $ echo 0 > /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness
    複製程式碼

    這樣設定完成之後,即使系統開啟了交換空間,當前 cgroup 也不會使用交換空間。

  2. 直接關閉系統的交換空間:

    $ swapoff -a
    複製程式碼

    如果想永久生效,還要註釋掉 /etc/fstab 檔案中的 swap。

如果你既不想關閉系統的交換空間,又想讓 tom 不使用 swap 空間,上面給出的第一個方法是有問題的:

  • 你只能在 tom 使用者登入的時候修改 memory.swappiness 檔案的值,因為如果 tom 使用者沒有登入,當前的 cgroup 就會消失。
  • 即使你修改了 memory.swappiness 檔案的值,也會在重新登入後失效

如果按照常規思路去解決這個問題,可能會非常棘手,我們可以另闢蹊徑,從 PAM 入手。

Linux PAM(Pluggable Authentication Modules) 是一個系統級使用者認證框架,PAM 將程式開發與認證方式進行分離,程式在執行時呼叫附加的“認證”模組完成自己的工作。本地系統管理員通過配置選擇要使用哪些認證模組,其中 /etc/pam.d/ 目錄專門用於存放 PAM 配置,用於為具體的應用程式設定獨立的認證方式。例如,在使用者通過 ssh 登入時,將會載入 /etc/pam.d/sshd 裡面的策略。

/etc/pam.d/sshd 入手,我們可以先建立一個 shell 指令碼:

$ cat /usr/local/bin/tom-noswap.sh
#!/bin/bash

if [ $PAM_USER == 'tom' ]
  then
    echo 0 > /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness
fi
複製程式碼

然後在 /etc/pam.d/sshd 中通過 pam_exec 呼叫該指令碼,在 /etc/pam.d/sshd 的末尾新增一行,內容如下:

$ session optional pam_exec.so seteuid /usr/local/bin/tom-noswap.sh
複製程式碼

現在再使用 tom 使用者登入,就會發現 memory.swappiness 的值變成了 0。

這裡需要注意一個前提:至少有一個使用者 tom 的登入會話,且通過 systemctl set-property user-1000.slice MemoryLimit=200M 命令設定了 limit,/sys/fs/cgroup/memory/user.slice/user-1000.slice 目錄才會存在。所以上面的所有操作,一定要保證至少保留一個使用者 tom 的登入會話。

3. 控制記憶體使用

關閉了 swap 之後,我們就可以嚴格控制程式的記憶體使用量了。還是使用開頭提到的例子,使用使用者 tom 登入該系統,先在第一個 shell 視窗執行以下命令:

$ journalctl -f
複製程式碼

開啟第二個 shell 視窗(還是 tom 使用者),通過 stress 命令產生 8 個子程式,每個程式分配 256M 記憶體:

$ stress --vm 8 --vm-bytes 256M
stress: info: [30150] dispatching hogs: 0 cpu,0 io,8 vm,0 hdd
stress: FAIL: [30150] (415) <-- worker 30152 got signal 9
stress: WARN: [30150] (417) stress: FAIL: [30150] (415) <-- worker 30151 got signal 9
stress: WARN: [30150] (417) now reaping child worker processes
stress: FAIL: [30150] (415) <-- worker 30154 got signal 9
stress: WARN: [30150] (417) now reaping child worker processes
stress: FAIL: [30150] (415) <-- worker 30157 got signal 9
stress: WARN: [30150] (417) now reaping child worker processes
stress: FAIL: [30150] (415) <-- worker 30158 got signal 9
stress: WARN: [30150] (417) now reaping child worker processes
stress: FAIL: [30150] (451) failed run completed in 0s
複製程式碼

現在可以看到 stress 程式很快被 kill 掉了,回到第一個 shell 視窗,會輸出以下資訊:

由此可見 cgroup 對記憶體的限制奏效了,stress 程式的記憶體使用量超出了限制,觸發了 oom-killer,進而殺死程式。

4. 更多檔案

加個小插曲,如果你想獲取更多關於 cgroup 的檔案,可以通過 yum 安裝 kernel-doc 包。安裝完成後,你就可以進入 /usr/share/docs 的子目錄,檢視每個 cgroup controller 的詳細檔案。

$ cd /usr/share/doc/kernel-doc-3.10.0/Documentation/cgroups
$ ll
總用量 172
 4 -r--r--r-- 1 root root   918 6月  14 02:29 00-INDEX
16 -r--r--r-- 1 root root 16355 6月  14 02:29 blkio-controller.txt
28 -r--r--r-- 1 root root 27027 6月  14 02:29 cgroups.txt
 4 -r--r--r-- 1 root root  1972 6月  14 02:29 cpuacct.txt
40 -r--r--r-- 1 root root 37225 6月  14 02:29 cpusets.txt
 8 -r--r--r-- 1 root root  4370 6月  14 02:29 devices.txt
 8 -r--r--r-- 1 root root  4908 6月  14 02:29 freezer-subsystem.txt
 4 -r--r--r-- 1 root root  1714 6月  14 02:29 hugetlb.txt
16 -r--r--r-- 1 root root 14124 6月  14 02:29 memcg_test.txt
36 -r--r--r-- 1 root root 36415 6月  14 02:29 memory.txt
 4 -r--r--r-- 1 root root  1267 6月  14 02:29 net_cls.txt
 4 -r--r--r-- 1 root root  2513 6月  14 02:29 net_prio.txt
複製程式碼

下一篇文章將會討論如何使用 cgroup 來限制 I/O,敬請期待~