Heath Kang

容器虚拟化学习

“容器就是轻量虚拟机”是一个方便但不准确的比喻。虚拟机虚拟的是一台机器:虚拟硬件、独立内核、再到用户空间;Linux 容器主要虚拟的是一个进程看到的操作系统视图。它和宿主机共享内核,隔离和限制则来自一组内核机制。

这一区别决定了两个事实:容器启动很快、密度很高;但它并不天然构成和虚拟机同等级的安全边界。

先拆开一个容器:它仍是一组普通进程

启动一个容器后,在宿主机上依然能用 ps 看到其进程。不同之处在于,这些进程带着一组 namespace 和 cgroup 归属。前者回答“它能看见什么”,后者回答“它最多能使用什么”。

宿主机 kernel
 ├─ namespace:进程看到的 PID / 网络 / 挂载 / 用户等视图
 ├─ cgroup:CPU / 内存 / I/O / PIDs 等资源的计量与限制
 └─ 容器进程:应用 + 依赖 + root filesystem

常用 namespace 各自隔离一种视图:

namespace隔离的东西直接后果
PID进程号与进程树容器内的 PID 1 并不等于宿主机 PID 1
Mount挂载表容器可以有自己的根目录与挂载点
Network网卡、路由表、端口、协议栈两个容器都可以监听 8080
UTShostname/domainname容器有自己的主机名
IPC共享内存、消息队列、信号量IPC 不会默认跨容器泄露
UserUID/GID 映射容器内 root 可映射为宿主机非特权用户

这也是 nsenterunsharelsns 能工作的原因: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
  • 内存

相关文档