一次 429 限流事故排查记录:43 个定时任务里 5 个挂了

写这篇的时候是个周二的凌晨,我在翻上一个周期的任务输出,发现心跳检测那个任务已经连续第 12 天返回 HTTP 429。而上周我刚好给它加了个"自动降级"的脚本,专门用来对付这种情况。

最讽刺的是——那个负责检测 429 的任务,它自己也在 429。

这篇是当时的真实排查记录。里面有我判断对的地方,也有后来被证明错的地方。


现场情况

一套部署在 Windows 上的自动化系统,一共 43 个 cron 定时任务,主模型走的是云端 API,兜底方案是本地 ollama 上的一个小模型。

某次巡检,任务状态是这样的:

Name                          Schedule         Last Run
────────────────────────────────────────────────
心跳检测                     0 3 * * *         error: HTTP 429
自动学习循环                */30 0-23 * * *    error: HTTP 429
日报生成                    0 18 * * 1-5      ok
429降级检查                 0 */4 * * *        error: HTTP 429   ← 讽刺
周度汇总                    0 9 * * 1          error: HTTP 429
...(其余 38 个任务)                  ok
────────────────────────────────────────────────
总计:38 ok / 5 error = 86% 健康

第一个错觉:我当时觉得,"就 5 个任务挂了,86% 健康率,可以接受"。

过了三天再查,5 个变成了 7 个,失败率拉到 16%。这才反应过来——这不是偶发,是系统性问题在扩散。


拉出 128 次 429 的时间分布

失败日志散落在每个任务的输出目录里,每份都带时间戳。写了个小脚本扫了一遍:

# scan_429.py
import re, glob, os
from collections import Counter

hour_counter = Counter()
job_counter = Counter()

for f in glob.glob('~/hermes/cron/output/*/*.md'):
    txt = open(f, encoding='utf-8').read()
    if '429' not in txt and 'rate limit' not in txt.lower():
        continue
    for m in re.finditer(r'(\d{2}:\d{2})', txt):
        hour_counter[m.group(1)[:2]] += 1
    name = os.path.basename(os.path.dirname(f))
    job_counter[name] += 1

print("=== 按小时 ===")
for h, c in sorted(hour_counter.items()):
    bar = '█' * c
    print(f"  {h}:00 → {c:3d}  {bar}")

print("\n=== 按任务 Top5 ===")
for j, c in job_counter.most_common(5):
    print(f"  {c:3d}  {j}")

跑出来:

=== 按小时 ===
  03:00 →   4  ████
  07:00 →  20  ████████████████████
  08:00 →  27  ███████████████████████████  ← 峰值
  21:00 →  13  █████████████

=== 按任务 Top5 ===
   50  心跳检测
   18  429降级检查    ← 讽刺
   12  周度汇总
    8  自动学习循环
    5  日报生成

第二个错觉:我第一反应是"07-08 时集中了 47% 的错误,肯定是模型 API 那个时段流量高"。

这个判断是错的。真正原因在后面。


拆时间轴:是不是撞车了

cron list 完整列表打开,看 07-08 时这个窗口里排了多少任务:

  • 07:00 心跳检测
  • 07:00 自动学习循环(每半小时一次,0-8 时)
  • 07:30 自动学习循环
  • 07:00 429降级检查
  • 07:30 429降级检查
  • 08:00 日报生成
  • 08:00 周度汇总(周一时)

7 个任务挤在同一个小时窗口里。而主模型的调用频率额度是共享的,不是每个任务各一份配额。

换句话说——不是模型那个时段流量高,是我自己把那个时段的调用量堆到了额度上限。

这个认知一翻过来,修复方向就很清楚了:错开时间


检查兜底链路

429 降级检查那个任务的配置,期望行为是"主模型 429 时自动切到本地 ollama":

fallback_providers:
  - name: ollama
    model: qwen3:1.7b
    endpoint: http://localhost:11434/v1

理论上主模型挂了,fallback 到本地就该跑通。实际查了一下:

$ tasklist /FI "IMAGENAME eq ollama.exe"
INFO: 没有运行的任务匹配查询标准。

ollama 根本没在跑。所以 fallback 压根触发不了——配置是对的,但运行前提不存在


三个修复一起上

修复 1:错开时间

改前:
  07:00  心跳检测
  07:00  自动学习循环
  07:00  429降级检查
  08:00  日报生成
  08:00  周度汇总

改后:
  03:00  心跳检测            ← 凌晨流量低
  10:00  自动学习循环        ← 避开07-08
  11:00  429降级检查
  18:00  日报生成
  09:00  周度汇总(周一)

改完再扫 07-08 窗口:0 个任务。

修复 2:加 ollama 守门脚本

fallback 能不能用,得先看 ollama 在不在。写了个极简 wrapper:

#!/bin/bash
if ! tasklist /FI "IMAGENAME eq ollama.exe" 2>/dev/null | grep -q ollama; then
    echo "ollama 未运行,跳过本轮任务"
    exit 42  # 特殊退出码,标识"非故障跳过"
fi
echo "ollama 运行中,继续"
exit 0

任务前置检查,ollama 不在就不打主模型,直接跳。省一次 API 调用,就少一次 429 风险。

修复 3:给关键任务单独设额度预算

心跳检测是最容易挂的,因为它每天必跑。给它单独设了降级阈值:

model: sensenova-6.7-flash-lite
fallback_providers:
  - name: ollama
    model: qwen3:1.7b
    endpoint: http://localhost:11434/v1
max_retries: 1        # 只重试 1 次,不无限重试
retry_delay: 30       # 秒

重试次数从默认 3 次降到 1 次,失败就立刻 fallback,不拖。


结果

改完观察了一周:

改前:5/43 error (16%)
改后:1/43 error (2.3%)   ← 剩那个是周度汇总,周一 09:00 撞了别的事

没到 0,因为周度汇总日志量本身就大,主模型处理时间最长。但它已经从"必挂"变成"偶尔",可以接受。


复盘:下次怎么提前防

  1. 新任务上线前先看时间表——不是"加个 cron"就完事,先看那个时段有没有别的任务,避免扎堆
  2. fallback 要配运行检查——配了 fallback 不等于 fallback 能工作,前提条件要先验证
  3. 连续失败 ≥2 次 = 系统问题——不要容忍"就 5 个任务挂了",那是雪崩前兆
  4. 写检测脚本前先测检测脚本自己会不会挂——429 检测任务自己 429,这件事我花了半天才想明白

2026-08-31

posted @ 2026-09-02 05:26  changan2026  阅读(4)  评论(0)    收藏  举报