兩種隔離模型#
「跑別人的程式不要互相干擾」── 兩條路:
| 模型 | 代表 | 機制 |
|---|---|---|
| 虛擬機器 | 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。原理:
- CPU 硬體要支援虛擬化擴充(Intel VT-x、AMD-V、ARM VHE)
- KVM 模組載入後,Linux 取得對 VMX root mode 的控制
- userspace(QEMU)建立 VM、配置記憶體與 vCPU
- 進入 VM = CPU 切到 guest mode(VMX non-root),執行 guest code
- 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-net、virtio-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) |
mount | mount table、看到不同的檔案系統階層 |
net | 網路裝置、IP、route、iptables |
uts | hostname、domain name |
ipc | System V IPC、POSIX message queue |
user | UID / GID 對應 |
cgroup | 看到的 cgroup root |
time | CLOCK_BOOTTIME、CLOCK_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)── 限制與計量資源。v1 與 v2 是兩個世代的設計,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):
| 控制器 | 限制什麼 |
|---|---|
| cpu | CPU 使用比例、weight |
| memory | RSS、kernel memory、swap、page cache |
| io | 磁碟讀寫速率 |
| pids | fork 數量上限 |
| cpuset | 可用的 CPU 與 NUMA node |
| hugetlb | huge page 用量 |
| rdma | RDMA 資源 |
| 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_BOOT | reboot |
| CAP_DAC_OVERRIDE | 繞過 file 權限檢查 |
容器執行時通常 drop 大部分 capability,只保留必要:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimgSeccomp:限制可呼叫的 syscall#
進一步約束容器:定義「只允許這些 syscall」。Docker 預設 profile 已封禁約 50 個高風險 syscall(reboot、kexec_load、mount 等)。
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 容器的取捨#
| 維度 | VM | Container |
|---|---|---|
| 啟動時間 | 數十秒 | 數百 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小結#
虛擬化兩個世界:
- VM(KVM/QEMU):硬體虛擬化,強隔離、跑任何 OS、開銷較大
- 容器:共享 kernel,靠 namespace 隔離 + cgroup 限制 + capabilities/seccomp 安全
容器是 Linux 既有功能的重組,沒有「container」這個 kernel 物件。Docker 的偉大不在於核心創新,而在於包裝:把 namespace + cgroup + overlayfs + 映像格式整合成 docker run 一行命令。
未來方向是兩者融合 ── 微 VM(Firecracker)與沙箱化容器(gVisor)模糊邊界,給最佳的隔離 / 效能取捨。