从卡顿到丝滑:FastAPI 调用外部 API 的正确姿势(httpx 实战)

前段时间借助FastAPI 搭建了一个本地词典API ,用来翻译专业词汇和单词详细释意,但词典未收录的单词就没招了,于是又借助LibreTranslate 实现了离线翻译,这下内容是都能出结果了,但两套API不在一个接口里,把它集成到PotKoodo Reader等使用时,还是不方便。

今天就借助httpx 把他们集成在一起,让后台自动判断,本地词典找不到,那就从argos里拿,而不是我在外面一个搜索不到再开别一个窗口搜索。

📝 文章摘要

本文先简单说明下 requests、aiohttp 与 httpx 的优劣以及选择建议,然后使用 httpx 从安装到能写出异步客户端,同时分享下连接池耗尽、超时未设等实际开发中可能出现的踩坑经验。花点时间认真读完,这样以后你再写微服务调用时就会又快又稳,也不用再背锅。

🤔 你写的接口,到底慢在哪?

你可能也遇到过这种诡异场景:

本地跑得飞快的 FastAPI 服务,一上测试环境就卡得跟啥似的,CPU 还没跑满,请求就已经像是假期景点在排队等着上厕所一样。

结果你打开日志一看,好家伙,时间全耗在了调用那些下游 API 上。

不难想像,问题有很大概率出在你随手 import 的那个 requests 身上。

先不急着看代码,一起把这件事从头捋一捋,了解下背后的原理,这样才能做到应对不同突发情况时,心里都有个谱。

🎯 本文能帮你解决什么

读完这篇,你基本上就知道了 FastAPI 里到底该用什么工具去访问其他的 API 拿数据,其实主要是学会怎么避开同步阻塞的大坑,还有就是 httpx 这个六边形战士怎么用、怎么配。也算一些经验的总结!
👩‍💻我是爱折腾的一名程序媛 ,喜欢研究全栈开发 的各种实践,热爱分享踩坑后的收获与思考 ,也享受用代码写出各种实用小工具解决问题的快乐。

如果你也在技术这条路上向前走,关注我,愿我们能彼此陪伴,一起成为更好的自己 🌱

🗺️ 主要内容脉络

🔹 揪出让 FastAPI 变慢的真凶

🔹 三款常用 HTTP 客户端横评

🔹 httpx 安装与异步调用实战

🔹 那些一踩就炸的坑和最佳实践

🔹 总结与互动

🧱 第一部分:问题与背景------别让同步调用背刺你的并发

FastAPI 本身是基于 asyncio 的异步框架,像闪电一样快。但如果你在 async def 的异步接口函数里,直接调用 requests.get() 这种同步阻塞代码,那就相当于在高速公路的快车道上停了一辆拖拉机,那就只能中午出门了,因为早晚会出事。

requests 会把当前线程整个阻塞住,等待网络 IO 返回。而事件循环里的其他任务就全被卡在后面了,无法被调度。那了就没啥并发的事了,一秒回到解放前。

早期会使用一些小技巧:"用 asyncio.to_threadrequests 包起来不就行了?"

这确实能缓解,但那也只是贴个创可贴。线程切换是有开销的,而且当调用量一大起来,线程池本身又会成为瓶颈。结果就是治标不治本。

⚔️ 第二部分:工具横评------到底谁才是真命天子?

目前主流的在 FastAPI 里调用外部 API 的工具,就这三种:

🔹 requests

同步老炮,接口简洁,生态完美。可惜,它不属于异步时代。

🔹 aiohttp

纯异步,性能强悍。

但缺点也很扎眼------原生不支持 HTTP/2,API 的设计上偏底层,写起来代码量有点大,而且会话管理容易漏关

之前写代码时就出现过因为没关 ClientSession 结果导致文件描述符泄漏......

🔹 httpx

新晋卷王,同时支持同步和异步,API 几乎是复刻 requests,上手零门槛。

而且原生支持 HTTP/1.1 和 HTTP/2,内置连接池,和 FastAPI 简直是天作之合。

说句主观的:

顺手的工具才是最好的!httpx 让我能用写 requests 的习惯,享受异步的红利,还要啥自行车?

你可能又会说,requests 早就被丢一边去了,还管它什么习惯?但它在咱们用 Python 写爬虫时,还是有大用的,异步是好,但不能解决所有问题!

🛠️ 第三部分:实战演示------装上 httpx,直接开干

安装上面还是老样子,包管理,一步搞定:
$ pip install httpx

但如果你需要 HTTP/2 支持,就把 httpx 换成 httpx[http2] 特定包就行。

接下来重点来了,怎么在 FastAPI 里写一个漂亮的异步调用呢?看代码:
from fastapi import FastAPI, Depends, Request

from contextlib import asynccontextmanager

import httpx

生命周期里全局定义一个异步客户端实例,用完记得关

@asynccontextmanager

async def httpx_lifespan(app: FastAPI):

try:

app.state.client = httpx.AsyncClient(

base_url="https://api.example.com", timeout=10.0)

logger.info(f"✅ Httpx客户端初始化完成!")

