AI Infra 训练营
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
  • Day 0 · 环境与硬件

    • Day 0 新手现场接管 Runbook:先看懂,再动手
    • Day 0:5 节点裸 Ubuntu → K8s 装机基线
  • Week 1:K8s 内核 + 周边基础设施

    • Day 1:3 CP HA 集群 + CNI 选型 + DNS 调优
    • Day 2:控制面 deep dive + etcd 内核 + chaos drill
    • Day 3:CRD + Operator (kubebuilder 从 0 写)
    • Day 4:Longhorn 存储 + Cilium 二探(Hubble / NetworkPolicy / L7)
    • Day 5:PVC 在线扩容 + K8s 安全基线(RBAC / PSA / Secret 加密 / Kyverno)
    • Day 6:调度策略 + Prometheus / Loki 观测栈
    • Day 7:Harbor 私有镜像 + ArgoCD GitOps + Cilium WireGuard
  • Week 2:制品 + GitOps + AI Infra + 综合

    • Day 8:k3s 单节点 + NVIDIA Device Plugin + vLLM 跑 Qwen2.5-3B
    • Day 8(attempt 1):跨 WAN GPU 加入主集群(失败复盘)
    • Day 8:AlertManager 真接入 + PrometheusRule 实战
    • Day 8:集群内 CI 闭环 — Gitea + Jenkins + Kaniko
    • Day 9:Triton 多框架推理 + DCGM 跨集群可观测 + vLLM 实测
    • Day 10:MIG 硬切片 + AWQ 量化 + HPA Custom Metrics
    • Day 11:AI 业务端到端 —— chainlit + GitOps + 跨 WAN vLLM
    • Day 12:灾难恢复 + 生产事故注入
    • Day 13:LLM Operator + 多集群联邦 + Ambient Mesh + RAG
    • Day 14:CKA / CKS 真题演练 + 14 天技术栈横向汇总

Day 11:AI 业务端到端 —— chainlit + GitOps + 跨 WAN vLLM

Day 1-10 把基础设施一层一层垒起来:CNI、存储、Ingress、cert-manager、Operator、Harbor、ArgoCD、Gitea/Jenkins、GPU、MIG、vLLM。Day 11 第一次把这些全部用起来 —— 一个真实的 chainlit chat UI 应用,从 git push 开始,经过完整 GitOps 流水线,跨 WAN 调到 GPU 集群上的 vLLM endpoint。

整篇按 A → F 6 个阶段走:写业务代码 → 容器化 → CI 推 Harbor → ArgoCD 拉到主集群 → 跨 WAN 接 vLLM → 业务指标可观测。每个阶段都有一个真坑。


端到端拓扑

git push → Gitea(notes-app) → Jenkins(Kaniko build) → Harbor(chat-ui:<sha>)
                                                              │
              Gitea(notes-deploy) ◄── 改 image tag ───────────┘
                     │
                     ▼ ArgoCD watch + sync
              主集群 chat ns: chat-ui Deploy + vllm-upstream Svc/Endpoints
                     │
                     ▼ cross-WAN (m1:30800 SSH tunnel → gpu1)
              GPU 集群 k3s: vllm-3b → Qwen2.5-3B on A800 MIG 2g.10gb

Day 1-10 能力首次全部「用起来」:Cilium 跨节点 Service + Hubble L7(D1)、Kyverno + PSA baseline(D5)、kube-prometheus-stack ServiceMonitor(D6)、Harbor + ArgoCD(D7)、Gitea + Jenkins(D8 番外)、vLLM(D8 主线)、DCGM cross-WAN(D9)、A800 MIG 2g.10gb slice(D10)。Longhorn(D4)这次留作可选 —— chainlit 用 in-memory session 简化。


A. chainlit 业务代码

chainlit 是给 LLM 应用做 chat UI 最快的方案 —— 一个装饰器 @cl.on_message + 异步 streaming,几十行代码就有 WebSocket + 流式打字效果 + 多轮 session 管理。OpenAI-compat 接口让它直接对接 vLLM。

A.1 app.py

