跳到正文

上线后的运行机制:定时、排空、回滚与切换 ​

一句话:发布成功只是开始。真正决定系统稳不稳的,是四件每天都会发生的事:定时任务能不能补跑、进程退出会不会丢掉正在跑的任务、指标变坏时能不能自动退回上一版、知识库换版本时能不能不停机切换。

适用:2026 年在线 Agent 服务。文中的时间窗与比例是起点值,需按任务时长与流量规模校准。

为什么需要 ​

治理与降级回答“出事时退到哪”,这一篇回答“日常怎么不产生事故”:

  • 定时任务被当成普通请求跑,整点把模型打满;错过一次执行就永远不补;
  • 发布时直接杀进程,正在执行的会话被腰斩;指标变坏靠人发现、靠人回滚,恢复时间取决于谁在看群;
  • 知识库换版本用“删了重建”,切换窗口内检索为空或新旧混杂。

这四件事的共同点是:它们都有明确的状态和时间边界,可以用程序保证,不该靠人盯着。

核心机制 ​

一、定时任务:调度、补跑与零 Token 巡检 ​

定时任务不是“到点发一次请求”,而是三件事分开:

  1. 调度:任务定义存数据库或配置中心,调度器周期扫描到期任务;多副本部署时用分布式锁或选主保证同一时刻只有一个实例触发。
  2. 补跑:进程停机、发布、机器故障都会错过触发。要定义错过策略——立即补一次、跳过并记录、还是只补最近一次;三者对应不同业务语义,必须显式配置,不能默认“丢了就丢了”。
  3. 执行:真正耗时的工作交给任务队列,调度器只负责“到点投递”,不阻塞在业务里。

一个常被忽略的成本点:每次 tick 都调用模型做判断,一天 2,880 次 tick(30 秒一次)就是 2,880 次调用,而其中绝大多数没有到期任务。正确做法是先用数据库或内存判断“有没有事”,有事才进模型;健康巡检、状态对齐这类高频动作要设计成零 Token 路径。

重试要有退避和上限:固定间隔重试会在下游故障时形成风暴,指数退避加抖动(例如 30 秒起、倍增至 1 小时、±25% 抖动)能把重试打散;连续失败到上限后转死信队列,等人处理。

二、优雅关闭:给在途任务一个收尾窗口 ​

容器收到 SIGTERM 后默认还有一段宽限期,但应用不做处理,进程会直接退出。正确顺序是:

text
SIGTERM → 立即拒绝新请求 → 从负载均衡摘除 → 等待在途会话
→ 到截止时间前写 Checkpoint → 释放资源 → 退出

每一段都要有预算。例如总宽限期 35 秒:0 秒开始拒新请求,0–5 秒等摘流生效(注册中心与网关有传播延迟),5–25 秒等在途任务自然结束,25 秒把未完成的写 Checkpoint,30 秒强制退出,留 5 秒给内核回收。

顺序不能反:先摘流再拒请求,会有一小段时间请求仍被转发进来;先写 Checkpoint 再停新请求,检查点会不断被新任务改写。宽限期必须大于“摘流延迟 + 最长单步耗时”,否则每次发布都会产生一批半成品。

三、自动回滚:指标、窗口与整包回退 ​

自动回滚不是“错误率超过阈值就回滚”,而是三个条件同时成立:

条件作用典型起点
指标P95 延迟、错误率、单位成功成本、用户负反馈延迟 2 倍、错误率 3 倍、成本 1.5 倍
窗口持续多久才算真异常3–5 分钟,短于一次抖动
样本量小流量下比例指标不可信至少几百次请求

只有比例没有样本量,灰度 1% 时一次失败就会被算成 100% 错误率,触发误回滚;只有窗口没有基线,业务高峰的延迟上升会被当成故障。所以回滚规则要带“对比基线”——和上一版本同期比,而不是和固定阈值比。

回滚必须是整包:提示词、模型版本、工具描述、配置开关、索引版本一起退。只回退代码不回退提示词,会出现“旧代码 + 新提示词”的混合态,问题更难查。发布单元因此要以“可整体回退”为边界设计。

