2Kubernetes 实战

理解容器

容器技术基础

容器命名空间Cgroups

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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系统调用参数隔离内容
PIDCLONE_NEWPID进程ID
NetworkCLONE_NEWNET网络栈、IP地址、端口
MountCLONE_NEWNS文件系统挂载点
UserCLONE_NEWUSER用户和组ID
IPCCLONE_NEWIPC进程间通信
UTSCLONE_NEWUTS主机名和域名
bash
# 查看进程的 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 $$  # 输出: 1

PID 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:控制设备访问
bash
# 查看容器的 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 myapp

2.1.3 UnionFS(联合文件系统)

UnionFS 将多个目录联合挂载到同一个虚拟文件系统。Docker 使用 UnionFS 实现镜像的分层存储。

常见的 UnionFS 实现:

  • overlay2(默认):Linux 内核原生支持,性能最好
  • aufs:早期的 Docker 默认驱动
  • btrfs:支持快照和子卷
bash
# 查看 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
隔离性进程级隔离(较弱)硬件级隔离(较强)
操作系统共享宿主机内核独立内核
bash
# 容器的资源占用
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 等)。层与层之间是父子关系,每层只记录与上一层的差异(增量变更)。

dockerfile
# 每条指令创建一层
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 的劣势:

  • 写入性能:第一次写入时需要先复制文件
  • 额外空间:修改的文件会在可写层产生副本
bash
# 查看镜像层
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
bash
# 拉取镜像
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-alpine

2.4 容器运行时生态

2.4.1 Docker 的架构演进

Docker 最初是一个单体应用,所有功能都在 dockerd 中。随着容器技术的发展,Docker 的架构逐步拆分:

docker CLI → dockerd → libcontainer → Linux Kernel
                    ↓
              containerd → runc → Linux Kernel

2017 年,Docker 将容器运行时拆分为 containerd,并捐赠给 CNCF。Docker 自身变成了一个上层工具,负责镜像构建、Compose 等高级功能,而容器生命周期管理交给 containerd。

2.4.2 containerd

containerd 是一个工业级的容器运行时,专注于容器生命周期的管理。它是 Kubernetes 推荐的容器运行时。

bash
# 安装 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 list

2.4.3 CRI-O

CRI-O 是专为 Kubernetes 设计的轻量级容器运行时。它实现了 Kubernetes 的 Container Runtime Interface(CRI),只包含 K8s 需要的功能。

bash
# 安装 CRI-O
apt-get install cri-o

# CRI-O 的配置
cat /etc/crio/crio.conf

2.4.4 OCI 标准

OCI(Open Container Initiative)定义了容器的两个标准:

  • Image Specification:定义镜像的格式(config、layers、manifest)
  • Runtime Specification:定义如何运行容器(bundle 格式、配置)
json
// 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 指令开始一个新的构建阶段,后续阶段可以从前面的阶段复制文件。

多阶段构建的核心价值:将编译环境和运行环境分离,大幅减小最终镜像的体积。

dockerfile
# 阶段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
# 反面教材:低效的 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
# 正面教材:高效的 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 分析镜像

bash
# 安装 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 容器安全实践

dockerfile
# 安全最佳实践
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 观察资源限制的效果。

2/8理解容器
0%