phase 5 part 4: java-spring awareness, k8s basics with validated manifests, run_examples supports single-file Java
This commit is contained in:
1 parent
075872e31e
commit
f695bbd7cc
18 files changed
+1192
-23
No files matched your search
@@ -0,0 +1,118 @@
|
||||
---
|
||||
id: devops.k8s.core-objects
|
||||
title: Kubernetes 基础:Pod / Deployment / Service / Ingress、配置与密钥、探针、滚动更新和扩缩容
|
||||
type: lesson
|
||||
domain: devops
|
||||
stack: [k8s]
|
||||
tags: [Kubernetes, Pod, Deployment, Service, Ingress, ConfigMap, Secret, 探针, 滚动更新, HPA, PDB, kubectl]
|
||||
level: basic
|
||||
prerequisites: [devops.docker.image-dockerfile]
|
||||
est_hours: 3
|
||||
related: [devops.k8s.scenario.agent-rollout, devops.nginx.scenario.ai-gateway-sse-canary, devops.k8s.interview]
|
||||
week: [14]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Kubernetes 基础:Pod / Deployment / Service / Ingress、配置与密钥、探针、滚动更新和扩缩容
|
||||
|
||||
> 定位:**能读懂和修改清单、能用 kubectl 排查**,集群搭建和运维不在范围内。BOSS直聘 2026-10-23 的调研中有岗位把 K8s 列为要求或加分项。示例清单通过 kubeconform 结构校验(6 个资源全部有效)和团队规范测试(5 个测试通过),**没有在真实集群中部署**。以官方文档为准:https://kubernetes.io/zh-cn/docs/home/ 。
|
||||
|
||||
## 解释说明
|
||||
|
||||
### 核心对象
|
||||
|
||||
| 对象 | 作用 | 类比 |
|
||||
| --- | --- | --- |
|
||||
| **Pod** | 最小调度单位,一个或几个容器共享网络和存储 | 一组 `docker run` |
|
||||
| **Deployment** | 声明“要几个什么样的 Pod”,负责滚动更新和回滚 | Compose 的 service + 副本数 |
|
||||
| **Service** | 给一组 Pod 一个稳定的虚拟 IP / DNS 名,做负载均衡 | 内部的负载均衡器 |
|
||||
| **Ingress** | 集群入口的 HTTP 路由(域名、路径、TLS) | Nginx 反向代理配置 |
|
||||
| **ConfigMap / Secret** | 配置 / 敏感信息,以环境变量或文件注入 | `.env` |
|
||||
| **HPA** | 按指标自动调整副本数 | — |
|
||||
| **PDB** | 主动驱逐(节点维护)时至少保留多少可用 Pod | — |
|
||||
| Namespace | 资源隔离和配额 | — |
|
||||
|
||||
**声明式**:你提交“期望状态”(YAML),控制器不断把“实际状态”调整为期望状态。Pod 挂了会被重建,而不是由你手动重启。
|
||||
|
||||
**标签和选择器**:Deployment、Service 通过标签选择 Pod,标签不一致是新手最常见的问题(Service 没有后端)。
|
||||
|
||||
### 三种探针
|
||||
|
||||
| 探针 | 失败的后果 | 检查什么 |
|
||||
| --- | --- | --- |
|
||||
| startupProbe | 启动阶段超时则重启 | 启动是否完成(给慢启动的应用留时间) |
|
||||
| readinessProbe | 从 Service 的后端中摘除,不重启 | 能否处理请求(可检查关键依赖) |
|
||||
| livenessProbe | 重启容器 | 进程是否卡死(**不要检查外部依赖**) |
|
||||
|
||||
### 滚动更新和优雅退出
|
||||
|
||||
- `maxSurge` / `maxUnavailable` 控制更新节奏;`kubectl rollout undo` 回滚。
|
||||
- Pod 终止流程:从 Service 端点中移除 + 执行 `preStop` → 发送 SIGTERM → 等待 `terminationGracePeriodSeconds` → SIGKILL。应用要处理 SIGTERM:停止接收新请求、完成进行中的请求(AI 流式回答可能持续几十秒)。
|
||||
|
||||
### 常用排查命令
|
||||
|
||||
```bash
|
||||
kubectl get pods -l app.kubernetes.io/name=agent-api -o wide
|
||||
kubectl describe pod <pod> # 看事件:拉取镜像失败、探针失败、OOMKilled、调度失败
|
||||
kubectl logs <pod> --previous # 看上一次崩溃前的日志
|
||||
kubectl get endpoints agent-api # Service 是否有后端
|
||||
kubectl rollout status deploy/agent-api
|
||||
kubectl rollout undo deploy/agent-api
|
||||
```
|
||||
|
||||
## 案例
|
||||
|
||||
把 AI Agent API 部署到公司的 K8s 集群:3 副本、零不可用的滚动更新、启动慢时用 startupProbe 保护、密钥由 Secret 注入、Ingress 关闭缓冲以支持 SSE、HPA 按负载扩到 10 个、PDB 保证节点维护时至少 2 个可用。
|
||||
|
||||
## 代码示例
|
||||
|
||||
清单:[examples/agent-api](examples/agent-api/)(`deployment.yaml`、`service.yaml`、`ingress.yaml`、`configmap.yaml`、`hpa.yaml`),校验结果:
|
||||
|
||||
```text
|
||||
$ kubeconform -strict -summary *.yaml
|
||||
Summary: 6 resources found in 5 files - Valid: 6, Invalid: 0, Errors: 0, Skipped: 0
|
||||
$ pytest -q test_manifests.py
|
||||
5 passed
|
||||
```
|
||||
|
||||
Deployment 关键部分:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
|
||||
template:
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 90
|
||||
containers:
|
||||
- name: api
|
||||
image: registry.example.com/agent-api:1.4.2
|
||||
env:
|
||||
- name: LLM_API_KEY
|
||||
valueFrom: { secretKeyRef: { name: agent-api-secrets, key: llm-api-key } }
|
||||
resources:
|
||||
requests: { cpu: "250m", memory: "256Mi" }
|
||||
limits: { memory: "512Mi" }
|
||||
startupProbe: { httpGet: { path: /healthz, port: http }, periodSeconds: 5, failureThreshold: 24 }
|
||||
readinessProbe: { httpGet: { path: /readyz, port: http }, periodSeconds: 5 }
|
||||
livenessProbe: { httpGet: { path: /healthz, port: http }, periodSeconds: 10 }
|
||||
```
|
||||
|
||||
团队规范测试([test_manifests.py](examples/agent-api/test_manifests.py))检查:选择器与标签一致、镜像固定版本、有资源和探针、密钥不明文、PDB 小于最小副本数、仓库中没有 Secret 对象。
|
||||
|
||||
## 易错点
|
||||
|
||||
- Service 选择器和 Pod 标签不一致。
|
||||
- 存活探针检查数据库,数据库抖动时所有 Pod 被重启,雪上加霜。
|
||||
- 没有 startupProbe,慢启动的应用被存活探针反复杀掉(CrashLoopBackOff)。
|
||||
- 镜像用 `latest`,无法确定线上跑的是哪个版本,回滚也不可靠。
|
||||
- 内存没有限制或限制过小(OOMKilled)。
|
||||
- 应用不处理 SIGTERM,滚动更新时正在进行的流式回答被切断。
|
||||
- 把 Secret 的 YAML(只是 base64,不是加密)提交到仓库。
|
||||
|
||||
## 面试常问
|
||||
|
||||
- Pod、Deployment、Service 的关系?
|
||||
- 三种探针的区别?
|
||||
- 滚动更新怎么做到不中断服务?
|
||||
- Pod 一直 CrashLoopBackOff 怎么排查?
|
||||
@@ -0,0 +1,11 @@
|
||||
# 非敏感配置。敏感信息(模型 API Key、数据库密码)放 Secret,并且不要提交到仓库
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: agent-api-config
|
||||
labels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
data:
|
||||
LOG_LEVEL: "info"
|
||||
MODEL_TIMEOUT_SECONDS: "60"
|
||||
REDIS_URL: "redis://redis.default.svc.cluster.local:6379/0"
|
||||
@@ -0,0 +1,75 @@
|
||||
# AI Agent API 的 Deployment:滚动更新、三种探针、资源限制、非 root 运行、优雅退出
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: agent-api
|
||||
labels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
spec:
|
||||
replicas: 3
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1 # 更新时最多多出 1 个 Pod
|
||||
maxUnavailable: 0 # 更新过程中可用 Pod 数不减少
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 90 # 留时间让正在进行的流式回答结束
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
containers:
|
||||
- name: api
|
||||
image: registry.example.com/agent-api:1.4.2 # 使用固定版本,不用 latest
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: agent-api-config
|
||||
env:
|
||||
- name: LLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: agent-api-secrets # 由运维或密钥管理系统创建,不在仓库中
|
||||
key: llm-api-key
|
||||
resources:
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
memory: "512Mi" # 内存设上限防止拖垮节点;CPU 不设上限避免被限流(按团队规范)
|
||||
startupProbe: # 启动慢(加载模型配置、预热连接)时,避免被存活探针误杀
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
failureThreshold: 24
|
||||
readinessProbe: # 未就绪时不接收流量(例如依赖的 Redis 不可用)
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
livenessProbe: # 进程卡死时重启;不要检查外部依赖,否则依赖故障会引发全体重启
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
lifecycle:
|
||||
preStop: # 先等负载均衡摘除,再收到 SIGTERM。sleep 动作需要较新的 Kubernetes(1.30+,以官方文档为准),旧版本用 exec sleep
|
||||
sleep:
|
||||
seconds: 5
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
@@ -0,0 +1,33 @@
|
||||
# 按 CPU 自动扩缩容。AI 服务多为 I/O 等待,CPU 不一定能反映负载,可改用自定义指标(并发请求数、队列长度)
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: agent-api
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: agent-api
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300 # 缩容慢一点,避免抖动
|
||||
---
|
||||
# 节点维护(驱逐)时至少保留 2 个可用 Pod
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: agent-api
|
||||
spec:
|
||||
minAvailable: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
@@ -0,0 +1,24 @@
|
||||
# 以 ingress-nginx 为例:SSE 流式接口要关闭缓冲、加长读超时。注解名称以所用 Ingress 控制器的文档为准
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: agent-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts: ["agent.example.com"]
|
||||
secretName: agent-example-tls
|
||||
rules:
|
||||
- host: agent.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: agent-api
|
||||
port:
|
||||
name: http
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: agent-api
|
||||
labels:
|
||||
app.kubernetes.io/name: agent-api
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: agent-api # 必须和 Pod 的标签一致,否则 Service 没有后端
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
@@ -0,0 +1,63 @@
|
||||
# requires: yaml
|
||||
"""对清单做团队规范检查(kubeconform 只校验结构,这里检查“写对了但不合规范”的问题)。
|
||||
|
||||
运行:pytest -q test_manifests.py
|
||||
结构校验(可选):kubeconform -strict -summary *.yaml
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
|
||||
def load_all() -> list[dict]:
|
||||
docs = []
|
||||
for f in sorted(HERE.glob("*.yaml")):
|
||||
docs += [d for d in yaml.safe_load_all(f.read_text(encoding="utf-8")) if d]
|
||||
return docs
|
||||
|
||||
|
||||
DOCS = load_all()
|
||||
|
||||
|
||||
def by_kind(kind: str) -> list[dict]:
|
||||
return [d for d in DOCS if d["kind"] == kind]
|
||||
|
||||
|
||||
def test_selector_matches_pod_labels():
|
||||
for dep in by_kind("Deployment"):
|
||||
sel = dep["spec"]["selector"]["matchLabels"]
|
||||
labels = dep["spec"]["template"]["metadata"]["labels"]
|
||||
assert sel.items() <= labels.items()
|
||||
for svc in by_kind("Service"):
|
||||
assert svc["spec"]["selector"].items() <= labels.items(), "Service 选择器和 Pod 标签不一致"
|
||||
|
||||
|
||||
def test_containers_follow_rules():
|
||||
for dep in by_kind("Deployment"):
|
||||
for c in dep["spec"]["template"]["spec"]["containers"]:
|
||||
image = c["image"]
|
||||
assert ":" in image.rsplit("/", 1)[-1] and not image.endswith(":latest"), "镜像必须固定版本"
|
||||
assert "requests" in c["resources"] and "memory" in c["resources"].get("limits", {})
|
||||
for probe in ("readinessProbe", "livenessProbe"):
|
||||
assert probe in c, f"缺少 {probe}"
|
||||
assert c["securityContext"]["allowPrivilegeEscalation"] is False
|
||||
for e in c.get("env", []):
|
||||
assert "value" not in e or "KEY" not in e["name"].upper(), "密钥不能明文写在清单中"
|
||||
|
||||
|
||||
def test_rolling_update_keeps_capacity():
|
||||
for dep in by_kind("Deployment"):
|
||||
ru = dep["spec"]["strategy"]["rollingUpdate"]
|
||||
assert ru["maxUnavailable"] == 0
|
||||
|
||||
|
||||
def test_pdb_lower_than_min_replicas():
|
||||
hpa_min = min(h["spec"]["minReplicas"] for h in by_kind("HorizontalPodAutoscaler"))
|
||||
for pdb in by_kind("PodDisruptionBudget"):
|
||||
assert pdb["spec"]["minAvailable"] < hpa_min, "PDB 要求的可用数不小于副本数会导致节点无法驱逐"
|
||||
|
||||
|
||||
def test_no_secret_objects_in_repo():
|
||||
assert not by_kind("Secret"), "Secret 不应提交到仓库"
|
||||
@@ -4,12 +4,37 @@ title: Kubernetes 基础
|
||||
type: index
|
||||
domain: devops
|
||||
stack: [k8s]
|
||||
tags: []
|
||||
tags: [Kubernetes, 部署, 探针, 滚动更新]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# Kubernetes 基础
|
||||
|
||||
Pod、Deployment、Service、Ingress、配置与密钥、探针、滚动发布。
|
||||
能读懂、修改清单,能用 kubectl 排查;理解探针、滚动更新和优雅退出(AI 流式服务的关键)。BOSS直聘 2026-10-23 的调研中有岗位把 K8s 列为要求或加分项。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
## 学习路线
|
||||
|
||||
| 顺序 | 课程 | 级别 | 预计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [Kubernetes 基础:Pod / Deployment / Service / Ingress、配置与密钥、探针、滚动更新和扩缩容](01-core-objects.md) | 基础 | 3 小时 |
|
||||
|
||||
## 企业场景
|
||||
|
||||
- [AI Agent 服务上 K8s:发布时流式回答不中断、慢启动、密钥管理和按负载扩容](scenarios/agent-rollout.md)
|
||||
|
||||
## 面试题
|
||||
|
||||
- [Kubernetes 基础面试题](interview.md)
|
||||
|
||||
## 示例代码
|
||||
|
||||
- [examples/agent-api](examples/agent-api/):Deployment / Service / Ingress / ConfigMap / HPA / PDB 清单(kubeconform 校验 6 个资源有效;规范测试 5 个通过;未在真实集群部署)
|
||||
|
||||
## 在 16 周计划中
|
||||
|
||||
- [第 14 周:后端扩展与部署](../../../plan/week14-backend-extend-deploy.md)
|
||||
|
||||
## 官方资料
|
||||
|
||||
- https://kubernetes.io/zh-cn/docs/home/
|
||||
- kubeconform:https://github.com/yannh/kubeconform
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: devops.k8s.interview
|
||||
title: Kubernetes 基础面试题
|
||||
type: interview
|
||||
domain: devops
|
||||
stack: [k8s]
|
||||
tags: [Kubernetes, 探针, 滚动更新, 排查, 面试]
|
||||
level: basic
|
||||
related: [devops.k8s.core-objects]
|
||||
week: [14, 16]
|
||||
---
|
||||
|
||||
# Kubernetes 基础面试题
|
||||
|
||||
## 基础
|
||||
|
||||
### 【基础】Pod、Deployment、Service 是什么关系?
|
||||
|
||||
**参考答案**:Pod 运行容器;Deployment 管理一组相同的 Pod(副本数、滚动更新、回滚),通过 ReplicaSet 实现;Service 通过标签选择器找到这些 Pod,提供稳定的访问地址和负载均衡。
|
||||
|
||||
**追问**:Pod 的 IP 能直接用吗?→ 不建议,Pod 重建后 IP 会变,应通过 Service 名访问。
|
||||
|
||||
### 【基础】readinessProbe 和 livenessProbe 的区别?
|
||||
|
||||
**参考答案**:就绪探针失败时 Pod 从 Service 端点中移除但不重启,用于“暂时不能处理请求”;存活探针失败时重启容器,用于“进程卡死”。慢启动应用再加 startupProbe。
|
||||
|
||||
**追问**:存活探针为什么不能检查数据库?→ 数据库故障时所有 Pod 会被同时重启,问题不能解决反而扩大。
|
||||
|
||||
### 【基础】ConfigMap 和 Secret 的区别?
|
||||
|
||||
**参考答案**:都用于注入配置;Secret 用于敏感信息,有单独的权限控制,可以配置静态加密,但其 YAML 中的数据只是 base64 编码,不能提交到仓库。
|
||||
|
||||
**追问**:生产环境密钥怎么管理?→ 使用外部密钥管理系统同步到集群,或平台提供的加密方案;做好轮换和访问审计。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】滚动更新如何做到不中断服务?
|
||||
|
||||
**参考答案**:`maxUnavailable: 0` 保证可用数量;就绪探针确保新 Pod 准备好才接流量;旧 Pod 优雅退出:先摘除端点(preStop 等待),应用处理 SIGTERM 完成进行中的请求,宽限期足够长。
|
||||
|
||||
**追问**:发布后发现问题怎么办?→ `kubectl rollout undo` 回滚到上个版本;更好的做法是灰度发布(金丝雀)观察指标后再全量。
|
||||
|
||||
### 【进阶】Pod 一直 CrashLoopBackOff,怎么排查?
|
||||
|
||||
**参考答案**:`kubectl describe pod` 看事件和退出码;`kubectl logs --previous` 看崩溃前日志;常见原因:配置或密钥缺失、启动命令错误、依赖连不上、存活探针过早失败、OOMKilled(退出码 137)。
|
||||
|
||||
**追问**:镜像拉取失败显示什么状态?→ `ErrImagePull` / `ImagePullBackOff`,检查镜像名、标签和拉取凭证。
|
||||
|
||||
### 【进阶】requests 和 limits 有什么区别?
|
||||
|
||||
**参考答案**:requests 是调度依据(保证分配的资源),limits 是上限:超过内存上限会被 OOMKill,超过 CPU 上限会被限流。两者关系决定 QoS 等级。
|
||||
|
||||
**追问**:CPU 一定要设 limits 吗?→ 有争议:设置后可能出现限流导致延迟抖动;很多团队只设 CPU requests 和内存 limits,按团队规范执行。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】AI 服务每次发布,用户的流式回答都会中断,怎么解决?
|
||||
|
||||
**参考答案**:优雅退出:就绪探针在退出时立即失败、停止接新请求、等待流结束;preStop 留出摘除时间;宽限期大于最长回答时间;`maxUnavailable: 0`;Ingress 关闭缓冲。前端也可以实现断线提示和重试。详见 [Agent 上 K8s 场景](scenarios/agent-rollout.md)。
|
||||
|
||||
**追问**:怎么验证改好了?→ 在测试环境持续发起长回答请求的同时执行滚动更新,统计中断数为 0。
|
||||
|
||||
### 【场景题】HPA 配置了 CPU 70% 扩容,但高峰期 AI 服务很慢却没有扩容,为什么?
|
||||
|
||||
**参考答案**:AI 服务大部分时间在等待模型响应(I/O),CPU 不高;应该用反映负载的指标:进行中请求数、队列长度、首 token 延迟,通过自定义指标扩容;同时检查是否被上游模型的限流卡住(扩容也没用)。
|
||||
|
||||
**追问**:扩容太慢怎么办?→ 提高最小副本、缩短启动时间(镜像体积、预热)、根据可预期的高峰提前扩容。
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: devops.k8s.scenario.agent-rollout
|
||||
title: AI Agent 服务上 K8s:发布时流式回答不中断、慢启动、密钥管理和按负载扩容
|
||||
type: scenario
|
||||
domain: devops
|
||||
stack: [k8s, docker, nginx, cicd]
|
||||
tags: [滚动更新, 优雅退出, SSE, 探针, HPA, Secret, 回滚]
|
||||
level: intermediate
|
||||
prerequisites: [devops.k8s.core-objects, devops.docker.image-dockerfile]
|
||||
est_hours: 2.5
|
||||
related: [devops.k8s.interview, devops.cicd.release-rollback]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# AI Agent 服务上 K8s:发布时流式回答不中断、慢启动、密钥管理和按负载扩容
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
Agent API 从虚拟机迁移到公司的 K8s 集群后:
|
||||
|
||||
1. 每次发布,正在进行中的流式回答被切断,用户看到“网络错误”;
|
||||
2. 新版本启动需要预热(加载提示词模板、建立连接池),偶尔被重启循环;
|
||||
3. 有人把模型 API Key 直接写在 Deployment 的环境变量里提交到了仓库;
|
||||
4. 高峰期响应变慢,但 CPU 使用率并不高,HPA 没有扩容。
|
||||
|
||||
## 约束
|
||||
|
||||
- 集群由平台团队维护,应用团队只能修改自己的清单。
|
||||
- 回答可能持续 60 秒以上。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **优雅退出**:
|
||||
- 应用收到 SIGTERM 后:就绪检查立即返回失败、不再接受新请求、等待进行中的流结束(最长 80 秒)后退出;
|
||||
- `preStop` 短暂等待,给 Ingress / Service 摘除端点的时间;
|
||||
- `terminationGracePeriodSeconds: 90` 大于最长回答时间;
|
||||
- 滚动更新 `maxUnavailable: 0`。
|
||||
2. **慢启动**:startupProbe 给足启动时间;就绪探针在预热完成后才返回成功。
|
||||
3. **密钥**:轮换已泄露的 Key;清单中只引用 Secret;Secret 由密钥管理系统同步(或平台提供的方案);CI 中加密钥扫描。
|
||||
4. **扩容指标**:AI 服务主要在等待模型返回,CPU 低但并发连接数高。改用“每个 Pod 的进行中请求数”作为自定义指标(需要平台支持指标适配器),或者先按经验提高副本下限。
|
||||
5. **发布与回滚**:镜像固定版本;CI 中先 `kubeconform` + 规范测试;发布后观察错误率和首 token 延迟,异常时 `kubectl rollout undo`。
|
||||
|
||||
## 关键代码
|
||||
|
||||
FastAPI 应用的优雅退出(示意):
|
||||
|
||||
```python
|
||||
import asyncio, signal
|
||||
|
||||
active_streams = 0
|
||||
shutting_down = False
|
||||
|
||||
@app.get("/readyz")
|
||||
async def readyz():
|
||||
if shutting_down:
|
||||
raise HTTPException(503) # 让 K8s 尽快摘除这个 Pod
|
||||
return {"ok": True}
|
||||
|
||||
async def drain(timeout: float = 80):
|
||||
global shutting_down
|
||||
shutting_down = True
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while active_streams > 0 and loop.time() < deadline:
|
||||
await asyncio.sleep(0.5) # 等待进行中的流式回答结束
|
||||
```
|
||||
|
||||
(Uvicorn 自身也会在收到 SIGTERM 后停止接收新连接并等待请求完成,具体参数如 `--timeout-graceful-shutdown` 以官方文档为准。)
|
||||
|
||||
完整清单:[examples/agent-api](../examples/agent-api/)。SSE 在 Nginx 层的配置见 [AI 网关场景](../../nginx/scenarios/ai-gateway-sse-canary.md)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 选择 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **优雅退出 + 长宽限期(本方案)** | 用户无感知 | 发布时间变长 |
|
||||
| 前端断线自动续传(从断点继续) | 对任何中断都有效 | 后端要保存生成进度,复杂 |
|
||||
| 发布时间放在低峰期 | 简单 | 治标不治本 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 宽限期比最长回答时间短,SIGKILL 依然切断。
|
||||
- 应用作为 shell 脚本的子进程启动,收不到 SIGTERM(使用 exec 形式的启动命令)。
|
||||
- 就绪探针在退出阶段仍然返回成功,新请求继续进来。
|
||||
- 以为 Secret 是加密的。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “迁移到 K8s 后发布会切断流式回答。我做了优雅退出:收到 SIGTERM 后就绪探针立即失败,停止接新请求,等进行中的流结束;preStop 留出摘除端点的时间,宽限期设为 90 秒,大于最长回答;滚动更新不减少可用副本。慢启动用 startupProbe。泄露的 Key 立即轮换,清单只引用 Secret,CI 加密钥扫描。扩容方面,AI 服务 CPU 不高但并发高,改用进行中请求数作为指标。”
|
||||
Reference in new issue
Block a user