机器学习模型生产部署实战:K8s+CI/CD+可观测性闭环
2026/6/15 5:08:58 网站建设 项目流程

1. 这不是又一篇“概念科普”,而是一份压在工位抽屉底下的实操手记

“Deployment ML-OPS Guide Series – 2”——看到这个标题,别急着划走。它不是系列第一篇的重复堆砌,也不是PPT里那种“模型上线三步走”的抽象流程图。这是我在过去18个月里,亲手把7个生产级机器学习服务从Jupyter Notebook推上Kubernetes集群、又在凌晨三点被告警电话叫醒、反复回滚、重调、重测后,用红笔在A4纸上划掉又补上的第二版部署手册。它不讲MLOps是什么,只讲当你的模型准确率98.7%、业务方催着明天上线、运维同事发来一份带17个待确认项的资源申请表时,你该敲哪几行命令、改哪三个配置、盯哪两个指标

核心关键词——模型部署、CI/CD流水线、Kubernetes编排、模型监控、环境一致性——这些词不是贴在墙上的标语,而是我每天在终端里输入的kubectl apply -f、在Prometheus里盯的model_latency_p95、在Dockerfile里反复调试的FROM python:3.9-slim-bullseye。它适合三类人:刚把模型训出来、第一次面对“那接下来怎么让业务系统调用它”的算法工程师;被算法团队甩来一个.pkl文件、却要保证它7×24小时稳定响应的SRE;还有正在设计公司级AI平台架构、需要避开前人踩过所有坑的技术负责人。这篇文章里没有“理论上可行”的方案,只有“我们试过、崩过、修好、跑稳了”的路径。下面所有内容,都建立在一个前提之上:部署不是模型训练的终点,而是模型真正开始创造价值的起点,而这个起点,必须足够坚固、可追溯、可度量

2. 内容整体设计与思路拆解:为什么是这套组合拳,而不是别的?

2.1 从“能跑通”到“可交付”的范式转移

很多团队卡在部署环节,根本原因在于思维惯性——把模型部署当成“把训练好的文件拷贝到服务器上运行”。这在单机小模型时代或许凑合,但在今天,一个典型的推荐模型可能依赖23个Python包、4种不同版本的CUDA库、3个外部API密钥、2套特征工程缓存策略,还要和上游数据管道、下游业务网关、中间件消息队列深度耦合。此时,“能跑通”和“可交付”之间,隔着一条由环境漂移、配置散落、依赖冲突、可观测性缺失组成的鸿沟。

我们这套方案的设计起点,就是彻底斩断这条鸿沟。它不追求“最前沿”,而追求“最稳当”;不堆砌工具链,而聚焦最小可行闭环(Minimum Viable Deployment Loop)。这个闭环包含四个不可分割的齿轮:代码即配置(Code-as-Config)、一次构建,处处运行(Build Once, Run Anywhere)、自动化的健康守门员(Automated Health Gate)、以及故障时的秒级回溯能力(Blameless Post-Mortem)。每一个齿轮的选型,都源于对真实战场的观察。

2.2 工具链选型:拒绝“全家桶”,拥抱“瑞士军刀”

