兩種隔離模型#

「跑別人的程式不要互相干擾」── 兩條路:

模型代表機制
虛擬機器KVM、Xen、VMware虛擬化整顆機器(virtual hardware),裡面跑完整 OS
容器Docker、LXC、k8s共用 host kernel,靠 namespace + cgroup 隔離
┌──────────────────────────────┐    ┌──────────────────────────────┐
│  Guest App │ Guest App        │    │  App   │ App   │ App         │
├────────────┼────────────────  │    ├────────┼───────┼─────────────┤
│  Guest OS  │ Guest OS         │    │  Container namespaces/cgroup │
├────────────┴──────────────── │    ├──────────────────────────────┤
│  Hypervisor (KVM/QEMU)       │    │  Host Kernel                 │
├──────────────────────────────┤    ├──────────────────────────────┤
│  Host OS / bare metal        │    │  Host OS / bare metal        │
├──────────────────────────────┤    ├──────────────────────────────┤
│  Hardware                    │    │  Hardware                    │
└──────────────────────────────┘    └──────────────────────────────┘
       虛擬化(VM)                          容器(Container)

VM 隔離強,但開銷大(每個 guest 一份 OS)。容器輕,但所有容器共享同一 kernel ── kernel bug 會影響所有容器。

KVM ── Linux 內建的 hypervisor#

KVM(Kernel-based Virtual Machine)把 Linux 變成 type-1 hypervisor。原理:

  1. CPU 硬體要支援虛擬化擴充(Intel VT-x、AMD-V、ARM VHE)
  2. KVM 模組載入後,Linux 取得對 VMX root mode 的控制
  3. userspace(QEMU)建立 VM、配置記憶體與 vCPU
  4. 進入 VM = CPU 切到 guest mode(VMX non-root),執行 guest code
  5. guest 執行某些指令(如 I/O port、CR 寫入)會觸發 VM exit,回到 KVM/QEMU 處理
QEMU (userspace)
   │
   ├─ create VM:    ioctl(KVM_CREATE_VM)
   ├─ alloc memory: ioctl(KVM_SET_USER_MEMORY_REGION)
   ├─ create vCPU:  ioctl(KVM_CREATE_VCPU)
   └─ run vCPU:     ioctl(KVM_RUN)  ─►  guest code 執行
                                          │
                              VM exit ◄───┘
                              處理(emulate I/O 等)
                              再進入 KVM_RUN

/dev/kvm 是 KVM 對 userspace 的介面,是個字元裝置。

virtio:半虛擬化#

純模擬硬體(如模擬 e1000 網卡)效能差 ── 每個 I/O 都觸發 VM exit。virtio 設計一組為虛擬化而生的介面:

  • 共享記憶體做傳輸(不需要 trap)
  • 環狀佇列(vring)批次處理
  • 一條 doorbell 通知 host 有新工作

guest driver virtio-netvirtio-blk 直接寫進共享 ring,host 端 vhost-net 在 kernel thread 處理。每秒幾百萬 PPS 不成問題。

容器是「行程 + 隔離 + 限制」#

容器不是 Linux 核心一個獨立功能,而是組合幾個既有機制:

元件提供
Namespaces隔離(看到不同的 PID、網路、檔案系統等)
Cgroups限制(CPU、記憶體、I/O 上限與計量)
Capabilities拆分 root 權限
Seccomp限制可呼叫的 syscall
Mount / chroot給容器自己的根檔案系統
Overlayfs不可變映像 + 可寫層

「容器」是這些東西組成的工程模式,不是核心物件。Docker、podman、containerd 都是 userspace 拼裝這些原語的工具。

Namespaces#

每個 namespace 隔離一類資源:

Namespace隔離
pid行程 ID 空間(容器內 init = PID 1)
mountmount table、看到不同的檔案系統階層
net網路裝置、IP、route、iptables
utshostname、domain name
ipcSystem V IPC、POSIX message queue
userUID / GID 對應
cgroup看到的 cgroup root
timeCLOCK_BOOTTIMECLOCK_MONOTONIC 偏移

幾個 syscall 操作 namespace:

clone(child_func, stack, CLONE_NEWPID | CLONE_NEWNET | ..., arg);
unshare(CLONE_NEWNET);          // 當前 task 進入新 namespace
setns(fd, CLONE_NEWPID);        // 加入既有 namespace(fd 來自 /proc/<pid>/ns/...)
# 看一個 process 在哪些 namespace
ls -l /proc/<pid>/ns/
# pid -> 'pid:[4026531836]'   數字相同 = 同一個 ns
# net -> 'net:[4026531840]'

# unshare 工具
unshare --pid --fork --mount-proc bash    # 進入新 PID + mount ns
echo $$       # 1   ← 你是這個 ns 裡的 init
ps aux        # 只看到這個 ns 內的行程

Cgroups#

控制群組(control groups)── 限制與計量資源。v1v2 是兩個世代的設計,v2 已成為主流:

/sys/fs/cgroup/                          ← v2 統一階層
├── cpu.max
├── memory.max
├── io.max
├── mygroup/
│   ├── cgroup.procs                     ← 把行程寫進這裡
│   ├── memory.max                       ← 限制記憶體
│   ├── cpu.max                          ← 限制 CPU
│   └── ...
# 建立 cgroup
mkdir /sys/fs/cgroup/mygroup
echo "100M" > /sys/fs/cgroup/mygroup/memory.max
echo "50000 100000" > /sys/fs/cgroup/mygroup/cpu.max   # 100ms 內最多 50ms

