土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】容器架构:从 namespace 到生产运行时

文章导航

分类入口
architecture
标签入口
#container#Docker#containerd#CRI-O#namespace#cgroup#OCI

目录

2013 年 3 月,Solomon Hykes 在 PyCon 上用五分钟演示了 Docker 的原型:一条命令就能把应用连同依赖打包成一个可移植的单元,在任何 Linux 机器上秒级启动。这个演示没有展示任何新技术——namespace 在 2002 年就进入了 Linux 内核,cgroup 在 2007 年由 Google 工程师贡献到上游——Docker 做的事情是把这些已经存在十年的内核原语封装成了一个开发者能直接用的工具

但”容器就是轻量虚拟机”这个说法至今仍在误导人。虚拟机通过 Hypervisor 虚拟化硬件,每个 VM 运行一个完整的操作系统内核;容器则是共享宿主机内核,通过内核提供的隔离机制划分出独立的运行环境。这个本质区别决定了容器的所有优势和所有局限:启动快是因为不需要引导内核,隔离弱也是因为共享同一个内核。

本文要回答的核心问题是:Linux 的哪些原语让容器成为可能? 从 namespace 的六种隔离维度到 cgroup v2 的资源限制,从 OverlayFS 的分层存储到 OCI 规范的标准化,从 Docker 的架构演进到 CRI-O 的精简设计,我们会逐层拆解容器技术栈的每一层。

上一篇 中我们讨论了事件响应与故障复盘,本文进入 Cloud Native 架构的第一个核心主题:容器。


一、Linux namespace:隔离的六个维度

容器的隔离能力来自 Linux namespace。每种 namespace 把一类全局系统资源包装成一个独立的抽象层,让 namespace 内部的进程看到的是属于自己的资源视图,而不是宿主机的全局视图。

截至 Linux 5.x 内核,共有 8 种 namespace,其中 6 种是容器运行时核心使用的。

1.1 PID namespace

PID namespace(进程 ID 命名空间)让容器内的进程拥有独立的进程号空间。容器内的第一个进程 PID 为 1,它看不到宿主机上的其他进程。

# 在新的 PID namespace 中启动 bash
sudo unshare --pid --fork --mount-proc bash

# 容器内只能看到自己的进程
ps aux
# USER  PID %CPU %MEM    VSZ   RSS TTY STAT START   TIME COMMAND
# root    1  0.0  0.0  18508  3400 pts/0 S  10:00  0:00 bash
# root    8  0.0  0.0  34404  2900 pts/0 R+ 10:00  0:00 ps aux

关键限制:PID namespace 提供的是可见性隔离,不是安全隔离。宿主机上的 root 用户仍然可以看到并操控容器内的所有进程。从宿主机角度看,容器进程只是普通进程,拥有宿主机上的真实 PID。

1.2 NET namespace

NET namespace(网络命名空间)为容器提供独立的网络协议栈:独立的网络设备、IP 地址、路由表、端口空间、iptables 规则。

# 创建一个新的网络命名空间
sudo ip netns add container1

# 在新命名空间中查看网络设备
sudo ip netns exec container1 ip link show
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
#    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

# 创建 veth pair 连接宿主机和容器网络
sudo ip link add veth-host type veth peer name veth-container
sudo ip link set veth-container netns container1

# 配置 IP 地址
sudo ip addr add 10.200.1.1/24 dev veth-host
sudo ip link set veth-host up
sudo ip netns exec container1 ip addr add 10.200.1.2/24 dev veth-container
sudo ip netns exec container1 ip link set veth-container up
sudo ip netns exec container1 ip link set lo up

# 测试连通性
sudo ip netns exec container1 ping -c 2 10.200.1.1

每个容器都有自己的 eth0,可以绑定任意端口而不会与宿主机或其他容器冲突。这也是为什么多个容器可以同时监听 80 端口——它们在不同的 NET namespace 中。

1.3 MNT namespace

MNT namespace(挂载命名空间)让容器拥有独立的文件系统挂载视图。容器内的挂载和卸载操作不会影响宿主机或其他容器。

这是容器文件系统隔离的基础。配合 pivot_rootchroot,容器可以拥有完全独立的根文件系统(rootfs),看到的是自己的 /bin/etc/usr,而不是宿主机的。

1.4 UTS namespace

UTS namespace 隔离主机名(hostname)和域名(domainname)。每个容器可以有自己的 hostname,hostname 命令返回的是容器自己的名字,不是宿主机的。

sudo unshare --uts bash
hostname container-web-01
hostname
# container-web-01

看起来简单,但在服务发现和日志收集中非常重要——每个容器的日志需要标识来源,独立的 hostname 是最基础的标识手段。

1.5 IPC namespace

IPC namespace 隔离 System V IPC(消息队列、信号量、共享内存)和 POSIX 消息队列。同一个 IPC namespace 内的进程可以通过共享内存通信,不同 namespace 之间互相不可见。

1.6 User namespace

User namespace 是安全隔离中最重要的一种。它允许容器内的进程以 root(UID 0)身份运行,但这个 root 映射到宿主机上的一个非特权用户。

# 创建 user namespace,内部 UID 0 映射到外部 UID 100000
sudo unshare --user --map-root-user bash
id
# uid=0(root) gid=0(root) groups=0(root)

# 但在宿主机上,这个进程实际运行在非特权用户下

User namespace 是 Rootless 容器(rootless containers)的基础。没有 User namespace,容器内的 root 就是宿主机的 root,一旦容器逃逸,攻击者直接获得宿主机最高权限。

1.7 namespace 隔离边界总结

