预计阅读 5 分钟

生产 2:Kubernetes 部署

谁该读这一篇? 要把 SGLang 跑进 K8s 集群、做多副本扩缩容的 SRE / 平台工程师。 前置阅读: 01-docker.md05-distributed/02-data-parallelism.md耗时: 35 分钟 学完能: 1. 写 Deployment / Service / HPA 给 SGLang; 2. 给 Pod 分配 GPU 资源; 3. 用 LWS(LeaderWorkerSet)跑 TP > 1(跨 Pod 分布式推理); 4. 配 cache-aware router 接入; 5. 给 Pod 加生命周期 hook 做优雅退出。


1. 最简 Deployment(单 Pod / TP=1)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sglang-7b
  namespace: llm
spec:
  replicas: 4
  selector:
    matchLabels: {app: sglang-7b}
  template:
    metadata:
      labels: {app: sglang-7b}
    spec:
      nodeSelector:
        nvidia.com/gpu.product: NVIDIA-H100-80GB-HBM3
      containers:
      - name: sglang
        image: lmsysorg/sglang:0.5.0
        args:
          - python
          - -m
          - sglang.launch_server
          - --model-path
          - /models/qwen-7b
          - --host
          - "0.0.0.0"
          - --port
          - "30000"
          - --enable-metrics
        ports:
          - containerPort: 30000
            name: http
          - containerPort: 30000
            name: metrics
        resources:
          limits:
            nvidia.com/gpu: 1
            memory: "64Gi"
            cpu: "8"
        volumeMounts:
          - mountPath: /models
            name: models
          - mountPath: /dev/shm
            name: dshm
        readinessProbe:
          httpGet: {path: /health_generate, port: 30000}
          initialDelaySeconds: 180
          periodSeconds: 10
          failureThreshold: 6
        livenessProbe:
          httpGet: {path: /health, port: 30000}
          initialDelaySeconds: 300
          periodSeconds: 30
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 30"]
      volumes:
        - name: models
          persistentVolumeClaim: {claimName: hf-models-pvc}
        - name: dshm
          emptyDir: {medium: Memory, sizeLimit: 64Gi}
      terminationGracePeriodSeconds: 120
---
apiVersion: v1
kind: Service
metadata:
  name: sglang-7b
  namespace: llm
spec:
  selector: {app: sglang-7b}
  ports:
    - name: http
      port: 30000
      targetPort: 30000
  type: ClusterIP

要点:

  • nvidia.com/gpu: 1 让 NVIDIA device plugin 分配 1 张 GPU。
  • /dev/shm 用 emptyDir Memory medium = --ipc=host --shm-size
  • readinessProbe/health_generate 真跑一个请求验证(/health 只 ping)。
  • terminationGracePeriodSeconds: 120 + preStop sleep 给优雅退出留时间。
  • nodeSelector 锁住 GPU 类型。

2. 多 GPU TP=4(单节点)

resources:
  limits:
    nvidia.com/gpu: 4
args:
  - ...
  - --tp
  - "4"

K8s 给单 Pod 分 4 张 GPU,SGLang 内部起 4 个 scheduler 子进程。


3. 跨节点(TP × PP,用 LWS)

LWS = LeaderWorkerSet,K8s 官方的"多 Pod 分布式 workload"原语。 适合 70B+ 模型跨节点跑。

apiVersion: leaderworkerset.x-k8s.io/v1
kind: LeaderWorkerSet
metadata:
  name: sglang-deepseek
spec:
  replicas: 2                       # 2 个完整 group
  leaderWorkerTemplate:
    size: 2                         # 每 group 2 个 Pod (leader + 1 worker)
    leaderTemplate:
      metadata:
        labels: {role: leader}
      spec:
        containers:
        - name: sglang
          image: lmsysorg/sglang:0.5.0
          args:
            - python
            - -m
            - sglang.launch_server
            - --model-path
            - /models/deepseek-v3
            - --tp
            - "16"
            - --nnodes
            - "2"
            - --node-rank
            - "0"
            - --dist-init-addr
            - "$(LWS_LEADER_ADDRESS):5000"
          resources:
            limits: {nvidia.com/gpu: 8}
    workerTemplate:
      spec:
        containers:
        - name: sglang
          image: lmsysorg/sglang:0.5.0
          args:
            - python
            - -m
            - sglang.launch_server
            - --tp
            - "16"
            - --nnodes
            - "2"
            - --node-rank
            - "1"
            - --dist-init-addr
            - "$(LWS_LEADER_ADDRESS):5000"
          resources:
            limits: {nvidia.com/gpu: 8}

