课程下半场,主角换人。容器经常被叫「轻量级虚拟机」,这个叫法方便但误导:容器不是把 VM 做小了,而是压根没有虚拟化硬件,它只是「被关起来看、被限制用」的普通 Linux 进程。本章先把这个概念扶正,再拆开容器的两大内核机关(namespace 和 cgroup,操作系统课第 10 章预告过的),最后用一条命令亲手摸一下隔离。
把两种技术画在同一张图上,差异一目了然:
VM: [App] [App] 容器: [App] [App] [App]
[Guest OS 内核] [共享宿主机内核]
[Hypervisor] [容器引擎(只是普通进程的管理层)]
[宿主 OS] [宿主 OS]
[硬件] [硬件]
VM 每台带一个完整内核(几 GB 起、开机几十秒、隔离硬);容器共享宿主内核,没有 guest OS 的开销(启动按秒甚至毫秒计、开销近乎零),代价是隔离变软:所有容器和宿主共用一个内核,内核出洞全完蛋,容器里的系统调用直接打到宿主内核上。另一个实际后果:容器里不能跑另一个内核(Linux 容器里装不了 Windows,VM 可以);反过来,容器对宿主内核版本敏感(老内核跑不了新特性)。
选型口径沿用第 1 章:要强隔离、跑不同内核、按整机粒度分配,VM;要轻、快、密度高、同一内核下的进程级隔离,容器。云厂商的标配是两层都用(VM 里跑容器)。
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 做的事,本质就是「用代码把你刚才手动做的事全套一遍,再加上文件系统和网络」。
cgroup(控制组)把进程按组管理,限制每组能用的资源:CPU 份额/核数、内存上限(超了触发 OOM killer,操作系统课第 7 章的老熟人)、磁盘 IO、进程数。docker run --memory=512m --cpus=1 的每个限制,最终都落在 cgroup 的几个文件上(cgroup v2 直接在 /sys/fs/cgroup/ 下)。现代 cgroup v2 统一了层级,systemd 和容器运行时都基于它。
namespace 管「看见」,cgroup 管「配额」,两者合体 = 容器的全部内核基础。剩下的(镜像分层、仓库、网络)都是用户态工程,Docker 了不起的地方在于把这些散件拧成了一个顺手的工具链。
2013 年 Docker 开源,把「Linux 已有多年的 namespace/cgroup」包装成一条 docker run 就能用的体验,容器瞬间出圈;2015 年 OCI 开放标准统一镜像与运行时格式;2014 年起 Kubernetes 横空出世,管起成千上万容器(编排),容器从单机工具升级为云平台地基。
典型场景按频率排:CI/CD(每次构建一个干净环境,测完即弃)、微服务部署(一个服务一个容器,独立升级回滚)、开发环境标准化(「在我机器上是好的」终结者)、密度敏感的多租户(一台物理机几百个服务)。判断口径延续 7.1:进程级隔离够用、追求密度和速度,容器;要内核级隔离和异构系统,VM。
lsns | head,对比 PID 1 和你自己 shell 的 namespace ID;用 readlink /proc/$$/ns/pid 看 namespace 的「身份证」。