graph TB
    subgraph "宿主机 Linux 内核"
        K[Linux Kernel 共享]
    end

    subgraph "容器 A"
        A_PID["PID namespace<br/>PID 1: nginx"]
        A_NET["NET namespace<br/>eth0: 10.0.1.2"]
        A_MNT["MNT namespace<br/>rootfs: alpine"]
        A_UTS["UTS namespace<br/>hostname: web-01"]
        A_IPC["IPC namespace"]
        A_USER["User namespace<br/>UID 0 → 100000"]
    end

    subgraph "容器 B"
        B_PID["PID namespace<br/>PID 1: postgres"]
        B_NET["NET namespace<br/>eth0: 10.0.1.3"]
        B_MNT["MNT namespace<br/>rootfs: debian"]
        B_UTS["UTS namespace<br/>hostname: db-01"]
        B_IPC["IPC namespace"]
        B_USER["User namespace<br/>UID 0 → 200000"]
    end

    K --> A_PID
    K --> A_NET
    K --> A_MNT
    K --> A_UTS
    K --> A_IPC
    K --> A_USER
    K --> B_PID
    K --> B_NET
    K --> B_MNT
    K --> B_UTS
    K --> B_IPC
    K --> B_USER

每种 namespace 提供一个维度的隔离,它们组合在一起构成容器的完整隔离边界。但要注意:namespace 只解决”看不到”的问题,不解决”用多少”的问题——资源限制需要 cgroup。


二、cgroup v2:资源限制与 QoS 机制

cgroup(Control Groups,控制组)是 Linux 内核提供的资源限制机制。如果说 namespace 回答的是”进程能看到什么”,cgroup 回答的是”进程能用多少”。

2.1 cgroup v1 vs v2

cgroup v1 从 Linux 2.6.24 开始引入,采用多层级(multi-hierarchy)设计:每种资源控制器(CPU、Memory、IO 等)有自己独立的层级树,一个进程可以在不同控制器的不同层级中。

这种设计带来了严重的一致性问题。假设进程 P 在 CPU 控制器中属于组 A,在 Memory 控制器中属于组 B,当你需要对一组进程同时限制 CPU 和内存时,必须分别在两个层级树中操作,且无法保证原子性。

cgroup v2 从 Linux 4.5 开始引入,采用统一层级(unified hierarchy)设计:所有控制器共享一棵层级树。一个进程只出现在一个节点上,该节点的所有控制器设置同时生效。

特性 cgroup v1 cgroup v2
层级结构 每个控制器独立层级 统一单一层级
进程归属 可在不同控制器的不同组 一个进程只属于一个组
内存 OOM 控制 粗粒度,难以预测 精细化 OOM 评分
IO 控制 blkio 仅支持直接 IO 支持 buffered IO 回写控制
PSI(Pressure Stall Information) 不支持 原生支持,可监控资源压力
线程级控制 部分支持 原生 threaded 模式
内核版本要求 2.6.24+ 4.5+(推荐 5.8+)

2.2 CPU 限制

cgroup v2 通过 cpu.max 控制 CPU 时间分配,格式为 quota period(单位微秒)。

# 创建 cgroup
sudo mkdir -p /sys/fs/cgroup/container-web

# 限制为 0.5 个 CPU 核心(50000us / 100000us = 50%)
echo "50000 100000" | sudo tee /sys/fs/cgroup/container-web/cpu.max

# 设置 CPU 权重(类似 v1 的 cpu.shares),范围 1-10000,默认 100
echo "200" | sudo tee /sys/fs/cgroup/container-web/cpu.weight

# 将进程加入 cgroup
echo $PID | sudo tee /sys/fs/cgroup/container-web/cgroup.procs

cpu.max硬限制(hard limit):即使 CPU 空闲,进程也不能超过配额。cpu.weight软限制(soft limit):只在 CPU 竞争时生效,权重高的进程获得更多时间片。

在 Kubernetes 中,resources.limits.cpu 对应 cpu.maxresources.requests.cpu 对应 cpu.weight。一个常见的生产问题是:设置了过低的 CPU limit 导致进程被频繁节流(throttled),表现为延迟毛刺而非 CPU 使用率告警。Linux 5.4 引入的 cpu.stat 中的 nr_throttledthrottled_usec 字段可以帮助诊断这类问题。

2.3 Memory 限制

# 设置内存硬限制为 256MB
echo "268435456" | sudo tee /sys/fs/cgroup/container-web/memory.max

# 设置内存软限制为 128MB(超过时被优先回收)
echo "134217728" | sudo tee /sys/fs/cgroup/container-web/memory.high

# 设置 swap 限制(0 表示禁止使用 swap)
echo "0" | sudo tee /sys/fs/cgroup/container-web/memory.swap.max

# 查看当前内存使用
cat /sys/fs/cgroup/container-web/memory.current

cgroup v2 的内存控制引入了 memory.high 语义:当使用量超过 memory.high,内核会积极回收该 cgroup 的页面,但不会直接触发 OOM。只有超过 memory.max 才会触发 OOM Killer。这种分级机制让应用有机会通过 GC 或缓存释放来自行降低内存使用。

2.4 IO 限制

# 查看块设备编号
lsblk -d -o NAME,MAJ:MIN
# sda 8:0

# 限制 IO 带宽:读 50MB/s,写 20MB/s
echo "8:0 rbps=52428800 wbps=20971520" | \
  sudo tee /sys/fs/cgroup/container-web/io.max

# 限制 IOPS:读 1000,写 500
echo "8:0 riops=1000 wiops=500" | \
  sudo tee /sys/fs/cgroup/container-web/io.max

cgroup v2 的 IO 控制器相比 v1 的 blkio 有一个关键改进:支持 buffered IO 的回写控制。v1 只能限制直接 IO(O_DIRECT),而大多数应用使用 buffered IO。这意味着在 v1 中,IO 限制在很多场景下形同虚设。

