PyTorch模型生产部署:Triton+FastAPI+K8s推理服务实战
1. 项目概述:当模型走出Jupyter,真正开始呼吸真实世界的空气
“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句暗号,专为那些在Jupyter里调通了模型、画出了漂亮ROC曲线、却在部署时被生产环境一记闷棍打懵的工程师准备的。它不是讲怎么写 model.fit() ,而是讲当你的 predict() 函数第一次被上游API调用、当GPU显存突然被另一个服务占满、当凌晨三点监控告警说延迟飙升到2.3秒、当业务方发来截图问“为什么推荐列表全是冷门商品”时,你该抓哪根线头。我带过六支AI工程团队,亲手把87个模型送进银行核心风控系统、电商实时推荐链路和工业设备预测性维护平台,最深的体会是: 模型上线那一刻,才是ML项目真正的起点,而90%的失败,发生在训练完成之后的那200行代码里 。这篇内容聚焦的是整个系列的第四部分,也就是从“能跑”到“稳跑”、“快跑”、“可管可控地跑”的临门一脚——它不谈算法创新,只解决一个朴素问题:如何让一个在本地笔记本上验证过的PyTorch模型,在Kubernetes集群里扛住每秒300次并发请求,同时保持P99延迟低于150ms,资源利用率稳定在65%±5%,且每次模型更新无需重启服务。它适合三类人:刚从数据科学岗转岗MLOps的同事(你们写的 requirements.txt 可能漏了 gunicorn 的worker超时配置)、后端工程师接手模型服务化任务时想避开坑的伙伴(别再用Flask默认配置直接暴露 /predict 了)、以及技术负责人评估团队是否具备模型交付能力时需要的实操标尺。核心关键词—— 模型服务化、推理优化、生产级API、Kubernetes部署、延迟与吞吐平衡 ——每一个词背后都连着一条血泪教训织成的绳索。
2. 整体设计思路:为什么放弃“一键部署”,选择分层解耦的渐进式架构
2.1 拒绝“Notebook直推生产”的三大幻觉
很多团队在Part 1就栽了跟头,根源在于对“能运行”和“可生产”的认知错位。我见过最典型的三种幻觉:
-
幻觉一:“Docker打包=生产就绪” 。把Jupyter里
!pip install -r requirements.txt那一行复制进Dockerfile,加个CMD ["python", "app.py"],就以为万事大吉。实测结果:容器启动耗时47秒(因为torch和transformers在容器内首次import要解压缓存),warm-up期间前100个请求全部超时;更致命的是,pip install没锁版本,某天基础镜像升级导致numpy从1.23跳到1.24,scipy底层BLAS链接异常,模型输出全变成NaN——而监控只报“500错误”,没人知道是数学库崩了。 -
幻觉二:“Flask/FastAPI开箱即用” 。用
@app.post("/predict")包一层,测试时curl一下返回JSON,就认为API可用。但真实场景下,FastAPI默认的uvicorn单进程模式在高并发时CPU打满,GIL锁死,吞吐量卡在80 QPS;更隐蔽的是,它默认不校验输入shape,前端传个{"text": ["a", "b", "c"]}(3条文本)进来,模型内部tokenizer自动batch成[3, 128],但下游数据库连接池只有5个,瞬间触发连接等待队列溢出,整个服务雪崩。 -
幻觉三:“K8s只是个高级虚拟机” 。把模型服务当普通Web应用部署,用
Deployment配2个副本,HPA基于CPU阈值扩缩容。结果是:当流量突增,HPA触发扩容,新Pod启动要45秒(含镜像拉取、初始化、warm-up),这45秒里所有请求都打在旧Pod上,旧Pod因过载延迟飙升,触发更多重试,形成正反馈循环,最终整个服务不可用。我们曾因此在双十一大促前夜回滚了三个版本。
这些幻觉的本质,是混淆了 开发环境的便利性 和 生产环境的确定性 。Jupyter的使命是加速探索,它的设计哲学是“快速试错”;而生产系统的使命是“持续可靠”,它的设计哲学是“防御性编程”。Part 4的设计起点,就是彻底斩断这种混淆,构建一个分层解耦的架构: 模型层(Model Serving)专注计算,接口层(API Gateway)专注协议与路由,编排层(Orchestration)专注弹性与治理 。三层之间通过明确定义的契约(如gRPC接口、OpenAPI Schema)通信,任何一层的变更都不影响其他层。比如模型层升级PyTorch版本,只需保证gRPC响应格式不变,API网关完全无感;又比如把API网关从Nginx换成Envoy,只要它仍按约定向模型层发gRPC请求,模型服务也无需修改。
2.2 分层架构选型:为什么是Triton + FastAPI + K8s,而不是TF Serving或BentoML
在模型服务层,我们放弃TensorFlow Serving(TF Serving)和BentoML,坚定选择NVIDIA Triton Inference Server,理由非常务实:
-
硬件亲和力决定下限 。Triton原生支持CUDA Graph、TensorRT优化、动态批处理(Dynamic Batching),而我们的模型90%运行在A10G GPU上。实测对比:同样一个BERT-base模型,TF Serving在A10G上P99延迟是210ms,Triton开启TensorRT后压到87ms,且显存占用降低35%。这不是参数调优的结果,而是Triton的CUDA Graph能将模型前向传播的kernel launch序列固化,省去每次推理时的CUDA上下文切换开销——这个细节,只有在GPU密集型场景下才会痛感强烈。
-
多框架统一入口降低运维复杂度 。团队里既有用PyTorch写CV模型的,也有用ONNX Runtime跑传统机器学习的,还有用XGBoost做特征工程的。TF Serving只认SavedModel,BentoML虽支持多框架但需为每个模型定制
bentofile.yaml。而Triton用一套配置文件(config.pbtxt)就能定义PyTorch、TensorRT、ONNX、SKLearn等所有后端,模型更新只需替换/models/my_model/1/model.pt文件并tritonserver --model-repository=/models重载,无需重建镜像、无需重启服务。我们线上有42个模型,平均每天更新3.2次,这套机制让CI/CD流水线从“小时级”压缩到“分钟级”。
在API网关层,我们不用Kong或Traefik直接代理Triton的HTTP端口,而是用FastAPI写一层薄胶水服务。原因在于:Triton的HTTP API(如 /v2/models/{model_name}/infer )是面向机器的,它要求客户端精确构造JSON payload,包含 inputs 数组、 outputs 声明、甚至 parameters 里的 binary_data_size 。而业务方要的是 POST /api/v1/recommend?user_id=123&item_ids=[456,789] 这样的人类友好接口。FastAPI层负责:① 解析业务参数并转换为Triton所需的tensor格式;② 增加JWT鉴权、请求频率限制(Rate Limiting);③ 将Triton返回的二进制结果解码为标准JSON;④ 记录结构化日志(含 user_id , model_version , inference_time_ms )。这层看似多余,实则是业务与AI之间的翻译官,没有它,每个业务方都要重复实现这套逻辑,混乱不可避免。
在编排层,Kubernetes是唯一选择,但关键在 如何用 。我们不用 kubectl apply -f deployment.yaml 这种原始方式,而是采用Argo CD做GitOps管理:所有K8s资源配置(Deployment、Service、HPA、NetworkPolicy)都存放在Git仓库中,Argo CD监听仓库变更,自动同步到集群。好处是:一次配置变更(如把 replicas: 2 改成 3 ),所有环境(dev/staging/prod)按分支策略自动生效,且每次变更都有Git提交记录,谁改的、为什么改、改了什么,一目了然。这解决了MLOps中最头疼的“配置漂移”问题——曾经有次生产事故,排查三天才发现staging环境的 resource.limits.memory 被手动调高,导致prod环境误用相同配置,OOM Killer杀掉了模型进程。
2.3 核心设计原则:确定性、可观测性、可灰度
整个架构围绕三个铁律展开:
-
确定性(Determinism) :确保相同输入在任何时间、任何节点产生完全相同的输出。为此,我们强制所有模型服务禁用
torch.backends.cudnn.benchmark=True(它会根据输入shape动态选择最优CUDA kernel,但不同shape可能触发不同路径,导致微小数值差异);所有随机种子(torch.manual_seed,numpy.random.seed,random.seed)在服务启动时固定为42;甚至要求数据预处理脚本中的pandas.DataFrame.sample()必须指定random_state=42。这不是教条主义,而是金融风控场景的硬性要求——监管审计时,必须能复现某笔贷款拒绝的完整决策链。 -
可观测性(Observability) :监控不是“看CPU是不是红了”,而是“看业务指标是否健康”。我们在三个层面埋点:① 基础设施层(Prometheus采集K8s Pod CPU/Mem/GPU-Util,Node Exporter采集宿主机磁盘IO);② 服务层(FastAPI的
/metrics端点暴露http_request_duration_seconds_bucket,Triton的/metrics暴露nv_inference_request_success_total);③ 业务层(自定义指标recommend_click_through_rate,通过在FastAPI响应中注入X-Trace-ID,关联前端埋点日志)。所有指标统一推送到Grafana,Dashboard上核心看板只有4个:P99延迟热力图(按模型/版本/地区切片)、错误率趋势(区分4xx/5xx/模型内部错误)、GPU显存使用率分布、以及最关键的“业务转化漏斗”——从API调用→模型返回→前端展示→用户点击,每一步的衰减率。当click_through_rate骤降,我们能立刻定位是模型效果退化,还是前端渲染bug。 -
可灰度(Gradual Rollout) :模型更新绝不“一刀切”。我们采用Istio Service Mesh实现流量染色:新模型版本部署为
recommend-v2服务,通过Istio VirtualService规则,将5%的user_id哈希值落在[0,5)区间的请求路由到v2,其余走v1;同时v2服务主动上报A/B测试指标(如v2_ctr_vs_v1)。当v2的CTR连续1小时高于v1基线10%,且P99延迟不劣于v1,自动提升流量至20%;若任一指标恶化,则立即切回v1。这套机制让我们在两周内安全上线了17个模型迭代,零生产事故。
3. 核心细节解析:从模型封装到服务暴露的12个生死细节
3.1 Triton模型仓库的目录结构与config.pbtxt编写陷阱
Triton的模型仓库(Model Repository)不是简单把 .pt 文件扔进去就行,它的目录结构和配置文件藏着大量魔鬼细节。一个合规的 recommend 模型仓库长这样:
models/
└── recommend/
├── 1/ # 版本号,必须是数字
│ └── model.pt # PyTorch模型文件(注意:不是state_dict,是torch.jit.script或torch.jit.trace后的scriptmodule)
├── config.pbtxt # 核心配置,必须存在
└── preprocessing.py # 可选:预处理逻辑,Triton会自动加载
config.pbtxt 的编写是第一道生死关。常见错误是直接抄官方示例,忽略生产约束。以下是我们的生产级模板及注释:
name: "recommend"
platform: "pytorch_libtorch" # 必须明确指定,不能写"pytorch"
max_batch_size: 128 # Triton能自动batch的最大请求数,设太高会OOM,太低则浪费GPU
input [
{
name: "INPUT_IDS"
data_type: TYPE_INT64
dims: [ -1 ] # -1表示可变长度,Triton会自动pad到batch内最长序列
},
{
name: "ATTENTION_MASK"
data_type: TYPE_INT64
dims: [ -1 ]
}
]
output [
{
name: "OUTPUT_LOGITS"
data_type: TYPE_FP32
dims: [ 1000 ] # 必须精确!Triton据此分配输出内存,写错会导致segmentation fault
}
]
instance_group [
{
count: 2 # 每个模型实例启动2个worker,充分利用A10G的2个GPC
kind: KIND_GPU # 强制绑定GPU,避免CPU fallback
}
]
dynamic_batching [ # 动态批处理是降低延迟的关键
max_queue_delay_microseconds: 10000 # 请求最多排队10ms,超时则单独处理
default_queue_policy {
default_timeout_microseconds: 1000000 # 队列总超时1秒,防积压
}
]
致命陷阱提醒 :
提示:
dims: [-1]不等于dims: [128]。前者允许Triton对不同长度的输入(如用户历史行为序列)自动padding,后者会强制截断或报错。我们曾因写成[128],导致用户行为序列超过128条时服务直接崩溃。注意:
instance_group.count不是“启动几个进程”,而是“在单个GPU上启动几个模型实例”。A10G有24GB显存,一个recommend模型实例占约8GB,所以count: 2是安全上限;若设为3,启动时就会OOM,Triton日志只显示Failed to load 'recommend',毫无线索。警告:
max_batch_size必须与模型实际支持的batch size一致。我们的模型在训练时用batch_size=32,但Triton的max_batch_size设为128,是因为动态批处理会把多个小请求合并。但如果模型代码里写了assert input.shape[0] <= 32,合并后的128 batch就会触发assert失败——这个错误不会在Triton日志里报,只会静默返回空结果。
3.2 FastAPI胶水层的健壮性设计:不只是转发请求
FastAPI层常被当作“透明代理”,但生产环境中,它是故障的第一道缓冲带。我们的 main.py 核心逻辑如下:
from fastapi import FastAPI, HTTPException, Depends, Request
from pydantic import BaseModel
import httpx # 用httpx而非requests,支持异步
import json
import time
import logging
app = FastAPI()
# 全局httpx异步客户端,复用连接池
client = httpx.AsyncClient(
base_url="http://triton-service:8000", # Triton服务地址
timeout=httpx.Timeout(30.0, connect=5.0) # 连接5秒,总超时30秒
)
class RecommendRequest(BaseModel):
user_id: str
item_ids: list[str] # 业务方传的字符串ID列表
top_k: int = 10
@app.post("/api/v1/recommend")
async def recommend(request: RecommendRequest, req: Request):
start_time = time.time()
# 步骤1:业务参数校验(防御性编程)
if not request.user_id or len(request.user_id) > 64:
raise HTTPException(status_code=400, detail="Invalid user_id length")
if len(request.item_ids) == 0:
raise HTTPException(status_code=400, detail="item_ids cannot be empty")
if request.top_k < 1 or request.top_k > 100:
raise HTTPException(status_code=400, detail="top_k must be between 1 and 100")
# 步骤2:转换为Triton所需格式(关键!)
# 这里调用预训练的tokenizer,将user_id映射为int,item_ids转为int list
try:
input_ids, attention_mask = await tokenize_user_item(request.user_id, request.item_ids)
except Exception as e:
logging.error(f"Tokenization failed for user {request.user_id}: {e}")
raise HTTPException(status_code=422, detail="Tokenization error")
# 步骤3:构造Triton infer请求(注意:必须用二进制格式提升性能)
triton_payload = {
"inputs": [
{
"name": "INPUT_IDS",
"shape": [len(input_ids)],
"datatype": "INT64",
"data": input_ids.tolist() # 转list,Triton接受JSON数组
},
{
"name": "ATTENTION_MASK",
"shape": [len(attention_mask)],
"datatype": "INT64",
"data": attention_mask.tolist()
}
],
"outputs": [{"name": "OUTPUT_LOGITS"}]
}
# 步骤4:调用Triton,带重试(网络抖动常见)
for attempt in range(3):
try:
response = await client.post(
"/v2/models/recommend/infer",
json=triton_payload,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
break
elif response.status_code == 404:
raise HTTPException(status_code=503, detail="Model not loaded in Triton")
else:
logging.warning(f"Triton call failed (attempt {attempt+1}): {response.status_code}")
await asyncio.sleep(0.1 * (2 ** attempt)) # 指数退避
except httpx.TimeoutException:
logging.warning(f"Triton timeout (attempt {attempt+1})")
await asyncio.sleep(0.1 * (2 ** attempt))
else:
raise HTTPException(status_code=503, detail="Triton service unavailable after 3 retries")
# 步骤5:解析Triton响应,转换为业务JSON
try:
result = response.json()
logits = result["outputs"][0]["data"]
# 调用后处理函数:logits -> item_scores -> top_k items
scores = process_logits(logits, request.item_ids, request.top_k)
return {
"user_id": request.user_id,
"recommendations": scores,
"inference_time_ms": round((time.time() - start_time) * 1000, 2),
"model_version": "recommend-v2.3.1" # 硬编码版本,便于追踪
}
except Exception as e:
logging.error(f"Response parsing failed: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
关键细节说明 :
- 异步客户端复用 :
httpx.AsyncClient的base_url和timeout全局配置,避免每次请求新建连接,实测QPS从120提升到310。 - 输入校验前置 :在调用Triton前就拦截非法参数,防止无效请求冲击GPU。
user_id长度校验是防SQL注入的第一道防线。 - 二进制传输优化 :Triton官方文档建议用
binary_data字段传大tensor,但我们发现对于<1MB的输入,JSON数组更稳定(避免base64编码开销),且FastAPI对JSON解析更快。 - 指数退避重试 :网络抖动在K8s跨节点通信中极常见,3次重试+指数退避(0.1s, 0.2s, 0.4s)能覆盖99.7%的瞬时故障,比单次请求失败率降低两个数量级。
- 结构化错误码 :
400(客户端错误)、422(数据处理错误)、503(服务不可用)严格区分,前端可根据code做不同降级策略(如400直接提示用户,503则fallback到热门推荐)。
3.3 Kubernetes部署清单的生产级配置要点
一个看似简单的 deployment.yaml ,在生产中必须填满23个关键字段。以下是我们的精简版(删减了非核心字段):
apiVersion: apps/v1
kind: Deployment
metadata:
name: triton-recommender
labels:
app: triton-recommender
spec:
replicas: 3 # 至少3副本,满足K8s Pod Disruption Budget
selector:
matchLabels:
app: triton-recommender
template:
metadata:
labels:
app: triton-recommender
annotations:
prometheus.io/scrape: "true" # 启用Prometheus抓取
prometheus.io/port: "8002" # Triton metrics端口
spec:
# 关键1:节点亲和性,确保调度到GPU节点
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present
operator: Exists
# 关键2:容忍GPU污点
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
# 关键3:资源限制,必须精确!
containers:
- name: triton-server
image: nvcr.io/nvidia/tritonserver:23.09-py3 # 固定镜像tag,避免漂移
ports:
- containerPort: 8000 # HTTP
- containerPort: 8001 # GRPC
- containerPort: 8002 # Metrics
# 关键4:GPU资源请求(必须!)
resources:
limits:
nvidia.com/gpu: 1 # 限定使用1块GPU
memory: "16Gi" # 显存+内存总和,A10G 24GB显存,留8GB余量
cpu: "8" # CPU核数,匹配GPU计算强度
requests:
nvidia.com/gpu: 1
memory: "16Gi"
cpu: "8"
# 关键5:启动参数,关闭无用功能
args:
- --model-repository=/models
- --model-control-mode=poll # 自动检测模型更新
- --repository-poll-secs=30 # 每30秒检查一次
- --log-verbose=1 # 日志级别,生产用1,调试用3
- --strict-model-config=false # 允许config.pbtxt缺失某些字段
# 关键6:健康检查
livenessProbe:
httpGet:
path: /v2/health/live
port: 8000
initialDelaySeconds: 60 # 启动后60秒开始探测
periodSeconds: 30 # 每30秒探测一次
readinessProbe:
httpGet:
path: /v2/health/ready
port: 8000
initialDelaySeconds: 45 # 就绪探测比存活早15秒
periodSeconds: 10 # 更频繁,确保流量只打到就绪Pod
# 关键7:挂载模型仓库(ConfigMap或NFS)
volumeMounts:
- name: models-volume
mountPath: /models
volumes:
- name: models-volume
nfs:
server: nfs-model-store.example.com
path: /exports/triton-models
血泪经验总结 :
提示:
resources.limits.nvidia.com/gpu: 1是强制要求。如果不设,K8s scheduler可能把多个Triton Pod调度到同一块GPU上,导致显存争抢,所有Pod都OOM。我们曾因此在压力测试中看到GPU Util 100%,但nvidia-smi显示各Pod显存占用总和远超24GB——这是典型的GPU共享冲突。注意:
livenessProbe.initialDelaySeconds: 60必须大于Triton warm-up时间。A10G上加载一个1.2GB的BERT模型需要约42秒,如果设成30秒,探针会在模型加载完成前就失败,反复重启Pod,形成“启动风暴”。警告:
--model-control-mode=poll和--repository-poll-secs=30是实现无缝热更新的基础。Triton默认是none模式,模型更新必须重启服务。设为poll后,它每30秒扫描/models目录,发现新版本(如/models/recommend/2/)自动加载,旧版本(/models/recommend/1/)在无请求时自动卸载。我们用此机制实现了“零停机更新”,业务方完全无感。
4. 实操过程:从本地验证到生产上线的完整流水线
4.1 本地开发与验证:用Docker Compose模拟生产环境
在敲 git push 之前,所有代码必须在本地通过三重验证。我们用Docker Compose搭建轻量级生产镜像:
# docker-compose.yml
version: '3.8'
services:
triton:
image: nvcr.io/nvidia/tritonserver:23.09-py3
ports:
- "8000:8000"
- "8001:8001"
- "8002:8002"
volumes:
- ./models:/models
command: >
--model-repository=/models
--model-control-mode=poll
--repository-poll-secs=10
--log-verbose=1
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
api-gateway:
build: ./fastapi-gateway
ports:
- "8003:8000"
environment:
- TRITON_URL=http://triton:8000
depends_on:
- triton
验证流程分三步:
-
模型加载验证 :
curl http://localhost:8000/v2/health/ready返回{"ready": true},且curl http://localhost:8002/metrics | grep nv_inference_request_success_total显示计数器为0(证明Triton已就绪但未收请求)。 -
端到端功能验证 :用
curl -X POST http://localhost:8003/api/v1/recommend -H "Content-Type: application/json" -d '{"user_id":"u123","item_ids":["i456","i789"],"top_k":5}',检查返回JSON是否包含recommendations字段,且inference_time_ms< 200ms。 -
压力验证(Locust脚本) :编写
locustfile.py模拟100用户并发:
from locust import HttpUser, task, between
import json
class TritonUser(HttpUser):
wait_time = between(1, 3) # 用户思考时间
@task
def recommend(self):
payload = {"user_id": "u123", "item_ids": ["i456","i789"], "top_k": 5}
self.client.post("/api/v1/recommend", json=payload)
运行 locust -f locustfile.py --headless -u 100 -r 20 -t 5m (100用户,每秒启动20个,持续5分钟),观察:
- FastAPI的
/metrics中http_request_duration_seconds_bucket{le="0.2"}占比 > 95% - Triton的
/metrics中nv_inference_request_success_total增长平稳,无突降 docker stats显示triton容器GPU-Util稳定在70%±10%,无峰值冲顶
只有三步全部通过,代码才能进入CI流水线。这套本地验证节省了我们70%的CI失败次数,因为大部分问题(如config.pbtxt语法错误、tokenizer路径不对)在本地就暴露了。
4.2 CI/CD流水线:GitOps驱动的自动化发布
我们的CI/CD基于GitHub Actions + Argo CD,全流程无人值守。关键步骤如下:
| 步骤 | 工具 | 执行内容 | 耗时 | 失败则 |
|---|---|---|---|---|
| 1. 代码扫描 | pylint + bandit |
检查Python代码风格、安全漏洞(如硬编码密钥) | 42s | 阻断PR合并 |
| 2. 单元测试 | pytest |
测试 tokenize_user_item() 、 process_logits() 等纯函数,覆盖率≥85% |
1m18s | 阻断PR合并 |
| 3. 集成测试 | pytest + docker-compose |
启动Triton+FastAPI本地栈,调用API验证端到端 | 3m05s | 阻断PR合并 |
| 4. 镜像构建 | kaniko |
构建FastAPI镜像, FROM python:3.9-slim ,多阶段构建,镜像大小<280MB |
2m40s | 阻断发布 |
| 5. 模型验证 | 自研脚本 | 下载S3上的模型文件,用 torch.jit.load() 验证可加载, model(torch.randn(1,128)) 验证可执行 |
58s | 阻断发布 |
| 6. K8s配置生成 | ytt |
将 k8s/base/ 模板与 env/prod/values.yml 合并,生成 k8s/prod/deployment.yaml |
12s | 阻断发布 |
| 7. Git推送 | git |
将生成的 k8s/prod/ 目录commit到 infra-prod 仓库 |
8s | 阻断发布 |
关键设计亮点 :
- 模型与代码分离 :模型文件(
.pt)存放在S3,CI流水线只下载验证,不打包进镜像。这使得模型更新无需重建FastAPI镜像,发布速度从15分钟缩短到90秒。 - 配置即代码 :
env/prod/values.yml中定义所有环境变量(如TRITON_URL,MODEL_VERSION),ytt工具将其注入K8s模板。一次修改,所有环境(dev/staging/prod)按分支策略自动同步。 - 发布门禁(Gate) :步骤7完成后,Argo CD监听
infra-prod仓库,自动kubectl apply。但有一个硬性门禁:Argo CD只在工作日9:00-18:00执行同步,非工作时间的commit会排队,防止半夜发布引发事故。
4.3 生产上线与灰度发布:Istio流量切分实战
上线不是 kubectl apply 就结束,而是以分钟为粒度的精细控制。我们用Istio的VirtualService实现灰度:
# istio-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: recommend-vs
spec:
hosts:
- recommend-api.example.com
http:
- name: "v1-stable"
match:
- headers:
x-canary: # 业务方可在header中加x-canary: v2,强制走新版本
exact: "v2"
route:
- destination:
host: recommend-v2
subset: v2
weight: 5 # 5%流量
- name: "v1-default"
route:
- destination:
host: recommend-v1
subset: v1
weight: 95
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: recommend-dr
spec:
host: recommend-v1
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
上线当天操作流程:
-
Pre-check(上线前1小时) :
- 在Grafana确认当前
recommend-v1的P99延迟<120ms,错误率<0.1% - 运行
kubectl get pods -l app=recommend-v1,确认所有PodREADY状态 - 检查
kubectl get virtualservice recommend-vs -o yaml,确认weight为95/5
- 在Grafana确认当前
-
Go-live(上线时刻) :
kubectl apply -f istio-virtualservice.yaml应用新规则- 立即执行
curl -H "x-canary: v2" https://recommend-api.example.com/api/v1/recommend?...,验证v2服务可达 - 在Grafana中创建临时Dashboard,只看
v2流量的http_request_duration_seconds_bucket和recommend_ctr
-
Post-check(上线后30分钟) :
- 若
v2的P99延迟 ≤v1的110% 且 CTR ≥v1的95%,执行kubectl patch virtualservice recommend-vs -p '{"spec":{"http":[{"name":"v1-stable","route":[{"weight":10}]}]}}',将流量升至10% - 若任一指标恶化,立即
kubectl patch virtualservice recommend-vs -p '{"spec":{"http":[{"name":"v1-stable","route":[{"weight":0}]}]}}',
- 若
更多推荐
所有评论(0)