容器基礎-- namespace,Cgoup 和 UnionFS

2023-06-24 21:00:44

Namespace

什麼是 Namespace ?

這裡的 "namespace" 指的是 Linux namespace 技術,它是 Linux 核心實現的一種隔離方案。簡而言之,Linux 作業系統能夠為不同的程序分配不同的 namespace,每個 namespace 都具有獨立的資源分配,從而實現了程序間的隔離。如果你的 Linux 安裝了 GCC,可以通過執行 man namespaces 命令來檢視相關檔案,或者你也可以存取線上手冊獲取更多資訊。

介紹

下圖為各種 namespace 的引數,支援的起始核心版本,以及隔離內容。

Namespace 系統呼叫引數 核心版本 隔離內容
UTS (Unix Time-sharing System) CLONE_NEWUTS Linux 2.4.19 主機名與域名
IPC (Inter-Process Communication) CLONE_NEWIPC Linux 2.6.19 號誌、訊息佇列和共用記憶體
PID (Process ID) CLONE_NEWPID Linux 2.6.19 程序編號
Network CLONE_NEWNET Linux 2.6.24 網路裝置、網路棧、埠等等
Mount CLONE_NEWNS Linux 2.6.29 掛載點(檔案系統)
User CLONE_NEWUSER Linux 3.8 使用者和使用者組
  1. PID Namespace:

    • 不同使用者的程序通過 PID Namespace 進行隔離,並且不同的 Namespace 中可以有相同的程序 ID。在 Docker 中,所有的 LXC(Linux 容器)程序的父程序是 Docker 程序,每個 LXC 程序具有不同的 Namespace。由於支援巢狀 Namespace,因此可以方便地實現 Docker 中的 Docker(Docker in Docker)。
  2. Net Namespace:

    • 有了 PID Namespace,每個 Namespace 中的程序能夠相互隔離,但是網路埠仍然共用主機的埠。通過 Net Namespace 實現網路隔離,每個 Net Namespace 具有獨立的網路裝置、IP 地址、IP 路由表和 /proc/net 目錄。這樣,每個容器的網路就能夠得到隔離。Docker 預設使用 veth(虛擬乙太網)方式將容器中的虛擬網路卡與主機上的 Docker 橋接器(docker0)連線起來。
  3. IPC Namespace:

    • 容器中的程序仍然使用常見的 Linux 程序間通訊(IPC)方法,包括號誌、訊息佇列和共用記憶體。然而,與虛擬機器器不同的是,容器中的程序實際上是在具有相同 PID Namespace 的主機程序之間進行通訊,因此在申請 IPC 資源時需要加入 Namespace 資訊,每個 IPC 資源都有一個唯一的 32 位 ID。
  4. MNT Namespace:

    • 類似於 chroot,將程序限制在特定的目錄下執行。MNT Namespace 允許不同 Namespace 的程序看到不同的檔案結構,從而隔離了每個 Namespace 中程序所看到的檔案目錄。與 chroot 不同的是,每個 Namespace 中的容器在 /proc/mounts 中的資訊僅包含所在 Namespace 的掛載點。
  5. UTS Namespace:

    • UTS("UNIX Time-sharing System")Namespace 允許每個容器擁有獨立的主機名和域名,使其在網路上可以被視為一個獨立的節點,而不僅僅是主機上的一個程序。
  6. User Namespace:

    • 每個容器可以具有不同的使用者和組 ID,這意味著容器內部的程式可以使用容器內部的使用者執行,而不是主機上的使用者。

涉及到三個系統呼叫(system call)的 API:

  1. clone():用於建立新程序。與 fork() 建立新程序不同的是,clone() 建立程序時可以傳遞 CLONE_NEW* 型別的名稱空間隔離引數,以控制子程序共用的內容。要了解更多資訊,請查閱clone 手冊
  2. setns():用於將某個程序與指定的名稱空間分離。通過 setns(),程序可以脫離一個特定的名稱空間,使其不再與該名稱空間中的其他程序共用資源。
  3. unshare():用於將某個程序加入到指定的名稱空間中。通過 unshare(),程序可以加入到一個特定的名稱空間,與該名稱空間中的其他程序共用資源。