2.5 PSI:资源压力监控

PSI(Pressure Stall Information,压力阻塞信息)是 cgroup v2 引入的资源压力监控机制,由 Facebook 工程师贡献。

# 查看 CPU 压力
cat /sys/fs/cgroup/container-web/cpu.pressure
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

# some: 至少有一个任务因资源不足而阻塞的时间比例
# full: 所有任务都因资源不足而阻塞的时间比例

PSI 提供了比传统 CPU 使用率更准确的资源饱和度指标。CPU 使用率 80% 可能意味着系统运行良好(计算密集型),也可能意味着严重阻塞(等待 IO)。PSI 的 somefull 指标直接反映”有多少工作因为资源不足而无法推进”。


三、OverlayFS 与分层存储

容器镜像的魔法在于分层存储:一个 200MB 的 Python 基础镜像,被 100 个不同的应用容器共享,磁盘上只需要存一份。这种机制依赖联合文件系统(Union Filesystem),其中 OverlayFS 是当前 Linux 内核原生支持且性能最好的实现。

3.1 OverlayFS 工作原理

OverlayFS 将多个目录层叠合并成一个统一的文件系统视图,包含以下层:

# 手动挂载 OverlayFS 演示
mkdir -p /data/overlay/{lower1,lower2,upper,work,merged}

# 在下层创建文件
echo "base config" > /data/overlay/lower1/config.yaml
echo "app binary" > /data/overlay/lower2/app

# 挂载 overlay
sudo mount -t overlay overlay \
  -o lowerdir=/data/overlay/lower2:/data/overlay/lower1,\
upperdir=/data/overlay/upper,\
workdir=/data/overlay/work \
  /data/overlay/merged

# 合并视图中同时可以看到两个下层的文件
ls /data/overlay/merged/
# app  config.yaml

# 修改文件时,触发 copy-up 到上层
echo "modified config" > /data/overlay/merged/config.yaml
cat /data/overlay/upper/config.yaml
# modified config
# 下层原文件不受影响
cat /data/overlay/lower1/config.yaml
# base config

3.2 Copy-up 与写时复制

当容器修改一个来自下层(镜像层)的文件时,OverlayFS 会将整个文件复制到上层(容器层),然后在上层进行修改。这个过程叫做 copy-up(写时复制)。

Copy-up 的性能影响: - 首次修改大文件代价高:修改一个 100MB 的日志文件,即使只改一个字节,也要先复制 100MB 到上层。 - 后续修改无额外开销:copy-up 只发生一次,之后直接读写上层文件。 - 删除文件使用 whiteout:在上层创建一个特殊的 whiteout 文件标记删除,下层文件仍然存在但在合并视图中不可见。

3.3 镜像层的实际结构

一个典型的容器镜像由多个只读层组成,每一层对应 Dockerfile 中的一条指令:

Layer 4 (upperdir, 可读写): 容器运行时的修改
Layer 3 (lowerdir): COPY app.py /app/          — 2KB
Layer 2 (lowerdir): RUN pip install flask       — 45MB
Layer 1 (lowerdir): FROM python:3.12-slim       — 150MB

当多个容器使用同一个镜像时,Layer 1-3 在磁盘上只存一份,每个容器只需要自己的 Layer 4(容器层)。这就是为什么在一台机器上运行 50 个相同镜像的容器,磁盘占用远小于 50 倍镜像大小。


四、OCI 规范:容器的标准化

2015 年,Docker 将其容器运行时 runc 捐献给新成立的 OCI(Open Container Initiative,开放容器倡议),标志着容器技术从 Docker 的私有实现走向行业标准。OCI 定义了两个核心规范。

4.1 Image Spec(镜像规范)

OCI Image Spec 定义了容器镜像的格式,包括:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:a1b2c3d4...",
    "size": 7023
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:e5f6a7b8...",
      "size": 32654321
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:c9d0e1f2...",
      "size": 16724531
    }
  ]
}

4.2 Runtime Spec(运行时规范)

OCI Runtime Spec 定义了容器的运行时配置——config.json,包括:

{
  "ociVersion": "1.0.2",
  "process": {
    "terminal": false,
    "user": { "uid": 1000, "gid": 1000 },
    "args": ["python", "app.py"],
    "env": [
      "PATH=/usr/local/bin:/usr/bin:/bin",
      "FLASK_ENV=production"
    ],
    "cwd": "/app",
    "capabilities": {
      "bounding": ["CAP_NET_BIND_SERVICE"],
      "effective": ["CAP_NET_BIND_SERVICE"],
      "permitted": ["CAP_NET_BIND_SERVICE"]
    }
  },
  "root": {
    "path": "rootfs",
    "readonly": true
  },
  "linux": {
    "namespaces": [
      { "type": "pid" },
      { "type": "network" },
      { "type": "ipc" },
      { "type": "uts" },
      { "type": "mount" },
      { "type": "user" }
    ],
    "resources": {
      "memory": { "limit": 268435456 },
      "cpu": { "quota": 50000, "period": 100000 }
    }
  }
}

OCI 规范的意义在于解耦:镜像构建工具(Docker、Buildah、kaniko)只需要遵循 Image Spec,容器运行时(runc、crun、gVisor)只需要遵循 Runtime Spec,注册中心(Docker Hub、Harbor、GitHub Container Registry)只需要遵循 Distribution Spec。任何符合规范的组件可以自由替换。


五、容器运行时架构演进

容器运行时是真正执行”创建容器”操作的组件。从 Docker 的单体架构到当前的分层设计,运行时经历了三次重大演进。

