生产 2:Kubernetes 部署
谁该读这一篇? 要把 SGLang 跑进 K8s 集群、做多副本扩缩容的 SRE / 平台工程师。 前置阅读:
01-docker.md、05-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 突发时来不及。
对策:
- 预留副本:HPA
minReplicas设得比真实需求高 20%。 - 预热池:用
07-hands-on/01-setup.md§8 的 warmup 脚本作为 initContainer 或 lifecycle.postStart。 - 模型预下载到节点:DaemonSet / Init job 跑
hf download把权重 prefetch 到本地 SSD。 - 滚动更新 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. 自检
- 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 是上限,不是预留。- 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、生命周期联动也得自己写,复杂得多。- 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 适合:流量极平稳、希望按业务总量做容量预算的场景。- 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 分钟可服务。- 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. 下一步
03-monitoring.md— Prometheus + Grafana。04-slo.md— 容量规划。07-router.md— Router 深入。- 参考:LWS docs、KEDA docs。
上游:
sglang/docker/配合 K8s YAML 模板。