返回 文章 apply CMS 文章

金丝雀发布:在生产中无停机升级模型

用金丝雀发布安全升级生产模型:分阶段迁移流量,指标门控自动暂停,人工决定回滚。

模型部署金丝雀发布生产环境无停机升级
成长分 / 100 74 综合收获、行动、留存与影响

金丝雀发布:在生产中无停机升级模型
为什么值得读了解如何在不中断服务的情况下将生产流量从旧模型迁移到新模型,避免硬切换的风险。

学习指标门控(如 p95 延迟回归)如何自动检测问题并暂停发布,将爆炸半径控制在金丝雀流量比例内。

关键洞察
  1. 金丝雀发布通过分阶段流量迁移(如 10% → 50% → 100%)和指标门控,在每一步后检查目标与源的性能差异,若回归超过阈值则自动暂停。
  2. 发布状态机包含 PENDING、PAUSED、SYSTEM_PAUSED、COMPLETED、CANCELED 等,不存在 FAILED 终态,流量不会悬而未决。
  3. 指标门控基于路由器侧指标(router_error_rate、router_latency、inflight_requests),支持相对回归检查和绝对阈值检查。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:18453

摘要

滚动发布通过分阶段步骤将实时流量从当前模型迁移到新检查点。流量迁移前始终运行健康检查;在金丝雀发布中,你还可以添加指标门控(例如 p95 延迟或错误率),在每个步骤后运行。如果门控触发,滚动发布会在金丝雀流量比例处暂停,你可以取消它并反向运行。下面我们实际运行一次:一个 Qwen2.5-7B → Qwen3.5-9B 的金丝雀发布,其门控在 10% 流量时捕获到 137% 的 p95 回归;我们取消了它并反向运行,期间实时请求得到服务且无一失败。

更换模型是常见做法

如果你在生产环境中运行模型,你已深知需要换入新检查点或新模型系列:开放模型生态发展迅速,候选模型通常在评估中表现优异,或承诺更好的吞吐量。你希望它无故障地服务于每个用户,并在令人失望时能够回滚。通常的选项迫使你做出权衡:

硬切换:将端点指向新模型,所有用户同时暴露。如果 p95 翻倍,你会从仪表盘或更糟——从客户那里得知,然后在压力下回滚到冷启动的旧模型。DIY 分阶段切换:第二个部署加上一个脚本,在你盯着 Grafana 时调整流量百分比,并记得在将流量切回之前将旧部署扩容回来。

这两种选项都将人置于循环中作为安全机制。滚动发布将该机制移入平台:你描述源、目标、步骤以及“健康”的含义,平台在每个阶段按照此计划工作。

滚动发布如何工作

滚动发布在同一端点上的两个部署之间迁移流量:一个(当前正在服务的)和一个目标(你明天想要服务的)。你可以选择三种策略之一:

金丝雀:流量按你定义的分阶段百分比移动(例如 10% → 50% → 100%;默认阶梯是 5% → 25% → 50% → 100%),步骤之间有等待期和可选的指标检查。蓝绿:一次门控的 0% → 100% 切换。你可以将其视为单步金丝雀。滚动:原地、逐个副本的替换,保持总容量。在容量受限时最佳,特别是对于不需要流量爬坡的同模型配置更改。

以下是每个金丝雀步骤内部发生的情况:

我们有意选择了这种顺序;每一项都防止一类事件:

目标扩容新部署没有容量意味着没有重定向的请求:滚动发布首先停驻。任何流量移动之前。健康门控在流量切换之前运行。流量只到达引擎已加载并响应的副本,而不仅仅是已启动的副本。传播等待位于切换和排空之间。路由缓存在任何源容量被移除之前收敛。源排空容量在上升时领先流量;流量在下降时领先容量。流量移动之后。等待期和指标门控在步骤被记录为完成之前。一个出现回归的步骤永远不会被标记为通过。

通过 API 或控制台,滚动发布创建时处于 PENDING

状态,在你明确启动之前不执行任何操作(CLI 的 rollout

命令一步完成创建和启动)。这种创建/启动两步式设计是有意为之,因为你可以先创建 rollout、审查它(或让队友审查),然后在真正盯着它的时候再启动。

上图中的两个状态值得说明:

表示PAUSED

按下了暂停。rollout 会精确停在当前位置,并从同一步恢复。表示SYSTEM_PAUSED

平台发现了问题,例如指标门禁失败、容量不足或指标缺失,于是停下来等待人工批准。它会暂停、通知你并等待;取消始终是的决定。