import os, time, chainlit as cl
from openai import AsyncOpenAI
from prometheus_client import Counter, Histogram, start_http_server

CHAT_REQUESTS = Counter("chat_requests_total", "chat completions", ["model", "status"])
CHAT_TOKENS   = Counter("chat_tokens_total", "tokens", ["model", "kind"])
CHAT_LATENCY  = Histogram("chat_first_token_latency_seconds", "TTFT", ["model"],
                          buckets=[0.1, 0.25, 0.5, 1, 2, 5, 10])
CHAT_E2E      = Histogram("chat_e2e_latency_seconds", "E2E", ["model"],
                          buckets=[0.5, 1, 2, 5, 10, 30, 60])
start_http_server(9100)

client = AsyncOpenAI(base_url=os.getenv("VLLM_URL"), api_key="not-needed")
MODEL = os.getenv("MODEL", "qwen2.5-3b")

@cl.on_message
async def on_message(msg: cl.Message):
    response = cl.Message(content="")
    first_token, t0, status = None, time.time(), "success"
    prompt_tokens = completion_tokens = 0
    history = (cl.user_session.get("history") or []) + \
              [{"role": "user", "content": msg.content}]
    try:
        stream = await client.chat.completions.create(
            model=MODEL, messages=history, stream=True,
            stream_options={"include_usage": True},
            max_tokens=512, temperature=0.7,
        )
        async for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                if first_token is None:
                    first_token = time.time() - t0
                    CHAT_LATENCY.labels(model=MODEL).observe(first_token)
                await response.stream_token(chunk.choices[0].delta.content)
            if chunk.usage:
                prompt_tokens = chunk.usage.prompt_tokens
                completion_tokens = chunk.usage.completion_tokens
    except Exception as e:
        status = "error"
        response.content = f"error: {e}"
    finally:
        CHAT_REQUESTS.labels(model=MODEL, status=status).inc()
        CHAT_TOKENS.labels(model=MODEL, kind="prompt").inc(prompt_tokens)
        CHAT_TOKENS.labels(model=MODEL, kind="completion").inc(completion_tokens)
        CHAT_E2E.labels(model=MODEL).observe(time.time() - t0)
    cl.user_session.set("history", history + [{"role": "assistant", "content": response.content}])
    await response.update()

LLM 业务必埋的 4 个 metric:TTFT(用户感知最强,p95 > 2s 用户就跑)、E2E latency(整轮对话,反映 generation throughput)、tokens by kind(prompt/completion 分开计费 + 容量规划)、requests by status(QPS + 错误率)。

stream_options={"include_usage": True} 是 vLLM 流式取 token 数的官方姿势 —— 流式默认不返回 usage,必须显式开启,否则 chunk.usage 永远是 None。

A.2 Dockerfile(multi-stage,~140 MB final)

FROM python:3.11-slim AS builder
WORKDIR /app
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY app.py .
EXPOSE 8000 9100
CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "8000", "--headless"]

--headless 关掉首次启动的浏览器打开和遥测,是生产姿势。


B. CI:Gitea push → Jenkins → Kaniko → Harbor

Jenkins agent 是 K8s Pod,每次 build 拉起一个 Kaniko 容器构建镜像并推 Harbor,build 完销毁。无需 Docker daemon,K8s 内安全构建。

B.1 Jenkinsfile