LWS 自动管理 leader / worker 的 DNS、生命周期。 每 group = 1 个 leader + 1 个 worker = 2 节点 × 8 GPU = TP=16。


4. Router 部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sglang-router
spec:
  replicas: 2
  selector: {matchLabels: {app: sglang-router}}
  template:
    metadata:
      labels: {app: sglang-router}
    spec:
      containers:
      - name: router
        image: lmsysorg/sglang-router:latest
        args:
          - --policy
          - cache_aware
          - --worker-urls
          - http://sglang-7b:30000
          - --port
          - "30000"
        ports: [{containerPort: 30000}]
---
apiVersion: v1
kind: Service
metadata: {name: sglang-router}
spec:
  selector: {app: sglang-router}
  ports: [{port: 80, targetPort: 30000}]
  type: LoadBalancer        # 或 ClusterIP + Ingress

router 用 K8s 服务名 sglang-7b:30000 自动发现 worker pods。


5. HPA(自动扩缩容)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: sglang-7b}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sglang-7b
  minReplicas: 2
  maxReplicas: 16
  metrics:
    - type: Pods
      pods:
        metric: {name: sglang_num_running_reqs}
        target:
          type: AverageValue
          averageValue: "30"            # 每副本平均 30 in-flight 触发扩
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0     # 立刻扩
    scaleDown:
      stabilizationWindowSeconds: 300   # 5 分钟稳定才缩

需要 Prometheus + Adapter 把 sglang_num_running_reqs 暴露给 HPA。 或者用 KEDA(更灵活):

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: {name: sglang-7b}
spec:
  scaleTargetRef: {name: sglang-7b}
  minReplicaCount: 2
  maxReplicaCount: 16
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        metricName: sglang_qps
        query: sum(rate(sglang_request_total[1m])) by (deployment)
        threshold: "10"

按 QPS 扩缩。


6. 冷启动慢的问题

7B 启动 30s,70B 启动 2-5 分钟,加 CUDA Graph 又是 30-60s。 扩容时新 Pod 从 Pending → Ready 慢,QPS 突发时来不及。

对策:

  1. 预留副本:HPA minReplicas 设得比真实需求高 20%。
  2. 预热池:用 07-hands-on/01-setup.md §8 的 warmup 脚本作为 initContainer 或 lifecycle.postStart。
  3. 模型预下载到节点:DaemonSet / Init job 跑 hf download 把权重 prefetch 到本地 SSD。
  4. 滚动更新 maxSurge 高:滚动期间多副本扛流量。

7. 优雅退出 / PreStop

K8s 删 Pod 流程:

1. Service endpoint 移除(新流量不来)
2. SIGTERM 进容器
3. terminationGracePeriodSeconds 倒计时
4. SIGKILL

SGLang 收到 SIGTERM 会等当前请求做完,但要给够时间。 preStop hook 加 sleep 30 给 Service endpoint 更新留缓冲:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 30 && curl -X POST localhost:30000/shutdown"]
terminationGracePeriodSeconds: 120

8. NetworkPolicy

最小化暴露:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: sglang-7b-policy}
spec:
  podSelector: {matchLabels: {app: sglang-7b}}
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: {matchLabels: {app: sglang-router}}
        - podSelector: {matchLabels: {app: prometheus}}
      ports:
        - {protocol: TCP, port: 30000}

只允许 router 和 monitor 访问。


9. Pod Disruption Budget

防 node drain 一次干掉所有副本:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: sglang-7b-pdb}
spec:
  minAvailable: 2
  selector: {matchLabels: {app: sglang-7b}}

10. 监控接入

ServiceMonitor (Prometheus Operator):

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: {name: sglang-7b}
spec:
  selector: {matchLabels: {app: sglang-7b}}
  endpoints:
    - port: http
      path: /metrics
      interval: 15s

11. 完整生产架构示意

flowchart TB
    Client --> Ingress
    Ingress --> RouterSvc[Service: router]
    RouterSvc --> Router1[Pod: router-1]
    RouterSvc --> Router2[Pod: router-2]
    Router1 --> Sglang1[Pod: sglang-7b-0]
    Router1 --> Sglang2[Pod: sglang-7b-1]
    Router1 --> Sglang3[Pod: sglang-7b-2]
    Router2 --> Sglang1
    Router2 --> Sglang2
    Router2 --> Sglang3
    Prom[Prometheus] -.metrics.-> Sglang1
    Prom -.metrics.-> Sglang2
    Prom -.metrics.-> Sglang3
    HPA[HPA / KEDA] -.scale.-> Sglang1