还要防抖动:同一版本在短时间内反复回滚会放大故障。常见做法是限制发布频率(例如每天不超过一次自动回滚触发),并让下一次发布必须先通过上一轮的失败用例。

四、知识库切换:影子索引与零窗口 ​

检索系统的版本切换最怕两件事:切换瞬间检索为空,以及新旧索引混用导致答案口径不一致。稳妥做法是五步:

  1. 建新索引:新版本写到独立索引,不影响线上;
  2. 影子流量:把一部分查询同时打到新旧索引,只比较不返回;
  3. 验证:对比召回率、Top-K 重合度、关键问题命中、延迟;设定通过线;
  4. 切换:改别名或指针,原子生效;回滚就是指针改回去;
  5. 延迟清理:旧索引保留一段时间再删,防止发现问题时无路可退。

关键在“别名切换”而不是“删了重建”:别名切换是 O(1) 的指针变更,删了重建有分钟级空窗。影子阶段要控制成本——只抽样一部分流量,且影子结果不进入用户响应。

五、限流、排队与隔离:把过载挡在模型之外 ​

降级解决“已经坏了怎么办”,限流解决“别让它坏”:

手段防什么要点
令牌桶 / 漏桶瞬时突发打满下游桶容量决定允许多大的突发,速率决定长期均值
并发上限慢请求占满连接与显存按实例设,不按全局设
排队直接把超额请求打成失败队列要有长度上限和等待超时,否则延迟无限增长
优先级队列关键业务被批处理挤掉按租户/任务类型分级,低优先级可被抢占
舱壁隔离一个租户拖垮全部按租户或任务类型分池,池之间不共享并发

熔断要写清三个参数:统计窗口(例如 10 秒)、触发条件(错误率或连续失败数)、半开策略(放多少探测请求、成功几次恢复)。没有半开,熔断后永远不知道下游有没有恢复;半开放太多,等于没熔断。

图里三件事必须物理分开:在线请求、定时任务、关闭流程。在线路径要抓的是回滚判定——指标越线就整包回退,不做局部修补;关闭路径要抓的是顺序——拒新与摘流必须在等待之前,否则“等在途”等的是源源不断的新请求,永远等不完;Checkpoint 必须在退出之前,否则能续跑的任务也变成要重跑。定时任务和在线请求共用一个队列,整点任务会把用户请求挤到超时。

实战场景 ​

场景一:一次发布腰斩 214 个在途会话,37 个带着副作用 ​

现象:某周二 19:30 业务高峰滚动发布,容器收到 SIGTERM 后应用不做处理直接退出——代码里根本没有关闭钩子。214 个在途会话被腰斩,其中 37 个已经创建了工单但后续步骤没跑完,需要人工和业务系统逐条对账,事故处理共 4 小时 20 分(示例数字,经验值,需校准)。

排查过程:按“拒新请求 → 摘流 → 等在途 → Checkpoint → 退出”逐段核对,发现三个断点:没有拒新与摘流动作,网关在整个终止期间继续向这个 Pod 转发;没有在途等待窗口,进程零收尾;没有 Checkpoint,5–25 秒本可自然结束的任务和只能续跑的任务一起丢。再看任务画像:最长单步工具链 60 秒,超过 drain_until 的 25 秒——这类任务本来就只能靠检查点续跑,等是等不来的。

对症做法:排空预算显式配置,宽限期必须大于“摘流延迟 + 最长单步耗时”,超出部分交给 Checkpoint 与幂等键:

yaml
shutdown:
  grace: 35s
  stop_accepting_at: 0s     # 立即拒新请求
  drain_until: 25s          # 0–5s 等摘流生效,5–25s 等在途自然结束
  checkpoint_at: 25s        # 未完成的落检查点
  force_exit_at: 30s        # 留 5 秒给内核回收

