python高并发与高性能编程:原理与实践

接入与开发

讲透 Python 高并发与高性能编程的工程落地做法:并发模型选择、限流降级、超时重试、日志埋点与压测评估。

写脚本调 API 时跑几十个请求就卡住、一上量就报限流、想加到几百并发又不知道该用线程、协程还是多进程——这类问题基本不是语法不会,而是并发模型和工程保护没设计好。这篇不讲语言基础,按「选模型→控并发→做降级→留日志→压测验证」的顺序,给一套可以直接套到项目里的判断标准和动作清单。文中涉及具体价格、限额数字的地方,一律以站内当时数据为准。

调 API 的脚本一开始跑得挺顺,加到几十个并发就卡住,再往上直接一片限流报错——这基本不是语法问题,而是并发模型选错了、并发预算没定、保护措施没做。这篇按「选模型、控并发、做降级、留日志、压测验证」五步走,给一套能直接套进项目的判断标准和动作清单。

文中凡是涉及具体价格、限额、速率数字的地方,一律不写死,请到站内对应页面核对当时的数值。下面每一节都尽量给可执行的判断方法,而不是概念解释。

先选并发模型:IO 密集和 CPU 密集是两条路

调用大模型 API 这类活,绝大部分时间在等网络返回,属于典型的 IO 密集任务。用 asyncio 协程或者线程池都能撑起可观的并发量,不需要一开始就上多进程。只有本地推理、向量计算、批量文件解析这种真吃 CPU 的场景,才值得考虑多进程绕开 GIL。

判断方法很土但有效:拿一个典型请求,分别量「等网络的时间」和「本地计算的时间」。前者明显更大就别上多进程,进程间通信和内存复制的开销会把好不容易省下的时间全吃回去,代码还难调。

  • 协程最大的坑是同步库阻塞事件循环:requests、部分数据库驱动、普通文件读写都会把整个循环卡死,要么换异步版本,要么丢进线程池跑。
  • 线程池适合调用那些没有异步版本的老库,但线程数量别开太大,上下文切换一样耗资源。
  • 多进程适合纯计算任务,且要注意任务粒度不能太碎,否则光进程调度就够亏。
  • 不适用的情况:任务之间有强顺序依赖,或者总请求量只有个位数,这时上协程只是徒增调试成本,同步写反而更稳。

并发不是越大越好:先定预算再写代码

并发上限由三样东西共同决定:账号侧的速率限制、对方服务的承载能力、你自己机器的内存与连接数。三者取最小值,超过这个数多开的并发只会换来一大片报错,还可能触发风控。具体额度口径到站内官方 API 页面核对当时的数字,不要凭印象估。

控制维度管什么失控后果
同时在飞的请求数用信号量卡住瞬时并发内存和连接被打满,报错集中爆发
单位时间发起数用令牌桶或计数器限速触发对方限流,连续失败
单请求超时区分连接超时与读取超时慢请求堆着不释放,拖垮整批
重试策略退避等待 + 只重可恢复错误无效重试放大流量,浪费额度

把并发拆成两层来控制比较稳:一层是「同时在飞的请求数」,用信号量卡住;另一层是「单位时间发起的请求数」,用令牌桶或简单计数器限。两层都设,只设一个容易出现一边超速一边堆积的怪现象。

  1. 量基线先同步跑几个请求,记下稳定耗时和成功率,作为后面所有对比的参照。
  2. 设信号量按账号额度和机器能力取一个保守值,宁可先低后调,不要先高再降。
  3. 配超时连接超时和读取超时分开设,流式接口额外设「多久没收到新片段就判死」。
  4. 接退避失败按递增加等待再重试,且只重试可恢复的错误;鉴权失败、参数错误重试多少次都一样。

失败立刻重试是最常见的错误做法,它会在对方已经限流时继续加压。递增等待的退避加上熔断,比硬打到底更省额度也更友好。

降级与隔离:别让一个坏请求拖垮整批任务

批量任务最怕一处卡住全批停摆。常见的做法是排队加分批:把大任务切成小批,每批之间留一点间隔,单批失败不阻断后续批次,最后统一汇总失败清单重跑。这样即使中间有波动,整体仍然能跑完。

  • 设熔断阈值:连续失败到一定比例就暂停发送,等一段时间再试探性放开,比一路硬打到底更安全。
  • 按优先级分队列:重要任务保留固定并发额度,别被批量任务挤占。
  • 坏请求要隔离:把超时和报错的任务单独放到一个清单里,不让它们反复占用正常并发。
  • 日志要能复盘:至少记请求标识、开始与结束时间、状态码、重试次数、耗时、消耗量。

日志字段怎么设计直接影响排查效率,字段太少事后根本对不上是哪一次请求出的问题,可以参考站内日志字段那篇的清单来定字段,别等到出事才临时加。

怎么验证真的变快了:压测与基线

没有基线就没法判断优化有没有用。先量单请求的稳定耗时,再到站内的接口测速页面看当时的网络延迟水平,不然你根本分不清是代码慢还是链路慢。这两个数不分开看,优化方向全是猜。

压测时盯住三个值:平均耗时、尾部耗时(最慢的那一小撮)、以及成功率。平均值好看但尾部很长,说明还有个别请求在拖后腿,这种问题在真实流量里往往集中爆发。

  1. 逐步加压从低并发开始,每次加一点,记录成功率与耗时变化,画出一条曲线。
  2. 找拐点成功率开始下滑的那个点就是拐点,日常压在它之下运行,留出余量应对突发。
  3. 持续观察上线后用质量监控类手段盯住失败率和耗时漂移,比出事后再翻日志省事得多。
  4. 回归对比每次调参或改代码后,用同一套压测脚本复测,和上一次结果对比再决定保留哪套参数。

压测环境毕竟和真实链路有差别,如果压测结果好但线上仍慢,优先怀疑 DNS 解析、连接复用、代理中转这几处。若要换中转通道,建议先到站内的中转站列表和可信度检测页面看当时的实测信息,再决定是否迁移。

下一步把你当前脚本的并发数、超时和重试策略对着上面几节改一遍,然后到站内测速页跑一次基线对比,看看尾部耗时和成功率有没有变化。

常见问题

Python 并发调 API,到底该用 asyncio 还是线程池?

绝大多数情况选 asyncio,因为调外部接口几乎全是等网络,协程切换开销小、能开的并发更多。但如果你用的客户端库只有同步版本、又不想重写,线程池是更省事的退路,代价是线程数量上去后内存和切换成本会涨。两者也可以混用:协程里把同步调用丢进线程池执行,避免阻塞事件循环。

并发一提高就开始报错,是不是我代码写错了?

先别怀疑代码,八成是踩到了速率限制。检查两件事:你是不是只控制了「同时在飞的数量」而没控制「单位时间发起量」;以及失败后是不是立刻重试导致雪上加霜。正确做法是加上递增等待的退避、把大批任务切成小批留出间隔,同时记录失败原因。相关排查顺序可以对照 批量跑任务不被限流(/guides/batch-concurrency-throttle-guide) 那篇。

怎么判断该优化代码还是该换个接入方式?

先量两件事:单个请求的耗时里,本地处理占多少、等网络占多少。本地占比很低说明瓶颈在链路,这时候重构 Python 代码收益有限,更该关注接入点选择、超时设置和请求合并。本地占比高才轮到优化算法、换异步库或上多进程。判断不了就先到 接口测速(/tools/speed-test) 拿一个延迟基线再对比。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。