市面上有太多MLOps平台,动辄号称“一站式解决所有问题”。但我的经验是:越“全”的平台,越容易在关键节点上成为黑盒,也越难在出问题时快速定位。因此,我们坚持“工具链解耦、职责清晰”的原则,每个组件只做一件事,并且做到极致:

  • 模型打包与环境固化:选用Docker而非conda-packpip freeze。理由很实在:conda-pack生成的tar包在不同Linux发行版上常因glibc版本不兼容而崩溃;pip freeze则完全无法锁定C扩展库(如numpy底层的OpenBLAS)的二进制版本。Docker镜像则是一个自包含的、可验证的、可签名的原子单元。我们甚至会为每个模型镜像打上sha256:abc123...的摘要标签,确保从开发机构建的镜像,和最终在生产集群里运行的,是字节级完全一致的产物。

  • 流水线编排:选用GitHub Actions(或GitLab CI)而非专用MLOps平台的内置流水线。这不是技术保守,而是为了将CI/CD逻辑完全暴露在代码仓库中,实现100%的版本化与可审计。每一次部署触发、每一次参数变更、每一次回滚操作,都对应着一次git commit。当新来的同事问“上次模型v2.1.3是怎么部署的?”,你不需要翻查平台日志,只需要git checkout到那个commit,cat .github/workflows/deploy.yml,答案一目了然。

  • 服务编排与扩缩容:选用Kubernetes原生Deployment+HPA(Horizontal Pod Autoscaler),而非封装了K8s的抽象层。抽象层固然简化了初期上手,但一旦遇到网络策略、亲和性调度、GPU显存隔离等高级需求,你就得钻进它的源码去debug。而直接使用K8s原语,意味着你可以用kubectl describe pod看到最原始的事件,用kubectl logs -f实时追踪容器输出,用kubectl exec -it直接进入容器排查——所有控制权,始终握在自己手中

  • 模型监控:选用Prometheus+Grafana+custom metrics exporter,而非平台自带的“模型健康分”。平台的健康分往往是几个预设指标的加权平均,它告诉你“分数低了”,但从不告诉你“是延迟高了,还是错误率突增了,还是某个特征的分布偏移了”。而Prometheus的指标是开放的、可自定义的、可下钻的。我们可以轻松定义model_prediction_count_total{model="recommendation_v3", version="2.1.3"},也可以定义model_feature_drift_score{feature="user_age_bucket", model="fraud_detection"}。当告警响起,Grafana面板上滑动鼠标,就能看到是哪个维度、哪个时间点、哪个具体值出了问题。

这套组合拳的核心思想,就是用业界最成熟、文档最完善、社区最活跃的通用基础设施,去承载机器学习这个相对新兴的工作负载。它不创造新范式,而是把已知的、经过千锤百炼的工程实践,严谨地迁移到ML领域。这听起来不够酷,但它能让你在老板问“为什么线上服务宕机了?”时,底气十足地回答:“因为昨天下午3点17分,特征工程服务返回了空数组,我们的监控在3点18分就捕获到了prediction_error_rate的尖峰,并自动触发了回滚,整个过程耗时47秒。”

2.3 架构分层:每一层都必须有明确的“死亡证明”

一个健壮的部署架构,必须像一栋抗震建筑一样,有清晰的承重墙和明确的失效边界。我们的架构严格分为四层,每一层都定义了其“死亡”时的预期行为:

  1. 模型服务层(Model Serving Layer):这是最内核的一层,只负责接收HTTP/gRPC请求、加载模型、执行predict()、返回结果。它不处理任何业务逻辑、不连接数据库、不调用外部API。它的“死亡证明”是:当它挂了,所有对该服务的请求立即返回503 Service Unavailable,且不会有任何超时等待。我们通过K8s的livenessProbe(探针)每10秒检查一次/healthz端点,如果连续3次失败,K8s会立即杀死并重启Pod。这个层的代码,我们要求100%单元测试覆盖,且所有测试必须在pytest中模拟torch.load()model.forward()绝不允许任何真实的I/O操作

  2. 特征服务层(Feature Serving Layer):这一层负责从特征存储(如Feast、Redis、PostgreSQL)中,根据请求ID拉取实时特征。它的“死亡证明”是:当它挂了,模型服务层必须能优雅降级,使用预设的默认特征值(如用户平均点击率)进行预测,并记录feature_unavailable_count指标。我们通过readinessProbe(就绪探针)检查其与特征存储的连接池是否健康,如果连接池耗尽,它会主动报告“未就绪”,K8s将停止向其转发流量,但不会杀死它,给它留出恢复时间。

  3. API网关层(API Gateway Layer):这是面向业务方的唯一入口,负责路由、鉴权、限流、熔断。它的“死亡证明”是:当它挂了,整个服务对外表现为502 Bad Gateway,但模型服务和特征服务本身依然健康,只是失去了入口。我们采用Envoy作为网关,其配置完全由xDS协议动态下发,这意味着网关的扩缩容、配置更新,完全独立于后端服务,互不影响。

  4. 可观测性层(Observability Layer):这是整个系统的“神经系统”,由Prometheus(指标)、Loki(日志)、Tempo(链路追踪)组成。它的“死亡证明”是:当它挂了,你依然能通过kubectl logskubectl describe获取基础信息,但所有聚合视图、历史趋势、关联分析都将消失。因此,我们将其部署在独立的、高可用的K8s命名空间中,并为其配置了比业务服务更高的资源配额和更严格的备份策略。

