歡迎光臨
每天分享高質量文章

Docker容器實現原理及容器隔離性踩坑介紹

正如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實現原理,簡單總結如下:
  • 使用Namespaces實現了系統環境的隔離,Namespaces允許一個行程以及它的子行程從共享的宿主機核心資源(網路棧、行程串列、掛載點等)裡獲得一個僅自己可見的隔離區域,讓同一個Namespace下的所有行程感知彼此變化,對外界行程一無所知,彷彿執行在一個獨佔的作業系統中;

  • 使用CGroups限制這個環境的資源使用情況,比如一臺16核32GB的機器上只讓容器使用2核4GB。使用CGroups還可以為資源設定權重,計算使用量,操控任務(行程或執行緒)啟停等;

  • 使用映象管理功能,利用Docker的映象分層、寫時複製、內容定址、聯合掛載技術實現了一套完整的容器檔案系統及執行環境,再結合映象倉庫,映象可以快速下載和共享,方便在多環境部署。

正因為Docker不像虛機虛擬化一個Guest OS,而是利用宿主機的資源,和宿主機共用一個核心,所以會存在下麵問題:
註意:存在問題並不一定說就是安全隱患,Docker作為最重視安全的容器技術之一,在很多方面都提供了強安全性的預設配置,其中包括:容器root使用者的 Capability 能力限制,Seccomp系統呼叫過濾,Apparmor的 MAC 訪問控制,ulimit限制,pid-limits的支援,映象簽名機制等。
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行程。可以在使用docker 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原始碼實現(https://github.com/opencontainers/runc/tree/master/libcontainer/cgroups/fs)。
使用LXCFS

由於習慣性等原因,在容器中使用top、free等命令仍然是一個較為普遍存在的需求,但是容器中的/proc、/sys目錄等還是掛載的宿主機目錄,有一個開源專案:LXCFS。LXCFS是基於FUSE實現的一套使用者態檔案系統,使用LXCFS,讓你在容器裡面繼續使用free等命令變成了可能。註意,LXCFS目前只支援為容器生成下麵檔案:
/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

如果命令是透過解析這些檔案實現,那麼在容器裡面可以繼續使用,否則只能透過讀取CGroups獲取資源情況。
總結

容器給大家帶來了很多便利,很多公司已經或正在把業務往容器上遷移。在遷移過程中,需要清楚上面介紹的這個問題是不是會影響應用的正常執行,並採取相應的辦法繞過這個坑。
已獲得原創公眾號:小米生態雲授權,點選檢視原文,如需轉載請聯絡原作者。

贊(0)

分享創造快樂