容器 = cgroup + namespace + rootfs + 容器引擎
- Cgroup: 資源控制
- namespace: 訪問隔離
- rootfs:文件系統隔離。鏡像的本質就是一個rootfs文件
- 容器引擎:生命周期控制
一、Cgroup
Cgroup 是 Control group 的簡稱,是 Linux 內核提供的一個特性,用於限制和隔離一組進程對系統資源的使用。對不同資源的具體管理是由各個子系統分工完成的。
Cgroup 可以對進程進行任意分組,如何分組由用戶自定義。
子系統介紹
- cpuset 子系統
cpuset 可以為一組進程分配指定的CPU和內存節點。 cpuset 一開始用在高性能計算上,在 NUMA(non-uniform memory access) 架構的服務器上,通過將進程綁定到固定的 CPU 和內存節點上,來避免進程在運行時因跨節點內存訪問而導致的性能下降。
cpuset 的主要接口如下:
- cpuset.cpus: 允許進程使用的CPU列表
- cpuset.mems: 允許進程使用的內存節點列表
- cpu 子系統
cpu 子系統用於限制進程的 CPU 利用率。具體支持三個功能
第一,CPU 比重分配。使用 cpu.shares 接口。
第二,CPU 帶寬限制。使用 cpu.cfs_period_us 和 cpu.cfs_quota_us 接口。
第三, 實時進程的 CPU 帶寬限制。使用 cpu_rt_period_us 和 cpu_rt_quota_us 接口。
- cpuacct 子系統
統計各個 Cgroup 的 CPU 使用情況,有如下接口:
- cpuacct.stat: 報告這個 Cgroup 在用戶態和內核態消耗的 CPU 時間,單位是 赫茲。
- cpuacct.usage: 報告該 Cgroup 消耗的總 CPU 時間。
- cpuacct.usage_percpu:報告該 Cgroup 在每個 CPU 上的消耗時間。
- memory 子系統
限制 Cgroup 所能使用的內存上限。
- memory.limit_in_bytes:設定內存上限,單位字節。
默認情況下,如果使用的內存超過上限,Linux 內核會試圖回收內存,如果這樣仍無法將內存降到限制的范圍內,就會觸發 OOM,選擇殺死該Cgroup 中的某個進程。 - memory.memsw,limit_in_bytes: 設定內存加上交換內存區的總量。
- memory.oom_control: 如果設置為0,那么內存超過上限時,不會殺死進程,而是阻塞等待進程釋放內存;同時系統會向用戶態發送事件通知。
- memory.stat: 報告內存使用信息。
- blkio
限制 Cgroup 對 阻塞 IO 的使用。
- blkio.weight: 設置權值,范圍在[100, 1000],屬於比重分配,不是絕對帶寬。因此只有當不同 Cgroup 爭用同一個 阻塞設備時才起作用
- blkio.weight_device: 對具體設備設置權值。它會覆蓋上面的選項值。
- blkio.throttle.read_bps_device: 對具體的設備,設置每秒讀磁盤的帶寬上限。
- blkio.throttle.write_bps_device: 對具體的設備,設置每秒寫磁盤的帶寬上限。
- blkio.throttle.read_iops_device: 對具體的設備,設置每秒讀磁盤的IOPS帶寬上限。
- blkio.throttle.write_iops_device: 對具體的設備,設置每秒寫磁盤的IOPS帶寬上限。
- devices 子系統
控制 Cgroup 的進程對哪些設備有訪問權限
-
devices.list: 只讀文件,顯示目前允許被訪問的設備列表,文件格式為
類型[a|b|c] 設備號[major:minor] 權限[r/w/m 的組合]
a/b/c 表示 所有設備、塊設備和字符設備。 -
devices.allow: 只寫文件,以上述格式描述允許相應設備的訪問列表。
-
devices.deny: 只寫文件,以上述格式描述禁止相應設備的訪問列表。
二、 Namespace
舉個例子,執行
sethostname
這個系統調用會改變主機名,這個主機名就是全局資源,內核通過
UTS Namespace可以將不同的進程分隔在不同的 UTS Namespace 中,
與命名空間相關的三個系統調用:
clone創建全新的Namespace,由clone創建的新進程就位於這個新的namespace里。創建時傳入 flags參數,可選值有 CLONE_NEWIPC, CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWUTS, CLONE_NEWUSER, 分別對應上面六種namespace。
unshare
為已有進程創建新的namespace。
setns
把某個進程放在已有的某個namespace里。
6種命名空間
-
UTS namespace
UTS namespace 對主機名和域名進行隔離。為什么要隔離主機名?因為主機名可以代替IP來訪問。如果不隔離,同名訪問會出沖突。 -
IPC namespace
Linux 提供很多種進程通信機制,IPC namespace 針對 System V 和 POSIX 消息隊列,這些 IPC 機制會使用標識符來區別不同的消息隊列,然后兩個進程通過標識符找到對應的消息隊列。
IPC namespace 使得 相同的標識符在兩個 namespace 代表不同的消息隊列,因此兩個namespace 中的進程不能通過 IPC 來通信。 -
PID namespace
隔離進程號,不同namespace 的進程可以使用相同的進程號。
當創建一個 PID namespace 時,第一個進程的PID 是1,即 init 進程。它負責回收所有孤兒進程的資源,所有發給 init 進程的信號都會被屏蔽。 -
Mount namespace
隔離文件掛載點,每個進程能看到的文件系統都記錄在/proc/$$/mounts
里。在一個 namespace 里掛載、卸載的動作不會影響到其他 namespace。 -
Network namespace
隔離網絡資源。每個 namespace 都有自己的網絡設備、IP、路由表、/proc/net 目錄、端口號等。網絡隔離可以保證獨立使用網絡資源,比如開發兩個web 應用可以使用80端口。
新創建的 Network namespace 只有 loopback 一個網絡設備,需要手動添加網絡設備。 -
User namespace
隔離用戶和用戶組。它的厲害之處在於,可以讓宿主機上的一個普通用戶在 namespace 里成為 0 號用戶,也就是 root 用戶。這樣普通用戶可以在容器內“隨心所欲”,但是影響也僅限在容器內。最后,回到 Docker 上,經過上述討論,namespace 和 cgroup 的使用很靈活,需要注意的地方也很多。 Docker 通過Libcontainer
來做這些臟活累活。用戶只需要使用 Docker API 就可以優雅地創建一個容器。docker exec
的底層實現就是上面提過的setns
。
三、rootfs
先來看一下,Linux 操作系統內核啟動時,內核會先掛載一個只讀的 rootfs,當系統檢測其完整性之后,決定是否將其切換到讀寫模式。
Docker 沿用這種思想,不同的是,掛載rootfs 完畢之后,沒有像 Linux 那樣將容器的文件系統切換到讀寫模式,而是利用 聯合掛載技術,
假設運行了一個 Ubuntu 鏡像,其文件系統簡略如下
