2. K8s 基础回顾:Pod、Deployment、Service、Ingress 核心概念

好,咱们正式开始第二讲。在动手调优 vLLM 之前,我强烈建议咱们先把 K8s 的这几个核心概念捋一遍。说白了,你后面所有的推理服务、弹性伸缩、流量接入,都是围绕着这几个东西转的。我见过不少同学,YAML 文件能写,但一问 Pod 和 Deployment 到底啥区别,就含糊了。嗯,这不行,咱们得把地基打牢。

2.1 Pod:K8s 里最小的“计算单元”

Pod 是什么?你可以把它想象成一个“逻辑主机”。这个主机里可以跑一个或多个容器。但在 vLLM 推理的场景下,我强烈建议你一个 Pod 只跑一个推理容器。为什么?因为方便管理资源,也方便排查问题。

每个 Pod 都有自己独立的 IP 地址、端口空间和存储卷。Pod 里的容器共享这些资源。举个例子,你可以在同一个 Pod 里放一个 vLLM 主容器和一个日志采集的 sidecar 容器,它们共享网络栈,通过 localhost 就能通信。

核心要点:Pod 是“临时”的。它可能会因为节点故障、资源不足、或者你手动更新而被销毁重建。所以,永远不要直接依赖 Pod 的 IP 地址。

我在项目中遇到过一个问题:有个同事把推理服务的客户端写死了 Pod IP,结果 Pod 一重启,服务就断了。排查了半天,最后发现是 IP 变了。所以,记住:Pod 是“牲口”,不是“宠物”。

2.2 Deployment:你的“应用管家”

Deployment 是干嘛的?说白了,它就是帮你管理 Pod 副本集的。你告诉它:“我要跑 3 个 vLLM 推理 Pod”,它就会帮你维持这 3 个 Pod 一直运行。如果某个 Pod 挂了,它会自动再拉起一个。

Deployment 最核心的能力是“声明式更新”。你只需要更新 YAML 文件里的镜像版本,Deployment 就会自动执行滚动更新,逐个替换旧 Pod,保证服务不中断。

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
        ports:
        - containerPort: 8000
        resources:
          requests:
            memory: "16Gi"
            cpu: "4"
          limits:
            memory: "32Gi"
            cpu: "8"

你看,这个 YAML 里我指定了副本数是 3,每个 Pod 请求 16G 内存、4 核 CPU,限制是 32G 内存、8 核 CPU。这里有个坑:vLLM 推理对显存要求很高,但 K8s 默认不管理 GPU 显存。我曾经因为没配 GPU 资源限制,导致一个 Pod 把整张 A100 的 80G 显存全占了,其他 Pod 直接 OOM。所以,记得加上 GPU 的 resource 声明。

我的习惯:在 Deployment 的 annotations 里加上版本号和 commit ID,这样回滚时一眼就能看出哪个版本有问题。

2.3 Service:稳定的“流量入口”

好,现在 Pod 有了,Deployment 也管着它们。但问题来了:Pod 的 IP 是动态的,客户端怎么访问?这时候 Service 就登场了。

Service 为一组 Pod 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名称。客户端只需要访问这个 Service 的名字,K8s 就会自动把请求转发到后端的某个 Pod 上。说白了,Service 就是一个“负载均衡器”。

apiVersion: v1
kind: Service
metadata:
  name: vllm-service
spec:
  selector:
    app: vllm
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000
  type: ClusterIP

这个 Service 会把 80 端口的流量转发到所有带 app: vllm 标签的 Pod 的 8000 端口上。默认类型是 ClusterIP,只能在集群内部访问。如果你想让外部也能访问,可以改成 NodePort 或者 LoadBalancer。

我记得有一次,线上推理服务的延迟突然飙升。查了半天,发现是 Service 的 sessionAffinity 没配,导致同一个用户的请求被分发到了不同的 Pod 上,缓存命中率极低。后来我加上了 sessionAffinity: ClientIP,问题就解决了。

2.4 Ingress:集群的“大门”

Service 解决了内部访问的问题,但如果你想让外部的 HTTP/HTTPS 请求进来,还得靠 Ingress。Ingress 是 K8s 里的“七层网关”,它可以根据域名、路径等规则,把请求路由到不同的 Service 上。

举个例子,你可以让 api.example.com/v1 指向 vLLM 推理服务,api.example.com/v2 指向另一个版本的推理服务。Ingress 还能帮你做 TLS 终止、限流、白名单等操作。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vllm-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: inference.example.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: vllm-service
            port:
              number: 80

注意:Ingress 本身只是一个 API 对象,真正干活的是 Ingress Controller(比如 Nginx Ingress Controller、Traefik)。你得先在集群里部署好 Ingress Controller,否则 Ingress 规则不会生效。

我曾经踩过一个坑:部署了 Ingress 规则,但访问一直 404。查了半天,发现是 Ingress Controller 的 Pod 没有暴露端口。嗯,这种低级错误,犯过一次就记住了。

2.5 它们之间的关系

说了这么多,咱们用一张图来总结一下这几个概念的关系:

K8s 核心概念关系图 用户 Ingress 七层路由 Service 负载均衡 Deployment 副本管理 Pod 1 Pod 2 Pod 3 外部请求 → 域名路由 → 服务发现 → 副本管理 → 实际容器

你看,整个链路很清晰:用户请求先到 Ingress,Ingress 根据域名和路径把请求转发给对应的 Service,Service 再负载均衡到后端的 Pod 上,而 Pod 的副本数由 Deployment 来管理。

2.6 避坑指南

  • Pod 资源限制:vLLM 推理对显存和内存需求很大。我曾经因为没设置 limits,导致一个 Pod 把节点内存吃光,其他 Pod 全被 OOMKill。记住:requests 是保证,limits 是上限。
  • Service 的 sessionAffinity:如果你的推理服务有状态(比如缓存),记得开启 sessionAffinity: ClientIP,否则同一个用户的请求会被分散到不同 Pod,缓存命中率会很低。
  • Ingress 的 pathType:注意 PrefixExact 的区别。我遇到过因为用了 Exact 导致路径匹配不上,返回 404 的情况。
  • Deployment 的更新策略:默认是滚动更新,但如果你有多个推理 Pod,建议把 maxSurgemaxUnavailable 调小一点,避免更新期间服务抖动。

我的小技巧:在调试阶段,可以用 kubectl port-forward 直接把本地端口映射到 Pod 上,这样不用经过 Service 和 Ingress,排查问题更快。

好了,这一章的内容就到这里。这几个概念是 K8s 的基石,你后面做 vLLM 推理的弹性伸缩、蓝绿部署、流量管理,都离不开它们。下一章咱们会深入聊聊 K8s 的资源调度和 GPU 管理,那才是真正考验功底的地方。


专注资料整理