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

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

相关推荐
游客52024 分钟前
自动化办公|使用Python重命名并移动文件到对应文件夹
python·自动化
互联网时光机28 分钟前
基于Python机器学习的双色球数据分析与预测
人工智能·python·机器学习
一名技术极客41 分钟前
Python 进阶 - Excel 基本操作
android·python·excel
小龙在山东1 小时前
基于tldextract提取URL里的子域名、主域名、顶级域
python·tld
蹦蹦跳跳真可爱5891 小时前
Python----Python高级(文件操作open,os模块对于文件操作,shutil模块 )
python
大模型铲屎官1 小时前
玩转 LangChain:从文档加载到高效问答系统构建的全程实战
人工智能·python·ai·langchain·nlp·文档加载·问答系统构建
大数据张老师2 小时前
【实践】Python封装植物的健康状态,确保数据访问安全
开发语言·python·安全
五味香2 小时前
Java学习,List截取
android·java·开发语言·python·学习·golang·kotlin
FHYAAAX2 小时前
Flask简介与安装以及实现一个糕点店的简单流程
python·mysql·flask
web150854159352 小时前
Flask:后端框架使用
后端·python·flask