在 Kubernetes 上把 LLM 冷启动压到 30 秒——网易游戏的实战路径

2026-05-21 32 预计阅读时间:1 分钟
来源:cncf.io AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

弹性扩容听起来很美:流量来了就加 Pod,流量走了就缩容。但把这套逻辑搬到 LLM 推理服务上,现实会给你一记闷棍——模型权重动辄几十 GB,Pod 从零到可用,数据搬运的时间远比计算调度慢。网易游戏在生产环境中把这个冷启动过程压到了 30 秒,核心认知只有一句:弹性计算只有在数据能同等速度移动时才有意义

冷启动的真正瓶颈不是 CPU,是磁盘和网络

一个典型 LLM 推理 Pod 的冷启动链路大致如下:

  1. 调度器分配节点 → 拉取容器镜像 → 启动容器 → 加载模型权重到 GPU → 就绪

其中"加载模型权重"这一步,在传统方案里通常意味着从远端存储(S3、NAS、HTTP)下载几十 GB 文件到本地,再从本地读入 GPU 显存。网络带宽和存储 I/O 成了整条链路的瓶颈。

网易游戏的实测数据表明:在未优化前,一个 13B 参数模型的冷启动耗时在 3–5 分钟,其中 80% 以上时间花在数据搬运上。GPU 在这段时间里空转等待,资源利用率极低。

三层加速策略

网易游戏的方案不是某一个黑科技,而是把数据搬运拆成三层,逐层击破:

第一层:镜像内嵌 vs 运行时拉取——选运行时挂载

把模型权重塞进容器镜像是最直觉的做法,但代价巨大:镜像膨胀到 50GB+,每次版本更新都要重新构建和推送,节点拉镜像的时间反而更长。

网易选择了镜像与权重分离:镜像只包含推理框架和依赖,模型权重通过分布式缓存系统在运行时挂载。这样镜像体积回到几百 MB,拉取秒级完成。

第二层:分布式权重缓存——让数据"已经在那里"

关键设计:在集群中部署一套模型权重缓存层(基于分布式文件系统 + 本地 SSD 缓存),让每个节点上"热"保留最近使用过的模型文件。

当一个新 Pod 被调度到节点 N 上:

  • 如果节点 N 的本地缓存已有该模型权重 → 直接从本地 SSD 读入 GPU,耗时约 10–15 秒
  • 如果节点 N 缓存未命中 → 从集群内缓存网络拉取,耗时约 20–25 秒(内网带宽远优于公网)

这比从远端对象存储拉取快 5–10 倍。

第三层:调度感知——把 Pod 送到数据已经在的地方

光有缓存还不够,如果调度器随机分配节点,缓存命中率会很低。网易在调度层面做了定制:优先将 LLM 推理 Pod 调度到已有对应模型权重缓存的节点上。

这需要调度器能感知缓存状态。实现方式是通过一个 Cache Status CRD 定期上报每个节点的缓存清单,调度器在 Filter 扩展点优先打分有缓存的节点。

实践:搭建一个最小化的 LLM 冷启动加速方案

以下示例展示如何在 Kubernetes 中实现"镜像与权重分离 + 本地缓存挂载 + 调度感知"的最小可行方案。你可以直接在测试集群中运行并改造。

1. 用 PersistentVolume + nodeAffinity 做本地模型缓存

首先,在每个可能运行 LLM 推理的节点上准备一块本地 SSD,注册为 Local PersistentVolume:

# local-model-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: model-cache-pv-node1
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain  # 缓存数据不随 Pod 删除
  storageClassName: local-model-cache
  local:
    path: /mnt/model-cache  # 节点上的 SSD 挂载路径
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node1  # 替换为你的节点名

每个提供缓存的节点都需要一个对应的 PV,path 指向本地 SSD 目录。Retain reclaimPolicy 确保缓存数据在 Pod 退出后保留——下一个 Pod 调度到同一节点时直接命中。

2. 推理 Pod:镜像与权重分离

推理 Pod 的镜像只包含 vLLM(或其他推理框架),模型权重通过 PVC 挂载:

# llm-inference-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: llm-infer-13b
  labels:
    app: llm-inference
    model: llama-13b  # 标记所需模型,调度器可据此筛选
spec:
  containers:
    - name: infer
      image: your-registry/vllm:0.6.0  # 镜像不含模型权重,体积 ~500MB
      command: ["python", "-m", "vllm.entrypoints.openai.api_server"]
      args:
        - "--model"
        - "/models/llama-13b"  # 从挂载路径加载
        - "--tensor-parallel-size"
        - "2"
      resources:
        limits:
          nvidia.com/gpu: 2
      volumeMounts:
        - name: model-volume
          mountPath: /models
  volumes:
    - name: model-volume
      persistentVolumeClaim:
        claimName: model-cache-pvc-llama-13b
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          preference:
            matchExpressions:
              - key: cache-models
                operator: In
                values:
                  - llama-13b  # 优先调度到已有该模型缓存的节点

