容器虚拟化学习
“容器就是轻量虚拟机”是一个方便但不准确的比喻。虚拟机虚拟的是一台机器:虚拟硬件、独立内核、再到用户空间;Linux 容器主要虚拟的是一个进程看到的操作系统视图。它和宿主机共享内核,隔离和限制则来自一组内核机制。
这一区别决定了两个事实:容器启动很快、密度很高;但它并不天然构成和虚拟机同等级的安全边界。
先拆开一个容器:它仍是一组普通进程
启动一个容器后,在宿主机上依然能用 ps 看到其进程。不同之处在于,这些进程带着一组 namespace 和 cgroup 归属。前者回答“它能看见什么”,后者回答“它最多能使用什么”。
宿主机 kernel
├─ namespace:进程看到的 PID / 网络 / 挂载 / 用户等视图
├─ cgroup:CPU / 内存 / I/O / PIDs 等资源的计量与限制
└─ 容器进程:应用 + 依赖 + root filesystem
常用 namespace 各自隔离一种视图:
| namespace | 隔离的东西 | 直接后果 |
|---|---|---|
| PID | 进程号与进程树 | 容器内的 PID 1 并不等于宿主机 PID 1 |
| Mount | 挂载表 | 容器可以有自己的根目录与挂载点 |
| Network | 网卡、路由表、端口、协议栈 | 两个容器都可以监听 8080 |
| UTS | hostname/domainname | 容器有自己的主机名 |
| IPC | 共享内存、消息队列、信号量 | IPC 不会默认跨容器泄露 |
| User | UID/GID 映射 | 容器内 root 可映射为宿主机非特权用户 |
这也是 nsenter、unshare、lsns 能工作的原因:namespace 不是 Docker 私有魔法,而是 Linux 内核对象。
cgroup 不是“资源隔离”这么简单
cgroup 同时做三件事:记账、限制、调度。例如 memory cgroup 限制的是该组可用内存,而不是让进程永远优雅地得到 ENOMEM;超限时内核会先回收,最后可能发生 cgroup OOM。CPU limit 通常意味着 CFS 配额,结果可能是 throttle 而不是“最多占 N 个核”的直观体验。I/O 限制又要看底层设备与控制器是否支持。
因此排查容器“卡顿”时,不能只看应用 CPU:应同时看 CPU throttling、memory working set、OOM event、PIDs 上限和 I/O wait。给容器设置 limit 是一种调度与故障隔离策略,不是性能保证书。
镜像不是容器,rootfs 也不是完整操作系统
镜像是一组按层组织的文件系统变更;运行容器时,运行时把只读层和一个可写层组合为 rootfs。容器里看似有 /bin/sh、/usr/lib,但并没有自己的 kernel。FROM ubuntu 带来的是用户空间文件,不是 Ubuntu 的内核。
这解释了几个常见现象:
- 容器能否运行取决于宿主机 kernel 能否提供所需系统调用;
- 镜像层适合分发和复用,不适合作为可写数据盘;
- 挂载 volume 才是把持久化数据从容器生命周期中分离出来的正确方式。
到 Kubernetes:Pod 是共享部分隔离的边界
Kubernetes 没有把“一个容器”作为最小运行单位,而是选择了 Pod。一个 Pod 先由 runtime 建立 sandbox,再启动业务容器;同 Pod 中的容器共享网络 namespace,因此共享 IP 和 port space,也可共享定义好的 volume。所谓 pause/sandbox 容器的意义,就在于让网络与生命周期锚定在 Pod,而不是某一个短命业务容器上。
Node
└─ Pod sandbox(网络 namespace)
├─ app container
└─ sidecar container
这也给出设计准则:紧密耦合、必须共享 localhost 或卷的进程放同一个 Pod;需要独立扩缩、独立故障域和独立发布节奏的组件,应该是不同 Pod。
容器安全从来不只靠“隔离”
共享内核意味着特权容器、hostPath、hostNetwork、过宽的 Linux capability 和 root 用户映射都可能穿透原本的边界。实际部署中应优先采用非 root 用户、只读根文件系统、最小 capability、资源限制和受限的 volume;有更强隔离需求时,再考虑 gVisor、Kata Containers 或 VM 类运行时。
References
Notion 相关零散笔记
- Docker
- 虚拟化
- Filesystem
- Storage
- Kubernetes
- 内存