namespace 的操作

  • 檢視當前系統的 namespace
lsns –t <type>
  • 檢視某程序的 namespace
ls -la /proc/<pid>/ns/
  • 進入某 namespace 執行命令
nsenter -t <pid> -n ip addr

Test:

# Linux命令列中,可以使用`unshare`命令結合`clone()`建立一個新的程序,並在其中使用名稱空間隔離引數。
# 建立一個新的程序,並在其中使用名稱空間隔離引數
unshare --pid --net -- sleep 600

ps -ef|grep sleep
root       37915   34572  0 08:53 pts/1    00:00:00 sudo unshare --pid --net -- sleep 600
root       37916   37915  0 08:53 pts/3    00:00:00 sudo unshare --pid --net -- sleep 600
root       37917   37916  0 08:53 pts/3    00:00:00 sleep 600
zhy        37919   37896  0 08:53 pts/2    00:00:00 grep --color=auto sleep

sudo lsns -t net
[sudo] password for zhy:
        NS TYPE NPROCS   PID USER    NETNSID NSFS                           COMMAND
4026531840 net     277     1 root unassigned                                /sbin/init
4026532656 net       1 37347 root          0 /run/docker/netns/c986b82be683 bash
4026532718 net       1 37917 root unassigned                                sleep 600

sudo nsenter -t 37917 -n ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  1. docker 啟動一個 ubuntu
docker run --rm -it docker.m.daocloud.io/ubuntu:22.10 bash
  1. 用另一個視窗 找到這個程序
ps -ef|grep ubuntu
# zhy        37247   34017  0 08:20 pts/0    00:00:00 docker run --rm -it docker.m.daocloud.io/ubuntu:22.10 bash
  1. 檢視這個程序的 namespace
ls -la /proc/37247/ns/
total 0
dr-x--x--x 2 zhy zhy 0 May 27 08:24 .
dr-xr-xr-x 9 zhy zhy 0 May 27 08:23 ..
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 net -> 'net:[4026531840]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 time -> 'time:[4026531834]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 user -> 'user:[4026531837]'
lrwxrwxrwx 1 zhy zhy 0 May 27 08:24 uts -> 'uts:[4026531838]'
  1. 檢視namespace
sudo lsns -t pid
        NS TYPE NPROCS   PID USER COMMAND
4026531836 pid     275     1 root /sbin/init
4026532654 pid       1 37347 root bash

sudo lsns -t net
        NS TYPE NPROCS   PID USER    NETNSID NSFS                           COMMAND
4026531840 net     275     1 root unassigned                                /sbin/init
4026532656 net       1 37347 root          0 /run/docker/netns/c986b82be683 bash

為什麼查出來執行 bash 的 pid 和 ps -ef 的不一樣?

一個是docker run的程序 PID

一個是 容器內部 'bash' 程序的 PID 這個程序是由docker run的程序通過程序複製(process cloning)建立的子程序。

  1. 在 ubuntu 中執行 ip addr 在主機執行 nsenter -t <pid> -n ip addr
# 容器內
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
       
ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 08:23 pts/0    00:00:00 bash
root         360       1  0 09:12 pts/0    00:00:00 ps -ef

# 主機
sudo nsenter -t 37347 -n -- ip addr # -n 進入網路namespace執行
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
      
sudo nsenter -t 37347 -a -- ps -ef # -a 進入所有namespace執行
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 08:23 pts/0    00:00:00 bash
root         359       0  0 09:12 ?        00:00:00 ps -ef

Cgroup

什麼是 Cgroup

