一次 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,因为周度汇总日志量本身就大,主模型处理时间最长。但它已经从"必挂"变成"偶尔",可以接受。
复盘:下次怎么提前防
- 新任务上线前先看时间表——不是"加个 cron"就完事,先看那个时段有没有别的任务,避免扎堆
- fallback 要配运行检查——配了 fallback 不等于 fallback 能工作,前提条件要先验证
- 连续失败 ≥2 次 = 系统问题——不要容忍"就 5 个任务挂了",那是雪崩前兆
- 写检测脚本前先测检测脚本自己会不会挂——429 检测任务自己 429,这件事我花了半天才想明白
2026-08-31
浙公网安备 33010602011771号