3. 初始化:首次缓存填充

当节点缓存未命中时,需要一个 Init Container 从远端存储拉取模型文件到本地 SSD:

# 在 Pod spec 中添加 initContainers
initContainers:
  - name: model-downloader
    image: your-registry/model-downloader:latest
    command: ["sh", "-c"]
    args:
      - |
        MODEL_DIR=/models/llama-13b
        if [ ! -f "$MODEL_DIR/config.json" ]; then
          echo "Cache miss, downloading model from remote storage..."
          mkdir -p $MODEL_DIR
          # 从内网对象存储拉取(比公网快得多)
          mc cp minio-internal/models/llama-13b/* $MODEL_DIR/
          echo "Download complete."
        else
          echo "Cache hit, skipping download."
        fi
    volumeMounts:
      - name: model-volume
        mountPath: /models

mc 是 MinIO Client,替换为你实际使用的对象存储客户端。关键是内网对象存储的带宽远优于公网,这一步即使缓存未命中也能在 20 秒内完成。

4. 节点缓存标签自动更新

用一个轻量 CronJob 定期扫描各节点的缓存目录,更新节点标签,让调度器感知缓存状态:

# update-cache-labels.sh — 在每个节点上运行
#!/usr/bin/env bash
CACHE_DIR="/mnt/model-cache"
LABEL_PREFIX="cache-models"

# 扫描缓存目录,发现已有模型
models=$(ls $CACHE_DIR | tr '\n' ',')

# 更新节点标签
kubectl label node $(hostname) ${LABEL_PREFIX}=${models} --overwrite

把这段脚本放进 DaemonSet 或 CronJob,每 30 秒执行一次。调度器就能通过 nodeAffinity 把 Pod 优先送到有缓存的节点。

5. 验证冷启动时间

部署完成后,用以下命令测量从 Pod 创建到服务就绪的时间:

# 记录 Pod 创建时间
start_time=$(date +%s)
kubectl apply -f llm-inference-pod.yaml

# 等待 Pod Ready
kubectl wait --for=condition=Ready pod/llm-infer-13b --timeout=120s

# 计算耗时
end_time=$(date +%s)
echo "Cold start time: $(($end_time - $start_time)) seconds"

在缓存命中的节点上,你应该能看到冷启动时间从分钟级降到 30 秒以内。

还有哪些坑要注意

这套方案不是银弹,实际落地时有几个边界条件:

  • GPU 显存加载仍是硬开销:即使权重已在本地 SSD,从磁盘读入 GPU 显存仍需 10–15 秒(取决于模型大小和 PCIe 带宽),这部分无法绕过。30 秒的目标意味着数据搬运必须控制在 15 秒以内。
  • 缓存容量有限:本地 SSD 通常 100–500 GB,只能缓存 2–5 个大模型。需要配合淘汰策略(LRU),并接受某些模型首次调度必然缓存未命中。
  • 调度器与缓存的耦合:如果用默认 kube-scheduler,preferredDuringScheduling 只是软偏好;在高负载时 Pod 可能被调度到无缓存节点。网易的做法是开发调度扩展插件,把缓存命中变成硬约束(在高优先级场景下)。
  • 模型版本更新:权重文件更新后,需要主动刷新节点缓存,否则 Pod 会加载旧版本。建议在 CI 流程中触发缓存预热 Job。

落地检查清单

在把这套方案推向生产前,逐项确认:

检查项 状态
推理镜像不含模型权重,体积 < 1 GB
每个推理节点有本地 SSD,注册为 Local PV(reclaimPolicy=Retain)
内网对象存储带宽 ≥ 10 Gbps,模型拉取可在 20 秒内完成
Init Container 在缓存未命中时自动拉取,命中时跳过
节点标签定期更新,反映当前缓存模型清单
Pod nodeAffinity 优先调度到有缓存的节点
模型版本更新流程包含缓存刷新步骤
冷启动时间有监控和 SLA 定义

弹性扩容的承诺是"秒级响应",但 LLM 的数据密度让这个承诺在默认架构下根本无法兑现。网易游戏的实践说明:不要只优化调度,要优化数据的位置。当模型权重已经在 Pod 要去的地方时,30 秒冷启动不是魔法,是工程。


相关推荐