第三章:vLLM推理高可用架构
聊到vLLM推理的高可用,我脑子里第一个蹦出来的场景,就是去年那次线上事故。
凌晨三点,告警电话响了。一个GPU节点挂了,所有推理请求全部超时。用户那边直接炸了锅。嗯,从那以后,我对高可用架构的执念,就刻进了骨子里。
说白了,单点部署的vLLM服务,就是一颗定时炸弹。你永远不知道它什么时候会炸。所以,这一章我们重点聊聊,怎么把vLLM从“单打独斗”变成“团队作战”。
1. 多副本部署:最基础的保命手段
多副本部署,听起来很简单,就是多跑几个vLLM实例嘛。但这里面的门道,我踩过不少坑。
核心原则:副本数至少为2,建议3副本起步。别问我为什么,我曾经就因为省那一个副本,半夜爬起来修过三次。
在K8s里,多副本部署用Deployment或StatefulSet都行。我个人习惯用Deployment,因为vLLM本身是无状态的——模型权重挂载在共享存储上就行。
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-inference
spec:
replicas: 3
selector:
matchLabels:
app: vllm
template:
metadata:
labels:
app: vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args: ["--model", "/models/llama-2-7b", "--port", "8000"]
ports:
- containerPort: 8000
volumeMounts:
- name: model-storage
mountPath: /models
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-pvc
这里有个细节要注意:每个副本加载模型时,会占用显存。如果你的GPU显存是80GB,模型需要40GB,那一个节点最多跑2个副本。别贪心,留点余量。
小技巧:我建议用节点亲和性(nodeAffinity)把副本分散到不同GPU节点上。这样即使一个节点挂了,其他节点上的副本还能继续服务。
3. 主从架构:谁说vLLM不能有主从?
你可能会问:“vLLM不是无状态的吗?搞什么主从?”
嗯,这里说的主从,不是数据库那种读写分离。而是针对推理请求的优先级和资源分配。
我在项目中遇到过一种场景:线上有高优先级请求(比如实时对话)和低优先级请求(比如批量离线推理)。如果混在一起,高优先级请求经常被低优先级的排队堵死。
解决方案很简单——搞两套vLLM集群:
- 主集群:处理高优先级请求,副本数多,资源充足,响应时间有保障。
- 从集群:处理低优先级请求,副本数少,甚至可以动态扩缩容。
两套集群共用同一个模型存储,但独立部署。请求路由由上层网关根据优先级分发。
注意:主从集群之间不要共享GPU资源。我曾经试过让它们混部,结果高优先级请求的显存被低优先级的抢走了,推理速度直接掉了一半。
4. 负载均衡策略:别让一个副本累死
多副本部署好了,接下来就是怎么把请求分发给它们。负载均衡,说白了就是“雨露均沾”。
K8s里最常用的方式是Service + Ingress。但默认的轮询(Round Robin)策略,在vLLM场景下并不完美。
为什么?因为每个请求的推理时间不一样。有的请求生成长文本,耗时几十秒;有的只生成几个字,几毫秒就完事。轮询策略会把长请求和短请求均匀分配,但长请求会阻塞后续请求,导致其他副本空闲,这个副本却排起长队。
我建议用最少连接数(Least Connections)策略。Nginx Ingress支持这个配置:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vllm-ingress
annotations:
nginx.ingress.kubernetes.io/load-balance: "least_conn"
spec:
rules:
- host: inference.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vllm-service
port:
number: 8000
还有一个更高级的做法:在应用层做负载均衡。比如用Envoy或自定义的Gateway,根据每个vLLM副本的当前排队长度来分发请求。这个我在一个高并发项目里用过,效果很好,但实现起来稍微复杂一些。
避坑指南:我曾经遇到过负载均衡配置错误,导致所有请求都打到一个副本上。排查了半天,发现是Service的sessionAffinity设置成了ClientIP。如果你不需要会话保持,记得关掉它。
5. 健康检查配置:让K8s自动“治病”
健康检查,是K8s自愈能力的核心。配置好了,节点挂了K8s自动帮你重启;配置不好,服务挂了K8s还傻傻地往里转发请求。
vLLM的健康检查,我一般分三层:
| 检查类型 | 检查方式 | 作用 |
|---|---|---|
| 存活检查(livenessProbe) | 检查vLLM进程是否存活 | 进程挂了就重启Pod |
| 就绪检查(readinessProbe) | 检查vLLM是否准备好接收请求 | 模型加载中或OOM时,从Service中摘除 |
| 启动检查(startupProbe) | 检查模型是否加载完成 | 防止在模型加载期间被误杀 |
具体配置如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-inference
spec:
template:
spec:
containers:
- name: vllm
# ... 其他配置
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 5
startupProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30 # 给模型加载留足时间
vLLM自带的/health端点,会返回200表示服务正常。但这里有个坑:/health只检查进程是否存活,不检查模型是否真的能推理。
我建议自己写一个更“智能”的健康检查端点。比如发一个简单的推理请求,如果返回正常结果,才算健康。这样能避免“进程活着但模型挂了”的尴尬情况。
警告:启动检查的failureThreshold一定要设大一点。大模型加载动辄几分钟,设小了K8s会以为Pod启动失败,反复重启。我曾经设成3,结果模型还没加载完就被杀了,陷入无限重启循环。
知识体系总览
下面这张图,是我梳理的vLLM推理高可用架构的核心逻辑。你看一眼,基本就能明白各个组件之间的关系。
从这张图你能看到,整个架构的核心就是:用户请求 → 负载均衡 → 服务发现 → 多副本推理 → 健康检查兜底。每一层都有它的职责,缺一不可。
好了,这一章的内容就到这里。多副本部署、主从架构、负载均衡、健康检查,这四个点你吃透了,vLLM的高可用基本就稳了。下一章我们聊聊更进阶的话题——推理性能调优。