这种分层设计,带来的最大好处是故障域隔离。当线上出现500 Internal Server Error时,我们不再需要大海捞针。第一步,看网关日志:是网关自身报错,还是它转发给后端后收到的错误?第二步,看模型服务日志:是模型加载失败,还是predict()函数抛出了异常?第三步,看特征服务日志:是特征查询超时,还是返回了格式错误的数据?每一层都像一个独立的、有明确接口契约的微服务,它们之间的交互,必须通过明确定义的、可监控的、可测试的API完成。这看似增加了初期的复杂度,但换来的是后期维护成本的指数级下降。

3. 核心细节解析与实操要点:那些文档里不会写的“魔鬼”

3.1 Dockerfile:从“能用”到“极致精简”的12个优化点

一个模型服务的Docker镜像,绝不是FROM python:3.9 && pip install -r requirements.txt这么简单。一个未经优化的镜像,体积可能高达2GB,其中80%是编译缓存、文档、测试套件等无用之物。这不仅拖慢CI/CD流水线,更在K8s节点上浪费宝贵的磁盘和内存。以下是我们在生产环境中强制执行的12个优化点,每一条都经过了至少3个项目的验证:

  1. 多阶段构建(Multi-stage Build):这是最核心的优化。我们将构建过程分为builderruntime两个阶段。builder阶段使用python:3.9-slim-bullseye,安装所有构建依赖(如gcc,libpq-dev),执行pip wheel --no-deps --wheel-dir /wheels -r requirements.txt,将所有Python包编译成wheel文件。runtime阶段则使用更轻量的python:3.9-slim-bullseye(注意,这里再次使用slim镜像,而非alpine,因为alpine的musl libc与许多Python C扩展不兼容),仅从/wheels目录复制wheel文件并安装。这一步,通常能将镜像体积从1.8GB压缩到350MB。

  2. 非root用户运行:在runtime阶段末尾,添加USER 1001:1001。K8s的安全策略强烈建议容器以非root用户运行。我们创建一个名为mluser的用户,UID/GID固定为1001,避免因用户ID动态分配导致权限问题。

  3. 删除构建缓存与临时文件:在builder阶段的最后,执行rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*。Debian系镜像的apt-get update会下载大量索引文件,这些文件在构建完成后毫无用处。

  4. 使用--no-cache-dir--find-links:在pip install命令中,永远加上--no-cache-dir,避免pip在镜像内创建/root/.cache/pip目录。同时,使用--find-links /wheels --no-index,强制pip只从本地wheel目录安装,跳过PyPI索引查询,极大提升安装速度。

  5. Pin所有依赖的精确版本requirements.txt中,不能出现scikit-learn>=1.0.0,而必须是scikit-learn==1.2.2。我们使用pip-toolspip-compile)来生成这份文件,它会递归解析所有传递依赖,并生成一个完全锁定的版本列表。这是保证环境一致性的基石。

  6. 分离requirements.txtdev-requirements.txt:生产镜像中,只安装requirements.txt。所有开发、测试、linting相关的包(如pytest,black,mypy)都放在dev-requirements.txt中,并且绝不打入生产镜像

  7. COPY比ADD更安全:永远使用COPY指令,而非ADDADD具有自动解压和URL下载的隐式行为,这会增加安全风险和不可预测性。COPY的行为是确定且透明的。

  8. 利用Docker BuildKit的缓存:在CI/CD中启用DOCKER_BUILDKIT=1。BuildKit提供了更智能的分层缓存机制,当requirements.txt未变时,pip install步骤的缓存将被完美复用,无需重新下载和安装任何包。

  9. 设置合理的WORKDIR:使用WORKDIR /app,并将所有应用代码COPY至此。避免使用//home等系统目录,防止权限冲突。

  10. 暴露正确的端口EXPOSE 8000只是元数据,不开启端口。但它是重要的文档,告诉使用者这个服务监听在哪个端口。我们统一约定,所有模型服务监听8000端口。

  11. 健康检查端点:在应用代码中,必须实现/healthz(liveness)和/readyz(readiness)两个端点。/healthz只检查进程是否存活(如return {"status": "ok"}),/readyz则需检查所有依赖(如数据库连接、特征服务连通性)。Dockerfile中通过HEALTHCHECK指令定义检查方式。

  12. 镜像标签策略:镜像标签必须包含git commit shabuild timestamp,例如my-model-service:2.1.3-abc123-20231027-1422。这确保了任何一个镜像,都能被精确地追溯到其构建时的代码状态和时间点。