12. 小结

  • Deployment + Service + readinessProbe 是基本结构。
  • 跨节点用 LWS。
  • Router 部署在前,用 K8s Service 自动发现 worker。
  • HPA / KEDA 按 metric 扩缩,注意冷启动。
  • preStop sleep + terminationGracePeriodSeconds 配优雅退出。
  • PDB 防 drain 全死。

13. 自检

  1. Deployment 里为什么要挂 emptyDir Memory 到 /dev/shm
答案 K8s 容器默认 `/dev/shm` 只有 64 MB(Docker 默认),PyTorch / NCCL 用 shared memory 做进程间张量传递时立刻爆,TP 启动卡住。 `emptyDir { medium: Memory, sizeLimit: 64Gi }` 让 K8s 给容器挂一块 64 GB 内存盘当 `/dev/shm`,等价于 Docker `--shm-size=64g`。 不能用 `--ipc=host`:K8s 不直接支持,要走 Pod hostIPC(安全风险)。 监控:实际占用通常 < 5 GB,sizeLimit 给 64 GB 是上限,不是预留。
  1. LWS 比普通 StatefulSet 在跨节点 TP 上的优势?
答案 LWS(LeaderWorkerSet)是 K8s sig-networking 的专门多 Pod 协同原语: (1) **Leader / Worker 角色明确**:leader 跑 rank 0、worker 跑 rank > 0,LWS 自动给 worker 注入 `LWS_LEADER_ADDRESS` 环境变量。 (2) **生命周期联动**:leader 重启时所有 worker 一起重启(避免 stale state 导致 NCCL hang)。 (3) **size 字段**:声明每 group 几个 Pod,K8s 调度时一起调(gang scheduling),避免 4 个 Pod 调 3 个、1 个 pending 卡死。 (4) **副本组语义**:replicas=2 是两个独立的 (leader+worker) group,可以独立扩缩。 StatefulSet 没有这些,需要自己写 init container 做 leader discovery、生命周期联动也得自己写,复杂得多。
  1. HPA 用 in-flight 请求数 vs QPS 哪个更稳?
答案 **in-flight 请求数(`sglang_num_running_reqs`)更稳**。 QPS 反映流量速率,但 LLM 请求时长方差大(300ms - 30s),突发长请求会让"瞬时 QPS"短暂飙高但 server 实际能承受;HPA 误判扩容。 in-flight 请求数 = QPS × 平均时长,是 Little's Law 的直接体现,反映 server 真实压力。`max_running_requests` 是固定上限,按 in-flight 占比扩缩最贴合。 配置:`averageValue` 设为 max_running_requests × 0.7,触发扩容。 QPS 适合:流量极平稳、希望按业务总量做容量预算的场景。
  1. 70B 模型 Pod 启动 5 分钟,怎么让扩容更快?
答案 组合策略: (1) **预留副本**:`minReplicas` 比平均需求高 20-30%,扩容只是补充而不是从零。 (2) **预热池**:用 K8s Job 起几个 warm pool,需要时 promote 为 active。 (3) **模型本地 SSD 预热**:DaemonSet 把模型权重 prefetch 到每节点本地 SSD(绕开 NFS / S3),加载快 5-10×。 (4) **减 CUDA Graph 数**:`--cuda-graph-bs "1,16,64,128"` 砍档,capture 时间从 60s → 20s。 (5) **容器镜像本地拉取**:用 K8s registry mirror + 节点 image cache。 (6) **快速健康检查**:startupProbe 用 `/health`(200ms ready),readinessProbe 才用 `/health_generate`。 实测能把 5 分钟压到 1-2 分钟可服务。
  1. PreStop sleep 30s 的作用是什么?
答案 K8s 删 Pod 流程:(1) Service endpoint 移除(标记 NotReady);(2) SIGTERM 进容器;(3) 等 `terminationGracePeriodSeconds`;(4) SIGKILL。 问题:(1) 和 (2) 几乎同时发生,但 **Service endpoint 移除有传播延迟**(kube-proxy 更新 iptables / IPVS 大约 10-30s);这段时间内仍有新流量来。 `preStop: sleep 30` 让容器在 SIGTERM 之前先等 30s,给 endpoint 移除留足时间。30s 后才开始优雅退出。 配合:`terminationGracePeriodSeconds: 120` = 30s preStop + 90s 处理 in-flight 请求。 不加 sleep 的后果:滚动更新时 5-10% 请求被路由到正在退出的 Pod,客户端看到 5xx。

14. 下一步

上游:sglang/docker/ 配合 K8s YAML 模板。