# 把 shell 加進去
echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs

# 之後所有 fork 出來的都受限制

控制器(controllers):

控制器限制什麼
cpuCPU 使用比例、weight
memoryRSS、kernel memory、swap、page cache
io磁碟讀寫速率
pidsfork 數量上限
cpuset可用的 CPU 與 NUMA node
hugetlbhuge page 用量
rdmaRDMA 資源
misc其他(GPU 等)

容器的根檔案系統:overlayfs#

Docker 把映像存成多層唯讀層 + 一個可寫層,靠 overlayfs 合成:

container's view (合成):
    /etc/   /usr/   /var/   /tmp/

upperdir (writable, 容器專屬):
    /var/log/myapp.log

lowerdir1 (image layer N, RO):
    /etc/    /usr/   /var/

lowerdir2 (image layer N-1, RO):
    /etc/    /usr/

lowerdir3 (base image, RO):
    /etc/    /usr/   /lib/

讀取時走訪 upper ➡️ lower1 ➡️ lower2 ➡️ … 找第一個存在的;寫入時若是 lower 的檔案,先複製到 upper(CoW)再修改。

mount -t overlay overlay \
  -o lowerdir=/lower1:/lower2,upperdir=/upper,workdir=/work \
  /merged

這就是 docker pull 為什麼省空間 ── 多個容器共用相同 base layer。

Capabilities:拆分 root#

傳統 UNIX 二元:root(uid=0)萬能、非 root 受限。Capabilities 把 root 權力切成 ~40 個能力:

Cap允許的事
CAP_NET_BIND_SERVICE綁定 < 1024 的 port
CAP_NET_ADMIN配置網路(iptables、route)
CAP_NET_RAW原始 socket(ping、tcpdump)
CAP_SYS_ADMIN一堆系統管理(mount 等)
CAP_SYS_BOOTreboot
CAP_DAC_OVERRIDE繞過 file 權限檢查

容器執行時通常 drop 大部分 capability,只保留必要:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimg

Seccomp:限制可呼叫的 syscall#

進一步約束容器:定義「只允許這些 syscall」。Docker 預設 profile 已封禁約 50 個高風險 syscall(rebootkexec_loadmount 等)。

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

實務上用 libseccomp 的高階 API,配合 JSON profile。

容器網路#

每個容器在自己的 net namespace ── 它有自己的 lo、自己的 route table。連到外網靠:

container ns                     host ns                   外網
┌────────────┐                  ┌────────────┐
│            │   veth pair      │            │
│  eth0 ─────────────────────── │ vethXXXX   │── docker0 bridge
│  10.0.0.2  │                  │            │      │
└────────────┘                  └────────────┘      │ NAT (iptables)
                                                    ▼
                                                physical NIC

兩端 veth pair 一邊在容器、一邊在 host,host 端接到 bridge,再 NAT 出去。Kubernetes 進一步把這做成 CNI 介面,各種網路 plugin(Calico、Cilium、Flannel)替換實作。

Cilium 用 eBPF 直接在 kernel 處理封包,繞過傳統 iptables ── 是雲原生網路的重要演進。

容器執行階段(runtime)的層次#

       Kubernetes
          │
          │ CRI (Container Runtime Interface)
          ▼
       containerd / CRI-O
          │
          │ OCI runtime spec
          ▼
       runc / crun / kata-runtime
          │
          ▼
       Linux kernel (namespace, cgroup, seccomp, capability)
  • runc:實際呼叫 syscall 建立容器(Go 寫的)
  • containerd:管映像、生命週期、log
  • k8s:叢集編排,透過 CRI 與 containerd 對話

OCI 規範把「容器是什麼」定義成 JSON 配置(runtime spec)+ tar 檔結構(image spec),讓不同實作可互通。

VM vs 容器的取捨#

維度VMContainer
啟動時間數十秒數百 ms
記憶體開銷數百 MB(OS)幾 MB
隔離強度強(不同 kernel)弱(共享 host kernel)
可跑不同 OS可(Linux 主機跑 Windows VM)不可
核心 bug 影響只影響該 VM影響所有容器
效能接近原生(virtio)原生(無 hypervisor 開銷)

混合方案 Kata Containers / Firecracker / gVisor:用輕量 VM 跑容器,兼顧隔離與啟動速度。AWS Lambda 用 Firecracker 在 ms 級啟動 microVM,每個函式執行都是獨立 VM。

觀測容器#

# cgroup 角度
systemd-cgls
systemd-cgtop
cat /sys/fs/cgroup/<group>/memory.current

# Docker 內建
docker stats
docker top <container>
docker exec <container> ps aux
docker inspect <container>

# 從 host 看容器行程
ps -ef | grep <name>
# 容器的 PID 在 host 看是隨機的,但在容器內 init = 1

# 進去容器的 namespace
nsenter -t <host-pid> -a

小結#

虛擬化兩個世界:

  1. VM(KVM/QEMU):硬體虛擬化,強隔離、跑任何 OS、開銷較大
  2. 容器:共享 kernel,靠 namespace 隔離 + cgroup 限制 + capabilities/seccomp 安全

容器是 Linux 既有功能的重組,沒有「container」這個 kernel 物件。Docker 的偉大不在於核心創新,而在於包裝:把 namespace + cgroup + overlayfs + 映像格式整合成 docker run 一行命令。

未來方向是兩者融合 ── 微 VM(Firecracker)與沙箱化容器(gVisor)模糊邊界,給最佳的隔離 / 效能取捨。