Linux cgroups 的全稱是 Linux Control Groups,它是 Linux 核心的特性,主要作用是限制、記錄和隔離行程群組(process groups)使用的物理資源(cpu、memory、IO 等)

為什麼要使用Cgroup?

可以做到對 cpu,記憶體等資源實現精細化的控制,容器技術就使用了 cgroups 提供的資源限制能力來完成cpu,記憶體等部分的資源控制。

核心概念

  • task:任務,對應於系統中執行的一個實體,一般是指程序
  • subsystem:子系統,具體的資源控制器(resource class 或者 resource controller),控制某個特定的資源使用。比如 CPU 子系統可以控制 CPU 時間,memory 子系統可以控制記憶體使用量
  • cgroup:控制組,一組任務和子系統的關聯關係,表示對這些任務進行怎樣的資源管理策略
  • hierarchy:層級有一系列 cgroup 以一個樹狀結構排列而成,每個層級通過繫結對應的子系統進行資源控制。層級中的 cgroup 節點可以包含零個或多個子節點,子節點繼承父節點掛載的子系統。一個作業系統中可以有多個層級。

subsystem

subsystem 是一組資源控制的模組,一般包含有:

  • blkio 設定對塊裝置 (比如硬碟) 的輸入輸出的存取控制 (block/io)
  • cpu 設定 cgroup 中的程序的 CPU 被排程的策略
  • cpuacct 可以統計 cgroup 中的程序的 CPU 佔用 (cpu account)
  • cpuset 在多核機器上設定 cgroup 中的程序可以使用的 CPU 和記憶體 (此處記憶體僅使用於 NUMA 架構)
  • devices 控制 cgroup 中程序對裝置的存取
  • freezer 用於掛起 (suspends) 和恢復 (resumes) cgroup 中的程序
  • memory 用於控制 cgroup 中程序的記憶體佔用
  • net_cls 用於將 cgroup 中程序產生的網路包分類 (classify),以便 Linux 的 tc (traffic controller) (net_classify) 可以根據分類 (classid) 區分出來自某個 cgroup 的包並做限流或監控。
  • net_prio 設定 cgroup 中程序產生的網路流量的優先順序
  • ns 這個 subsystem 比較特殊,它的作用是 cgroup 中程序在新的 namespace fork 新程序 (NEWNS) 時,建立出一個新的 cgroup,這個 cgroup 包含新的 namespace 中程序。

v2

Cgroup v2手冊

是否載入了Cgroup v2核心模組

cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

test

Cpu

執行一段go程式碼

package main

func main() {
    go func() { for{} }()
    for {}
}

/*
執行 go run test.go
top
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  39268 zhy       20   0  709572    868    584 R 200.0   0.0   2:12.27 test
  
  可以看到使用了2個cpu 因為開個兩個goroutine for阻塞
*/

限制cpu

sudo mkdir /sys/fs/cgroup/test
sudo echo "100000 100000" | sudo tee /sys/fs/cgroup/test/cpu.max >/dev/null
sudo echo "39268" | sudo tee /sys/fs/cgroup/test/cgroup.procs >/dev/null

# top
#    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
#  39268 zhy       20   0  709572    868    584 R 100.3   0.0   7:45.04 test
# 馬上就只佔用一個cpu了
Memory
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define BLOCK_SIZE (100 * 1024 * 1024)
#define NUM_ALLOCATIONS 10
#define SLEEP_SECONDS 30

char* allocMemory(int size) {
    char* out = (char*)malloc(size);
    memset(out, 'A', size);
    return out;
}

int main() {
    int i;

    for (i = 1; i <= NUM_ALLOCATIONS; i++) {
        char* block = allocMemory(i * BLOCK_SIZE);
        printf("Allocated memory block of size %dMB at address: %p\n", i * 100, block);
        sleep(SLEEP_SECONDS);
    }

    return 0;
}

/*
 ps -p 3243 -o rss=,unit=M,cmd=
      M
308512 session-4.scope                ./test2
*/

限制記憶體

sudo echo "300000000" |sudo tee /sys/fs/cgroup/test/memory.max >/dev/null
sudo echo "64417" | sudo tee /sys/fs/cgroup/test/cgroup.procs >/dev/null

#cat memory.current
#299839488

UnionFS

聯合檔案系統(UnionFS)是一種分層、輕量級並且高效能的檔案系統,它支援對檔案系統的修改作為一次提交來一層層的疊加,同時可以將不同目錄掛載到同一個虛擬檔案系統下 (unite several directories into a single virtual filesystem)。

聯合檔案系統是 Docker 映象的基礎。映象可以通過分層來進行繼承,基於基礎映象(沒有父映象),可以製作各種具體的應用映象。

另外,不同 Docker 容器就可以共用一些基礎的檔案系統層,同時再加上自己獨有的改動層,大大提高了儲存的效率。

最新版 Docker 使用的是 overlay2。

overlay2

現在主流基本都是 overlayFS

OverlayFS 屬於檔案級的儲存驅動,包含了最初的 Overlay 和更新更穩定的 overlay2。

Overlay 只有兩層:upper 層和 lower 層,Lower 層代表映象層,upper 層代表容器可寫層。

test

mkdir test && cd test
mkdir upper lower merged work
echo "file1 from lower" > lower/file1.txt
echo "file2 from lowerr" > lower/file2.txt
echo "file3 from lower" > lower/file3.txt
echo "file2 from upper" > upper/file2.txt
echo "file4 from upper" > upper/file4.txt

current_dir=$(pwd)
sudo mount -t overlay -o lowerdir="$current_dir/lower",upperdir="$current_dir/upper",workdir="$current_dir/work" overlay "$current_dir/merged"

cat merged/file1.txt
file1 from lower
cat merged/file2.txt
file2 from upper
cat merged/file3.txt
file3 from lower
cat merged/file4.txt
file4 from upper

docker iamge

每一條指令是一層, 下層可以共用

Docker 的檔案系統

典型的Linux檔案系統組成如下:

  • Bootfs(引導檔案系統)
    • Bootloader(引導載入程式):負責載入核心。
    • Kernel(核心):一旦核心載入到記憶體中,就會解除安裝bootfs。
  • Rootfs(根檔案系統)
    • /dev、/proc、/bin、/etc等標準目錄和檔案。
    • 對於不同的Linux發行版,bootfs基本上是一致的,但rootfs會有所差異。

Docker 啟動

Linux

  • 在啟動後,首先將 rootfs 設定為 readonly, 進行一系列檢查,然後將其切換為 「readwrite」 供使用者使用。

Docker 啟動

  • 初始化時也是將 rootfs 以 readonly 方式載入並檢查,然而接下來利用 union mount 的方式將一個 readwrite 檔案系統掛載在 readonly 的 rootfs 之上;
  • 並且允許再次將下層的 FS(file system) 設定為 readonly 並且向上疊加。 這樣一組 readonly 和一個 writeable 的結構構成一個 container 的執行時態,每一個 FS 被稱作一個 FS 層。

寫操作

由於映象具有共用特性,所以對容器可寫層的操作需要依賴儲存驅動提供的寫時複製和用時分配機制,以此來 支援對容器可寫層的修改,進而提高對儲存和記憶體資源的利用率。

  • 寫時複製 即 Copy-on-Write。
    • 一個映象可以被多個容器使用,但是不需要在記憶體和磁碟上做多個拷貝。
    • 在需要對映象提供的檔案進行修改時,該檔案會從映象的檔案系統被複制到容器的可寫層的檔案系統 進行修改,而映象裡面的檔案不會改變。
    • 不同容器對檔案的修改都相互獨立、互不影響。
  • 用時分配
  • 按需分配空間,而非提前分配,即當一個檔案被建立出來後,才會分配空間。