弹性扩容听起来很美:流量来了就加 Pod,流量走了就缩容。但把这套逻辑搬到 LLM 推理服务上,现实会给你一记闷棍——模型权重动辄几十 GB,Pod 从零到可用,数据搬运的时间远比计算调度慢。网易游戏在生产环境中把这个冷启动过程压到了 30 秒,核心认知只有一句:弹性计算只有在数据能同等速度移动时才有意义。
冷启动的真正瓶颈不是 CPU,是磁盘和网络
一个典型 LLM 推理 Pod 的冷启动链路大致如下:
- 调度器分配节点 → 拉取容器镜像 → 启动容器 → 加载模型权重到 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 目录。RetainreclaimPolicy 确保缓存数据在 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 秒冷启动不是魔法,是工程。