【infra之路】13-Docker与Kubernetes基础

学习目标

理解容器化的底层原理(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 跑在哪个节点