正如Docker官方的口號:「Build once,Run anywhere,Configure once,Run anything」,Docker被貼上了如下標籤:輕巧、秒級啟動、版本管理、可移植性等等,這些優點讓它出現之初就收到極大的關注。現在,Docker已經不僅僅是開發測試階段使用的工具,大家已經在生產環境中大量使用。今天我們給大家介紹關於容器隔離性的一個「坑」。在此之前,我們先來回顧一下Docker容器的底層實現原理。
容器底層實現
我們都知道,虛擬機與容器的底層實現原理是不同的,正如下圖對比:
虛擬機實現資源隔離的方法是利用一個獨立的Guest OS,並利用Hypervisor虛擬化CPU、內存、IO設備等實現的。例如,為了虛擬化內存,Hypervisor會創建一個shadow page table,正常情況下,一個page table可以用來實現從虛擬內存到物理內存的翻譯。相比虛擬機實現資源和環境隔離的方案,Docker就顯得簡練很多,它不像虛擬機一樣重新加載一個作業系統內核,引導、加載作業系統內核是一個比較耗時而又消耗資源的過程,Docker是利用Linux內核特性實現的隔離,運行容器的速度幾乎等同於直接啟動進程。
關於Docker實現原理,簡單總結如下:
正因為Docker不像虛機虛擬化一個Guest OS,而是利用宿主機的資源,和宿主機共用一個內核,所以會存在下面問題:
注意:存在問題並不一定說就是安全隱患,Docker作為最重視安全的容器技術之一,在很多方面都提供了強安全性的默認配置,其中包括:容器root用戶的 Capability 能力限制,Seccomp系統調用過濾,Apparmor的 MAC 訪問控制,ulimit限制,鏡像簽名機制等。
1、Docker是利用CGroups實現資源限制的,只能限制資源消耗的最大值,而不能隔絕其他程序占用自己的資源;
2、Namespace的6項隔離看似完整,實際上依舊沒有完全隔離Linux資源,比如/proc 、/sys 、/dev/sd*等目錄未完全隔離,SELinux、time、syslog等所有現有Namespace之外的信息都未隔離。
容器隔離性踩過的坑
在使用容器的時候,大家很可能遇到過這幾個問題:
1、在Docker容器中執行 top、free 等命令,會發現看到的資源使用情況都是宿主機的資源情況,而我們需要的是這個容器被限制了多少CPU,內存,當前容器內的進程使用了多少;
2、在容器里修改/etc/sysctl.conf,會收到提示」sysctl: error setting key 『net.ipv4….』: Read-only file system」;
3、程序運行在容器裡面,調用API獲取系統內存、CPU,取到的是宿主機的資源大小;
4、對於多進程程序,一般都可以將worker數量設置成auto,自適應系統CPU核數,但在容器裡面這麼設置,取到的CPU核數是不正確的,例如Nginx,其他應用取到的可能也不正確,需要進行測試。
這些問題的本質都一樣,在Linux環境,很多命令都是通過讀取/proc 或者 /sys 目錄下文件來計算資源使用情況,以free命令為例:
lynzabo@ubuntu:~$ strace free
execve("/usr/bin/free", ["free"], [/* 66 vars */]) = 0
...
statfs("/sys/fs/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
statfs("/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
open("/proc/filesystems", O_RDONLY) = 3
...
open("/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
...
open("/proc/meminfo", O_RDONLY) = 3
+++ exited with 0 +++
lynzabo@ubuntu:~$複製代碼
包括各個語言,比如Java,NodeJS,這裡以NodeJS為例:
const os = require('os');const total = os.totalmem();const free = os.freemem();const usage = (free - total) / total * 100;複製代碼
NodeJS的實現,也是通過讀取/proc/meminfo文件獲取內存信息。Java也是類似。
我們都知道,JVM默認的最大Heap大小是系統內存的1/4,假若物理機內存為10G,如果你不手動指定Heap大小,則JVM默認Heap大小就為2.5G。JavaSE8(<8u131) 版本前還沒有針對在容器內執行高度受限的Linux進程進行優化,JDK1.9以後開始正式支持容器環境中的CGroups內存限制,JDK1.10這個功能已經默認開啟,可以查看相關Issue (Issue地址:https://bugs.openjdk.java.net/browse/JDK-8146115 )。熟悉JVM內存結構的人都清楚,JVM Heap是一個只增不減的內存模型,Heap的內存只會往上漲,不會下降。在容器裡面使用Java,如果為JVM未設置Heap大小,Heap取得的是宿主機的內存大小,當Heap的大小達到容器內存大小時候,就會觸發系統對容器OOM,Java進程會異常退出。常見的系統日誌列印如下:
memory: usage 2047696kB, limit 2047696kB, failcnt 23543
memory+swap: usage 2047696kB, limit 9007199254740991kB, failcnt 0
......
Free swap = 0kB
Total swap = 0kB
......
Memory cgroup out of memory: Kill process 18286 (java) score 933 or sacrifice child複製代碼
對於Java應用,下面提供兩個辦法來設置Heap
1、對於JavaSE8(<8u131)版本,手動指定最大堆大小。
docker run的時候通過環境變量傳參確切限制最大heap大小:
docker run -d -m 800M -e JAVA_OPTIONS='-Xmx300m' openjdk:8-jdk-alpine複製代碼
2、對於JavaSE8(>8u131)版本,可以使用上面手動指定最大堆大小,也可以使用下面辦法,設置自適應容器內存限制。
docker run的時候通過環境變量傳參確切限制最大heap大小
docker run -d -m 800M -e JAVA_OPTIONS='-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1' openjdk:8-jdk-alpine複製代碼
對比這兩種方式,第一種方式缺乏靈活性,在確切知道內存限制大小的情況下可以使用,第二種方法必須在JavaSE8(>8u131)版本才能使用。
當你啟動一個容器時候,Docker會調用libcontainer實現對容器的具體管理,包括創建UTS、IPS、Mount等Namespace實現容器之間的隔離和利用CGroups實現對容器的資源限制,在其中,Docker會將宿主機一些目錄以只讀方式掛載到容器中,其中包括/proc、/dev、/dev/shm、/sys目錄,同時還會建立以下幾個連結:
保證系統IO不會出現問題,這也是為什麼在容器裡面取到的是宿主機資源原因。
了解了這些,那麼我們在容器里該如何獲取實例資源使用情況呢,下面介紹兩個方法。
從CGroups中讀取
Docker 在 1.8 版本以後會將分配給容器的CGroups信息掛載進容器內部,容器裡面的程序可以通過解析CGroups信息獲取到容器資源信息。
在容器裡面可以運行mount 命令查看這些掛載記錄
...
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (ro,nosuid,nodev,noexec,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
...複製代碼
在這裡我們不講解CGroups對CPU和內存的限制都有哪些,只介紹基於Kubernetes編排引擎下的計算資源管理,對容器CGroups都做了哪些支持:
Kubernetes 1.10以後支持為Pod指定固定CPU編號,我們在這裡不詳細介紹,就以常規的計算資源管理為主,簡單講一下以Kubernetes作為編排引擎,容器的CGroups資源限制情況:
1、讀取容器CPU核數
# 這個值除以100000得到的就是容器核數
~ # cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
400000複製代碼
2、獲取容器內存使用情況(USAGE / LIMIT)
~ # cat /sys/fs/cgroup/memory/memory.usage_in_bytes
4289953792
~ # cat /sys/fs/cgroup/memory/memory.limit_in_bytes
4294967296複製代碼
將這兩個值相除得到的就是內存使用百分比。
3、獲取容器是否被設置了OOM,是否發生過OOM
~ # cat /sys/fs/cgroup/memory/memory.oom_control
oom_kill_disable 0
under_oom 0
~ #
~ #複製代碼
這裡需要解釋一下:
4、獲取容器磁碟I/O
~ # cat /sys/fs/cgroup/blkio/blkio.throttle.io_service_bytes
253:16 Read 20015124480
253:16 Write 24235769856
253:16 Sync 0
253:16 Async 44250894336
253:16 Total 44250894336
Total 44250894336複製代碼
5、獲取容器虛擬網卡入/出流量
~ # cat /sys/class/net/eth0/statistics/rx_bytes
10167967741
~ # cat /sys/class/net/eth0/statistics/tx_bytes
15139291335
~ #複製代碼
如果你對從容器中讀取CGroups感興趣,可以點擊最下方「閱讀原文」了解docker stats源碼實現。
使用LXCFS
由於習慣性等原因,在容器中使用top、free等命令仍然是一個較為普遍存在的需求,但是容器中的/proc、/sys目錄等還是掛載的宿主機目錄,有一個開源項目:LXCFS。LXCFS是基於FUSE實現的一套用戶態文件系統,使用LXCFS,讓你在容器裡面繼續使用top、free等命令變成了可能。但需要注意,LXCFS可能會存在很多問題,建議在線上環境先不要使用。
總結
容器給大家帶來了很多便利,很多公司已經或正在把業務往容器上遷移。在遷移過程中,需要清楚上面介紹的這個問題是不是會影響應用的正常運行,並採取相應的辦法繞過這個坑。