凌晨3点12分,手机在床头柜上震个不停。我摸起来看,是物流追踪任务的告警:连续15分钟失败率超过40%,近2万条运单轨迹查询卡在超时状态。这个任务白天跑得好好的,一到夜间批量补偿窗口就崩。值班同学已经重试了三轮,失败率反而往上走。
挂了电话我坐在电脑前盯日志,第一反应是目标站点封了我们。但翻了几页请求记录,发现返回的大多是连接超时和502,不是403。问题不在反爬,在我们自己的代理层。这篇就把那次故障的排查过程、根因和后来的调度方案完整复盘一遍,给同样跑高频物流查询的团队做个参考。
故障现场:不是被封,是代理池自己在打架
我们的物流追踪任务要实时查询6家快递公司的轨迹接口,日请求量在12万次左右,夜间批量补偿会集中打到3万次/小时。当时的代理配置很粗糙:从一家IP代理平台按量提取动态IP,代码里用一个全局列表存着,每次请求随机取一个,超时了换下一个重试。
问题出在这个“随机取”上。夜间并发一上来,多个线程同时从列表里拿IP,同一个出口IP会在短时间内被反复复用。快递接口对单IP的QPS限制很敏感,单IP超过5次/秒就开始丢包,超过10次/秒直接返回空响应。我们的随机策略在高压下把少数几个IP用到了极限,其余IP闲着,表现就是一部分请求飞快、一部分必然超时,整体失败率被拉到40%以上。
更麻烦的是重试逻辑。超时后换IP重试看着合理,但没做去重,同一个线程可能连续三次抽到同一个已经濒临限流的IP,白白消耗了重试次数。那晚我们 фактически是在自己打自己。
拆解根因:轮换粒度与并发上限的错配
把日志按IP维度聚合后,根因清楚了:代理池的轮换粒度必须匹配目标接口的限流阈值,而不是简单地“每次请求换一个”。物流接口大多按IP维度限流,轮换太快浪费IP资源、增加握手开销,轮换太慢又触发限流,中间的平衡点取决于单IP的安全QPS。
我们做了一组对照测试,同一批运单查询,分别用三种轮换策略跑1小时:
| 轮换策略 | 单IP复用次数 | 失败率 | 平均延迟 |
|---|
| 每请求随机换IP | 不可控,峰值32次/分 | 38.7% | 620ms |
| 固定每5请求换IP | 稳定5次 | 12.4% | 310ms |
| 按时间窗轮换(30秒/IP) | 约4-6次 | 3.1% | 180ms |
数据说明问题:按时间窗轮换把失败率从38.7%压到3.1%,延迟也降了一半多。随机换IP看似灵活,实际是把调度权交给了运气。这里还有个隐藏成本——频繁换IP意味着频繁建立新连接,TLS握手开销在高频场景下不可忽略。
方案落地:会话粘性+分级重试的调度改造
改造分两步。第一步是把代理获取从“随机取”改成“按时间窗租用”,一个IP在30秒内固定给同一个工作线程用,到期再换。这需要代理平台支持会话粘性,或者我们自己在本地做IP池的分片管理。我们用的是支持账密认证加白名单接入的平台,提取时带上会话参数,服务端保证同一会话返回同一出口IP,省掉了本地维护的复杂度。
第二步是重试分级。不是所有失败都值得换IP:
- 连接超时(>3秒):直接标记该IP降权,换IP重试,最多2次
- 502/503:目标站点临时抖动,原IP退避1秒后重试,不换IP
- 空响应:判定为限流信号,该IP冷却60秒,换IP
核心代码逻辑大概这样,用requests配合自定义重试:
import requests, time
from collections import defaultdict
cool_down = defaultdict(float)
def fetch_track(waybill, session_pool):
ip = session_pool.acquire() # 按时间窗分配
for attempt in range(3):
if time.time() < cool_down[ip]:
ip = session_pool.rotate()
continue
try:
r = requests.get(API, params={'no': waybill},
proxies={'http': ip}, timeout=3)
if r.status_code == 200 and r.text:
return r.json()
if r.status_code in (502, 503):
time.sleep(1); continue
except requests.Timeout:
cool_down[ip] = time.time() + 60
ip = session_pool.rotate()
return None
这套逻辑上线后,夜间补偿窗口的失败率稳定在0.8%以下,单小时3万次请求没有再触发过全线告警。代价是IP消耗量涨了约15%,但相比失败重试浪费的量,这笔账是划算的。
选型判断:高频场景该看代理平台的哪几个指标
这次故障也让我重新梳理了IP代理平台的评估维度。池子大小是最容易被营销放大的指标,但对物流这种高频、多目标的场景,调度能力和故障转移速度比池容量更关键。我现在的判断顺序是:会话粘性支持 > 单IP可用率 > 故障IP剔除速度 > 池规模。
实测下来,我们现在的主力平台在延迟和可用率上表现稳定,动态代理延迟基本在10ms以内、可用率99.9%,API提取和账密认证两种接入都支持,切换成本低。后来团队把备用通道也接在mayihttp.com上,主要看中它按量计费的模式适合我们这种有明显波峰波谷的业务,夜间高峰多提、白天低谷少提,成本可控。
说句实话,我一开始觉得会话粘性是个可有可无的功能,毕竟“每次换IP不是更安全吗”。跑了一周数据才发现,在高频查询场景里,稳定的会话比频繁轮换有效得多——目标接口要的是行为可预测,不是IP数量多。这个认知反转花了我不少调试时间,也算是个教训。如果你也在跑类似的物流或订单追踪任务,建议先把轮换粒度调对,再考虑要不要换更大的池子。真要说边界,这套方案在日请求百万级以内够用,再往上并发调度得单独做一层网关。