首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从请求调度到失败重试:用 asyncio + httpx 构建抗 429 的股票数据客户端

从请求调度到失败重试:用 asyncio + httpx 构建抗 429 的股票数据客户端

原创
作者头像
小白学大数据
发布于 2026-10-27 16:55:24
发布于 2026-10-27 16:55:24
1620
举报

0. 引言做量化、做数据服务,第一步往往是把行情/基本面数据批量拉下来。你兴致冲冲写了个 for symbol in symbols: requests.get(...),跑个几千只股票——然后日志里开始刷屏 429 Too Many Requests,延迟越来越长,最后整批任务卡死。429 不是 bug,是服务端在说「你太猛了,慢点」。批量抓股票数据的核心矛盾永远是:你想快,服务端想限你。这篇文章不谈怎么「破解」限流(那不合规也不可持续),而是讲怎么把请求调度得优雅、失败得可控,让批量任务稳稳跑完。配套的可运行代码在 stock_fetcher.py,自带 mock 演示,不用真实网络就能看到重试效果。1. 先搞清楚:429 从哪来股票数据源(交易所行情网关、Tushare、东方财富/新浪的非官方接口、各种券商 openapi)基本都有速率限制(rate limit),常见两种口径:按 IP 限流:同一个出口 IP 在单位时间内请求数超过阈值就 429。这是最容易被「自己把 IP 打挂」的场景。按 token/账号限流:API key 维度的配额,比如 Tushare 的积分制调用频次。所以应对策略也分两层:请求侧(调度+重试,降速率)和 IP 侧(轮换出口,摊薄单 IP QPS)。下面逐个拆解。2. 请求调度:别把水龙头开到最大2.1 固定间隔为什么不够最朴素的做法是「每请求 sleep 一下」:

问题:sleep 是串行的,你想并发提速就失去了限流控制;而且它无法应对「攒一波令牌再突发」的真实场景。我们改用令牌桶——它允许短时突发(桶满时),同时长期平均速率被 cap 住,比死 sleep 平滑得多。2.2 异步令牌桶(重点)下面这个 TokenBucketLimiter 是核心调度组件,基于 asyncio,非忙等(用 sleep(deficit/rate) 精确等待缺口,而不是空转):

搭配一个并发信号量 asyncio.Semaphore(concurrency),双保险:信号量限制「同时在飞」的请求数,令牌桶限制「长期平均」速率。两者正交,缺一不可。3. 失败重试:退避不是 sleep 那么简单拿到 429(或 503/504 这类临时错误)后, naive 做法是 sleep(固定值) 再试。这有两个坑:没有指数增长:服务端恢复可能需要几秒到几分钟,固定 1s 重试会在恢复前疯狂空转,反而加重限流。惊群效应:一堆客户端被同时 429,又在同一时刻重试,瞬间又把服务端打爆。正确姿势 = 指数退避 + 抖动 + 尊重 Retry-After + 区分错误类型。3.1 三个关键细节指数退避:delay = base * 2**attempt,并设 max_delay 封顶(比如 30s),否则退避会膨胀到离谱。抖动(jitter):delay *= (1 ± 随机比例),把重试时间点打散,避免同时重试。尊重 Retry-After:很多服务端在 429 响应里直接告诉你「多久后再来」,优先用它的数值(秒),别自己瞎猜。3.2 区分可重试 vs 不可重试这点最容易被忽略:不是所有错误都该重试。可重试:429, 500, 502, 503, 504,以及连接超时/重置等 TransportError。不可重试:400, 401, 403, 404, 422——这些是请求本身有问题(参数错、鉴权失败、资源不存在),重试一百次也是同样结果,纯属浪费配额。装饰器版本一把梭:

经验值:max_retries=5、base_delay=1、max_delay=30 对大多数股票接口够用。重试总耗时上限约 1+2+4+8+16=31s,加上抖动不超过 ~40s,单只股票卡死也不会拖垮整批。4. IP 侧:代理轮换把单 IP 的 QPS 摊薄限流常常是按 IP 计的。如果你所有请求都从同一个公网 IP 出去,调到 5 QPS 可能就到天花板了。把出口分散到多个 IP,等于把配额乘以 N。两种思路:自己维护 IP 池:买一批代理,轮询/随机选,注意健康检查和失效剔除。用隧道代理:你只配一个入口地址,服务商在后端自动轮换出口 IP,省去自己维护池子的麻烦。比如 亿牛云代理 的隧道代理,配置一个地址即可自动换 IP,对批量抓取非常省心,单 IP 限流问题基本被摊平。接入方式就是在 httpx 客户端里挂上代理,并在客户端里做轮询:

提醒:代理能解决「单 IP 限流」,但不能突破「账号/token 维度的配额」。两层限流要分别对待;而且务必遵守目标站点的 robots.txt 与服务条款,抓取频率控制在合理范围。5. 熔断与监控:别让限流雪崩429 频繁出现往往是「要被封」的前兆。工程上建议再加两层:熔断(Circuit Breaker):连续 N 次 429/5xx 后,直接暂停整批任务一段时间(比如 5 分钟),而不是硬刚。等冷却后再逐步恢复。指标埋点:记录每批的 成功数 / 429 数 / 其他错误数 / 平均重试次数。一旦 429 占比超过阈值(比如 20%)就告警,说明速率设定或代理策略该调了。简单实现上,可以在 StockDataClient.fetch_batch 的 results 里统计错误类型,把聚合指标打到日志或 Prometheus。6. 完整可运行示例stock_fetcher.py 把上面的组件全串起来了,并用 httpx.MockTransport 模拟了一个「前 3 次返回 429、之后正常」的服务端,直接验证重试链路:

代码语言:txt
复制
pip install httpx
python stock_fetcher.py

预期输出(重试 3 次后第 4 次成功):

代码语言:txt
复制
最终状态码: 200, 服务端被调用次数: 4
返回数据: {'symbol': '600000', 'price': 100}

真实使用时,把 base_url 换成你的股票接口、按需填入 proxies,然后调用:

代码语言:txt
复制
async def main():
 limiter = TokenBucketLimiter(rate=8, capacity=4)
 client = StockDataClient(base_url="https://your-stock-api.com",
 limiter=limiter, concurrency=5, proxies=proxies)
 results = await client.fetch_batch(["600000", "000001", "300750", ...])
 print(results)
asyncio.run(main())

7. 避坑清单别用 time.sleep 做限流,它阻塞事件循环;异步里用 await asyncio.sleep。重试一定要加 jitter,否则惊群效应让 429 雪上加霜。永远尊重 Retry-After,服务端让你等几秒就等几秒。4xx 别重试(除 429),参数/鉴权错误重试纯浪费。信号量 + 令牌桶双保险,只靠其中一个都容易在突发时翻车。代理解决单 IP 限流,不解决 token 限流,别指望一招通吃。加熔断和监控,429 暴涨时该停就停,别硬扛到被封。8. 小结批量抓股票数据被 429 拖垮,根因通常是「调度太莽 + 重试太笨」。用令牌桶控速率、信号量控并发,用指数退避+抖动+Retry-After 做重试,再配合代理轮换摊薄单 IP 配额,最后用熔断和监控兜底——这套组合拳下来,几千只股票也能稳稳跑完。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档