pipeline {
  agent {
    kubernetes {
      yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:debug
    command: [/busybox/cat]
    tty: true
    volumeMounts: [{name: docker-config, mountPath: /kaniko/.docker}]
  volumes:
  - name: docker-config
    secret: {secretName: harbor-auth, items: [{key: .dockerconfigjson, path: config.json}]}
'''
    }
  }
  environment { REGISTRY = "10.0.24.28:30002"; PROJECT = "bootcamp"; IMAGE = "chat-ui" }
  stages {
    stage('Build') {
      steps { container('kaniko') { sh '''
        GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo dev)
        /kaniko/executor --dockerfile=Dockerfile --context=$(pwd) \
          --destination=${REGISTRY}/${PROJECT}/${IMAGE}:${GIT_SHA} \
          --destination=${REGISTRY}/${PROJECT}/${IMAGE}:latest \
          --insecure --skip-tls-verify
      ''' } }
    }
  }
}

--insecure --skip-tls-verify 是因为 Harbor 这次是 HTTP NodePort(学习集群简化)。生产必须走 HTTPS + cert-manager 证书,不要把这两个 flag 带进 production pipeline。

B.2 触发与产出

git push origin main      # Jenkins SCM 每分钟 poll 一次,自动触发 Build
# Build #2 SUCCESS (6 min),Harbor 收到 bootcamp/chat-ui:<sha> + :latest

为什么同时打 :<git-sha> 和 :latest:sha tag 用于精确部署 + 回滚审计,:latest 用于人肉调试。生产 ArgoCD manifest 一定 pin sha tag,否则 image 内容变了但 manifest 没变,ArgoCD 检测不到 drift。


C. CD:ArgoCD watch deploy repo

GitOps 的核心是「期望状态写在 git 里」。源码 repo(notes-app)和部署 repo(notes-deploy)分两个 —— 源码变动触发 CI 构建镜像,部署 repo 改 image tag 触发 CD。两者解耦。

C.1 注册 Gitea Repository

apiVersion: v1
kind: Secret
metadata:
  name: gitea-repo
  namespace: argocd
  labels: {argocd.argoproj.io/secret-type: repository}
stringData:
  type: git
  url: http://gitea-http.gitea.svc:3000/bootcamp/notes-deploy.git
  username: bootcamp
  password: bootcamp
  insecure: "true"

C.2 真坑:ArgoCD 默认不接 HTTP repo

第一次 sync 直接报 transport: authentication handshake failed: context deadline exceeded。

ArgoCD 默认假设 git remote 是 HTTPS + valid TLS。学习集群的 Gitea 是 ClusterIP HTTP,必须三处同时改:Secret 里 insecure: "true"、URL 写 http:// scheme(不是默认 git+SSH)、kubectl rollout restart -n argocd deploy/argocd-repo-server 刷连接池缓存。生产用 Ingress + cert-manager TLS 就没这事。

C.3 Application

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: {name: chat-ui, namespace: argocd}
spec:
  project: default
  source:
    repoURL: http://gitea-http.gitea.svc:3000/bootcamp/notes-deploy.git
    targetRevision: HEAD
    path: chat-ui
  destination: {server: https://kubernetes.default.svc, namespace: chat}
  syncPolicy:
    automated: {prune: true, selfHeal: true}
    syncOptions: [CreateNamespace=true, ServerSideApply=true]

三个关键 flag:prune 删 git 里去掉的资源(防集群只增不减脏掉);selfHeal 把 kubectl edit 改的状态自动改回 git 版本(GitOps 必开);ServerSideApply 避免 client-side merge 误删 controller 写入的 status 字段。


D. 部署 manifest:namespace + Deployment + 跨 WAN Service

部署文件全部放在 notes-deploy/chat-ui/ 目录。下面这一坨是 ArgoCD 实际拉到的全部 yaml。

D.1 namespace + chat-ui

apiVersion: v1
kind: Namespace
metadata:
  name: chat
  labels: {pod-security.kubernetes.io/enforce: baseline}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: chat-ui, namespace: chat}
spec:
  replicas: 1
  selector: {matchLabels: {app: chat-ui}}
  template:
    metadata: {labels: {app: chat-ui}}
    spec:
      imagePullSecrets: [{name: harbor-pull}]
      containers:
      - name: chat-ui
        image: 10.0.24.28:30002/bootcamp/chat-ui:latest
        imagePullPolicy: Always
        ports: [{containerPort: 8000, name: http}, {containerPort: 9100, name: metrics}]
        env:
        - {name: VLLM_URL, value: "http://vllm-upstream.chat.svc:8000/v1"}
        - {name: MODEL, value: "qwen2.5-3b"}
        resources:
          requests: {cpu: 100m, memory: 256Mi}
          limits:   {cpu: 1, memory: 1Gi}
        readinessProbe: {httpGet: {path: /, port: 8000}, initialDelaySeconds: 20}
---
apiVersion: v1
kind: Service
metadata: {name: chat-ui, namespace: chat, labels: {app: chat-ui}}
spec:
  type: NodePort
  selector: {app: chat-ui}
  ports:
  - {port: 80,   targetPort: 8000, nodePort: 30810, name: http}
  - {port: 9100, targetPort: 9100, nodePort: 30811, name: metrics}
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: {name: chat-ui, namespace: chat, labels: {release: kps}}
spec:
  selector: {matchLabels: {app: chat-ui}}
  endpoints: [{port: metrics, interval: 10s, path: /metrics}]

D.2 真坑:image pull secret 跨 namespace 不会自动带过来

Harbor 是私有 registry,需要 imagePullSecret。Day 7 在 harbor ns 创的 harbor-pull Secret 在 chat ns 看不到,chat-ui Pod 报 Failed to pull image: unauthorized —— Secret 不跨 namespace。

# 方式 1:每个业务 ns 各创一份
kubectl -n chat create secret docker-registry harbor-pull \
  --docker-server=10.0.24.28:30002 \
  --docker-username='robot$bootcamp' --docker-password='<token>'

# 方式 2:Kyverno generate policy 自动同步到所有 ns(Day 5 装好的可直接用)

学习集群用方式 1,生产多业务 ns 用方式 2 集中管理。Day 5 的 Kyverno generate rule 这次第一次派上用场。


E. 跨 WAN 接 vLLM:Service + 手动 Endpoints

vLLM 跑在 GPU 集群(独立 k3s on gpu1),主集群没有 GPU。跨 WAN 的方案选 SSH reverse tunnel:

# m1 上跑(systemd 持久化),把 gpu1 的 vllm-3b Service 8000 端口
# 反向暴露到 m1:30800
ssh -fN -p 15128 -L 0.0.0.0:30800:<vllm-svc-cluster-ip>:8000 root@gpu1

主集群里要让 chat-ui 通过 K8s 原生 Service DNS 调到 vllm —— 用 ClusterIP Service + 手动 Endpoints 把这个外部端点伪装成集群内 Service。

---
apiVersion: v1
kind: Service
metadata: {name: vllm-upstream, namespace: chat}
spec:
  type: ClusterIP
  ports:
  - {port: 8000, targetPort: 30800, name: http, protocol: TCP}
---
apiVersion: v1
kind: Endpoints
metadata: {name: vllm-upstream, namespace: chat}
subsets:
- addresses: [{ip: 10.0.24.31}]      # m1 hostIP(SSH tunnel 落点)
  ports: [{port: 30800, name: http, protocol: TCP}]

chat-ui 里 VLLM_URL=http://vllm-upstream.chat.svc.cluster.local:8000/v1 —— 业务代码完全看不出后端跨 WAN,迁移到 mesh 或同集群只改 Endpoints。

E.1 真坑:Headless Service 不做 port redirect

第一版想省 kube-proxy 一层用了 clusterIP: None(headless):chat-ui resolve vllm-upstream:8000 → DNS 直接返回 m1 IP 10.0.24.31 → chat-ui 直连 10.0.24.31:8000 → 连错端口(tunnel 在 :30800 不在 :8000)。

排查半小时才意识到:headless Service 只做 DNS round-robin,不做 port 映射,targetPort 字段被忽略。改回默认 ClusterIP,让 kube-proxy 做 8000 → 30800 的 DNAT 才对。

E.2 真坑:Service 和 Endpoints 的 port name 必须一致

K8s 用 port name 把两边对齐:

Service:    ports: [{port: 8000, targetPort: 30800, name: http}]
Endpoints:  ports: [{port: 30800,                    name: http}]   # 必须匹配

漏写 name 或两边名字不一样 → kube-proxy 不知道转到 Endpoints 的哪个端口 → Service 看着 healthy 但全 timeout。

E.3 真坑:ArgoCD 默认 exclude Endpoints

ArgoCD 默认认为 Endpoints 是 Service 自动生成的从属资源,sync 时报 ExcludedResourceWarning: /Endpoints vllm-upstream is excluded。实际行为:首次 apply 会创建,但 ArgoCD 不 track。手动 kubectl apply 一次后续 sync 不会删它。要让 ArgoCD 完全管理,改 argocd-cm 把 Endpoints 从 resource.exclusions 去掉。

E.4 真坑:SSH tunnel 死了 chat-ui 全 timeout

SSH tunnel 不是 K8s 一等公民,无健康检查无自动重连。tunnel 被 OOM kill 或网络抖动断开时,Endpoints 10.0.24.31:30800 还在,K8s 认为 Service healthy,但所有请求 Connection refused —— K8s 层面完全看不出问题。

学习集群对策:systemd + autossh 替代裸 ssh:

# /etc/systemd/system/vllm-tunnel.service
[Service]
ExecStart=/usr/bin/autossh -M 0 -N -p 15128 \
  -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
  -L 0.0.0.0:30800:<vllm-svc-ip>:8000 root@gpu1
Restart=always
RestartSec=5

生产对策:换 WireGuard / Tailscale(L3 隧道 + CNI 直接路由对端 Pod CIDR)或 Cilium ClusterMesh(K8s 原生多集群 Service 发现)。SSH tunnel 学习场景能用,不要带进生产。


F. 端到端验证 + 业务可观测

F.1 链路 9 项验证

按顺序逐项验,任一项 fail 都得回到对应 day 修复,不要跳过往下走:

验证项期望
git push 后 Gitea 看到 commitcommit hash 在 web
Jenkins Build HistorySUCCESS,约 6 min
Harbor /v2/bootcamp/chat-ui/tags/listtag 含新 sha
argocd app get chat-uiSynced / Healthy
kubectl -n chat get pod1/1 Running
Pod 内 curl vllm-upstream:8000/v1/models返回 qwen2.5-3b
浏览器开 <node-ip>:30810chainlit UI 流式回复
Prometheus /targetschat-ui target up
curl <pod-ip>:9100/metrics | grep chat_4 个 metric 在

F.2 真坑:metric 不增长

ServiceMonitor up、metric 端点 200 OK,但 chat_requests_total 一直 0。根因:chainlit 是 WebSocket 应用,只有 @cl.on_message handler 真正跑了 metric 才 +1。curl HTTP 路径绕过了 WebSocket,metric 不触发。

正确触发:浏览器开 <node-ip>:30810 发消息,或用 chainlit-client Python 库连 WebSocket 压测。业务 metric 验证一定要走真实业务路径,绕开业务逻辑的 curl 是自欺欺人。

F.3 业务面板的 4 个核心查询

Grafana LLM 业务面板的最小集:

sum(rate(chat_requests_total[1m])) by (status)                                  # QPS
histogram_quantile(0.95, sum(rate(chat_first_token_latency_seconds_bucket[5m])) by (le, model))  # TTFT p95
sum(rate(chat_tokens_total{kind="completion"}[1m])) by (model)                  # token throughput
sum(rate(chat_requests_total{status="error"}[5m])) / sum(rate(chat_requests_total[5m]))          # 错误率

加上 Day 9 的 DCGM DCGM_FI_DEV_GPU_UTIL 和 DCGM_FI_DEV_FB_USED,业务 + GPU 第一次能在同一个面板里看 —— 业务 QPS 涨的时候 GPU util 跟不跟得上一目了然。


整体回顾

走完这一天,chat ns 里:chat-ui Deployment 1/1 + NodePort Service + vllm-upstream ClusterIP Service + 手动 Endpoints + ServiceMonitor;argocd ns 里 chat-ui Application Synced / Healthy。

从 git push 到生产服务可用 < 10 分钟:commit → Gitea → Jenkins 拉 Kaniko build → push Harbor → 改 deploy repo image tag → ArgoCD 检测 drift 自动 sync → 主集群 chat-ui 滚动更新 → 流量走 Service/Endpoints 跨 WAN 到 GPU 集群 vLLM。


面试常见题

Q1:从 git commit 到生产 Pod 跑起来的完整 GitOps 链路?

8 跳:push 到源码 repo → Jenkins SCM watch 触发 build → K8s agent 拉 Kaniko Pod → push Harbor(:git-sha + :latest)→ 改 deploy repo 的 image tag commit → ArgoCD 检测 drift → ServerSideApply 到目标集群 → Deployment 滚动更新 + readinessProbe 过后接流量。

深问点:源码 repo 和部署 repo 为什么要拆开?—— 源码改动频繁需要 CI 测试;部署 repo 是声明式期望状态,只有 tag bump 或 config 变动,可以加 PR review 卡点。合一会让每次 push 都触发 ArgoCD sync,破坏 CD 的可审计性。

Q2:主集群没 GPU,调远程 GPU 集群的 LLM endpoint 工程方案怎么选?

方案优点缺点适合
SSH reverse tunnel + 手动 Endpoints0 网络改造进程级、无 K8s 健康检查PoC / 学习
WireGuard / TailscaleL3 隧道,CNI 直接路由对端 Pod CIDR需运维密钥中小生产
Cilium ClusterMeshK8s 原生多集群 Service 发现两边都要 Cilium多区 / 多云
公网 LB + mTLS简单粗暴公网暴露,需 WAF跨组织调用

实战教训:SSH tunnel 死了 K8s 看不出问题(Endpoints 还在但请求全 timeout),生产至少 WireGuard 起步。

Q3:chainlit / Streamlit / Gradio 给 LLM 做 UI 怎么选?

chainlit 是 chat 专用(@cl.on_message + WebSocket + 流式 token 原生 + cl.user_session 多轮),chat 产品首选;Streamlit 是 dashboard 通用(rerun 模型对 streaming 不友好,要手动 placeholder 刷新),适合内部数据看板;Gradio 是 demo 专用(HTTP-only),适合模型 demo / HF Spaces。LLM 业务产品级首选 chainlit。

Q4:LLM 业务的 timeout / retry / streaming 怎么处理?

3 个原则:

  1. timeout 分两段 —— connect timeout 5s 必须握上;overall timeout 不要设,让 stream 自然结束或客户端取消。LLM generation 时间不可预测,固定 30s overall 会切断长回答
  2. retry 只在 connect 层 —— 连接失败 / 5xx / 429 可重试;已开始 stream 中途断开不要重试,会让用户看到回答重复或前后矛盾
  3. client cancel 必须传到 vLLM —— WebSocket 断开时 asyncio.CancelledError 一路传到 client.chat.completions.create,让 vLLM 释放 KV cache 槽位。否则一个 32k context 占着 slot 到 max_tokens 跑完,TPS 暴跌

深问:流式怎么取 prompt/completion tokens?OpenAI compat 默认流式不返回 usage,必须 stream_options={"include_usage": True},最后一个 chunk 才带 usage(vLLM 0.6+ 支持)。

Q5:生产 AI 应用的容量规划怎么做?

3 维度叠加:GPU 并发上限(单 vLLM max concurrent ≈ available_gpu_mem / kv_cache_per_seq,Qwen2.5-3B + A800 2g.10gb 约 30-50 并发,超了 TTFT 飙升)+ token 预算(p95 input + p95 output × 目标 QPS,对照 vLLM TPS 1500-2500 tokens/s 算实例数)+ WebSocket 连接(chainlit 每用户一条长连接,100+ 并发需 HPA + Redis session store 替代 in-memory)。

扩容信号优先级:TTFT p95 > queueing time > GPU util。GPU util 50% 看着很闲但 TTFT 已被 KV cache 排队拖垮的场景常见,别只看利用率。


下一步

Day 1-11 把单业务从基础设施到 GitOps 走通。Day 12 进入多业务 / 多租户场景:namespace 配额(ResourceQuota + LimitRange)、租户隔离(NetworkPolicy + RBAC + image scan gate)、Argo Rollouts canary / blue-green、SLO 驱动的发布门禁(Flagger + Prometheus query)。

在 GitHub 上编辑此页
Prev
Day 10:MIG 硬切片 + AWQ 量化 + HPA Custom Metrics
Next
Day 12:灾难恢复 + 生产事故注入