顺序不能反:先摘流再拒请求会把请求转进正在死的 Pod;先写 Checkpoint 再停新请求,检查点会被新任务不断改写。副作用操作(建工单、扣款)全部带幂等键,续跑与补跑都不会重复执行。SIGTERM 演练进预发例行动作,验收固定三条:

  1. 拒新后,网关侧该实例进入请求数在 5 秒内归零;
  2. 在途会话数在 drain_until 内归零,未归零的在 checkpoint_at 全部落检查点;
  3. 新 Pod 从检查点续跑且业务系统里查不到重复工单;SIGTERM 到进程退出的实际耗时与预算对齐,超出即重审宽限期。

复盘要点:代码里写了关闭钩子不算数,演练证明检查点真能落才算数;发布窗口避开高峰不是礼节问题,是在途基数——19:30 发布丢 214 个会话,凌晨同样配置丢不到 20 个(经验值,需校准)。

场景二:知识库切换当天,一半答案新、一半答案旧 ​

现象:月初结算周,人事知识库从 v12 切 v13(请假扣款规则变更)。运维按老办法删旧索引再重建:14 分钟重建窗口内部分实例检索为空;恢复后各实例本地缓存仍指向旧索引,又出现约 6 小时“一半实例答新规则、一半实例答旧规则”。当天约 430 条会话引用了过期扣款口径,显式差评与转人工 37 次(示例数字,经验值,需校准)。

排查过程:靠 Trace 里可关联的检索结果字段,把每条错误答案回溯到“命中了哪个索引版本”,确认两段时间是两种故障:空窗是删了重建造成的,混用是别名变更没传播到实例缓存。本质是版本对齐没做——索引版本没有随发布单元整体切换,实例缓存也没有失效机制。

对症做法:切换走五步——建新索引、影子流量、验证、别名原子切换、延迟清理。影子阶段按 shadow_sample: 0.05 抽样,影子结果不进用户响应;验证设通过线,四项全达标才允许切:

验证项通过线(经验值,需校准)不达标动作
召回率v13 不低于 v12 同期修数据后重跑影子
Top-K 重合度非口径变更文档 ≥ 0.9差异集逐条归因,不放行
关键问题命中请假扣款 12 个关键问题全对阻断切换,人工核对
检索延迟P95 与旧索引持平扩容或调分片后重验

切换与回滚都只改指针;keep_old_for 的起点值 24h 覆盖不了结算场景,按“保留期覆盖一个完整业务周期”上调到月结结束后再删。别名变更后强制刷新实例侧缓存,指针与缓存两层都对齐才算切完。

复盘要点:索引版本必须同属发布单元的整包,自动回滚回退 scope: bundle 时才带得动 index;“删了重建”在有别名的系统里没有任何存在理由,它制造的不是风险而是双重风险——空窗加混用。

场景三:整点批任务打满实例池,自动回滚空转 38 分钟 ​

现象:月初结算期,财务对账批任务每天 09:00 投递 1,800 条,与在线会话共用一个队列和同一批实例。每到整点,在线 P95 从 2.1 秒升到 19 秒并持续 25 分钟,排队超时的在线请求直接失败。对账日当天,值班触发自动回滚,整包退回上一版后 38 分钟指标依旧翻倍——回滚白做,真正定位到根因已是当天下午(示例数字,经验值,需校准)。

排查过程:回滚是被误触发的:P95 对上一版本同期超 latency_ratio: 2.0、窗口 5 分钟、样本量超 min_samples: 300,三个形式条件都成立,但没人问过“流量结构变了没有”——对账批任务是本月新上的,上一版本同期基线里根本没有这部分流量。回滚后指标不恢复是最强的归因信号:问题不在版本,在容量。把池占用指标按任务类型拆分,09:00 的并发几乎全部来自定时任务;把批任务改成小批发投递并错峰开跑后,整点峰值当场消失,反向验证了容量归因。

对症做法:在线请求、定时任务、关闭流程三件事物理分开,配置落在同一份可审计的文件里:

yaml
schedule:
  scan_interval: 30s
  misfire: fire_once
  retry: {base: 30s, max: 1h, jitter: 0.25, dead_letter_after: 5}
  batch: {size: 50, emit_interval: 90s, start_jitter: 15m}   # 错峰小批发,经验值,需校准
