登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python requests 没设超时:一次任务队列卡住的排查和修复

来源:17golang原创

时间:2026-07-07 13:37:43 435浏览 收藏

凌晨跑的账单同步任务突然卡得离谱,队列监控曲线蹭蹭往上涨,查了下worker进程没崩,CPU占用也很低,翻日志发现所有记录全停在「正在请求供应商接口」那一行。折腾半天才找到根因,说出来特别小:Python 里的 requests.get() 没设 timeout,某次网络请求直接卡死之后,整个worker就彻底悬在那占着位置不释放,后面排着的任务全得原地等。

实践要点
  • requests 不会主动帮你设置请求超时,跑生产环境的脚本必须手动把 timeout 显式传进去。
  • 超时最好拆成连接超时和读取超时两部分,比如 timeout=(3, 10),别图省事只写一个特别大的数字糊弄过去。
  • 任务队列里要把「能重试的网络临时错误」和「业务侧直接失败」区分开,不然外部接口随便抖一下,就直接把问题放大成整个队列雪崩。
  • 修完问题别只跑个单任务看能不能跑完,要盯着队列堆积量、worker占用率、接口耗时分布、失败重试次数这几个指标核对才稳。
目录
  • 队列为什么先堆起来
  • 从日志时间线定位卡点
  • requests 默认超时为什么危险
  • 怎么给连接和读取分别设超时
  • 重试策略别把故障放大
  • 上线前的检查清单
  • 常见问题

队列为什么先堆起来

这类故障刚冒头的时候,特别像「第三方外部接口变慢」的问题。业务侧的同学看到的是账单到账延迟,监控面板里 billing_sync 队列长度从几十条一路冲到几千条,偏偏worker进程数还好好的没少。最关键的线索是:每个卡在队列里的worker,全停在请求外部HTTP接口的步骤,根本没进到解析响应、写库、给消息发确认这些后续流程里。

这时候别上来就直接扩容worker,扩完了只会让更多进程一起卡在那等死,队列可能短时间看着掉了点,等外部接口状态没恢复,没过多久老问题还会冒出来。先沉下心查单个任务为什么会占着worker不释放。

Python requests 没设置 timeout 导致 worker 卡住和队列堆积的决策路径图

从日志时间线定位卡点

排查的时候可以把同一个任务的全链路日志按时间先后串起来。下面这个示例里,任务启动后正常拿到了供应商编号,也拼完了请求URL,刚发出去请求之后就再也没往下打日志了。

10:02:11 job=88421 vendor=A start sync
10:02:12 job=88421 vendor=A build url ok
10:02:12 job=88421 vendor=A request remote api
# 后面没有 parse response,也没有 ack message

如果你的worker是多进程或者多线程模型,还可以直接拉一下进程栈信息看卡在哪。线上环境不方便随便装调试工具的话,至少在发请求的前后各打一行带任务ID的日志,直接就能区分出程序是卡在发请求前、请求过程中、还是拿到响应之后解析的环节。

现象 优先怀疑方向 下一步操作
发请求前的日志全正常,发完请求之后再也没输出 HTTP调用直接挂死 检查 timeout 和外部接口的实际耗时
拿到响应之后解析直接报错 接口返回的字段或者格式偷偷变了 把原始响应存下来补兼容逻辑
写库环节特别慢 索引坏了、行锁等不到或者单次批量写入的数据太大 查慢SQL记录和事务覆盖范围

requests 默认超时为什么危险

requests 用起来确实顺手,很多图省事的同学直接就写出下面这种代码:

import requests

resp = requests.get(api_url)
resp.raise_for_status()
data = resp.json()

这段代码的问题就在于,完全没告诉程序「最多等多久必须返回」。TCP连接建立慢、服务端拿到请求之后迟迟不回包、网络中间设备丢包没回应,随便哪种情况都能让这个请求永远占住当前的worker。如果只是本地跑个临时脚本,卡住几分钟顶多就是人等得烦;但放到任务队列场景里,一个worker卡死就等于少了一个处理槽位,十几个worker接连卡死,队列里的任务量直接就滚雪球越堆越多。

这里的修复思路不是把超时时间设得越短越好,而是让最长等待时长和业务的实际容忍度匹配就行。账单同步这种场景可以等得稍微久一点,用户触发的前端请求后面的调用就不能设太长的超时;内网服务调用和公网第三方供应商的接口,也别硬套同一套超时参数。

怎么给连接和读取分别设超时

requeststimeout 参数既可以传单个数字,也可以传二元组。跑生产环境更推荐用二元组的写法,前面的数是连接超时时间,后面的数是读取响应的超时时间。

import requests

CONNECT_TIMEOUT = 3
READ_TIMEOUT = 10

def fetch_bill(api_url: str, token: str) -> dict:
    resp = requests.get(
        api_url,
        headers={"Authorization": f"Bearer {token}"},
        timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
    )
    resp.raise_for_status()
    return resp.json()

连接超时可以设得短一点,毕竟连不上对方基本就是网络路径、DNS解析或者目标服务入口出问题了;读取超时可以按照接口平时的实际处理时间适当放宽。别为了省事儿直接写个特别大的数值比如 timeout=120,真出问题的时候worker还是会被占住很久,该拖垮队列还是拖垮队列。

Python requests 连接超时、读取超时和重试停止条件的决策路径图

重试策略别把故障放大

给请求加上超时之后,任务会更快失败,但这不代表你可以上来就无脑重试。外部接口本来就已经处理不过来了,你每个失败的请求都立刻重试三次,等于直接把打过去的请求量翻三倍,第三方接口会更卡,本地队列的堆积情况反而更难恢复。

比较稳妥的写法是:只对连接超时、读取超时、临时5xx错误做次数有限的重试;遇到4xx错误、参数不对、鉴权失败这类业务层面的问题直接标记失败就行不用重试。重试的时候顺便加一点退避间隔,别让所有失败任务同一时间又一窝蜂往对方接口打。

import time
import requests

RETRYABLE_STATUS = {502, 503, 504}

def fetch_with_retry(api_url: str, token: str, retries: int = 2) -> dict:
    last_error = None
    for attempt in range(retries + 1):
        try:
            resp = requests.get(
                api_url,
                headers={"Authorization": f"Bearer {token}"},
                timeout=(3, 10),
            )
            if resp.status_code in RETRYABLE_STATUS:
                raise RuntimeError(f"temporary status {resp.status_code}")
            resp.raise_for_status()
            return resp.json()
        except (requests.Timeout, RuntimeError) as err:
            last_error = err
            if attempt == retries:
                break
            time.sleep(0.5 * (attempt + 1))
    raise RuntimeError(f"remote api still unavailable: {last_error}")

上面的代码只演示了核心的判断逻辑。实际项目里完全可以把重试逻辑、日志打印、指标上报这些功能封装成一个统一的小HTTP客户端,不用每个任务函数都自己手写一遍。

上线前的检查清单

修复这类问题,把代码改完只是第一步,上线之前建议至少核对完下面这些点:

  • 所有发外部HTTP请求的地方都显式配置了 timeout,包括 requests.get()getpost 还有大文件下载的场景。
  • 日志里能明确看到任务ID、供应商编号、请求实际耗时、返回状态码和重试次数这些关键字段。
  • 监控面板里已经配好了队列长度、单任务平均耗时、超时次数、重试次数和失败原因这些指标的告警。
  • 超时失败之后的消息处理规则很明确,是直接确认掉、放回队列重投还是丢去死信队列,别含糊不清。
  • 等外部接口恢复正常之后,队列的消费速度确实能追上新进来的任务速度,不会一直有堆积。

更推荐先把全项目的公共HTTP客户端收拢统一管控,再逐个替换散落在各个业务脚本里的裸 requests.get() 调用。以后要调整全局超时、加代理、补指标上报这些操作,不用翻几十上百个业务文件挨个改代码。

常见问题

timeout 传单个数字和传二元组到底有啥区别?

传单个数字的时候,连接等待和读取响应这两个环节共用同一个超时时间;传二元组的话可以分别自定义连接和读取的最长等待时长,更适配生产环境的复杂场景。

超时时间应该设成多少比较合适?

先看接口平时正常跑的耗时分布,再结合业务能接受的最长等待时间来定。普通的外部查询类接口可以先用 (3, 10) 这类比较保守的数值起步,后面跑一段时间根据监控数据慢慢调就行,不建议一上来就设几十秒那么长。

所有抛出来的异常都应该重试吗?

肯定不是。连接超时、读取超时、临时5xx错误这种场景可以做有限次数的重试;参数错误、鉴权失败、资源不存在这类问题就算重试多少次也不会成功,直接记录清楚失败原因就行。

只靠任务队列自带的超时设置够不够防卡死?

完全不够。队列层面的超时一般是最后兜底用的,如果你不在HTTP调用这一层自己设超时,worker还是会占着处理槽位挂很久,出问题之后整个链路恢复速度会特别慢。

小结

Python写的任务队列卡住的时候,别光盯着worker数量和机器负载乱调。只要脚本里有发外部HTTP请求的逻辑,先确认 requests 有没有配置明确的 timeout,再核对重试规则、日志完整性和队列的消息确认逻辑。把连接超时、读取超时、有限重试和清晰监控这几点都做到位,哪怕外部接口临时抖一下,影响范围也只会停在局部,不会直接拖垮整条任务链路。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>