结论: 容器虚拟化的是操作系统层——同一台宿主机上的所有容器共享同一个 Linux 内核,仅通过 Namespace 做视图隔离、cgroup 做资源限额;虚拟机虚拟化的是硬件层——每台虚机都有独立的 Guest OS 与内核。因此容器启动快、体积小、密度高,而虚拟机的隔离强度远高于容器。一句话选择原则:需要运行不可信代码或不同内核版本,用虚拟机;跑无状态服务并追求迭代速度,用容器。
很多人第一次接触 Docker 时,会把它理解成"轻量版的虚拟机"。这个理解在日常使用中够用,但当你开始思考"能不能把数据库放进容器""多租户业务能不能共用一个宿主""容器被攻破会不会连累宿主机"这类问题时,这个理解就会给出错误答案。
容器不是"小虚拟机",它是"被隔离和限制了的一组进程"。 这句话后面会反复印证。
隔离层级:容器共享宿主内核,虚拟机拥有独立内核
结论前置:两者的根本差异在于虚拟化发生在哪一层——容器在操作系统层做隔离,虚拟机在硬件层做隔离。
- 虚拟机(Virtual Machine,VM):由 Hypervisor(如 KVM、VMware ESXi)虚拟出 CPU、内存、磁盘、网卡等硬件,虚拟机里跑的是完整的客户机操作系统(Guest OS),拥有自己的内核。宿主机内核与客户机内核互不知情,客户机内核崩溃不会直接影响宿主机。
- 容器(Container):宿主机只有一个内核,容器里的进程是宿主机内核上的普通进程,只是被 Namespace 限制了"能看到什么",被 cgroup 限制了"能用多少"。容器内没有内核,也不需要启动内核。
由此推出三条重要推论:
- 容器不能运行与宿主机内核不兼容的东西。 在 Linux 宿主机上跑 Windows 容器是做不到的(Windows 容器需要 Windows 宿主或 Hyper-V 隔离),容器内也不能加载宿主机没有的内核模块。
- 容器的内核版本就是宿主机的内核版本。
uname -r在容器内外看到的是同一个值,这是判断自己是否在容器里的常用方法之一。 - 容器的攻击面最终指向同一个内核。 所有容器共享这一个内核,一旦出现内核漏洞被利用,隔离就被击穿——这就是后文要讲的容器逃逸。
Linux 容器靠什么实现隔离?Namespace 与 cgroup
结论前置:Namespace 决定"能看到什么",cgroup 决定"能用多少",两者叠加再加上 capabilities、seccomp 等机制,构成了容器的隔离模型。
Linux 内核常见 Namespace 类型及其作用:
- Mount(mnt):隔离挂载点,容器看到自己的根文件系统。
- PID:隔离进程号,容器内 PID 1 是自己的 init 进程,看不到宿主机进程树。
- Network(net):隔离网络栈,容器拥有独立的网卡、路由表、iptables 规则与端口空间。
- UTS:隔离主机名与域名,容器可以拥有自己的 hostname。
- IPC:隔离 System V IPC 与 POSIX 消息队列。
- User(user):隔离用户与 UID 映射,是最重要的"根权限隔离"手段。
- Cgroup(cgroup ns):隔离 cgroup 层级视图,让容器看不到宿主机的控制组结构。
cgroup 的作用则是给容器套上资源上限,常见的有 CPU(cpu.max)、内存(memory.max)、进程数(pids.max)、块设备 I/O 限速等。注意:容器默认并不会自动限资源,如果不显式设置,一个容器可以把宿主机内存跑满,进而触发系统 OOM Killer 把宿主机上的其他进程杀掉——包括同机的另一个核心业务容器。
核心差异对照表:容器 vs 虚拟机
结论前置:这张表覆盖了选型时真正需要比较的九个维度,从启动到持久化一目了然。
| 对比项 | 容器 | 虚拟机 |
|---|---|---|
| 虚拟化层级 | 操作系统层(共享宿主内核) | 硬件层(Hypervisor 虚拟 CPU/内存/设备) |
| 是否有独立内核 | 否,与宿主机共享同一个 Linux 内核 | 是,每台虚机有独立 Guest OS 与内核 |
| 启动速度 | 亚秒~数秒(本质是启动进程) | 数十秒~数分钟(要引导完整操作系统) |
| 镜像体积 | 常见 20 MB ~ 500 MB( distroless/alpine 可到几十 MB) | 常见 500 MB ~ 十数 GB(含操作系统与更新) |
| 资源开销 | 接近裸进程,主要是进程自身内存 | 每台虚机自带一份 Guest OS 内存,叠加 Hypervisor 开销 |
| 部署密度 | 单台宿主常见几十到上百个容器 | 单台宿主常见几台到几十台虚机 |
| 隔离强度 | 较弱,共享内核是其上限 | 强,内核级别天然隔离 |
| 可移植性 | 强(镜像一次构建处处运行),但要求宿主内核 ABI 兼容 | 中等,需在不同虚拟化平台间做格式/驱动适配 |
| 持久化 | 容器层天然临时,数据必须挂在卷(Volume)或外部存储上 | 虚机磁盘天然持久,重启后数据仍在 |
| 安全边界 | 依赖 Namespace/cgroup/capabilities/seccomp 等多层机制,存在逃逸风险 | 依赖 Hypervisor,逃逸难度显著更高 |
一句话记忆:虚拟机是"房子里再盖一栋楼",容器是"同一栋楼里分隔出的房间"。 房间的墙没有楼那么厚,但造价低、拆改快。
上手验证:亲眼看到隔离边界
结论前置:三条命令就能验证"容器是进程"这件事——查 PID、看 Namespace、读 cgroup 限额。 下面以 Ubuntu 24.04 LTS + Docker Engine 为例。
# 起一个常驻容器
docker run -d --name ngx --restart unless-stopped -p 8080:80 nginx:1.27-alpine
# 关键一步:查出容器主进程在宿主机上的真实 PID
docker inspect --format '{{.State.Pid}}' ngx
# 输出比如 24578
# 在宿主机进程树里能直接看到它(容器并不"消失",只是被隔离了视图)
ps -p "$(docker inspect --format '{{.State.Pid}}' ngx)" -o pid,ppid,comm,etime
# 查看它所属的 Namespace;与宿主机 init 进程对比即可看出隔离
sudo ls -l /proc/24578/ns/
sudo ls -l /proc/1/ns/
# 用 nsenter 进入容器的网络命名空间,看到的是容器内部的网卡
sudo nsenter -t 24578 -n ip addr资源限额可以这样查看和验证(路径依赖 cgroup 版本与驱动,以下为 cgroup v2 + systemd 驱动的常见路径):
# 给正在运行的容器加上内存与 CPU 上限
docker update --memory=512m --memory-swap=512m --cpus=1.5 ngx
CID=$(docker inspect --format '{{.Id}}' ngx)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.max # 单位字节
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.max # 格式如 "150000 100000"
# 实时观察实际占用
docker stats --no-stream ngx如果 /sys/fs/cgroup/system.slice/docker-*.scope/ 不存在,说明宿主机使用了 cgroupfs 驱动或 cgroup v1,此时路径一般为 /sys/fs/cgroup/memory/docker/。这类路径差异,是"同一份容器在不同宿主机上行为不一致"的高频原因。
容器逃逸风险与安全边界:什么时候不该用容器
结论前置:容器的安全边界不是"墙",而是一组可以逐个被削弱的内核机制。默认配置下 Docker 容器的隔离强度不足以承载不可信代码,必须额外加固。
所谓容器逃逸(Container Escape),指的是容器内进程突破 Namespace/cgroup 限制,获得宿主机上的访问权限。常见成因有四类:
- 挂载了危险目录:
-v /:/host让容器可以读写整个宿主机文件系统,-v /var/run/docker.sock:/var/run/docker.sock更是等于把 Docker 控制权限交出去——拿到 socket 的进程可以直接创建特权容器接管宿主机。 - 使用
--privileged:特权容器几乎拥有宿主机的全部 capabilities,且能访问设备节点,逃逸难度大幅降低。 - 以 root 身份运行容器进程:容器内是 root 就意味着它是宿主机内核视角下的 root 进程(在未做 User Namespace 映射时),配合内核漏洞即可提权到宿主。
- 内核漏洞:因为共享内核,宿主机内核的提权漏洞(如历史上多个 cgroup、overlayfs 相关 CVE)会直接成为逃逸通道。
加固的标准动作如下,建议直接写进部署规范:
docker run -d --name app \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--security-opt seccomp=/etc/docker/seccomp/default.json \
--pids-limit 200 \
--memory=1g --cpus=2 \
--user 10001:10001 \
-v app-data:/data:rw \
-p 8000:8000 \
app:1.0镜像层面同样要做最小化与非 root:
# 多阶段构建:构建依赖不进运行镜像
FROM python:3.12-slim AS build
WORKDIR /src
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
# 运行阶段使用 distroless 非 root 镜像
FROM gcr.io/distroless/python3-debian12:nonroot
WORKDIR /app
COPY --from=build /deps /app/deps
COPY app.py /app/app.py
ENV PYTHONPATH=/app/deps
USER nonroot
EXPOSE 8000
ENTRYPOINT ["python", "/app/app.py"]如果业务确实需要运行不可信代码(在线评测、SaaS 多租户执行用户脚本、第三方插件环境),容器的隔离强度是不够的,应升级为下列之一:虚拟机沙箱(Kata Containers、Firecracker)、用户态内核(gVisor)、Rootless Docker/Podman 配合 User Namespace 映射,或者最稳妥的——直接用虚拟机。
各自适用场景:一张表定位该选谁
结论前置:不要让技术偏好决定选型,让"你要保护什么"和"你要迭代多快"决定。
| 场景 | 更适合 | 原因 |
|---|---|---|
| 微服务、无状态 Web、API 服务 | 容器 | 秒级扩缩、镜像一致、CI/CD 天然适配 |
| CI/CD 构建与测试环境 | 容器 | 用完即弃,干净可复现 |
| 需要不同内核版本或自定义内核模块 | 虚拟机 | 容器无法自带内核 |
| Windows/.NET Framework、Oracle 等传统闭源软件 | 虚拟机 | 依赖完整操作系统、注册表与特定内核行为 |
| 多租户 SaaS,租户代码互不可信 | 虚拟机或 Kata/gVisor | 共享内核的多租户风险过高 |
| 大型数据库、中间件(MySQL/Redis/Kafka) | 视情况,生产多用虚拟机或裸金属 | 追求稳定 IO 与可预期的内核参数,且避免重启丢状态 |
| 边缘/ARM、工控机资源受限环境 | 容器 | 体积小、常驻内存少 |
| 桌面虚拟化/VDI | 虚拟机 | 需要完整桌面操作系统体验 |
一个实用判据:如果你的服务重启后必须保留本机状态、或需要调优内核参数(sysctl)、加载模块、挂载特殊文件系统,那就用虚拟机;如果是无状态服务并且已经引入了 Kubernetes,容器是默认答案。
生产中最常见的形态:虚拟机上跑容器
结论前置:现实中的绝大多数容器,其实运行在虚拟机里——公有云的 ECS 本质就是虚拟机或轻量虚拟化实例,Node 上再跑容器运行时。 这不是妥协,而是取长补短。
典型分层:
- 第一层:物理机 + Hypervisor,切出虚拟机(通常是 KVM)。 提供强隔离,让不同业务、不同租户、不同环境各有边界。
- 第二层:虚拟机里跑容器运行时(containerd / Docker Engine)。 提供高效率与快速发布。
- 第三层:容器之上是编排(Kubernetes),负责调度、自愈、滚动发布、服务发现。
这样组合的好处很直接:虚拟机负责"安全边界",容器负责"交付效率"。 一个 Pod 出问题最多影响这个 Node,一个有问题的 Node 也不会影响同宿主机上其他业务的虚拟机。
代价也是真实的:多了一层资源损耗与管理复杂度,需要规划 Node 上的预留资源(--kube-reserved、--system-reserved)、合理设置 requests/limits、以及处理网络栈多一跳带来的排障复杂度(Node 上的 iptables/IPVS、CNI 插件、宿主机安全组三层要分别看)。
企业QQ咨询




