第 7 章 · 容器技术简介

课程下半场,主角换人。容器经常被叫「轻量级虚拟机」,这个叫法方便但误导:容器不是把 VM 做小了,而是压根没有虚拟化硬件,它只是「被关起来看、被限制用」的普通 Linux 进程。本章先把这个概念扶正,再拆开容器的两大内核机关(namespace 和 cgroup,操作系统课第 10 章预告过的),最后用一条命令亲手摸一下隔离。

7.1 VM 和容器:差一个内核

把两种技术画在同一张图上,差异一目了然:

VM:      [App] [App]           容器:    [App] [App] [App]
       [Guest OS 内核]              [共享宿主机内核]
       [Hypervisor]                 [容器引擎(只是普通进程的管理层)]
       [宿主 OS]                    [宿主 OS]
       [硬件]                       [硬件]

VM 每台带一个完整内核(几 GB 起、开机几十秒、隔离硬);容器共享宿主内核,没有 guest OS 的开销(启动按秒甚至毫秒计、开销近乎零),代价是隔离变软:所有容器和宿主共用一个内核,内核出洞全完蛋,容器里的系统调用直接打到宿主内核上。另一个实际后果:容器里不能跑另一个内核(Linux 容器里装不了 Windows,VM 可以);反过来,容器对宿主内核版本敏感(老内核跑不了新特性)。

选型口径沿用第 1 章:要强隔离、跑不同内核、按整机粒度分配,VM;要轻、快、密度高、同一内核下的进程级隔离,容器。云厂商的标配是两层都用(VM 里跑容器)。

7.2 Namespace:给进程戴上「只能看见自己」的眼罩

Linux 内核提供六七种命名空间(namespace),每种隔离一个维度的「世界观」:

Namespace隔离什么容器里的效果
PID进程编号容器里自己的 PID 从 1 开始(看不见宿主进程)
NET网络栈自己的网卡、IP、iptables、路由表
MNT挂载点自己的文件系统视图(根目录都不一样)
UTS主机名容器有自己的 hostname
IPC进程间通信信号量/共享内存互不干扰
USER用户映射容器里的 root 可以映射为宿主普通用户

关键认知:namespace 不限制资源,只改变「进程看见的世界」。一个容器里的进程可以看见一个空荡荡的 PID 空间,却把宿主 CPU 吃到 100%。管「能用多少」的是下一节的 cgroup。

亲手体验隔离,不用装 Docker,Linux 自带的 unshare 就行:

sudo unshare --pid --mount --uts --fork bash
hostname container-demo          # UTS:改主机名,只影响这个 namespace
ps aux                           # PID namespace:只看见 bash 和 ps 自己
echo $$                          # PID 是 1!这个进程以为自己住在孤岛
exit

ps 里看不到宿主任何进程、自己是 1 号进程,这就是容器里的进程每天的生活。Docker 做的事,本质就是「用代码把你刚才手动做的事全套一遍,再加上文件系统和网络」。

7.3 cgroup:配额管制

cgroup(控制组)把进程按组管理,限制每组能用的资源:CPU 份额/核数、内存上限(超了触发 OOM killer,操作系统课第 7 章的老熟人)、磁盘 IO、进程数。docker run --memory=512m --cpus=1 的每个限制,最终都落在 cgroup 的几个文件上(cgroup v2 直接在 /sys/fs/cgroup/ 下)。现代 cgroup v2 统一了层级,systemd 和容器运行时都基于它。

namespace 管「看见」,cgroup 管「配额」,两者合体 = 容器的全部内核基础。剩下的(镜像分层、仓库、网络)都是用户态工程,Docker 了不起的地方在于把这些散件拧成了一个顺手的工具链。

7.4 发展史一分钟与使用场景

2013 年 Docker 开源,把「Linux 已有多年的 namespace/cgroup」包装成一条 docker run 就能用的体验,容器瞬间出圈;2015 年 OCI 开放标准统一镜像与运行时格式;2014 年起 Kubernetes 横空出世,管起成千上万容器(编排),容器从单机工具升级为云平台地基。

典型场景按频率排:CI/CD(每次构建一个干净环境,测完即弃)、微服务部署(一个服务一个容器,独立升级回滚)、开发环境标准化(「在我机器上是好的」终结者)、密度敏感的多租户(一台物理机几百个服务)。判断口径延续 7.1:进程级隔离够用、追求密度和速度,容器;要内核级隔离和异构系统,VM。

练习

  1. 动手:用 unshare 分别试 --pid、--net、--uts、--mount 单独隔离,每次进新 shell 后跑 ps/ip a/hostname,记录「被隔离的那个维度」的真实表现;注意 --net 需要自己拉起 lo 和配置 IP,感受「网络 namespace 里的黑屋」。
  2. 动手:查 namespace:宿主机跑 lsns | head,对比 PID 1 和你自己 shell 的 namespace ID;用 readlink /proc/$$/ns/pid 看 namespace 的「身份证」。
  3. 动手:在 /sys/fs/cgroup(cgroup v2)里 mkdir 一个组,echo 写入 cpu.max 和 memory.max,把一个死循环进程塞进去,再用 top 观察它的 CPU 被压到限额,亲历 cgroup 管控。
  4. 思考:容器共享内核的「软隔离」出了名的安全事故是容器逃逸(内核漏洞直接打穿到宿主)。结合操作系统课第 1 章(用户态/内核态)和本章 namespace 的实现位置,解释为什么容器逃逸的危害远大于普通进程越权,以及「VM 里跑容器」(如 kata containers)是怎么折中的。