第三章: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推理高可用架构 用户请求 负载均衡(Ingress/Envoy) K8s Service(vllm-service) vLLM副本1 节点A vLLM副本2 节点B vLLM副本3 节点C liveness/readiness liveness/readiness liveness/readiness 用户层 负载均衡 服务发现 推理副本 健康检查

从这张图你能看到,整个架构的核心就是:用户请求 → 负载均衡 → 服务发现 → 多副本推理 → 健康检查兜底。每一层都有它的职责,缺一不可。

好了,这一章的内容就到这里。多副本部署、主从架构、负载均衡、健康检查,这四个点你吃透了,vLLM的高可用基本就稳了。下一章我们聊聊更进阶的话题——推理性能调优。


专注资料整理