不存在会让流量悬而未决的 FAILED

终态:rollout 要么以 COMPLETED

结束(目标开始服务),要么以 CANCELED

结束(分流冻结在当时的比例,你反向运行 rollout 即可回退)。

单步剖析

以下是对单个金丝雀步骤中所发生情况的拆解,基于本文末尾那次运行(Qwen2.5-7B → Qwen3.5-9B,各用一张 H100)测得。

传播等待是为了防止陈旧的全局路由缓存把请求发送到正在缩容的源端。等待时长会增长到指标窗口加上数据摄取延迟。冷启动主导了第一步;后续步骤向一个已经在服务且已预热的目标准入添加副本。

一览式策略选择

三种策略都通过同一引擎和同一健康门禁运行;它们的区别在于流量如何迁移、重叠需要多少额外容量,以及是否存在用于指标门禁的等待窗口。

创建 rollout

下面是一个三步金丝雀,从服务你当前模型的部署过渡到服务候选模型的部署,并带有延迟回归门禁。CLI 以 tg

的形式随 together

Python 包(2.34.0 或更新版本)提供。你传入目标部署;当恰好有一个部署在接收流量时,源端会被推断出来,否则传入 --source

源端会缩容到零副本,并在 rollout 完成时停止(--final-source-replicas

默认为 0),目标端则以源端的副本数作为其下限落地(--final-target-replicas

)。CLI 为每个 rollout 附加一个指标门禁;如需多条规则,请使用控制台或 API。

通过 REST API 的同样操作,其中创建和启动是分开的调用,且一个 rollout 可以携带多条指标规则:

API 严格要求的几点:percentile

是整数(95,而不是 "p95"),枚举值带有完整前缀(METRIC_STAT_TYPE_*

REGRESSION_DIRECTION_*

THRESHOLD_OPERATOR_*

),时长是 protobuf 字符串,如 "600s"

,而目录之外的指标名称会被拒绝,返回 400 并列出支持的名称。排空源端是默认行为,因此无需为其传入任何参数。

回归检查可以这样理解:在每个门禁处,将目标端过去 5 分钟的 p95 路由器延迟(在路由器处测量的每请求时长,以毫秒计)与源端进行比较。如果目标端差于 10% 以上,则不要继续。

Python SDK

用 Python 通过 together

包(2.34.0 或更新版本)执行同样的 rollout。这里的字段名是 snake_case,而传输时是 camelCase;SDK 会进行转换。

控制 rollout

每次发布都接受相同的四个控制项。一个端点最多只能有一个活跃的发布,因此 CLI 只需要端点 ID,你很少需要用到发布 ID。每个控制项在被接受后立即返回;轮询 tg beta endpoints get

(或 GET 端点)直到发布达到你预期的状态。当发布处于活跃状态时(包括暂停期间),端点的流量拆分会被锁定,其源和目标无法被停止或删除。

对已完成的发布(COMPLETED

CANCELED

)的控制会被拒绝。使用 tg beta endpoints rm $ROLLOUT_ID

从历史记录中删除已完成或从未启动的发布;删除记录不会改变它留下的流量拆分。

底层机制:配置门控

指标门控

指标门控是一项金丝雀功能:蓝绿和滚动发布仍会运行健康门控,但分阶段的指标比较需要金丝雀的步骤结构才有意义。门控基于三个路由器侧指标的封闭目录进行评估,对源和目标以相同方式测量(任何其他指标名称在创建时都会被拒绝):

router_error_rate

:路由器 5xx 响应除以所有推理响应,以 0-1 的比例表示(0.02 表示 2%)router_latency

:在路由器处测量的每请求持续时间,以毫秒为单位。它是双峰的(中位数尝试通常是快速拒绝),因此应基于p95

或更高值而非均值进行门控inflight_requests

:每个就绪副本的并发请求数,在窗口内取平均值(阈值按副本大小计算,而非整个集群)

每条规则使用两种检查之一:

regressionCheck

相对):“目标不得比源差超过 N%。”这是延迟的合适默认值,因为它能自我校准:你不需要知道绝对的 p95,只需要知道新模型不应使其退化。设置direction

以便平台知道哪个方向更好/更差。(thresholdCheck

绝对):“目标必须满足operator, value

”(例如错误率< 0.01

)。当你拥有硬性 SLO 时,或者当源本身可能不健康而相对比较会按曲线评分时,使用此检查。

两个配方覆盖了大多数服务:

时间窗口

三个持续时间相互作用,因此请牢记所有三个:

(默认 5 分钟):门控在比较指标时回溯的时间。window

(默认 3 分钟):每个步骤在其流量水平上等待多长时间后门控运行。stepInterval

指标摄取延迟(约 90 秒):从请求被服务到其数据点可查询之间的时间。

你必须等待至少 窗口 + 摄取延迟 的时间,以便门控的整个回溯期落在当前步骤的稳态内。如果你等待的时间短于窗口,门控将比较部分描述

先前流量拆分的指标。平台会为你强制执行这一点:如果你请求的等待时间对于你的窗口来说太短,它会自动增加。但最好在设计时就考虑到这一点:5 分钟的窗口需要 6.5 分钟的等待时间。使用默认的 5 分钟窗口时,平台会将默认的 3 分钟间隔增加到 390 秒(6 分 30 秒);如果你设置自己的

stepInterval

,请使其至少为 window + 90s

。### 回归时会发生什么

默认情况下,触发的门控会路由到 SYSTEM_PAUSED

,这意味着系统暂停以供审查。发布保持在其当前拆分(爆炸半径保持在你金丝雀百分比的水平),由你决定:恢复(门控重新评估)、提升或取消。

不存在自动中止:一旦确认出现回归,系统总会暂停发布,交由人工处理,因为将流量切回本身就是一项应当有人盯着的变更。可恢复的原因则不同,例如容量不足或指标管道缺口:平台会每 15 分钟重试一次,最多持续 3 小时,之后才会将发布保持暂停状态等待你处理。平台还会防范误报:在因回归而暂停之前,系统会在约 90 秒内多次重新查询,以确保看到的不是数据摄取延迟或瞬时波动;而无法获取可信数据的门控会以 METRICS_UNAVAILABLE 暂停,而不是计为回归。

平台保证什么

三种策略都运行在同一个步骤引擎上,因此以下保证对金丝雀、蓝绿和滚动发布同样适用。

1. 容量永远不会被向下取整。 目标副本数向上取整,源副本数排空时向下取整,因此等量替换的副本数永远不会少于起始数量。滚动发布在步骤中途增加一个副本;蓝绿发布会短暂地让两个部署都以完整规模运行。自动扩缩器在计划之外增加的副本会被保留。

2. 流量永远不会落到尚未就绪的容量上。 每个步骤都按同一顺序执行:扩缩目标、检查健康、切换流量、等待 30 秒让路由收敛、排空源、等待、评估门控、记录步骤。在等待期间发生回归的步骤永远不会被记录为通过。

3. 每一侧始终拥有其份额所需的副本数。 只有当目标拥有足够就绪副本以承接新份额时,流量才会切换;源也永远不会被排空到低于其剩余份额。如果某个副本死亡且无法再按该拆分比例提供服务,发布将保持其能够服务的最大拆分比例,并以 UNDER_SERVED 暂停。

4. 发布抬高下限;它不会与你的自动扩缩器对抗。 每个步骤只写入每个部署的最小副本数,不写其他内容,只有一个例外:目标的最大副本数会被抬高一次,以便承载整个端点,并保持抬高状态。源的最大副本数在排空期间随其份额缩减。如果将最大副本数降到低于该步骤所需,发布将以 POLICY_INFEASIBLE 暂停,而不是覆盖你的设置。

5. 门控总会给出裁决。 当目标在源的一定百分比预算范围内时,回归检查通过。没有源数据则通过;在越高越差的指标上,源为零而目标非零则失败;其他任何源为零的情况都通过。阈值检查忽略源,将目标与你的值进行比较。

6. 门控只读取当前步骤的流量,且只读取足够的部分。 等待时间至少为指标窗口加上约 90 秒的数据摄取延迟,因此 300 秒的窗口意味着 390 秒的等待。p95 需要窗口内有 20 个请求,p99 需要 100 个;少于这个数量,发布将以 METRICS_UNAVAILABLE 暂停。错误率和进行中请求需要一个。

边缘情况

1. 如果目标没有 GPU 容量怎么办?

发布会在开始前、在触碰任何东西之前,对整个过程进行可行性检查,并在每次扩容时再次检查。容量不足会以 SYSTEM_PAUSED 暂停发布,并带有 CAPACITY_EXHAUSTED 类别。此时尚未发生任何变动,你的源也未受影响。恢复会重新检查容量,如果容量已释放则继续。容量问题通常是暂时的,因此暂停胜过失败。

2. 我可以无限期暂停吗?

