Docker: 限制容器可用的記憶體
預設情況下容器使用的資源是不受限制的。也就是可以使用主機核心排程器所允許的最大資源。但是在容器的使用過程中,經常需要對容器可以使用的主機資源進行限制,本文介紹如何限制容器可以使用的主機記憶體。
為什麼要限制容器對記憶體的使用?
限制容器不能過多的使用主機的記憶體是非常重要的。對於 linux 主機來說,一旦核心檢測到沒有足夠的記憶體可以分配,就會扔出 OOME(Out Of Memmory Exception),並開始殺死一些程序用於釋放記憶體空間。糟糕的是任何程序都可能成為核心獵殺的物件,包括 docker daemon 和其它一些重要的程式。更危險的是如果某個支援系統執行的重要程序被幹掉了,整個系統也就宕掉了!這裡我們考慮一個比較常見的場景,大量的容器把主機的記憶體消耗殆盡,OOME 被觸發後系統核心立即開始殺程序釋放記憶體。如果核心殺死的第一個程序就是 docker daemon 會怎麼樣?結果是所有的容器都不工作了,這是不能接受的!
針對這個問題,docker 嘗試通過調整 docker daemon 的 OOM 優先順序來進行緩解。核心在選擇要殺死的程序時會對所有的程序打分,直接殺死得分最高的程序,接著是下一個。當 docker daemon 的 OOM 優先順序被降低後(注意容器程序的 OOM 優先順序並沒有被調整),docker daemon 程序的得分不僅會低於容器程序的得分,還會低於其它一些程序的得分。這樣 docker daemon 程序就安全多了。
我們可以通過下面的指令碼直觀的看一下當前系統中所有程序的得分情況:
#!/bin/bash
forprocin$(find /proc -maxdepth 1 -regex'/proc/[0-9]+');do
printf"%2d %5d %s\n"\
"$(cat $proc/oom_score)"\
"$(basename $proc)"\
"$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done2>/dev/null | sort -nr | head -n 40
此指令碼輸出得分最高的 40 個程序,並進行了排序:

第一列顯示程序的得分,mysqld 排到的第一名。顯示為 node server.js 的都是容器程序,排名普遍比較靠前。紅框中的是 docker daemon 程序,非常的靠後,都排到了 sshd 的後面。
有了上面的機制後是否就可以高枕無憂了呢!不是的,docker 的官方文件中一直強調這只是一種緩解的方案,並且為我們提供了一些降低風險的建議:
通過測試掌握應用對記憶體的需求
保證執行容器的主機有充足的記憶體
限制容器可以使用的記憶體
為主機配置 swap
好了,囉嗦了這麼多,其實就是說:通過限制容器使用的記憶體上限,可以降低主機記憶體耗盡時帶來的各種風險。
壓力測試工具 stress
為了測試容器的記憶體使用情況,筆者在 ubuntu 的映象中安裝了壓力測試工作 stress,並新建立了映象 u-stress。本文演示用的所有容器都會通過 u-stress 映象建立(本文執行容器的宿主機為 CentOS7)。下面是建立 u-stress 映象的 Dockerfile:
FROM ubuntu:latest
RUN apt-getupdate&& \
apt-getinstallstress
建立映象的命令為:
$ docker build -t u-stress:latest .
限制記憶體使用上限
在進入繁瑣的設定細節之前我們先完成一個簡單的用例:限制容器可以使用的最大記憶體為 300M。
-m(--memory=) 選項可以完成這樣的配置:
$ docker run -it -m300M --memory-swap-1--name con1 u-stress /bin/bash
下面的 stress 命令會建立一個程序並通過 malloc 函式分配記憶體:
# stress --vm 1 --vm-bytes 500M
通過 docker stats 命令檢視實際情況:

上面的 docker run 命令中通過 -m 選項限制容器使用的記憶體上限為 300M。同時設定 memory-swap 值為 -1,它表示容器程式使用記憶體的受限,而可以使用的 swap 空間使用不受限制(宿主機有多少 swap 容器就可以使用多少)。
下面我們通過 top 命令來檢視 stress 程序記憶體的實際情況:

上面的截圖中先通過 pgrep 命令查詢 stress 命令相關的程序,程序號比較大的那個是用來消耗記憶體的程序,我們就檢視它的記憶體資訊。VIRT 是程序虛擬記憶體的大小,所以它應該是 500M。RES 為實際分配的實體記憶體數量,我們看到這個值就在 300M 上下浮動。看樣子我們已經成功的限制了容器能夠使用的實體記憶體數量。
限制可用的 swap 大小
強調一下 --memory-swap 是必須要與 --memory 一起使用的。
正常情況下, --memory-swap 的值包含容器可用記憶體和可用 swap。所以 --memory="300m" --memory-swap="1g" 的含義為:
容器可以使用 300M 的實體記憶體,並且可以使用 700M(1G -300M) 的 swap。 --memory-swap 居然是容器可以使用的實體記憶體和可以使用的 swap 之和!
把 --memory-swap 設定為 0 和不設定是一樣的,此時如果設定了 --memory,容器可以使用的 swap 大小為 --memory 值的兩倍。
如果 --memory-swap 的值和 --memory 相同,則容器不能使用 swap。下面的 demo 演示了在沒有 swap 可用的情況下向系統申請大量記憶體的場景:
$ docker run -it --rm -m300M --memory-swap=300M u-stress /bin/bash
# stress --vm 1 --vm-bytes 500M

demo 中容器的實體記憶體被限制在 300M,但是程序卻希望申請到 500M 的實體記憶體。在沒有 swap 可用的情況下,程序直接被 OOM kill 了。如果有足夠的 swap,程式至少還可以正常的執行。
我們可以通過 --oom-kill-disable 選項強行阻止 OOM kill 的發生,但是筆者認為 OOM kill 是一種健康的行為,為什麼要阻止它呢?
除了限制可用 swap 的大小,還可以設定容器使用 swap 的緊迫程度,這一點和主機的 swappiness 是一樣的。容器預設會繼承主機的 swappiness,如果要顯式的為容器設定 swappiness 值,可以使用 --memory-swappiness 選項。
總結
通過限制容器可用的實體記憶體,可以避免容器內服務異常導致大量消耗主機記憶體的情況(此時讓容器重啟是較好的策略),因此可以降低主機記憶體被耗盡帶來的風險。
歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:860113481
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!