前言:为什么要在 K8s 上部署大模型推理服务?
很多团队初期使用 AI 大模型时,选择直接调用 OpenAI API 或云厂商托管的模型 API(通义千问、文心一言)。这完全没问题------直到你遇到以下场景:
- 数据合规:金融、医疗等行业客户要求数据不出私有环境,不能调用外部 API
- 成本控制:OpenAI API 按 Token 计费,高频场景成本迅速失控
- 延迟要求:内网自托管推理服务延迟 <100ms,外部 API 延迟 300-800ms
- 定制化:需要在基础模型上做微调(LoRA/QLoRA),外部 API 无法支持
将 LLM 推理服务部署到 K8s,你可以像管理 Java 微服务一样管理它:弹性伸缩、滚动更新、监控告警、灰度发布------本文给出完整的生产级方案。
一、整体架构设计
bash
外部请求 / Java SaaS 内部调用
│
▼
┌─────────────────────────────────────────────────────┐
│ ai-platform namespace │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ AI Gateway Service(Java/Spring Boot) │ │
│ │ - 统一鉴权、限流、计费 │ │
│ │ - 模型路由(按租户/场景选择不同模型) │ │
│ │ - 请求/响应日志记录 │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ vLLM │ │ vLLM │ │ vLLM │ │
│ │ Qwen-7B │ │ Llama3-8B │ │ Embedding │ │
│ │ (GPU Node) │ │ (GPU Node) │ │ (CPU Node) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────┘
▲
│ 直接调用(内部服务)
┌─────────────────────────┐
│ production namespace │
│ Java SaaS API │ → 通过 LangChain4j / Spring AI 调用
└─────────────────────────┘
二、GPU 节点准备与调度
2.1 为 GPU 节点安装 NVIDIA Device Plugin
K8s 调度 GPU 资源需要 NVIDIA Device Plugin,它让 K8s 感知每个节点的 GPU 数量,并在 Pod 调度时分配 GPU:
bash
# 安装 NVIDIA Device Plugin(DaemonSet,自动在所有 GPU 节点上运行)
kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.0/nvidia-device-plugin.yml
# 验证 GPU 节点资源
kubectl get nodes -o json | jq '.items[].status.capacity."nvidia.com/gpu"'
# 输出:每个 GPU 节点的 GPU 数量,如 "4"
# 查看 GPU 节点详情
kubectl describe node gpu-node-001 | grep -A 5 "Capacity"
# Capacity:
# nvidia.com/gpu: 4 ← 4 张 GPU
# cpu: 64
# memory: 256Gi
2.2 GPU 节点隔离(Taint + Toleration)
为 GPU 节点添加 Taint,确保只有需要 GPU 的 Pod 才会被调度到 GPU 节点,避免普通 CPU Pod 占用昂贵的 GPU 资源:
bash
# 为所有 GPU 节点打 Taint
kubectl taint nodes gpu-node-001 nvidia.com/gpu=present:NoSchedule
kubectl taint nodes gpu-node-002 nvidia.com/gpu=present:NoSchedule
# 也可以用 Label Selector 批量打 Taint
kubectl get nodes -l node-type=gpu -o name | \
xargs -I{} kubectl taint {} nvidia.com/gpu=present:NoSchedule
需要 GPU 的 Pod 必须声明 Toleration 才能调度到 GPU 节点:
yaml
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "present"
effect: "NoSchedule"
# 同时用 nodeSelector 精确定向调度(双重保险)
nodeSelector:
node-type: gpu
gpu-model: "A100-80G" # 可以按 GPU 型号选择节点
三、vLLM 推理服务部署
3.1 什么是 vLLM?
vLLM 是目前最主流的高性能 LLM 推理引擎,核心优势:
- PagedAttention:将 GPU 显存管理类比操作系统的虚拟内存,显存利用率提升 3-5 倍
- 连续批处理(Continuous Batching):动态合并多个请求,GPU 吞吐量比 Hugging Face Transformers 高 10-24 倍
- OpenAI 兼容 API :暴露与 OpenAI 完全兼容的
/v1/chat/completions接口,Java 客户端无需改代码
3.2 构建 vLLM 镜像
生产环境建议基于官方镜像构建,加入模型文件(或通过 PVC 挂载):
dockerfile
# Dockerfile.vllm
# 注意:vLLM 官方镜像约 15GB,请确保 Harbor 存储充足
FROM vllm/vllm-openai:v0.4.0
# 如果模型文件不大(<10GB),可以直接打入镜像
# 但通义千问 7B 模型约 14GB,建议通过 PVC 挂载(见下方)
# COPY ./models/Qwen-7B-Chat /models/Qwen-7B-Chat
# 生产中更常见的做法:镜像不含模型,模型文件通过 PVC 挂载
# 模型通过 InitContainer 从对象存储预下载到 PVC
3.3 vLLM Deployment 完整配置
这是本系列最关键的 YAML 配置之一,包含 GPU 资源申请、模型挂载、健康检查等所有生产要素:
yaml
# vllm-qwen-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen7b
namespace: ai-platform
labels:
app: vllm-qwen7b
model: qwen-7b-chat
spec:
replicas: 1 # GPU 资源有限,通常从 1 个副本开始
selector:
matchLabels:
app: vllm-qwen7b
template:
metadata:
labels:
app: vllm-qwen7b
model: qwen-7b-chat
spec:
# GPU 节点调度
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "present"
effect: "NoSchedule"
nodeSelector:
node-type: gpu
# InitContainer:启动前检查模型文件是否就绪
initContainers:
- name: model-checker
image: busybox:1.36
command:
- sh
- -c
- |
echo "检查模型文件..."
if [ ! -f "/models/Qwen-7B-Chat/config.json" ]; then
echo "模型文件不存在,请先运行模型下载 Job"
exit 1
fi
echo "模型文件就绪"
volumeMounts:
- name: model-storage
mountPath: /models
readOnly: true
containers:
- name: vllm-server
# 使用 Harbor 私有镜像(从官方镜像同步)
image: harbor.yoursaas.com/ai/vllm-openai:v0.4.0
command:
- python
- -m
- vllm.entrypoints.openai.api_server
args:
- --model=/models/Qwen-7B-Chat
- --served-model-name=qwen-7b-chat # API 中引用的模型名
- --host=0.0.0.0
- --port=8000
- --tensor-parallel-size=1 # 使用 1 张 GPU(多 GPU 并行设置为 GPU 数量)
- --gpu-memory-utilization=0.90 # 使用 90% GPU 显存(留 10% buffer)
- --max-model-len=8192 # 最大上下文长度(Token 数)
- --max-num-seqs=256 # 最大并发请求数
- --dtype=bfloat16 # 推理精度(A100 用 bfloat16,V100 用 float16)
- --trust-remote-code # 通义千问需要此参数
ports:
- containerPort: 8000
name: http
resources:
limits:
nvidia.com/gpu: "1" # 申请 1 张 GPU(关键!)
memory: "40Gi" # GPU 显存外的系统内存
cpu: "8"
requests:
nvidia.com/gpu: "1"
memory: "20Gi"
cpu: "4"
env:
- name: CUDA_VISIBLE_DEVICES
value: "0" # 使用第 0 张 GPU
- name: HUGGING_FACE_HUB_TOKEN
valueFrom:
secretKeyRef:
name: ai-secrets
key: HF_TOKEN
optional: true # 私有模型需要 HF Token
volumeMounts:
- name: model-storage
mountPath: /models
readOnly: true # 模型文件只读挂载
- name: shm
mountPath: /dev/shm # 共享内存(多进程推理必须)
# 健康检查:vLLM 启动加载模型需要 3-5 分钟
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 30 # 30 × 10s = 5 分钟等待启动
periodSeconds: 10
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 30
failureThreshold: 3
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-storage-pvc # 模型文件 PVC(见下方)
- name: shm
emptyDir:
medium: Memory
sizeLimit: 16Gi # 共享内存大小
---
# 模型存储 PVC(需要高速 SSD,读取大模型文件)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-storage-pvc
namespace: ai-platform
spec:
accessModes:
- ReadWriteMany # 多个推理 Pod 共享同一个模型文件
storageClassName: alicloud-nas-extreme # 使用极速 NAS(高 IOPS)
resources:
requests:
storage: 200Gi # 存放多个模型文件
---
# vLLM Service
apiVersion: v1
kind: Service
metadata:
name: vllm-qwen7b-service
namespace: ai-platform
spec:
selector:
app: vllm-qwen7b
ports:
- port: 80
targetPort: 8000
name: http
3.4 模型文件下载 Job
首次部署时,通过 K8s Job 把模型文件从对象存储下载到 PVC:
yaml
# model-download-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: download-qwen7b-model
namespace: ai-platform
spec:
template:
spec:
restartPolicy: Never
containers:
- name: downloader
image: python:3.11-slim
command:
- bash
- -c
- |
pip install modelscope -q
# 从 ModelScope 下载通义千问 7B Chat 模型
python -c "
from modelscope import snapshot_download
snapshot_download(
'qwen/Qwen-7B-Chat',
cache_dir='/models',
local_dir='/models/Qwen-7B-Chat'
)
print('模型下载完成')
"
env:
- name: MODELSCOPE_CACHE
value: /models/.cache
resources:
requests:
memory: "4Gi"
cpu: "2"
volumeMounts:
- name: model-storage
mountPath: /models
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-storage-pvc
⚠️ 踩坑记录 #1:vLLM Pod 反复 OOMKilled,但明明 GPU 显存够
GPU 显存和系统内存是独立的。vLLM 需要大量系统内存 来管理 KV Cache 的元数据、批处理队列等。7B 模型在 A100 80G 上运行,GPU 显存只用了 14GB,但系统内存(
spec.resources.limits.memory)设了 8Gi 就会 OOMKilled。经验值:7B 模型系统内存建议
requests: 20Gi, limits: 40Gi;13B 模型建议requests: 40Gi, limits: 80Gi。另外,/dev/shm(共享内存)也要通过emptyDir: {medium: Memory}挂载,否则 CUDA 多进程通信失败。
四、Java 客户端集成
4.1 方案一:Spring AI(Spring 官方推荐)
Spring AI 是 Spring 官方提供的 AI 集成框架,支持 OpenAI 兼容 API,与 vLLM 无缝对接:
pom.xml:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- OpenAI 兼容客户端(vLLM 暴露 OpenAI 兼容接口) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
application.yml 配置(指向 K8s 内的 vLLM Service):
yaml
spring:
ai:
openai:
# 指向 K8s 集群内的 vLLM Service(跨 namespace 完整 DNS)
base-url: http://vllm-qwen7b-service.ai-platform.svc.cluster.local
api-key: "not-needed" # vLLM 本地部署不需要真实 API Key,但字段不能为空
chat:
options:
model: qwen-7b-chat # 对应 vLLM 的 --served-model-name
temperature: 0.7
max-tokens: 2048
Java 服务代码:
java
@Service
@RequiredArgsConstructor
@Slf4j
public class AiSummaryService {
private final ChatClient chatClient;
private final MeterRegistry meterRegistry;
/**
* 生成文档摘要(同步调用)
*/
public String generateSummary(String tenantId, String content) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
String prompt = """
请为以下内容生成一段简洁的中文摘要,控制在 200 字以内:
%s
""".formatted(content);
String result = chatClient.prompt()
.user(prompt)
.call()
.content();
log.info("AI 摘要生成成功, tenantId={}, inputLen={}, outputLen={}",
tenantId, content.length(), result.length());
return result;
} catch (Exception e) {
log.error("AI 摘要生成失败, tenantId={}, error={}", tenantId, e.getMessage());
throw new AiServiceException("AI 摘要服务暂时不可用,请稍后重试", e);
} finally {
sample.stop(Timer.builder("ai.summary.duration")
.tag("tenantId", tenantId)
.register(meterRegistry));
}
}
/**
* 流式输出(SSE,适合长文本生成)
*/
public Flux<String> generateSummaryStream(String content) {
return chatClient.prompt()
.user("请总结:" + content)
.stream()
.content()
.doOnError(e -> log.error("流式 AI 调用失败", e))
.onErrorReturn("AI 服务暂时不可用");
}
/**
* 多模型路由:根据租户套餐选择不同模型
* 免费版 → 内部 7B 模型;企业版 → 内部 72B 模型或外部 GPT-4
*/
public String generateWithRouting(String tenantId, String plan, String content) {
String baseUrl = switch (plan) {
case "enterprise" -> "http://vllm-qwen72b-service.ai-platform.svc.cluster.local";
case "pro" -> "http://vllm-qwen7b-service.ai-platform.svc.cluster.local";
default -> "http://vllm-qwen7b-service.ai-platform.svc.cluster.local";
};
// 动态创建指向不同模型的 ChatClient
ChatClient routedClient = ChatClient.builder(
OpenAiChatModel.builder()
.openAiApi(new OpenAiApi(baseUrl, "not-needed"))
.build()
).build();
return routedClient.prompt()
.user(content)
.call()
.content();
}
}
4.2 方案二:LangChain4j(功能更丰富)
LangChain4j 提供了更完整的 LLM 应用开发套件(RAG、Agent、工具调用等):
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>0.31.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>0.31.0</version>
</dependency>
yaml
langchain4j:
open-ai:
chat-model:
base-url: http://vllm-qwen7b-service.ai-platform.svc.cluster.local/v1
api-key: not-needed
model-name: qwen-7b-chat
temperature: 0.7
max-tokens: 2048
timeout: 60s # AI 推理可能较慢,超时时间要放大
java
// LangChain4j AI 服务接口(声明式,框架自动实现)
@AiService
public interface DocumentSummaryAiService {
@SystemMessage("你是一个专业的文档摘要助手,请用中文给出简洁准确的摘要。")
@UserMessage("请总结以下内容:{{content}}")
String summarize(@V("content") String content);
@SystemMessage("你是一个代码审查专家。")
@UserMessage("请审查以下 Java 代码并指出潜在问题:{{code}}")
String reviewCode(@V("code") String code);
}
// 在业务服务中注入使用
@Service
@RequiredArgsConstructor
public class DocumentService {
private final DocumentSummaryAiService aiService;
public DocumentDTO processDocument(String tenantId, String content) {
// 直接调用,框架负责与 vLLM 通信
String summary = aiService.summarize(content);
return DocumentDTO.builder()
.content(content)
.aiSummary(summary)
.build();
}
}
4.3 RAG(检索增强生成)与向量数据库
生产级 AI 应用通常需要 RAG------让大模型能"查询"你的私有知识库:
yaml
# 部署 Milvus 向量数据库(K8s 上)
helm repo add milvus https://zilliztech.github.io/milvus-helm
helm install milvus milvus/milvus \
--namespace ai-platform \
--set cluster.enabled=false \ # 单机模式
--set persistence.enabled=true \
--set persistence.size=100Gi
java
// LangChain4j RAG 配置
@Configuration
public class RagConfig {
@Bean
public EmbeddingModel embeddingModel() {
// 使用 K8s 内部部署的 Embedding 模型(如 bge-m3)
return OpenAiEmbeddingModel.builder()
.baseUrl("http://vllm-embedding-service.ai-platform.svc.cluster.local/v1")
.apiKey("not-needed")
.modelName("bge-m3")
.build();
}
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
// 连接 K8s 内部的 Milvus
return MilvusEmbeddingStore.builder()
.uri("http://milvus.ai-platform.svc.cluster.local:19530")
.collectionName("java_saas_knowledge")
.dimension(1024) // bge-m3 的向量维度
.build();
}
@Bean
public RetrievalAugmentor retrievalAugmentor(
EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return DefaultRetrievalAugmentor.builder()
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.maxResults(5) // 检索最相关的 5 段内容
.minScore(0.6) // 相似度阈值
.build())
.build();
}
}
五、AI 推理服务的弹性伸缩
回顾第 04 篇的 KEDA 配置,为 vLLM 服务配置基于 Redis 队列的弹性伸缩:
yaml
# vllm 推理服务默认缩容到 0,有请求时扩容
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vllm-qwen7b-scaler
namespace: ai-platform
spec:
scaleTargetRef:
name: vllm-qwen7b
minReplicaCount: 0 # 无请求时缩容到 0,节省 GPU 成本
maxReplicaCount: 4 # 最多 4 个推理实例
cooldownPeriod: 600 # 缩容冷却 10 分钟(GPU 节点起停慢)
triggers:
- type: prometheus
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring:9090
metricName: vllm_pending_requests
# vLLM 内置 Prometheus 指标:排队请求数
query: sum(vllm:num_requests_waiting{model_name="qwen-7b-chat"})
threshold: "5" # 排队请求 > 5 时扩容
六、常见问题 FAQ
Q1:A100、H100、V100 如何选择?
H100 > A100 > V100 性能。对于推理场景:7B/13B 模型用 A100 40G 够用;70B+ 模型需要 A100 80G 或多卡。国内云厂商通常提供 A10(适合 7B-13B 推理,性价比高)和 A100。V100 较旧,bfloat16 支持不完整,不建议用于新部署。
Q2:一张 GPU 能同时跑多个模型吗?
通过 GPU 时间片(MIG 或 MPS),一张 A100 可以切分给多个小模型实例。nvidia.com/gpu: "0.5" 申请半张 GPU(需要 GPU Operator 支持)。但 vLLM 不支持分数 GPU,只能整卡或多卡。如需一卡多模型,可以用 TorchServe 或 Triton Inference Server。
Q3:vLLM 调用超时,Java 客户端应该如何处理?
推理时间与 Token 数成正比,生成 2048 Token 可能需要 30-60 秒。Spring AI 和 LangChain4j 的默认超时通常是 10-30 秒,必须手动设大:timeout: 120s。生产建议使用流式 API(SSE),边生成边传输,用户体验好且不容易超时。
Q4:如何实现 AI 调用的降级?
当内部 vLLM 服务不可用时,自动降级到外部 API:
java
@Service
public class AiServiceWithFallback {
@CircuitBreaker(name = "internal-llm", fallbackMethod = "fallbackToExternal")
public String generate(String prompt) {
return internalVllmClient.generate(prompt); // 优先调内部
}
private String fallbackToExternal(String prompt, Exception e) {
log.warn("内部 LLM 不可用,降级到外部 API: {}", e.getMessage());
return externalOpenAiClient.generate(prompt); // 降级到 OpenAI
}
}
Q5:大模型推理的费用如何监控和控制?
对于内部部署,主要成本是 GPU 机时。在 Prometheus 中监控 nvidia_gpu_utilization(GPU 利用率),利用率长期低于 30% 说明资源浪费。结合 KEDA 缩容到 0 + Cluster Autoscaler 自动回收 GPU 节点,可以将 GPU 成本降低 60-70%。
总结
| 环节 | 方案 | 关键配置 |
|---|---|---|
| GPU 调度 | Device Plugin + Taint/Toleration | nvidia.com/gpu: "1" |
| 推理引擎 | vLLM | PagedAttention、连续批处理、OpenAI 兼容 API |
| 模型存储 | NAS PVC(ReadWriteMany) | 多推理实例共享模型文件 |
| Java 集成 | Spring AI / LangChain4j | 指向 K8s 内 vLLM Service |
| 弹性伸缩 | KEDA + Prometheus 指标 | 可缩容到 0,节省 GPU 成本 |
💬 一句话总结 :vLLM + K8s 让 LLM 推理服务变成了又一个"普通微服务"------同样的 Deployment、同样的 Service、同样的 HPA,只是资源申请里多了一行
nvidia.com/gpu: "1",而 Java 业务层通过 Spring AI 调用它,就像调用任何其他 HTTP 服务一样自然。
上一篇 :K8s 安全加固指南:RBAC、NetworkPolicy、OPA------Java SaaS 多租户安全隔离深度实践
下一篇预告 :[《K8s 多集群与混合云:Karmada------Java SaaS 全球化部署架构设计》](#《K8s 多集群与混合云:Karmada——Java SaaS 全球化部署架构设计》)
📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!