1. 程式人生 > >Docker容器實現原理及容器隔離性踩坑介紹

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

正如Docker官方的口號:“Build once,Run anywhere,Configure once,Run anything”,Docker被貼上了如下標籤:輕巧、秒級啟動、版本管理、可移植性等等,這些優點讓它出現之初就收到極大的關注。現在,Docker已經不僅僅是開發測試階段使用的工具,大家已經在生產環境中大量使用。今天我們給大家介紹關於容器隔離性的一個“坑”。在此之前,我們先來回顧一下Docker容器的底層實現原理。

容器底層實現

我們都知道,虛擬機器與容器的底層實現原理是不同的,正如下圖對比:

vm_container_archs.png


虛擬機器實現資源隔離的方法是利用一個獨立的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原始碼實現

使用LXCFS

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

/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime


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

總結

容器給大家帶來了很多便利,很多公司已經或正在把業務往容器上遷移。在遷移過程中,需要清楚上面介紹的這個問題是不是會影響應用的正常執行,並採取相應的辦法繞過這個坑。

這篇文章的分享就到這裡,希望對大家有所幫助,歡迎與小米生