httpx上传文件/IO流缓慢的问题分析及解决

问题背景

项目需要通过并发方式对文件上传接口做压力测试,测试时从请求的数据发现这个接口的响应时间明显不正常,并发时最长耗时需要25s,由此逐步分析存在的问题。

问题分析

排除服务端问题

从服务端日志看出,该请求在服务端的实际响应时间约0.1秒,且在注释服务端保存文件相关代码,仅处理请求的情况下,客户端依然高延时,由此可确认此问题与服务端无关;

排除IO问题

起初上传文件接口测试的逻辑是创建一个本地文件,写入内容后将IO作为请求参数传递,这里推测是否并发情况下客户端IO阻塞导致缓慢。

因此修改测试逻辑为手动创建一个IO对象,不对本地文件做操作,此方法未生效,客户端延迟依旧,因此排除由于文件读写导致的IO问题;

尝试不同请求库

首先尝试更新请求库版本,当前使用的是httpx 0.24版本,更新至0.28.1,问题未解决;

尝试更换请求库为requests,问题现象消失

这里很奇怪,尝试了requests和httpx在所有请求参数完全一致的情况下,仅有httpx在并发条件下出现请求延迟异常的问题,而项目整体更换请求库成本过高,因此继续分析httpx的问题根因;

根因分析

首先在该请求函数中增加性能分析工具pyinstrument,用于找出阻塞的节点,代码如下:

python 复制代码
from pyinstrument import Profiler
from pyinstrument.renderers.html import HTMLRenderer
fake_file = BytesIO(f"TEST LOG FOR {ref}".encode("utf-8"))
fake_file.name = f"{ref}.log"
with Profiler(interval=0.001, async_mode="disabled") as profiler:
    start_time = time.time()
    resp = self.client.post(
        "/native/upload",
        headers={
            "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarybsdsr23r13Zu0gWI&T*&GDF"
        },
        files={"file": fake_file}
    ).raise_for_status()
Path(f".profile/{time.time()-start_time}.html").write_text(
        profiler.output(renderer=HTMLRenderer())
    )

运行结束后,观察性能报告,发现阻塞点在httpx的build_request函数,耗时函数为guess_type

其实看到这个函数名称就能猜到大概是什么问题了,多半就是在推测文件类型或是编码格式导致耗时 ,这里继续从httpx源码中分析怎么解决这个问题;

先看阻塞位置的代码,看怎样才能使其不执行guess_type:

python 复制代码
class FileField:
    """
    A single file field item, within a multipart form field.
    """

    CHUNK_SIZE = 64 * 1024

    def __init__(self, name: str, value: FileTypes) -> None:
        self.name = name

        fileobj: FileContent

        headers: dict[str, str] = {}
        content_type: str | None = None

        # This large tuple based API largely mirror's requests' API
        # It would be good to think of better APIs for this that we could
        # include in httpx 2.0 since variable length tuples(especially of 4 elements)
        # are quite unwieldly
        if isinstance(value, tuple):
            if len(value) == 2:
                # neither the 3rd parameter (content_type) nor the 4th (headers)
                # was included
                filename, fileobj = value
            elif len(value) == 3:
                filename, fileobj, content_type = value
            else:
                # all 4 parameters included
                filename, fileobj, content_type, headers = value  # type: ignore
        else:
            filename = Path(str(getattr(value, "name", "upload"))).name
            fileobj = value

        if content_type is None:
            content_type = _guess_content_type(filename)

这里可以看出,在传入的value中不附带content_type的时候,他会调用_guess_content_type来推测这个参数,而value从上级函数中可以看出,对应的是httpx请求中的files参数的value。因此,解决办法就是在这个参数中,除了IO对象外,content_type参数也一并提供。

解决方案

修复后的代码如下:

python 复制代码
    resp = self.client.post(
        "/native/upload",
        headers={
            "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarybsdsr23r13Zu0gWI&T*&GDF"
        },
        files={"file": (f"{ref}.log", fake_file, "application/octet-stream")} # 增加content_type
    ).raise_for_status()

修复后,再次执行压力测试,可见问题已解决;

相关推荐
汽车仪器仪表相关领域1 小时前
全自动化精准检测,赋能高效年检——NHD-6108全自动远、近光检测仪项目实战分享
大数据·人工智能·功能测试·算法·安全·自动化·压力测试
诸神缄默不语1 小时前
Python处理Word文档完全指南:从基础到进阶
python
海棠AI实验室2 小时前
第四章 项目目录结构:src/、configs/、data/、tests/ 的黄金布局
python·项目目录结构
爱笑的眼睛113 小时前
超越可视化:降维算法组件的深度解析与工程实践
java·人工智能·python·ai
清铎3 小时前
leetcode_day12_滑动窗口_《绝境求生》
python·算法·leetcode·动态规划
ai_top_trends3 小时前
2026 年工作计划 PPT 横评:AI 自动生成的优劣分析
人工智能·python·powerpoint
TDengine (老段)4 小时前
TDengine Python 连接器进阶指南
大数据·数据库·python·物联网·时序数据库·tdengine·涛思数据
vyuvyucd4 小时前
深入解析Python asyncio:异步编程核心原理
开发语言·python
brent4234 小时前
DAY50复习日
开发语言·python
万行4 小时前
机器学习&第三章
人工智能·python·机器学习·数学建模·概率论