5.1 Docker 的架构拆分

早期 Docker 是一个单体守护进程 dockerd,包揽了镜像管理、容器生命周期、网络、存储等所有功能。2016 年起,Docker 将运行时拆分为独立组件:

graph TB
    subgraph "Kubernetes 节点"
        KL[kubelet]
    end

    subgraph "高层运行时(High-level Runtime)"
        DOCK[dockerd<br/>Docker 守护进程]
        CTD[containerd<br/>容器生命周期管理]
        CRIO[CRI-O<br/>CRI 原生运行时]
    end

    subgraph "CRI shim"
        SHIM1[dockershim<br/>已废弃 K8s 1.24]
        SHIM2[cri-containerd<br/>CRI 插件]
    end

    subgraph "底层运行时(Low-level Runtime)"
        RUNC[runc<br/>OCI 参考实现]
        CRUN[crun<br/>C 语言实现]
        GVISOR[gVisor / runsc<br/>用户态内核]
        KATA[Kata Containers<br/>轻量 VM]
    end

    subgraph "容器实例"
        C1[Container 1]
        C2[Container 2]
        C3[Container 3]
    end

    KL -->|"CRI gRPC"| SHIM1
    KL -->|"CRI gRPC"| SHIM2
    KL -->|"CRI gRPC"| CRIO
    SHIM1 --> DOCK
    DOCK --> CTD
    SHIM2 --> CTD
    CTD -->|"OCI Spec"| RUNC
    CTD -->|"OCI Spec"| CRUN
    CTD -->|"OCI Spec"| GVISOR
    CRIO -->|"OCI Spec"| RUNC
    CRIO -->|"OCI Spec"| KATA
    RUNC --> C1
    CRUN --> C2
    GVISOR --> C3

调用链演进过程:

  1. Docker 时代(2013-2016)docker CLI → dockerd(单体,所有功能在一个进程中)。
  2. containerd 分离(2016-2020)docker CLI → dockerd → containerd → containerd-shim → runc。containerd 负责镜像管理和容器生命周期,runc 负责创建容器。
  3. Kubernetes CRI 时代(2020-至今)kubelet → CRI → containerd / CRI-O → runc。去掉了 dockerd 这一层。Kubernetes 1.24 正式移除 dockershim。

5.2 关键组件详解

runc 是 OCI Runtime Spec 的参考实现,用 Go 编写。它接收一个 OCI bundle(rootfs + config.json),创建 namespace、配置 cgroup、设置 seccomp 等,然后 exec 容器进程。runc 本身是短暂的:启动容器后立即退出,容器进程由 containerd-shim 接管。

containerd 是高层运行时,负责: - 镜像拉取、解压、存储 - 容器生命周期管理(创建、启动、停止、删除) - 快照管理(OverlayFS、devicemapper 等存储驱动) - 内容寻址存储(content-addressable storage) - 提供 gRPC API,支持 CRI 插件

containerd-shim 是每个容器的监护进程。它的作用是: - 允许 containerd 重启而不影响运行中的容器 - 持有容器的 stdin/stdout/stderr - 向 containerd 报告容器退出状态

CRI-O 是专为 Kubernetes 设计的轻量运行时。相比 containerd 的通用设计,CRI-O 只实现 Kubernetes CRI 需要的功能,不提供独立的 CLI 或 API。

5.3 运行时对比

维度 Docker(dockerd + containerd) containerd(独立) CRI-O
定位 完整容器平台 通用容器运行时 Kubernetes 专用运行时
代码规模 ~120K 行(含 dockerd) ~80K 行 ~50K 行
功能范围 构建 + 运行 + 网络 + 编排 运行 + 镜像管理 + 快照 仅 CRI 接口实现
CRI 支持 需 dockershim(已废弃) 内置 CRI 插件 原生 CRI
Kubernetes 兼容性 1.24 后不直接支持 完全支持 完全支持
独立使用 完整 CLI(docker run) 有 ctr/nerdctl 不支持独立使用
镜像构建 docker build 需配合 BuildKit 不支持
内存占用 ~100MB ~30-50MB ~20-40MB
启动延迟 较高(多一层转发) 最低
生态系统 最大(Docker Hub 等) CNCF 毕业项目 CNCF 孵化项目
典型用户 开发环境 AWS EKS、GKE Red Hat OpenShift

选择建议: - 开发环境:Docker Desktop,提供完整的开发体验。 - Kubernetes 生产集群:containerd 或 CRI-O。containerd 生态更广,CRI-O 更轻量。 - 需要多运行时(runc + gVisor):containerd,其 shim API 支持多运行时切换。


六、镜像构建最佳实践

6.1 多阶段构建

多阶段构建(multi-stage build)是减小镜像体积最有效的手段。核心思想是:构建阶段包含编译器和依赖,最终阶段只包含二进制产物和运行时依赖

# ========== 阶段一:构建 ==========
FROM golang:1.22-bookworm AS builder

WORKDIR /src

# 先复制依赖文件,利用缓存层
COPY go.mod go.sum ./
RUN go mod download

# 再复制源码
COPY . .

# 静态编译,关闭 CGO
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /app/server ./cmd/server

# ========== 阶段二:安全扫描 ==========
FROM aquasec/trivy:latest AS scanner
COPY --from=builder /app/server /scan/server
RUN trivy filesystem --exit-code 1 --severity HIGH,CRITICAL /scan/

# ========== 阶段三:最终镜像 ==========
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /app/server /usr/local/bin/server
COPY --from=builder /src/configs /etc/app/configs

EXPOSE 8080
USER nonroot:nonroot

ENTRYPOINT ["server"]

体积对比:

