学习目标
理解容器化的底层原理(namespace、cgroup)、Docker 在 AI 场景下的最佳实践(CUDA 镜像构建),以及 Kubernetes 的核心概念和调度机制。这是 AI Infra 从"能跑"到"能上线"的关键一步。
第一部分:容器底层原理
1.1 什么是容器?
容器本质上是一个受限的 Linux 进程,不是虚拟机。它和宿主机上的其他进程共享同一个内核,但通过两种机制实现了隔离:
虚拟机:
每个 VM 有自己的内核 → 重(GB 级)、启动慢(分钟级)
容器:
所有容器共享宿主机内核 → 轻(MB 级)、启动快(秒级)
通过 Linux 内核特性实现隔离:
- Namespace: 让进程"看不到"其他进程(隔离视图)
- Cgroup: 让进程"用不了"太多资源(限制用量)
1.2 Namespace(隔离视图)
Linux 提供了 6 种 namespace,每种隔离一个维度的系统资源:
PID namespace: 进程 ID 隔离
容器内的 PID 1 ≠ 宿主机的 PID 1
容器内进程看不到宿主机的其他进程
Network namespace: 网络隔离
容器有自己的 IP、端口空间
容器间网络默认不通(除非配置 bridge/overlay)
Mount namespace: 文件系统隔离
容器有自己的根文件系统(rootfs)
看不到宿主机的 /home、/etc 等
UTS namespace: 主机名隔离
容器可以有自己的 hostname
User namespace: 用户隔离
容器内的 root ≠ 宿主机的 root
IPC namespace: 进程间通信隔离
一个容器进程看到的"世界"是被 namespace 裁剪过的:
宿主机视角:
/proc 下有 500 个进程
网络有 eth0(192.168.1.100)
文件系统有 /home/user/data
容器内视角:
/proc 下只有 3 个进程(容器自己的)
网络有 eth0(172.17.0.2,Docker 分配的)
文件系统只有容器镜像里的文件
1.3 Cgroup(限制资源)
Cgroup(Control Group)限制容器能使用的资源量:
CPU cgroup:
限制容器最多用 N 个 CPU 核心
docker run --cpus=2 ... # 最多用 2 个核
Memory cgroup:
限制容器最多用 N GB 内存
docker run --memory=8g ... # 最多用 8GB RAM
Devices cgroup:
控制容器能访问哪些设备
docker run --gpus all ... # 允许访问 GPU(通过 NVIDIA Container Toolkit)
PID cgroup:
限制容器内的最大进程数
如果没有 cgroup,一个容器可以吃掉宿主机所有资源。有了 cgroup,Kubernetes 才能做资源调度------"这个 Pod 要 4 个 CPU 和 16GB 内存,调度到哪台节点上?"
1.4 UnionFS(镜像分层)
Docker 镜像不是一整个文件系统,而是多层叠加的:
Layer 3 (读写层): 容器运行时产生的文件
Layer 2: 你的应用代码
Layer 1: pip install 的 Python 包
Layer 0 (base): Ubuntu 22.04
读取文件时: 从上往下找,找到就返回
写入文件时: Copy-on-Write,复制到最上面的读写层
好处:
- 多个容器共享底层的只读层,节省磁盘
- 镜像可以增量拉取(只下载变化的层)
- Dockerfile 每一行命令生成一层,利用缓存
第二部分:Docker 与 CUDA
2.1 NVIDIA Container Toolkit
容器默认无法访问 GPU。需要安装 NVIDIA Container Toolkit 让容器"看到"宿主机的 GPU:
原理:
宿主机: /dev/nvidia0, /dev/nvidiactl, nvidia-uvm 等设备文件
宿主机: /usr/lib/x86_64-linux-gnu/libcuda.so 等驱动文件
NVIDIA Container Toolkit:
在容器启动时自动把 GPU 设备和驱动库挂载进容器
容器内的 CUDA 程序可以像在宿主机一样调用 GPU
安装:
# Ubuntu
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
使用:
docker run --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
2.2 AI 项目 Dockerfile 最佳实践
一个典型的 AI 推理服务 Dockerfile:
dockerfile
# 1. 选择 base 镜像(带 CUDA 运行时)
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
# 2. 设置环境变量(避免交互式安装卡住)
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# 3. 安装系统依赖(变化频率低,利用缓存)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.10 \
python3-pip \
git \
&& rm -rf /var/lib/apt/lists/* # 清理缓存,减小镜像体积
# 4. 安装 Python 依赖(变化频率中等)
COPY requirements.txt /app/requirements.txt
RUN pip3 install --no-cache-dir -r /app/requirements.txt
# 5. 复制应用代码(变化频率高,放最后)
COPY . /app
WORKDIR /app
# 6. 暴露端口
EXPOSE 8000
# 7. 启动命令
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
"--model", "meta-llama/Llama-2-7b-hf", \
"--port", "8000"]
Dockerfile 优化要点:
层缓存顺序(从不变到常变):
Layer 1: base 镜像 ← 几乎不变
Layer 2: 系统依赖 apt-get ← 很少变
Layer 3: Python 依赖 pip ← 偶尔变
Layer 4: 应用代码 COPY ← 经常变
好处: 修改代码时只需重建 Layer 4,前三层直接用缓存
减小镜像体积:
- 用 runtime 镜像而不是 devel 镜像(少 ~2GB)
- rm -rf /var/lib/apt/lists/*(删 apt 缓存)
- pip install --no-cache-dir(不缓存 pip 下载)
- 多阶段构建(编译在一个镜像,运行在另一个镜像)
2.3 多阶段构建(减小镜像体积)
dockerfile
# 阶段1: 编译(包含 gcc、make 等工具)
FROM nvidia/cuda:12.4.0-devel-ubuntu22.04 AS builder
COPY . /build
WORKDIR /build
RUN make all # 编译 CUDA kernel
# 阶段2: 运行(只包含运行时库)
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
COPY --from=builder /build/bin/* /app/
CMD ["/app/inference_server"]
# 最终镜像不包含 gcc、make 等编译工具
# 体积从 ~8GB 减小到 ~3GB
2.4 Docker Compose(多容器编排)
本地开发时可以用 Docker Compose 同时启动推理服务和监控:
yaml
# docker-compose.yml
version: "3.8"
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface # 模型缓存
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
command: >
--model meta-llama/Llama-2-7b-hf
--tensor-parallel-size 1
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
bash
docker-compose up -d # 启动所有服务
docker-compose logs vllm # 查看 vLLM 日志
docker-compose down # 停止所有服务
第三部分:Kubernetes 核心概念
3.1 为什么需要 Kubernetes?
Docker 解决了"一台机器上怎么跑容器"的问题,但生产环境需要:
- 多台机器上调度容器(哪台机器跑哪个容器?)
- 自动扩缩容(流量大了加容器,流量小了减容器)
- 故障自愈(容器挂了自动重启,机器挂了迁移到其他机器)
- 服务发现和负载均衡(请求怎么路由到正确的容器?)
- 滚动更新(新版本怎么平滑上线,不影响服务?)
Kubernetes(K8s)就是解决这些问题的容器编排系统。
3.2 核心概念
Node(节点)
Node = 一台物理机或虚拟机
Master Node:
- API Server: K8s 的"大脑",接收所有请求
- Scheduler: 决定 Pod 跑在哪个 Worker 上
- etcd: 存储集群状态(分布式 KV 数据库)
- Controller Manager: 维护集群期望状态
Worker Node:
- kubelet: 管理本节点上的 Pod
- kube-proxy: 管理网络规则(Service 的实现)
- Container Runtime: 运行容器(containerd/Docker)
Pod(最小调度单位)
Pod 不是一个容器,而是一组共享网络和存储的容器:
Pod:
├── Container 1: vLLM 推理服务
├── Container 2: 日志收集 sidecar
共享:
- 同一个 IP 地址
- 同一个 Network Namespace(localhost 互通)
- 可以挂载同一个 Volume
为什么不直接调度容器?
因为有些容器需要"绑定"在一起(比如应用 + 日志收集)
Pod 就是"绑定在一起的一组容器"的抽象
Pod 的 YAML 定义:
yaml
apiVersion: v1
kind: Pod
metadata:
name: vllm-pod
labels:
app: vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
resources:
requests:
memory: "16Gi"
cpu: "4"
nvidia.com/gpu: 1 # 请求 1 个 GPU
limits:
memory: "32Gi"
cpu: "8"
nvidia.com/gpu: 1
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
volumes:
- name: model-cache
hostPath:
path: /data/huggingface
Deployment(管理 Pod 的副本)
Deployment: 告诉 K8s "我要 N 个相同的 Pod"
K8s 会自动:
- 创建 N 个 Pod
- 监控 Pod 健康状态
- Pod 挂了自动重新创建
- 支持滚动更新和回滚
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deployment
spec:
replicas: 3 # 始终保持 3 个 Pod
selector:
matchLabels:
app: vllm
template: # Pod 模板
metadata:
labels:
app: vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
resources:
requests:
nvidia.com/gpu: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 更新时最多多出 1 个 Pod
maxUnavailable: 0 # 更新时不允许有 Pod 不可用
Service(负载均衡和服务发现)
问题: Pod 的 IP 是动态的(Pod 重启 IP 就变了)
客户端怎么找到 Pod?
Service: 提供稳定的访问入口
- ClusterIP: 集群内部访问(默认)
- NodePort: 通过节点端口访问(外部)
- LoadBalancer: 云厂商的负载均衡器
yaml
apiVersion: v1
kind: Service
metadata:
name: vllm-service
spec:
type: LoadBalancer
selector:
app: vllm # 匹配 label=app:vllm 的 Pod
ports:
- port: 80 # Service 暴露的端口
targetPort: 8000 # Pod 内的端口
请求流程:
客户端 → Service (vllm-service:80) → kube-proxy → Pod (10.244.1.5:8000)
→ Pod (10.244.2.3:8000)
→ Pod (10.244.3.7:8000)
kube-proxy 自动做负载均衡(round-robin 或 session affinity)
ConfigMap 和 Secret(配置管理)
yaml
# ConfigMap: 存储非敏感配置
apiVersion: v1
kind: ConfigMap
metadata:
name: vllm-config
data:
MODEL_NAME: "meta-llama/Llama-2-7b-hf"
MAX_MODEL_LEN: "4096"
GPU_MEMORY_UTILIZATION: "0.9"
---
# Secret: 存储敏感信息(Base64 编码)
apiVersion: v1
kind: Secret
metadata:
name: huggingface-token
type: Opaque
data:
HF_TOKEN: aGZf... # base64 编码的 token
在 Pod 中引用:
yaml
env:
- name: MODEL_NAME
valueFrom:
configMapKeyRef:
name: vllm-config
key: MODEL_NAME
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: huggingface-token
key: HF_TOKEN
3.3 调度流程
当用户提交一个 Pod 请求时,Scheduler 的决策过程:
1. 过滤(Predicates):
- 哪些节点有足够的 CPU/内存/GPU?
- 哪些节点满足 affinity/anti-affinity 规则?
- 哪些节点没有 taint(污点)排斥这个 Pod?
→ 得到候选节点列表
2. 打分(Priorities):
- 资源均衡度(CPU 和内存使用是否均衡)
- 数据局部性(Pod 需要的数据在哪个节点上)
- 节点负载(优先调度到空闲节点)
→ 得到最优节点
3. 绑定:
- 把 Pod 调度到得分最高的节点
- kubelet 在该节点上启动容器
3.4 K8s 架构全景
用户提交 YAML(kubectl apply -f deployment.yaml)
│
▼
┌───────────────────────────────────────────────────────┐
│ Master Node │
│ │
│ API Server ←──── kubectl / 其他组件的请求入口 │
│ │ │
│ ├──→ etcd(存储集群状态) │
│ │ │
│ ├──→ Scheduler(选择 Worker 节点) │
│ │ │
│ └──→ Controller Manager(维护期望状态) │
│ - Deployment Controller: 确保 Pod 副本数 │
│ - Node Controller: 监控节点健康 │
│ - Service Controller: 管理 Service │
└─────────────────────────┬─────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │
│ │ │ │ │ │
│ kubelet │ │ kubelet │ │ kubelet │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ Pod Pod │ │ Pod Pod │ │ Pod │
│ │ │ │ │ │
│ kube- │ │ kube- │ │ kube- │
│ proxy │ │ proxy │ │ proxy │
└──────────┘ └──────────┘ └──────────┘
第四部分:常用 kubectl 命令
bash
# 查看集群信息
kubectl cluster-info
kubectl get nodes # 查看所有节点
kubectl describe node worker-1 # 查看节点详情(GPU、CPU、内存)
# Pod 操作
kubectl get pods # 查看所有 Pod
kubectl get pods -o wide # 查看 Pod 详细信息(含 IP、节点)
kubectl describe pod vllm-pod-xxx # 查看 Pod 详细状态
kubectl logs vllm-pod-xxx # 查看 Pod 日志
kubectl logs -f vllm-pod-xxx # 实时跟踪日志
kubectl exec -it vllm-pod-xxx -- bash # 进入 Pod 内部
# 部署操作
kubectl apply -f deployment.yaml # 创建/更新资源
kubectl delete -f deployment.yaml # 删除资源
kubectl rollout status deployment/vllm-deployment # 查看滚动更新状态
kubectl rollout undo deployment/vllm-deployment # 回滚到上一版本
# 扩缩容
kubectl scale deployment/vllm-deployment --replicas=5 # 手动扩容到 5
# Service
kubectl get services # 查看所有 Service
kubectl port-forward svc/vllm-service 8080:80 # 本地端口转发
# 调试
kubectl top pods # 查看 Pod 资源使用
kubectl top nodes # 查看节点资源使用
kubectl events # 查看集群事件(排错用)
总结
| 概念 | 要点 |
|---|---|
| Namespace | Linux 内核特性,隔离进程视图(PID、网络、文件系统等) |
| Cgroup | 限制容器资源使用(CPU、内存、GPU) |
| NVIDIA Container Toolkit | 让容器访问宿主机 GPU |
| Dockerfile 优化 | 层缓存顺序、多阶段构建、清理缓存 |
| Pod | K8s 最小调度单位,一组共享网络的容器 |
| Deployment | 管理 Pod 副本数、滚动更新、故障自愈 |
| Service | 稳定访问入口 + 负载均衡 |
| Scheduler | 过滤 → 打分 → 绑定,决定 Pod 跑在哪个节点 |