isolation:
  queues: [online, scheduled]              # 独立队列
  concurrency_per_instance: {online: 8, scheduled: 2}   # 按实例设,不按全局设
  queue: {max_length: 200, wait_timeout: 10s}           # 有上限有超时,不做无限排队
  priority: scheduled_preemptible          # 批处理为低优先级,可被在线抢占

高频巡检走零 Token 路径,30 秒一次的 2,880 次 tick 里绝大多数不进模型;重试带指数退避加抖动,连续失败 5 次转死信队列,不在下游故障时形成风暴;熔断三个参数写全——统计窗口 10 秒、错误率或连续失败数触发、半开放 1 个探测请求且成功 3 次才恢复(探测规模经验值,需校准)。

复盘要点:自动回滚判定要在指标侧加流量结构维度,把容量事件和版本事件分开——同期基线里没有的批任务,不是上一版本的问题;回滚记录必须写“为什么回滚”,否则下一次发布照样在整点被同一个指标骗一遍。

最小上手 ​

yaml
schedule:
  scan_interval: 30s
  misfire: fire_once          # fire_once | skip | catch_up
  retry: {base: 30s, max: 1h, jitter: 0.25, dead_letter_after: 5}
shutdown:
  grace: 35s
  stop_accepting_at: 0s
  drain_until: 25s
  checkpoint_at: 25s
  force_exit_at: 30s
rollback:
  metrics: {latency_ratio: 2.0, error_ratio: 3.0, cost_ratio: 1.5}
  window: 5m
  min_samples: 300
  scope: bundle               # prompt + model + config + index
  max_auto_rollbacks_per_day: 1
switch:
  mode: alias                 # alias | blue_green
  shadow_sample: 0.05
  keep_old_for: 24h

验收看四件事:停机发布时没有会话被腰斩;错过的定时任务按策略补跑且只补一次;灰度期间指标变坏能在窗口内自动退回;索引切换没有空窗,回滚只需改指针。

生产动作 ​

  • 发布单元按“可整体回退”切:提示词、模型、配置、索引同属一个版本,回滚时一起退;关闭流程要定期在预发发一次 SIGTERM 演练,确认在途任务真能落检查点,而不是只看代码里写了。
  • 定时任务与在线流量分池:独立队列、独立并发配额,避免整点洪峰。
  • 回滚指标带基线:和上一版本同期比,不用绝对阈值;同时记录“为什么回滚”,供下次发布参考。
  • 索引切换留退路:旧索引保留期要覆盖一个完整业务周期(例如月末结算),别当天删。

边界 ​

  • 小规模不必全自动:日请求量很低时,人工看指标回滚更可靠,自动回滚的误判成本更高。
  • 优雅关闭受限于单步时长:一个工具调用要 60 秒,35 秒宽限期就只能靠检查点续跑,不能指望自然结束。
  • 影子索引有成本:双写双查会翻倍检索资源,只适合关键知识库且要抽样;限流同样牺牲体验,排队与拒绝都是坏体验,区别只是“可控的坏”与雪崩,阈值要按业务分层设。
  • 补跑可能重复副作用:补跑必须配合幂等键,否则“补一次”会变成“扣两次款”。

讲清楚 ​

上线后的稳定性由四件日常机制决定。定时任务要分开调度、补跑和执行,高频巡检走零 Token 路径,重试带退避与死信;关闭要按“拒新请求→摘流→等在途→写检查点→退出”的顺序,每段有预算;回滚要同时满足指标、窗口和样本量,并且整包回退,防止旧代码配新提示词;知识库切换用影子索引验证、别名原子切换、旧版本延迟清理。

再往前一层是限流、排队、舱壁和熔断:它们决定系统在过载时是排队还是雪崩。熔断必须带半开探测,否则永远不知道下游恢复没有。

这四件事都不难写,难在把它们当成一等机制:有配置、有演练、有指标、有 owner。缺任何一项,都会在某个整点或某次发布时以事故的形式补课。

适用:2026 年在线 Agent 服务 | 初稿 2026-10

把 AI 工程讲到既懂也会用