提示:我们有一个内部脚本docker-lint.sh,它会在CI流水线的build阶段自动运行,检查Dockerfile是否违反了以上任意一条规则。如果违反,流水线将直接失败。这不是为了刁难,而是为了把“最佳实践”变成“强制规范”,让每一个新加入的成员,从第一天起就写出符合生产标准的镜像。

3.2 Kubernetes Deployment:不只是YAML,更是服务契约

一个deployment.yaml文件,远不止是定义Pod数量的配置。它是模型服务与K8s集群之间的一份法律契约,明确规定了服务的“权利”与“义务”。以下是我们在生产环境中,对每一个Deployment都强制要求的8个关键字段及其背后的深意:

  1. replicas: 3:永远不要设为1。单副本意味着单点故障。3个副本是保证高可用的最低门槛,它允许在滚动更新或节点故障时,仍有2个副本在线提供服务。我们甚至会为关键服务设置replicas: 5,以应对更严苛的SLA要求。

  2. strategy.type: RollingUpdate&strategy.rollingUpdate.maxSurge: 1&maxUnavailable: 1:这是滚动更新的黄金法则。maxSurge: 1意味着在更新过程中,最多可以比期望副本数多启动1个Pod;maxUnavailable: 1意味着最多可以有1个Pod不可用。这确保了更新过程平滑,服务永不中断。我们曾见过一个团队将maxSurge设为100%,结果在更新时,旧Pod全部被杀,新Pod因OOM被K8s驱逐,导致服务完全不可用长达3分钟。

  3. resources.requestsandresources.limits:这是K8s调度器的“宪法”。requests是Pod启动时,K8s保证为其分配的最小资源(CPU毫核、内存字节);limits是Pod运行时,K8s允许其使用的最大资源。requests决定了Pod能被调度到哪个节点,limits决定了Pod在资源争抢时会被如何对待。对于一个CPU密集型的推理服务,我们通常设置requests.cpu: "1000m"(1核)和limits.cpu: "2000m"(2核),这样它既能获得稳定的1核算力,又能在空闲时借用额外的1核进行预热或后台任务。内存同理,requests.memory: "2Gi"limits.memory: "4Gi",并配合JVM的-Xmx或Python的ulimit进行精细控制。

  4. livenessProbeandreadinessProbe:如前所述,这是服务的“心跳”和“呼吸”。我们要求initialDelaySeconds必须足够长,以允许模型在冷启动时完成加载(大型Transformer模型可能需要30-60秒)。periodSeconds设为10秒,failureThreshold设为3,意味着连续30秒探测失败才会触发重启。readinessProbeinitialDelaySeconds通常比livenessProbe更长,因为它需要等待所有依赖(如特征服务)都准备就绪。

  5. affinityandtolerations:这是调度的“政治智慧”。affinity用于指定Pod偏好调度到哪些节点(如topologyKey: topology.kubernetes.io/zone,确保副本分散在不同可用区);tolerations则用于容忍节点的“污点”(Taint),例如,为GPU节点打上taint: nvidia.com/gpu=:NoSchedule,然后在需要GPU的模型服务Deployment中添加对应的toleration,确保只有它能被调度到GPU节点上。

  6. securityContext.runAsNonRoot: true&runAsUser: 1001:这是安全的底线。强制容器以非root用户运行,是防御容器逃逸攻击的第一道屏障。

  7. envFromwithconfigMapRefandsecretRef:所有配置,无论是数据库连接字符串、API密钥,还是模型版本号,都必须通过ConfigMapSecret注入,绝不在YAML中硬编码ConfigMap用于非敏感配置,Secret用于密码、Token等。envFrom可以一次性注入整个ConfigMap/Secret的所有键值对,简洁且安全。

  8. annotationsfor Observability:在metadata.annotations中,添加prometheus.io/scrape: "true"prometheus.io/port: "8000"。这是Prometheus自动发现并抓取该Pod指标的“许可证”。没有它,再好的监控代码也形同虚设。

