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()

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

相关推荐
奔跑吧邓邓子7 分钟前
【Python爬虫(36)】深挖多进程爬虫性能优化:从通信到负载均衡
开发语言·爬虫·python·性能优化·负载均衡·多进程
学长学姐我该怎么办32 分钟前
年前集训总结python
python
量化投资技术39 分钟前
【量化科普】Sharpe Ratio,夏普比率
python·量化交易·量化·量化投资·qmt·miniqmt
yanglamei196241 分钟前
基于Python+Django+Vue的旅游景区推荐系统系统设计与实现源代码+数据库+使用说明
vue.js·python·django
虚假程序设计44 分钟前
python用 PythonNet 从 Python 调用 WPF 类库 UI 用XAML
python·ui·wpf
胡桃不是夹子1 小时前
CPU安装pytorch(别点进来)
人工智能·pytorch·python
不会玩技术的技术girl3 小时前
使用Python和正则表达式爬取网页中的URL数据
开发语言·python·正则表达式
阿_旭3 小时前
基于YOLO11深度学习的糖尿病视网膜病变检测与诊断系统【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·视网膜病变检测
胖哥真不错3 小时前
Python实现GO鹅优化算法优化随机森林分类模型项目实战
python·机器学习·项目实战·go鹅优化算法·随机森林分类模型
小白今天也很酷4 小时前
Python与MCU通信:串口数据采集及CSV/Excel存储方法
python·单片机·excel