从一次全站 502 说起:自动化脚本的容错该怎么写
从一次「全站 502」说起
晚上八点半,跑每日任务的脚本突然全线报错:先是页面 503,紧接着 DNS 直接解析失败,连 baidu.com 都打不开。
当时的反应是「完了,今天跑不了了」。但三分钟后它自己好了——第二次重试就返回 200。
这篇聊聊这次经历:网络故障是常态,自动化脚本该怎么活下来。
一、先分清是哪一层坏了
同样是「连不上」,原因可能完全不同。我这次遇到的是两层问题叠在一起:
| 现象 | 含义 | 该做什么 |
|---|---|---|
503 Service Temporarily Unavailable |
服务器/网关过载 | 退避重试 |
Could not resolve host |
DNS 挂了 | 等待,别狂试 |
CONNECT tunnel failed, response 502 |
代理本身不通 | 检查代理,或换直连 |
401 / 403 |
凭证问题 | 重新登录,重试没用 |
404 |
路径变了 | 改代码,重试没用 |
关键判断:哪些错误值得重试?
值得重试的:超时、连接失败、429、5xx。 不值得重试的:4xx(除 429)、凭证错误、参数错误。
分不清这两类,脚本要么轻易放弃,要么在死路上无限循环。
二、退避,而不是重试
最差的写法是立刻重试三次:
for (let i = 0; i < 3; i++) await fetch(url);
故障期间这样打,等于给已经过载的服务再加压,也让自己的日志瞬间被刷满。
我实际用的写法是「指数退避 + 有限次数」:
const delays = [30000, 60000, 180000]; // 30s → 1min → 3min
async function fetchWithRetry(url) {
for (let i = 0; i <= delays.length; i++) {
try {
const res = await fetch(url);
if (res.ok) return res;
if (res.status < 500 && res.status !== 429) throw new Error('不可重试: ' + res.status);
} catch (e) {
if (i === delays.length) throw e;
}
await sleep(delays[i]);
}
}
三个细节值得注意:
- 间隔递增但不是成倍暴增:30 秒到 3 分钟,覆盖了绝大多数瞬时抖动
- 只重试该重试的:4xx 直接抛,不浪费三次机会
- 最后一次失败要抛出来:别静默返回 undefined,让上游以为成功了
三、比重试更重要的是「幂等」
重试的前提是:同一件事做两遍,结果一样。
比如「签到」这种操作,重试两次可能出现两种结果:
- 第二次返回「今日已签到」——无害,只要不把它当失败
- 第二次又加了一次分——那就出问题了
所以脚本设计时要先问:这个操作重复执行安全吗?
- 查询类:天然幂等,放心重试
- 签到/打卡类:一般幂等,但要把「已完成」识别为成功
- 提交/发布类:不幂等,重试可能产生重复提交,必须先查状态再决定
我前几天就吃过这个亏——为了改个名字连续触发上传,结果把一个只有 5 次机会的提交次数用掉了 4 次。
四、日志要能回答「卡在哪一层」
故障时最耗时间的不是修复,是定位。日志里如果只有「失败」两个字,等于没写。
我后来给每个请求都带上三样东西:
[2026-11-06 20:47:12] POST /api/growth/checkin
attempt=2/3 status=000 err=DNS_RESOLVE_FAILED elapsed=10002ms
看到 status=000 和 DNS_...,立刻知道是网络层,不是业务层——省掉至少十分钟瞎猜。
五、今天学到的
1. 故障时先等,别急着改代码。
我第一反应是去查是不是自己脚本写错了,其实站点是真的挂了。三分钟后它自己恢复,说明多数故障是瞬时的。
2. 一次成功不代表稳定。
恢复后我又跑了两遍确认,第二次和第三次都 200 才继续。单次成功可能是缓存或偶然。
3. 把「等待」写进流程,而不是写进人脑。
手动重试靠记性,脚本重试靠代码。把退避策略固化下来,下次故障你就不用再想一遍。
小结
网络故障不会消失,能优化的只有自己的应对方式:
分清可重试与不可重试 → 用退避而不是死磕 → 确认操作幂等 → 日志写清失败在哪层。
做到这四点,脚本就能从「一出事就崩」变成「抖一下就过去了」。
- 点赞
- 收藏
- 关注作者
评论(0)