是的。暂停并不是保持连接,而是一种一等状态。Rollout 被设计为能够承受多天的暂停,并在中断处精确恢复。

当平台暂停一个 rollout 时,status.condition

携带一个类型化的失败类别以及人类可读的 message

。这些包括:

文档列出了其余类别以及每个类别的处理方法:Rollout 故障排除

真实 rollout 演示

在亲眼看过一次实际运行后,上面的一切会更容易让人信服,所以这里展示一次在当前平台(2026 年 9 月)上的运行。我们将一个线上端点从 Qwen2.5-7B-Instruct 升级到 Qwen3.5-9B,每个模型都运行在单张 H100 上,同时整个过程中端点持续承载稳定的每秒 5 次聊天补全请求,并且记录了每个响应代码。较新的模型是我们想要的;rollout 要回答的问题是,它是否符合旧模型设定的延迟预算。我们给了它 25% 的 p95 预算。

设置。 7B 已经在提供服务。我们将 9B 作为同一端点上的第二个部署加入,没有流量也没有副本;rollout 在需要时启动它。

开始。 一条命令创建并启动金丝雀:10% → 50% → 100%,并在每一步之后通过一个门控,在 5 分钟窗口内比较目标的 p95 路由器延迟与源的 p95 路由器延迟。

按时间记录发生了什么(自 rollout 开始以来的时间):

+3:57 9B 完成冷启动,通过健康检查,10% 的请求开始落到它上面。rollout 等待 30 秒让路由收敛,排空 7B 对应的份额,然后进行浸泡。我们将步骤间隔保留为默认值,因此平台将其增长到 390 秒,以覆盖 300 秒窗口加上摄取延迟。+11:00 门控完成评估并触发。9B 的 p95 路由器延迟为 1,740 毫秒,而 7B 为 734 毫秒,相对于 25% 的预算出现了 137% 的回归。rollout 进入 SYSTEM_PAUSED

,仍有 10% 的流量在目标上,且没有任何东西被拆除。这就是 tg beta endpoints get $ROLLOUT_ID --json

返回的内容(值以毫秒为单位):

决定。 回归是真实的,不是短暂波动:9B 是一个推理模型,并且在相同的 max_tokens 下,每个请求会生成更多内容。这是一个产品决策,而不是可以通过恢复来绕过的事情,所以我们取消并回退了。

+11:09 CANCELED

。分流在命令发出后 0.2 秒内冻结在 90/10。+17:00 反向 rollout 完成,在开始后 5 分 49 秒:100% 的流量回到 7B,9B 排空到零并停止。其中大部分时间用于第二个 7B 副本冷启动,因为取消后默认的最终副本数是这一对副本数的总和。

探针在整个运行过程中的结论,包括切换、暂停、取消和反向:6,800 个请求,0 个非 200 响应

审计追踪。 上面的每一步都在端点的事件流中,可按 rollout ID 过滤:

20:39:45 rollout.created 金丝雀发布已创建:dep_src → dep_tgt,分 3 步达到 100%
20:39:45 rollout.started 发布已启动:第 1 步,共 3 步,目标流量 10%
20:43:42 rollout.traffic_shifted 0% → 10% 目标流量:0% → 10%
20:50:45 rollout.system_paused 在第 1 步(共 3 步)自动暂停:一项指标检查失败
20:50:54 rollout.canceled 已请求取消;流量将冻结在当前分流比例
20:50:54 rollout.canceled_complete 已取消:流量冻结在 90%/10%(源/目标)

7 月的一次更早运行,使用了一个故意设为不可能达到的阈值门控,产生了相同的形态:在 10% 流量下触发,1,198 个探测请求在整个恢复过程中零错误。

自己动手试试!

1. 在一个端点上进行两次部署。 将你当前的部署保留为源,并将候选部署作为目标添加,流量为零。pip install -U together

(2.34.0 或更新版本)会为你提供 tg

CLI:

2. 创建并启动一个金丝雀发布,使用默认阶梯(5% → 25% → 50% → 100%)和一个 router_latency 回归门控:

3. 观察它,使用 tg beta endpoints get $ENDPOINT_ID

(或控制台中该端点的 Rollouts 标签页),看它逐步推进。使用 tg beta endpoints rollout $ENDPOINT_ID --pause | --promote | --cancel 暂停、提升或取消它

在整个过程中,端点 URL 和你的客户端保持不变;只有它们背后的模型在移动。

📚 文档: 启动一次发布 · 使用指标对发布进行门控 · CLI 参考 · API 参考:创建一次发布