注意:我们有一个内部的k8s-yaml-linter工具,它会扫描所有提交的YAML文件,检查是否遗漏了上述任意一个关键字段。它不是一个简单的语法检查器,而是一个“契约合规性检查器”。当它报错时,不是YAML写错了,而是服务的“契约”不完整。

3.3 模型监控:从“看数字”到“读故事”

监控不是把一堆图表堆在Grafana里,然后等着告警。真正的模型监控,是用数据讲述一个关于模型健康状况的故事。这个故事有三个主角:准确性(Accuracy)、稳定性(Stability)、效率(Efficiency)。我们为每个主角,都定义了3个核心指标,并确保它们能相互印证。

主角一:准确性(Accuracy)

  • model_prediction_error_rate{model="credit_scoring", version="1.5.0"}:这是最直接的指标,计算prediction_error_count / prediction_count。但它有个陷阱:如果模型完全不工作,prediction_count为0,这个比率就失去意义。因此,我们永远将它与下一个指标一起看。
  • model_prediction_count_total{model="credit_scoring", version="1.5.0"}:这是一个单调递增的计数器。它的斜率,代表了服务的吞吐量。当error_rate飙升时,如果count_total的斜率同步变平,说明问题可能是上游流量中断;如果count_total斜率不变甚至变陡,那问题就一定出在模型或其依赖上。
  • model_prediction_latency_seconds_bucket{le="0.1", model="credit_scoring", version="1.5.0"}:这是直方图指标,记录了预测延迟落在各个区间(如0.1秒、0.2秒、0.5秒)内的请求数。我们关注p95(95%的请求延迟低于此值)和p99。一个健康的模型,p95应该稳定在50ms左右。如果p95突然跳到200ms,而error_rate没变,那很可能是特征服务响应变慢,或者模型加载了更大的权重文件。

主角二:稳定性(Stability)

  • model_feature_drift_score{feature="income_bracket", model="credit_scoring"}:我们使用Evidently库,在每次批量预测后,计算当前批次特征分布与基线(通常是训练集)分布的Wasserstein distance。当income_bracket的分布发生显著偏移(如经济下行导致高收入人群比例骤降),这个分数就会升高,提示我们需要重新训练模型。
  • model_prediction_drift_score{model="credit_scoring"}:同样使用Evidently,但这次是计算预测结果(如score)的分布偏移。如果预测分数整体变低,但特征分布没变,那可能是模型内部发生了“概念漂移”(Concept Drift)。
  • model_data_quality_null_ratio{feature="email_domain", model="user_segmentation"}:监控输入数据的质量。email_domain字段的空值率,如果从0.1%突然升到15%,那下游的聚类结果必然失真。这个指标提醒我们,问题可能出在上游数据管道,而非模型本身。

主角三:效率(Efficiency)

  • container_cpu_usage_seconds_total{container="model-server", namespace="ml-prod"}:这是K8s的cAdvisor指标,反映容器实际消耗的CPU时间。我们将它与model_prediction_count_total相除,得到“每万次预测消耗的CPU秒数”。这个值应该是一个稳定的常数。如果它突然翻倍,说明模型代码中可能引入了低效的循环,或者特征工程逻辑变得异常复杂。
  • container_memory_working_set_bytes{container="model-server", namespace="ml-prod"}:监控容器的内存工作集大小。一个健康的模型服务,其内存占用应该是平缓上升,然后趋于稳定。如果它呈现锯齿状剧烈波动,或者持续线性增长,那几乎可以肯定存在内存泄漏。
  • process_open_fds{process="uvicorn", namespace="ml-prod"}:监控进程打开的文件描述符数量。在高并发场景下,如果这个值接近系统上限(通常是1024或65536),服务就会开始拒绝新连接。这是一个非常早期的、关于连接池配置不当的预警信号。

