正如Docker官方的口號:“Build once,Run anywhere,Configure once,Run anything”,Docker被貼上了如下標簽:輕巧、秒級啟動、版本管理、可移植性等等,這些優點讓它出現之初就收到極大的關注?,F在,Docker已經不僅僅是開發測試階段使用的工具,大家已經在生產環境中大量使用。今天我們給大家介紹關于容器隔離性的一個“坑”。在此之前,我們先來回顧一下Docker容器的底層實現原理。
容器底層實現
我們都知道,虛擬機與容器的底層實現原理是不同的,正如下圖對比:
虛擬機實現資源隔離的方法是利用一個獨立的Guest OS,并利用Hypervisor虛擬化CPU、內存、IO設備等實現的。例如,為了虛擬化內存,Hypervisor會創建一個shadow page table,正常情況下,一個page table可以用來實現從虛擬內存到物理內存的翻譯。相比虛擬機實現資源和環境隔離的方案,Docker就顯得簡練很多,它不像虛擬機一樣重新加載一個操作系統內核,引導、加載操作系統內核是一個比較耗時而又消耗資源的過程,Docker是利用Linux內核特性實現的隔離,運行容器的速度幾乎等同于直接啟動進程。
關于Docker實現原理,簡單總結如下:
使用Namespaces實現了系統環境的隔離,Namespaces允許一個進程以及它的子進程從共享的宿主機內核資源(網絡棧、進程列表、掛載點等)里獲得一個僅自己可見的隔離區域,讓同一個Namespace下的所有進程感知彼此變化,對外界進程一無所知,仿佛運行在一個獨占的操作系統中;
使用CGroups限制這個環境的資源使用情況,比如一臺16核32GB的機器上只讓容器使用2核4GB。使用CGroups還可以為資源設置權重,計算使用量,操控任務(進程或線程)啟停等;
使用鏡像管理功能,利用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目錄,同時還會建立以下幾個鏈接 :
/proc/self/fd->/dev/fd
/proc/self/fd/0->/dev/stdin
/proc/self/fd/1->/dev/stdout
/proc/self/fd/2->/dev/stderr
保證系統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都做了哪些支持:
當為Pod指定了requests,其中requests.cpu會作為--cpu-shares 參數值傳遞給docker run 命令,當一個宿主機上有多個容器發生CPU資源競爭時這個參數就會生效,參數值越大,越容易被分配到CPU,requests.memory不會作為參數傳遞給Docker,這個參數在Kubernetes的資源QoS管理時使用;
當為Pod指定了limits,其中limits.cpu會作為--cpu-quota 參數的值傳遞給docker run 命令,docker run命令中另外一個參數--cpu-period 默認設置為100000,通過這兩個參數限制容器最多能夠使用的CPU核數,limits.memory會作為--memory 參數傳遞給docker run 命令,用來限制容器內存,目前Kubernetes不支持限制Swap大小,建議在部署Kubernetes時候禁用Swap。
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 ~ # ~ #
這里需要解釋一下:
oom_kill_disable默認為0,表示打開了oom killer,就是當內存超時會觸發kill進程??梢栽谑褂胐ocker run時候指定disable oom,將此值設置為1,關閉oom killer;
under_oom 這個值僅僅是用來看的,表示當前的CGroups的狀態是不是已經oom了,如果是,這個值將顯示為1。
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可能會存在很多問題,建議在線上環境先不要使用。
容器給大家帶來了很多便利,很多公司已經或正在把業務往容器上遷移。在遷移過程中,需要清楚上面介紹的這個問題是不是會影響應用的正常運行,并采取相應的辦法繞過這個坑。
這篇文章的分享就到這里,希望對大家有所幫助。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。