except Exception as e:

logger.error(f"❌ Httpx客户端初始化失败: {e}")

raise

yield

if hasattr(app.state, 'client'):

await app.state.client.aclose()

logger.info(f"✅ Httpx客户端已关闭!")

app = FastAPI(lifespan=httpx_lifespan)

核心:通过依赖函数获取客户端

async def get_http_client(request: Request) -> httpx.AsyncClient:

return request.app.state.client

@app.get("/user/{user_id}")

async def get_user(user_id: int, client: httpx.AsyncClient = Depends(get_http_client)):

resp = await client.get(f"/users/{user_id}")

resp.raise_for_status() # 非200直接抛异常,好习惯

return resp.json()

看出来没?跟 requests 像得亲兄弟似的,只不过多了个 await

利用 FastAPI 的 lifespan 生命周期管理,全局复用连接池,性能上也可以直接拉满。

这份代码你简单修改下里面的base_url基础链接,就可以用在你的项目上啦!

🚨 第四部分:注意事项与进阶思考------那些我踩过的坑

代码跑起来容易,但怎么让它跑得稳才是真本事。下面这些点,都是血的教训呀,最好了解下,心里有个数。

🔹 一定、一定、一定要设超时

默认情况下,httpx 的超时是 5 秒,但很多服务调用你等不了那么久。

更可怕的是,如果你忘了设,早期版本甚至默认无限等待,你在那刷半天,数据就是出不来,咋回事都不知道。

所以,在建客户端时,最好把 timeout=10.0 写得明明白白,这是保命符呀。

🔹 连接池耗尽比你想的常见

你可能会问:"我都异步了,怎么偶尔还卡?" 99% 是连接池满了。

httpx 默认连接池上限 100。如果下游接口响应慢,100 个连接全占着,新请求就只能干等。

最好的处理方式是,根据你的并发量,适当调大一下 limits 参数:
limits = httpx.Limits(max_connections=200, max_keepalive_connections=50)

client = httpx.AsyncClient(limits=limits)

🔹 别在响应里找不着北

正常我们获取返回数据,都是直接使用resp.json() ,但它是解析 JSON 的,如果下游返回 204 No Content,或者返回了个空 HTML,那这行就会炸掉。

最好的处理方式是,先判断 resp.status_code,或者用 resp.text 手动处理,稳一手。

如果你就就是想要 json ,最好也是加个 try/catch,能用json就json,不行咱就text

🔹 状态码的细节

resp.raise_for_status() 能在 4xx/5xx 时立刻抛异常,避免后续代码空转。这个习惯,建议刻在肌肉记忆里。它能帮你解决很多莫名其妙的问题!

🔹 再说个容易翻车的点

不建议在每个接口里临时 async with httpx.AsyncClient() as client: 去创实例。

每次都建连接、拆连接,TCP 握手开销巨大,那你的微服务可能秒变"假异步"了,虽然实际测试时,它内部维护了连接池,开销还好,但效果终究差点意思。

最好采用全局实例 + 事件管理,这个才是正道。

💬 做个总结

说到底,FastAPI 里的网络调用,核心就一句话:用异步客户端,全局管理,设好超时,看好连接池。

httpx 目前是我项目里的首选,不过技术没有银弹,如果你是追求极致吞吐量的场景,花点心思调教 aiohttp 也未尝不可,因为这个老将在能打方面还是胜过新兵的。

但大多数时候,httpx 的简洁与强大,已经完全足够的了。


这篇分享都是我在项目里一个Bug一个Bug改出来的经验总结,希望能帮你在写这类需求时少走点弯路。

如果你觉得对你有那么一...点点收获 ,那顺手点击收藏加个关注 ,以防下次用的时候划拉半天找不到在哪看过,也让我看到我的辛苦总结还是有用的,那我将甚感欣慰!

你还有什么踩坑经历或者不同意见,也欢迎评论区里聊聊,咱们一起学习,一起进步、成长!🎯

相关推荐
不瘦80斤不改名1 小时前
HalfCart:基于LBS的1\.5km本地拼单系统全栈实践与踩坑复盘
python·docker·typescript·pycharm
深盾科技_Virbox2 小时前
软件交付保护如何从加壳走向代码虚拟化
java·python·安全
YUS云生2 小时前
Python学习笔记·第28天:Git入门——版本控制与基础操作
笔记·python·学习
Bryce学亮2 小时前
1688 半自动化拟人浏览信息处理工具爬虫
爬虫·python
是枚小菜鸡儿吖2 小时前
服务器出问题别等用户反馈:用Python和钉钉搭一套轻量告警系统
服务器·python·钉钉
聪明的一休丶12 小时前
VLLM v0.24.0 版本深度解析:新引擎、新架构与大规模服务全家桶升级
python·架构·vllm
万亿少女的梦16814 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
闲猫15 小时前
Python FastAPI + SQLAlchemy 入门教程:从零搭建你的第一个 Web 应用
前端·python·fastapi
北极星日淘18 小时前
中古货品品相评级算法实战|Java权重计分实现标准化五级品相体系
开发语言·python