实操心得:我们有一个“监控仪表盘三板斧”原则。第一板斧:打开Grafana,看error_ratecount_total,判断是“没流量”还是“有流量但出错”。第二板斧:看latency_p95cpu_usage,判断是“慢”还是“卡”。第三板斧:看feature_driftprediction_drift,判断是“数据变了”还是“模型坏了”。这三步下来,80%的线上问题,都能在5分钟内定位到根因。

4. 实操过程与核心环节实现:一次完整的“灰度发布”全流程

4.1 流水线设计:从git pushkubectl rollout status的7个自动化环节

一个成熟的CI/CD流水线,其价值不在于它有多快,而在于它有多“傻瓜”。我们设计的流水线,目标是让一个刚入职的算法工程师,在熟悉了基本Git操作后,就能独立完成一次模型的迭代与发布。整个流程被分解为7个清晰、自动化的环节,全部定义在.github/workflows/ci-cd.yml中:

  1. on: [push]withbranches: [main]:流水线只在向main分支推送代码时触发。我们严禁直接向main推送,所有代码必须通过Pull Request(PR)合并。PR的描述模板中,强制要求填写“本次变更影响的模型名称、版本号、预期SLA变更”。

  2. Setup Python:使用actions/setup-python@v4,安装python-version: '3.9'。我们固定Python版本,避免因CI runner升级导致的环境不一致。

  3. Install Dependencies:运行pip install -r requirements.txt。这是对requirements.txt文件的一次“编译时验证”。如果这里失败,说明依赖声明有误,流水线立即终止。

  4. Run Unit Tests:执行pytest tests/ --cov=model --cov-report=term-missing。我们要求单元测试覆盖率不低于80%,且所有测试必须是“纯”的,不依赖任何外部服务。测试框架会自动mock掉torch.load()requests.get()等I/O操作。

  5. Build Docker Image:这是最关键的环节。我们使用docker/build-push-action@v4,并传入以下参数:

    • context: .
    • push: true
    • tags: ${{ secrets.REGISTRY_URL }}/my-model-service:${{ github.sha }},${{ secrets.REGISTRY_URL }}/my-model-service:latest
    • cache-from: type=registry,ref=${{ secrets.REGISTRY_URL }}/my-model-service:buildcache
    • cache-to: type=registry,ref=${{ secrets.REGISTRY_URL }}/my-model-service:buildcache,mode=max这段配置实现了:构建并推送到私有Registry;为镜像打上git commit shalatest两个标签;并利用Registry作为远程缓存源,极大加速后续构建。
  6. Deploy to Staging:使用kubectl工具,将新镜像部署到staging命名空间。命令为:kubectl set image deployment/my-model-service -n staging my-model-service=${{ secrets.REGISTRY_URL }}/my-model-service:${{ github.sha }}。这会触发K8s的滚动更新。紧接着,运行kubectl rollout status deployment/my-model-service -n staging --timeout=300s,等待更新完成。如果5分钟内未完成,流水线失败。

  7. Run Integration Tests:这是对staging环境的最终检验。我们有一个独立的integration-tests服务,它会向staging环境的API网关发送一系列预定义的、覆盖各种边界条件的请求(如空输入、超长文本、非法ID),并验证返回的状态码、响应体结构和业务逻辑。只有所有集成测试通过,流水线才进入最后一步。

  8. Manual Approval for Production:在integration-tests成功后,流水线会暂停,并在GitHub UI上弹出一个“Approve for Production”的按钮。这一步是人为的“质量守门员”。只有经过QA团队和业务方共同确认staging环境表现无误后,才能点击批准。批准后,流水线自动执行下一步。

  9. Deploy to Production:与staging部署类似,但命令指向production命名空间:kubectl set image deployment/my-model-service -n production my-model-service=${{ secrets.REGISTRY_URL }}/my-model-service:${{ github.sha }}。随后,同样运行kubectl rollout status等待完成。

  10. Send Slack Notification:无论成功或失败,流水线的最后一步,都会向#ml-deployments频道发送一条Slack消息,包含:部署的模型名称、版本、触发者、状态(✅成功 / ❌失败)、以及指向流水线详情页的链接。这确保了所有相关方,都能第一时间知晓部署动态。

提示:这个流水线的YAML文件,我们把它视为和模型代码同等重要的“基础设施即代码(IaC)”。它被存放在同一个Git仓库的.github/workflows/目录下,接受同样的Code Review流程。每一次对流水线的修改,都必须有充分的理由和测试验证。

4.2 灰度发布:用canary策略把风险降到最低

“全量发布”是生产环境的头号敌人。我们所有的生产部署,都强制采用canary(金丝雀)发布策略。其核心思想是:先让一小部分真实流量(如1%)流经新版本,严密监控其各项指标,确认无误后,再逐步扩大流量比例,直至100%。这为我们提供了宝贵的“后悔时间”。

我们使用Flagger这个开源工具来实现自动化canary发布。Flagger会与K8s和Prometheus深度集成,其工作流程如下:

  1. 定义Canary对象:我们创建一个canary.yaml文件,其中指定了:

    • targetRef: 指向我们要发布的Deployment
    • service: 定义了新旧版本共用的服务端口和路由规则。
    • analysis: 这是最关键的部分,定义了“什么才算成功”。例如:
      analysis: interval: 1m threshold: 10 maxWeight: 50 stepWeight: 10 metrics: - name: request-success-rate thresholdRange: min: 99 interval: 1m - name: request-duration-p95 thresholdRange: max: 500 interval: 1m
      这段配置的意思是:每1分钟检查一次,如果request-success-rate(成功率)不低于99%,且request-duration-p95(95%延迟)不高于500ms,则认为本轮测试通过,可以将流量权重从当前值(如10%)提升到下一个值(如20%)。整个过程最多进行10轮(threshold: 10),最大权重为50%(maxWeight: 50),每轮提升10%(stepWeight: 10)。
  2. 触发发布:当我们执行kubectl apply -f canary.yaml时,Flagger会自动创建两个Deploymentmy-model-service-primary(代表稳定版本)和my-model-service-canary(代表新版本)。它还会创建一个Service,将所有流量导向primary,并创建一个VirtualService(如果使用Istio)或Ingress(如果使用NGINX Ingress),通过权重路由,将1%的流量导向canary

  3. 自动化分析与决策:Flagger会持续从Prometheus拉取指标。如果在某一轮中,request-success-rate跌到了98.5%,低于阈值99%,Flagger会立即中止发布流程,并自动将canary的流量权重降为0%,同时向Slack发送告警。整个过程无需人工干预,秒级响应。

  4. 手动介入与回滚:如果Flagger检测到问题,它会将canaryDeployment标记为Failed。此时,运维人员可以登录K8s,执行kubectl get canary my-model-service -o wide查看详细状态,然后执行kubectl patch canary my-model-service -p '{"spec":{"abortOnFailure": true}}' --type=merge,强制Flagger执行回滚,将所有流量切回primary版本。

实操心得:我们为每个模型服务都配置了独立的canary对象,并且analysis中的指标阈值,都是基于该服务的历史基线数据设定的。例如,一个实时风控模型,其request-duration-p95的基线是100ms,那么我们的阈值就设为120ms;而一个离线报表生成模型,基线是5000ms,阈值就设为6000ms。没有放之四海而皆准的阈值,只有基于数据的、实事求是的阈值

4.3 回滚:当“一键回滚”成为最可靠的救命稻草

在MLOps的世界里,回滚不是失败的标志,而是工程成熟度的体现。一个无法快速、可靠回滚的系统,本质上就是一个定时炸弹。我们的回滚策略,建立在三个坚实的基础上:原子性、可追溯性、自动化

  • 原子性:回滚操作必须是“全有或全无”的。我们不采用“修改Deployment的镜像标签”这种半吊子做法,因为这可能导致部分Pod运行新镜像,部分Pod运行旧镜像,造成状态不一致。我们的标准回滚命令是:

    kubectl rollout undo deployment/my-model-service -n production --to-revision=12

    其中--to-revision=12指定了要回滚到的历史修订版本号。K8s会原子性地将所有Pod的镜像、配置、环境变量,全部恢复到revision 12时的状态。整个过程,和一次正常的滚动更新完全一样,平滑且无感。

  • 可追溯性:K8s的rollout history功能,是我们回滚的“导航仪”。执行`kubectl rollout history deployment/my-model-service

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询