基础镜像 体积 包含内容
golang:1.22 ~850MB Go 编译器 + 完整 Debian
ubuntu:24.04 ~78MB 完整的用户态工具
alpine:3.19 ~7MB musl libc + busybox
distroless/static ~2MB 仅静态链接二进制所需
scratch 0MB 空白,需自带一切

上面的 Dockerfile 最终镜像只有约 10MB(2MB 基础镜像 + 8MB Go 二进制),而不是 850MB 的构建镜像。

6.2 镜像瘦身技巧

合并 RUN 指令减少层数

# 反面示例:三条 RUN 产生三个层,中间层包含 apt 缓存
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 正确做法:合并为一条 RUN,确保缓存在同一层被清理
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

善用 .dockerignore

# .dockerignore
.git
.github
node_modules
*.md
tests/
docs/
.env
*.log

固定依赖版本

# 不要使用 latest 标签
FROM python:3.12.3-slim-bookworm@sha256:abcdef1234567890...

# 固定系统包版本
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      libpq5=15.6-0+deb12u1 \
    && rm -rf /var/lib/apt/lists/*

6.3 安全扫描集成

镜像安全扫描应该在 CI 流程中作为必要环节:

# GitHub Actions 中集成 Trivy 扫描
name: Container Security
on:
  push:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: 1
          severity: CRITICAL,HIGH
          ignore-unfixed: true

常见的高危漏洞类型: - 过时的 OpenSSL / glibc 版本 - 以 root 用户运行容器进程 - 镜像中包含密钥或凭证 - 未设置 HEALTHCHECK 导致无法检测僵尸容器


七、容器安全

容器共享宿主机内核,安全边界天然弱于虚拟机。生产环境必须启用多层防御机制。

7.1 Rootless Containers

Rootless 容器让容器引擎和容器进程都以非 root 用户运行。即使容器逃逸,攻击者获得的也只是宿主机上的非特权用户。

# Podman 原生支持 rootless 模式
podman run --rm -it alpine:3.19 id
# uid=0(root) gid=0(root)
# 容器内看起来是 root,但在宿主机上映射为普通用户

# containerd 通过 rootlesskit 支持
rootlesskit --net=slirp4netns \
  containerd --config /home/user/.config/containerd/config.toml

Rootless 容器的限制: - 不能绑定 1024 以下的端口(除非配置 net.ipv4.ip_unprivileged_port_start) - 网络性能略低(需要通过 slirp4netns 进行用户态网络转发) - 部分存储驱动不支持(如 devicemapper)

7.2 Linux Capabilities

传统 Unix 安全模型只有两种权限级别:root(全部权限)和普通用户。Linux Capabilities(能力)将 root 权限拆分为细粒度的能力集合。

# 查看容器默认能力集
docker run --rm alpine:3.19 sh -c 'apk add -q libcap && capsh --print'

# 最小权限原则:删除所有默认能力,只保留需要的
docker run --rm \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  nginx:1.25

# Kubernetes Pod spec 中配置
# securityContext:
#   capabilities:
#     drop: ["ALL"]
#     add: ["NET_BIND_SERVICE"]

生产环境建议默认 drop ALL,按需添加。常见需要保留的能力: - NET_BIND_SERVICE:绑定 1024 以下端口 - SYS_PTRACE:调试(仅开发环境) - SETUID / SETGID:切换用户(某些初始化脚本需要)

7.3 Seccomp

Seccomp(Secure Computing Mode,安全计算模式)限制容器进程可以使用的系统调用。Linux 内核有 400+ 系统调用,大部分容器应用只需要其中 50-80 个。

Docker 默认的 seccomp profile 禁止了约 44 个危险系统调用,包括: - mount / umount2:防止挂载文件系统 - reboot:防止重启宿主机 - swapon / swapoff:防止操作 swap - ptrace:防止进程跟踪(调试) - keyctl:防止操作内核密钥环

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "close", "stat", "fstat",
        "mmap", "mprotect", "munmap", "brk", "rt_sigaction",
        "rt_sigprocmask", "ioctl", "access", "pipe", "select",
        "sched_yield", "clone", "execve", "exit", "wait4",
        "kill", "fcntl", "flock", "fsync", "fdatasync",
        "getcwd", "chdir", "rename", "mkdir", "rmdir",
        "link", "unlink", "chmod", "chown", "lstat",
        "poll", "lseek", "socket", "connect", "accept",
        "sendto", "recvfrom", "bind", "listen", "getsockname",
        "getpeername", "epoll_create", "epoll_ctl", "epoll_wait",
        "futex", "set_tid_address", "set_robust_list"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

7.4 AppArmor 与 SELinux

AppArmor 和 SELinux 是 Linux 强制访问控制(MAC,Mandatory Access Control)机制,在 namespace 和 seccomp 之上提供额外的文件系统和网络访问控制。

# Docker 默认的 AppArmor profile 片段
# 禁止写入 /proc 和 /sys
deny /proc/** w,
deny /sys/** w,

# 禁止挂载操作
deny mount,

# 允许读取 /etc/hostname
/etc/hostname r,

安全层次总结:

安全层 机制 防护范围
第一层 User namespace UID/GID 映射,限制特权
第二层 Capabilities 细粒度权限控制
第三层 Seccomp 系统调用白名单
第四层 AppArmor / SELinux 文件和网络访问控制
第五层 只读根文件系统 防止运行时篡改
第六层 网络策略 控制容器间网络通信

八、容器网络基础

容器网络需要解决三个问题:容器如何获得 IP 地址?同一宿主机上的容器如何通信?跨宿主机的容器如何通信?

8.1 veth pair 与 bridge

Linux veth pair(虚拟以太网对)是容器网络的基础原语。它创建一对虚拟网卡,从一端发送的数据包会从另一端接收,如同一根虚拟网线。

典型的单机容器网络拓扑:

容器 A                    容器 B
eth0 (10.0.0.2)          eth0 (10.0.0.3)
  │ (veth pair)             │ (veth pair)
  │                         │
veth-a                   veth-b
  │                         │
  └─────────┬───────────────┘
            │
       docker0 / cni0
       (Linux Bridge)
       10.0.0.1
            │
       iptables NAT
            │
        eth0 (宿主机)
        192.168.1.100

每个容器的 eth0 是 veth pair 的一端,另一端连接到宿主机的 Linux Bridge(docker0cni0)。同一 Bridge 上的容器通过二层转发直接通信,访问外部网络则通过 iptables NAT 进行地址转换。

# 查看 docker0 网桥
brctl show docker0
# bridge name   bridge id           STP enabled   interfaces
# docker0       8000.0242ac110001   no            veth1234567
#                                                 veth89abcde

# 查看 NAT 规则
sudo iptables -t nat -L POSTROUTING -n -v
# MASQUERADE  all  --  *  !docker0  172.17.0.0/16  0.0.0.0/0

8.2 CNI 插件模型

CNI(Container Network Interface,容器网络接口)是 CNCF 定义的容器网络标准。与 Docker 自有的 libnetwork 不同,CNI 采用插件化架构:每种网络方案实现一个 CNI 插件,容器运行时通过标准接口调用插件来配置网络。

CNI 插件的工作流程: 1. 容器运行时创建 NET namespace 2. 调用 CNI 插件的 ADD 命令,传入 namespace 路径和网络配置 3. CNI 插件创建 veth pair,配置 IP 地址、路由、DNS 4. 容器运行时启动容器进程 5. 容器停止时调用 CNI 插件的 DEL 命令清理网络

{
  "cniVersion": "1.0.0",
  "name": "mynet",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "10.244.1.0/24",
    "routes": [
      { "dst": "0.0.0.0/0" }
    ]
  }
}

主流 CNI 插件对比:

插件 网络模型 加密支持 NetworkPolicy 性能 适用场景
Flannel VXLAN / host-gw 支持 WireGuard 不支持(需配合 Calico) 中等 小型集群、入门
Calico BGP / VXLAN / eBPF 支持 WireGuard 完整支持 大规模生产集群
Cilium eBPF 支持 WireGuard / IPsec 完整支持(L3-L7) 最高 高性能、安全要求高
Weave VXLAN mesh 内置加密 基本支持 中等 多云互联

8.3 eBPF 对容器网络的革新

传统容器网络依赖 iptables 进行包过滤和 NAT,当规则数量增长到数千条时,iptables 的线性匹配成为性能瓶颈。eBPF(extended Berkeley Packet Filter)提供了一种在内核中安全执行自定义代码的机制,可以替代 iptables 实现更高效的数据面。

Cilium 基于 eBPF 实现的容器网络方案,在 1000 节点集群中将 Service 转发延迟从 iptables 的 3.5ms 降低到 0.3ms,规则更新时间从 O(n) 降低到 O(1)。


九、工程案例:Shopify 的容器化迁移

Shopify 是全球最大的电商 SaaS 平台之一,2023 年处理的 GMV(Gross Merchandise Value)超过 2350 亿美元。其容器化迁移历程是业界最具参考价值的案例之一。

9.1 迁移背景

2016 年之前,Shopify 的核心应用运行在约 1700 台物理服务器和虚拟机上,部署一次 Ruby on Rails 单体应用需要 20-30 分钟。核心问题包括:

9.2 迁移策略

Shopify 采用渐进式迁移策略,分三个阶段,历时约 18 个月:

第一阶段:容器化无状态服务(6 个月) - 将 Web 前端和 API 网关容器化 - 使用 Docker 镜像标准化构建流程 - 容器运行在 Kubernetes 集群上,与虚拟机共存

第二阶段:容器化核心服务(6 个月) - 将 Ruby on Rails 单体应用容器化 - 解决了 Rails 应用在容器中的内存管理问题(Ruby GC 与 cgroup 内存限制的交互) - 实现了蓝绿部署和金丝雀发布

第三阶段:全面容器化与优化(6 个月) - 将后台任务(Sidekiq workers)和定时任务容器化 - 实现自动扩缩容(HPA + 自定义指标) - 下线最后一批虚拟机

9.3 关键技术决策

运行时选择:Shopify 在生产环境使用 containerd 作为容器运行时,搭配 runc。选择 containerd 而非 CRI-O 的原因是 containerd 在 GKE 上经过大规模验证,且支持 lazy pulling(懒加载镜像层)优化启动速度。

镜像优化:Shopify 的 Rails 应用镜像从最初的 1.2GB 优化到 380MB,主要手段包括: - 多阶段构建:分离 gem 编译环境和运行环境 - 移除开发依赖:bundle install --without development test - 使用 jemalloc 替代 glibc malloc:减少 Ruby 进程内存碎片约 30%

网络方案:采用 Cilium 作为 CNI 插件,利用 eBPF 实现高性能的 Service mesh,替代了之前基于 iptables 的方案。

9.4 迁移成果

指标 虚拟机时代 容器化后 改善幅度
部署时间 20-30 分钟 2-4 分钟 约 85% 缩短
扩容延迟 5-10 分钟 15-30 秒 约 95% 缩短
CPU 利用率 12-18% 40-55% 约 3 倍提升
服务器数量 ~1700 台 ~600 台 约 65% 减少
日部署次数 5-10 次 80-100 次 约 10 倍提升
年度基础设施成本 基准 -40% 节省约 40%
P99 响应延迟 850ms 620ms 约 27% 下降

资源利用率的提升是成本节省的核心来源。通过 Kubernetes 的 bin-packing 调度和自动扩缩容,Shopify 将 CPU 利用率从 12-18% 提升到 40-55%,相同的计算量需要的服务器数量减少了 65%。

扩容延迟从分钟级降到秒级,使 Shopify 能够应对 Black Friday / Cyber Monday 的流量尖峰。2022 年 BFCM 期间,Shopify 的基础设施在数分钟内自动扩容了 3 倍以上的 Pod 数量,峰值处理每秒 1.3 百万个请求,零宕机。

9.5 遇到的问题与解决方案

问题一:Ruby GC 不感知 cgroup 内存限制

Ruby 的垃圾回收器通过 /proc/meminfo 读取系统总内存来决定 GC 策略,但 /proc/meminfo 显示的是宿主机内存,不是 cgroup 限制的内存。一个内存限制为 512MB 的容器,Ruby 以为自己有 64GB 内存可用,GC 触发过晚导致 OOM Kill。

解决方案:设置 RUBY_GC_HEAP_INIT_SLOTSMALLOC_ARENA_MAX 环境变量,手动限制 Ruby 的内存使用行为。在 Ruby 3.2+ 中,已支持通过 cgroup 信息自动调整 GC 参数。

问题二:容器内 DNS 解析延迟

默认配置下,容器使用 Kubernetes 的 kube-dns / CoreDNS 进行 DNS 解析。在高并发场景下,DNS 查询成为性能瓶颈。Shopify 通过在每个节点部署 NodeLocal DNSCache 将 DNS 解析延迟从 5-10ms 降低到 <1ms。

问题三:镜像拉取延迟

大规模滚动更新时,数百个节点同时拉取新镜像会导致镜像仓库带宽饱和。Shopify 采用了 P2P 镜像分发(基于 Dragonfly)和镜像预热(pre-pull)策略,将大规模更新的镜像拉取时间从 60 秒降低到 8 秒。


十、容器 vs 虚拟机 vs Unikernel

选择容器、虚拟机还是 Unikernel(单核)取决于工作负载对隔离性、性能、安全性的要求。

10.1 架构对比

虚拟机(Virtual Machine):通过 Hypervisor(KVM、Xen、VMware)虚拟化硬件,每个 VM 运行完整的操作系统内核。隔离粒度是硬件级别——不同 VM 之间没有共享内核,一个 VM 的内核漏洞不影响其他 VM。

容器(Container):共享宿主机内核,通过 namespace 和 cgroup 提供进程级隔离。优势是轻量快速,劣势是隔离边界弱于 VM——内核漏洞可能影响所有容器。

Unikernel:将应用代码与最小化的 library OS 编译成单一的可引导镜像,直接运行在 Hypervisor 上。没有多余的系统服务、shell、包管理器——只有应用需要的内核功能。代表项目包括 MirageOS、IncludeOS、Unikraft。

10.2 全面对比

维度 容器 虚拟机 Unikernel
隔离级别 进程级(namespace + cgroup) 硬件级(Hypervisor) 硬件级(Hypervisor)
内核 共享宿主机内核 独立内核 定制最小内核
启动时间 100ms-2s 30s-2min 10ms-100ms
内存开销 5-50MB(应用自身) 256MB-2GB(含 OS) 1-20MB
镜像体积 5MB-500MB 1GB-10GB 1MB-50MB
攻击面 中(共享内核 + 系统调用) 低(Hypervisor 层) 最低(无 shell、无多余系统调用)
运维复杂度 中(需容器编排) 高(需管理 OS 补丁) 高(需重新编译部署)
调试能力 强(可 exec 进入容器) 强(完整 OS,可 SSH) 弱(无 shell,需专用工具)
多语言支持 任意语言 任意语言 受限(通常 C/C++/OCaml/Rust)
生态成熟度 高(Docker/K8s 生态) 高(VMware/KVM 生态) 低(学术研究为主)
典型用途 微服务、CI/CD、无状态应用 传统企业应用、多租户 边缘计算、IoT、网络功能
实时迁移 不支持(通常重调度) 支持(live migration) 支持(轻量快速)
代表产品 Docker、containerd、Podman VMware、KVM、Hyper-V MirageOS、Unikraft、IncludeOS

10.3 混合方案:安全容器

Kata Containers 和 gVisor 是两种”安全容器”方案,试图结合容器的便捷性和虚拟机的隔离性。

Kata Containers:每个容器运行在一个轻量 VM 中,使用 QEMU/Cloud Hypervisor 作为 Hypervisor。兼容 OCI 标准,可以通过 containerd 的 shim API 无缝集成。启动时间约 100-200ms,内存开销约 20-40MB。

gVisor(runsc):Google 开发的用户态内核。容器的系统调用不直接到达宿主机内核,而是由 gVisor 的 Sentry 组件在用户态处理。不需要硬件虚拟化支持,但系统调用延迟较高(约 2-10 倍)。

选择建议: - 信任内部工作负载(同一团队的服务):标准容器(runc)即可。 - 运行不可信代码(用户提交的代码、第三方插件):gVisor 或 Kata Containers。 - 合规要求强隔离(金融、政府):Kata Containers 或虚拟机。 - 极致性能 + 极致安全:Unikernel(适用于单一功能的网络组件)。


十一、容器编排与生产部署考量

单机运行容器只是起点,生产环境需要解决编排、调度、服务发现、配置管理等问题。这些主题将在 下一篇 深入讨论,这里列出容器层面的关键考量。

11.1 健康检查

# Kubernetes Pod spec 中的三种健康检查
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
    - name: app
      image: myapp:v2.1.0
      ports:
        - containerPort: 8080
      # 存活探针:检测进程是否卡死
      livenessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 15
        failureThreshold: 3
      # 就绪探针:检测是否可以接收流量
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 5
      # 启动探针:给慢启动应用更多时间
      startupProbe:
        httpGet:
          path: /healthz
          port: 8080
        failureThreshold: 30
        periodSeconds: 10
      resources:
        requests:
          cpu: "250m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"

11.2 日志与可观测性

容器日志最佳实践是写入 stdout/stderr,由容器运行时(containerd)收集到磁盘,再由日志采集器(Fluentd、Fluent Bit、Vector)转发到集中式日志系统。

package main

import (
    "log/slog"
    "os"
)

func main() {
    // 结构化日志输出到 stdout
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))

    logger.Info("server starting",
        "port", 8080,
        "version", "v2.1.0",
        "env", os.Getenv("APP_ENV"),
    )

    // {"time":"2026-04-13T10:00:00Z","level":"INFO",
    //  "msg":"server starting","port":8080,
    //  "version":"v2.1.0","env":"production"}
}

不要在容器内写日志文件——容器是临时的(ephemeral),容器重启后容器层的文件会丢失。如果必须持久化,使用 Volume 挂载。

11.3 优雅终止

当 Kubernetes 删除 Pod 时,kubelet 向容器主进程发送 SIGTERM 信号,等待 terminationGracePeriodSeconds(默认 30 秒),然后发送 SIGKILL 强制终止。应用必须正确处理 SIGTERM:

package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    srv := &http.Server{Addr: ":8080"}

    // 监听终止信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("server error", "err", err)
            os.Exit(1)
        }
    }()

    <-quit
    slog.Info("shutting down server")

    // 给正在处理的请求最多 15 秒完成
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown error", "err", err)
    }
    slog.Info("server stopped")
}

11.4 资源规划原则

容器的资源请求(requests)和限制(limits)配置直接影响调度效率和运行稳定性:


十二、容器技术趋势

12.1 WebAssembly(Wasm)容器

WASI(WebAssembly System Interface)为 WebAssembly 提供了标准化的系统接口,使 Wasm 模块可以作为”容器”运行。containerd 已支持通过 runwasi shim 运行 Wasm 工作负载。

Wasm 容器的优势: - 启动时间 <1ms(冷启动) - 镜像体积通常 <1MB - 沙箱隔离由 Wasm 运行时保证,不依赖 Linux namespace - 跨平台:同一个 Wasm 模块在 x86、ARM、RISC-V 上运行

限制: - 生态不成熟,库和工具链有限 - 文件系统和网络访问受限于 WASI 接口 - 不适合需要完整 Linux 系统调用的应用

12.2 镜像懒加载

传统容器启动需要先拉取完整镜像(可能数百 MB),然后解压所有层。实际上,容器启动时通常只需要访问镜像中 6-10% 的数据。

Stargz(Seekable tar.gz)和 Nydus 等方案通过按需加载(lazy pulling)镜像层来加速启动。containerd 的 Stargz Snapshotter 可以在镜像未完全下载时就启动容器,所需文件按需从注册中心拉取。

据阿里巴巴的测试数据,Nydus 在大规模场景下将容器启动时间从平均 15 秒降低到 3 秒,镜像拉取带宽减少 80%。

12.3 Confidential Containers

机密容器(Confidential Containers)利用 CPU 的可信执行环境(TEE,Trusted Execution Environment),如 Intel TDX、AMD SEV-SNP,在加密的内存区域中运行容器。即使宿主机管理员也无法访问容器内的数据和代码。

这项技术主要面向金融、医疗等对数据隐私要求极高的行业,以及多租户云环境中的数据隔离。Azure 已提供 Confidential Containers 的商业化支持。


参考资料

  1. Biederman, E. W., & Networker, L. (2006). Multiple Instances of the Global Linux Namespaces. Proceedings of the Linux Symposium.
  2. Heo, T. (2015). Control Group v2. Linux Kernel Documentation. https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
  3. Open Container Initiative. (2023). OCI Image Format Specification v1.1. https://github.com/opencontainers/image-spec
  4. Open Container Initiative. (2023). OCI Runtime Specification v1.1. https://github.com/opencontainers/runtime-spec
  5. Docker, Inc. (2016). containerd: An Industry-Standard Container Runtime. https://containerd.io
  6. CRI-O Contributors. (2023). CRI-O: OCI-Based Implementation of Kubernetes CRI. https://cri-o.io
  7. Cilium Authors. (2023). CNI Benchmark: iptables vs eBPF. https://cilium.io/blog
  8. Shopify Engineering. (2019). Scaling Shopify’s Multi-Tenant Architecture. Shopify Engineering Blog.
  9. Shopify Engineering. (2020). Running Rails in a Container: Lessons Learned. Shopify Engineering Blog.
  10. Google. (2023). gVisor: Container Runtime Sandbox. https://gvisor.dev
  11. Kata Containers Community. (2023). Kata Containers Architecture. https://katacontainers.io
  12. Alibaba Cloud. (2022). Nydus: Dragonfly Container Image Service. https://nydus.dev
  13. Walsh, D. (2019). Rootless Containers with Podman. Red Hat Blog.
  14. Facebook Engineering. (2019). PSI: Pressure Stall Information. https://facebookmicrosites.github.io/psi/
  15. Manco, F., et al. (2017). My VM is Lighter (and Safer) than your Container. Proceedings of the 26th Symposium on Operating Systems Principles (SOSP).

上一篇:事件响应与故障复盘 下一篇:Kubernetes 架构深度解析

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .