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 小时前
TS-h3087XU-RP 汽车零部件质检追溯部署
python·汽车
你驴我1 小时前
WhatsApp 多账号场景下的会话归档与历史消息检索优化实践
后端·python
IT毕设实战小研2 小时前
基于安卓的空气质量查询系统
android·大数据·vue.js·爬虫·python·django·课程设计
harmful_sheep2 小时前
java group by常见用法
java·开发语言·python
whcyhhh3 小时前
头歌实践教学平台:数据科学与大数据技术导论(十六)
大数据·开发语言·python
宸津-代码粉碎机3 小时前
AI工程化高阶实战|从Demo到生产落地核心架构、全套源码与避坑指南
java·大数据·开发语言·人工智能·python·架构
郝学胜-神的一滴4 小时前
Effective Python 条款4:字符串格式化大乱斗
开发语言·网络·python·程序人生·软件工程
ynchyong4 小时前
Python 字符组合生成器:product 与 permutations 的实战对比
python·工具·字符串生产
benchmark_cc4 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
sel_95 小时前
【PEFT】参数高效微调(PEFT)技术详解:从原理到 LoRA/QLoRA 实战
人工智能·python·深度学习·算法·机器学习·参数高效微调