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

小米運維發表於2019-07-16

本文講述了 關於容器隔離性的一個“坑”

正如Docker官方的口號:“Build once,Run anywhere,Configure once,Run anything”,Docker 被貼上了如下標籤:輕巧、秒級啟動、版本管理、可移植性等等,這些優點讓它出現之初就收到極大的關注。現在,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 限制,映象簽名機制等。

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地址: )。熟悉 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
~ #

使用LXCFS

由於習慣性等原因,在容器中使用 top、free 等命令仍然是一個較為普遍存在的需求,但是容器中的/proc、/sys目錄等還是掛載的宿主機目錄,有一個開源專案:LXCFS.LXCFS 是基於 FUSE 實現的一套使用者態檔案系統,使用 LXCFS,讓你在容器裡面繼續使用 top、free 等命令變成了可能。但需要注意,LXCFS 可能會存在很多問題,建議線上上環境先不要使用。

總結

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

這篇文章的分享就到這裡,希望對大家有所幫助。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559359/viewspace-2650851/,如需轉載,請註明出處,否則將追究法律責任。

相關文章