第2章:理解容器(Understanding Containers)
一、导读
1.1 本章学习目标
在深入 Kubernetes 之前,本章先夯实容器技术的基础。Kubernetes 是容器编排系统,不理解容器就无法真正理解 K8s 的工作方式。本章从 Linux 内核的底层机制讲起,解释容器的本质——受限的 Linux 进程。然后介绍 Docker 镜像的分层架构、容器运行时的工作原理、以及 OCI 标准。
本章的核心学习目标包括:
- 理解容器的 Linux 内核基础:Namespace(隔离)、Cgroups(资源限制)、UnionFS(分层文件系统)三大核心技术。
- 区分容器与虚拟机:启动速度、资源占用、性能损耗的本质差异。
- 掌握 Docker 镜像的分层架构:UnionFS 的工作原理、写时复制(CoW)机制。
- 了解容器运行时生态:Docker、containerd、CRI-O 的关系和区别。
- 理解 OCI 标准:镜像规范(Image Spec)和运行时规范(Runtime Spec)。
- 熟练使用 Docker 构建和管理镜像:编写高效的 Dockerfile,使用多阶段构建。
1.2 为什么本章如此重要
很多 Kubernetes 用户只停留在"会用"的层面,不理解容器的底层机制。当遇到网络问题、存储问题或性能问题时,缺乏底层知识会让你束手无策。例如:
- 为什么容器重启后数据丢失?因为容器的可写层是临时的。
- 为什么容器之间可以网络隔离?因为 Network Namespace。
- 为什么容器比虚拟机更轻量?因为共享宿主机内核。
理解这些底层机制,你才能在 Kubernetes 中做出正确的架构决策:选择合适的网络模式、配置正确的存储方案、设置合理的资源限制。
1.3 核心问题引导
容器的本质是什么?它和虚拟机有什么根本区别?
Linux Namespace 隔离了哪些资源?每种 Namespace 的作用是什么?
Cgroups 如何限制容器的 CPU 和内存使用?
Docker 镜像的分层架构如何工作?为什么镜像可以共享层?
写时复制(CoW)机制如何提升容器的效率?
containerd 和 Docker 的关系是什么?为什么 K8s 不再直接使用 Docker?
二、核心概念详解
2.1 容器的本质:受限的 Linux 进程
容器的本质并不是什么神秘的技术——它就是一个普通的 Linux 进程,只是被限制在了一个"笼子"里。这个"笼子"由三个 Linux 内核特性构建:
- Namespace:让容器觉得自己是"独占"系统的(隔离)
- Cgroups:限制容器能使用的资源量(限制)
- UnionFS:为容器提供独立的文件系统(文件系统)
2.1.1 Linux Namespace
Namespace 是 Linux 内核提供的资源隔离机制。Kubernetes 使用以下 6 种 Namespace:
| Namespace | 系统调用参数 | 隔离内容 |
|---|---|---|
| PID | CLONE_NEWPID | 进程ID |
| Network | CLONE_NEWNET | 网络栈、IP地址、端口 |
| Mount | CLONE_NEWNS | 文件系统挂载点 |
| User | CLONE_NEWUSER | 用户和组ID |
| IPC | CLONE_NEWIPC | 进程间通信 |
| UTS | CLONE_NEWUTS | 主机名和域名 |
# 查看进程的 Namespace
ls -la /proc/$$/ns
# 输出示例:
# lrwxrwxrwx -> ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx -> mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx -> net -> 'net:[4026531992]'
# lrwxrwxrwx -> pid -> 'pid:[4026531836]'
# lrwxrwxrwx -> user -> 'user:[4026531837]'
# lrwxrwxrwx -> uts -> 'uts:[4026531838]'
# 在新的 Namespace 中运行进程
unshare --pid --fork --mount-proc bash
# 在这个 shell 中,PID 从 1 开始
echo $$ # 输出: 1PID Namespace:每个 PID Namespace 有自己的 PID 1(init 进程)。容器内的进程看到的 PID 和宿主机看到的不同。
Network Namespace:每个 Network Namespace 有独立的网络栈——独立的网卡、IP 地址、路由表、iptables 规则。这就是为什么容器有自己的 IP 地址。
Mount Namespace:每个 Mount Namespace 有独立的文件系统挂载视图。容器只能看到自己挂载的文件系统。
User Namespace:允许容器内的 root 用户映射为宿主机上的普通用户,提升安全性。
2.1.2 Cgroups(Control Groups)
Cgroups 限制进程能使用的资源量。主要的子系统:
- cpu:限制 CPU 使用率
- memory:限制内存使用量
- pids:限制进程数量
- blkio:限制块设备 I/O
- devices:控制设备访问
# 查看容器的 cgroup 配置
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes
# 使用 systemd-run 创建 cgroup
systemd-run --property=MemoryMax=512M --property=CPUQuota=50% /bin/bash
# Docker 的资源限制
docker run --cpus=1.5 --memory=512m --pids-limit=100 myapp2.1.3 UnionFS(联合文件系统)
UnionFS 将多个目录联合挂载到同一个虚拟文件系统。Docker 使用 UnionFS 实现镜像的分层存储。
常见的 UnionFS 实现:
- overlay2(默认):Linux 内核原生支持,性能最好
- aufs:早期的 Docker 默认驱动
- btrfs:支持快照和子卷
# 查看 Docker 使用的存储驱动
docker info | grep "Storage Driver"
# Storage Driver: overlay2
# 查看容器的层
docker inspect <container-id> --format '{{.GraphDriver.Data}}'2.2 容器 vs 虚拟机
| 特性 | 容器 | 虚拟机 |
|---|---|---|
| 抽象级别 | 操作系统级别(共享内核) | 硬件级别(独立内核) |
| 启动速度 | 秒级(< 1秒) | 分钟级(数十秒到数分钟) |
| 镜像大小 | 几十 MB | 几 GB |
| 性能损耗 | 接近原生 | 5-20%(Hypervisor 开销) |
| 密度 | 单机数百个容器 | 单机数十个 VM |
| 隔离性 | 进程级隔离(较弱) | 硬件级隔离(较强) |
| 操作系统 | 共享宿主机内核 | 独立内核 |
# 容器的资源占用
docker stats --no-stream
# CONTAINER CPU % MEM USAGE / LIMIT MEM %
# nginx 0.01% 5.2Mi / 7.7Gi 0.07%
# 对比虚拟机的资源占用
# 一个最小的 Linux VM 至少需要 128MB 内存 + 1GB 磁盘容器的核心优势在于共享内核:不需要为每个容器运行一个完整的操作系统,省去了内核启动和系统服务初始化的开销。但这也意味着容器的隔离性不如虚拟机——所有容器共享同一个内核,内核漏洞可能影响所有容器。
2.3 Docker 镜像分层架构
2.3.1 镜像层的概念
Docker 镜像由多个只读层(Layer)组成。每层对应 Dockerfile 中的一条指令(FROM、RUN、COPY、ADD 等)。层与层之间是父子关系,每层只记录与上一层的差异(增量变更)。
# 每条指令创建一层
FROM ubuntu:22.04 # 基础镜像层
RUN apt-get update # 第2层:apt 缓存
RUN apt-get install -y nodejs # 第3层:nodejs 安装
COPY app.js /app/ # 第4层:应用代码
EXPOSE 3000 # 元数据变更(不创建新层)
CMD ["node", "app.js"] # 元数据变更(不创建新层)2.3.2 写时复制(CoW)
当容器需要修改镜像中的文件时,Docker 不会直接修改镜像层(只读),而是将文件复制到容器的可写层,然后在可写层进行修改。这就是写时复制(Copy-on-Write)。
CoW 的优势:
- 节省空间:多个容器共享同一个镜像的只读层
- 快速启动:不需要复制整个镜像,只在需要时复制单个文件
- 节省内存:未修改的文件不占用额外空间
CoW 的劣势:
- 写入性能:第一次写入时需要先复制文件
- 额外空间:修改的文件会在可写层产生副本
# 查看镜像层
docker image history nginx:1.25
# IMAGE CREATED SIZE COMMENT
# abc123 2 days ago 187MB nginx:1.25 base
# def456 2 days ago 23MB RUN apt-get update && apt-get install
# ghi789 2 days ago 1.2kB COPY nginx.conf /etc/nginx/
# 使用 dive 分析镜像层
# dive nginx:1.25
# 可以看到每一层的文件变更、大小、层效率2.3.3 镜像仓库(Registry)
镜像仓库存储和分发 Docker 镜像。常见的镜像仓库:
- Docker Hub:Docker 官方的公共仓库
- Harbor:VMware 开源的企业级仓库
- ACR:阿里云容器镜像服务
- ECR:AWS Elastic Container Registry
- GCR:Google Container Registry
# 拉取镜像
docker pull nginx:1.25-alpine
# 标记镜像
docker tag nginx:1.25-alpine registry.example.com/nginx:1.25
# 推送镜像
docker push registry.example.com/nginx:1.25
# 查看镜像信息
docker image inspect nginx:1.25-alpine2.4 容器运行时生态
2.4.1 Docker 的架构演进
Docker 最初是一个单体应用,所有功能都在 dockerd 中。随着容器技术的发展,Docker 的架构逐步拆分:
docker CLI → dockerd → libcontainer → Linux Kernel
↓
containerd → runc → Linux Kernel2017 年,Docker 将容器运行时拆分为 containerd,并捐赠给 CNCF。Docker 自身变成了一个上层工具,负责镜像构建、Compose 等高级功能,而容器生命周期管理交给 containerd。
2.4.2 containerd
containerd 是一个工业级的容器运行时,专注于容器生命周期的管理。它是 Kubernetes 推荐的容器运行时。
# 安装 containerd
apt-get install containerd
# 使用 ctr(containerd CLI)管理容器
ctr images pull docker.io/library/nginx:latest
ctr containers create docker.io/library/nginx:latest my-nginx
ctr tasks start my-nginx
ctr tasks list2.4.3 CRI-O
CRI-O 是专为 Kubernetes 设计的轻量级容器运行时。它实现了 Kubernetes 的 Container Runtime Interface(CRI),只包含 K8s 需要的功能。
# 安装 CRI-O
apt-get install cri-o
# CRI-O 的配置
cat /etc/crio/crio.conf2.4.4 OCI 标准
OCI(Open Container Initiative)定义了容器的两个标准:
- Image Specification:定义镜像的格式(config、layers、manifest)
- Runtime Specification:定义如何运行容器(bundle 格式、配置)
// OCI 镜像配置示例
{
"architecture": "amd64",
"os": "linux",
"config": {
"Env": ["PATH=/usr/local/sbin:/usr/local/bin"],
"Cmd": ["/bin/bash"],
"WorkingDir": "/"
},
"rootfs": {
"type": "layers",
"diff_ids": ["sha256:abc123...", "sha256:def456..."]
}
}2.5 多阶段构建
多阶段构建是 Docker 17.05 引入的特性,允许在同一个 Dockerfile 中使用多个 FROM 指令。每个 FROM 指令开始一个新的构建阶段,后续阶段可以从前面的阶段复制文件。
多阶段构建的核心价值:将编译环境和运行环境分离,大幅减小最终镜像的体积。
# 阶段1:编译环境(包含编译器、依赖库等)
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
# 阶段2:运行环境(只包含运行时必要的文件)
FROM alpine:3.18
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app /app
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app"]构建结果对比:
- 单阶段构建:~800MB(包含 Go 编译器、源码、依赖等)
- 多阶段构建:~15MB(只有编译产物 + Alpine 基础镜像)
三、实战案例
3.1 编写高效的 Dockerfile
# 反面教材:低效的 Dockerfile
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN pip3 install flask
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
# 问题分析:
# 1. 每条 RUN 创建一层,层数过多
# 2. ubuntu 基础镜像太大(77MB)
# 3. 没有利用缓存
# 4. 以 root 用户运行# 正面教材:高效的 Dockerfile
FROM python:3.11-slim AS base
# 利用缓存:先复制依赖文件,再安装依赖
COPY requirements.txt /app/
WORKDIR /app
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 安全:使用非 root 用户
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]3.2 使用 dive 分析镜像
# 安装 dive
wget https://github.com/wagoodman/dive/releases/latest/download/dive_0.10.0_linux_amd64.deb
sudo dpkg -i dive_0.10.0_linux_amd64.deb
# 分析镜像
dive nginx:1.25-alpine
# 输出分析:
# Image efficiency score: 92 %
# Wasted space: 3.2 MB
# 可以看到每一层的文件变更
# 识别可以优化的层3.3 容器安全实践
# 安全最佳实践
FROM node:18-alpine
# 1. 使用特定版本标签
# 2. 使用 alpine 减小攻击面
# 3. 不存储敏感信息
# 4. 使用非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 5. 设置只读文件系统
# 6. 删除不必要的工具
RUN rm -rf /var/cache/apk/*
USER appuser
EXPOSE 3000
# 7. 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]四、本章小结
本章深入讲解了容器技术的底层原理:
容器的本质是受限的 Linux 进程:Namespace 提供隔离,Cgroups 提供资源限制,UnionFS 提供文件系统。
容器 vs 虚拟机:容器共享内核,启动快、体积小、密度高,但隔离性较弱。
镜像分层架构:每层是增量变更,多个容器共享只读层,写时复制机制提升效率。
容器运行时生态:Docker → containerd → runc 的分层架构,OCI 标准统一了镜像格式和运行时接口。
多阶段构建:分离编译环境和运行环境,大幅减小镜像体积。
这些知识是理解 Kubernetes 的基础。在后续章节中,你将看到 K8s 如何管理和编排这些容器。
五、思考与练习
思考题
如果两个容器使用同一个基础镜像(如 nginx:1.25),它们在磁盘上会占用多少空间?解释 UnionFS 的层共享机制。
为什么 Kubernetes 1.24+ 移除了对 Docker 的内置支持?containerd 和 Docker 的核心区别是什么?
如果一个容器修改了镜像中的文件,然后重启容器,修改会保留吗?为什么?
Namespace 的隔离是否足够安全?在什么场景下容器的隔离可能被突破?
多阶段构建除了减小镜像体积,还有哪些好处?
练习题
编写一个多阶段 Dockerfile,构建一个 Node.js 应用:第一阶段安装依赖和编译,第二阶段只复制产物到 alpine 镜像。
使用 docker image history 对比单阶段和多阶段构建的镜像层差异。
使用 unshare 命令手动创建 PID 和 Network Namespace,观察隔离效果。
使用 docker run --cpus=0.5 --memory=256m 运行一个容器,用 docker stats 观察资源限制的效果。