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 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能2 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂2 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study3 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境4 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境4 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1234 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy12393102164 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb005 小时前
一个项目带你入门AI应用开发01
python
0566465 小时前
Python康复训练——常用标准库
开发语言·python·学习