Fastapi 最快的序列化返回-ORJSONResponse
Python 开发

Fastapi 最快的序列化返回-ORJSONResponse

JACIN··10 分钟阅读

ORJSONResponse 确实比 FastAPI 默认的 JSONResponse 快很多,通常被称为目前 Python 生态中最快的 JSON 序列化方案。 image|690x447

以下是它为什么快的核心原因(底层原理拆解):

1. 核心差异:跳过了“字符串编码”这一步 (最关键)#

这是 orjson 能够秒杀标准库 json 的最大杀手锏。

  • 标准 json (默认):
    1. 把 Python 对象转换成 Python 字符串 (str)
    2. Web 服务器(Uvicorn/Starlette)把字符串 编码 (Encode)字节 (bytes) (通常是 UTF-8)。
    3. 通过网络发送字节。
    • 缺点: 只要是 Web 传输,最终必须是 bytes。先转成 str 再转成 bytes 是多余的步骤,且 Python 的字符串在内存中开销很大。
  • orjson:
    1. 把 Python 对象 直接 序列化成 UTF-8 字节 (bytes)
    2. 通过网络发送字节。
    • 优势: 少了一次全量的内存拷贝和编码转换。对于几 MB 的大 JSON 来说,这一步能节省大量的 CPU 时间。

2. 实现语言:Rust vs C/Python#

  • 标准 json: 虽然也是 C 语言写的,但它是为了通用性和兼容性设计的,历史包袱重,很多逻辑还需要回调 Python 解释器。
  • orjson: 是用 Rust 编写的。
    • 它利用了 Rust 极致的内存管理安全性。
    • 使用了 SIMD (单指令多数据流) 技术来加速解析和生成。
    • 它是专门为“速度”而生的,牺牲了一些极少用到的灵活性,换取了极致的性能。

3. 原生支持复杂类型 (无需回调)#

这是处理数据库数据时快的主要原因。

  • 标准 json: 遇到 datetime (时间)、numpy 数组、UUID 等类型时,它不知道怎么处理。你必须提供一个 default 函数(比如 str(obj)),这会导致每遇到一个时间字段,都要切回 Python 代码执行一次函数调用,开销极大
  • orjson: 原生内置 了对 datetime (RFC 3339 格式)、numpyUUIDdataclasses 的支持。它在 Rust 层面直接处理这些类型,不需要切回 Python,速度有数量级的提升。

性能差距有多少?#

  • 微基准测试 (Micro-benchmarks): 序列化纯数据,orjson 通常比标准库快 5倍 到 10倍
  • 实际 Web 场景:
    • 如果你的接口只返回 {"msg": "ok"},那没区别(因为瓶颈在网络 IO)。
    • 如果你的接口返回一个 几千行的列表(比如查询数据库返回 1000 条用户记录,包含时间字段),使用 ORJSONResponse 可能会让整个接口响应时间缩短 20% - 50%
python
pip install orjson

#配置
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

# 设置为全局默认,所有路由都会自动享受加速
app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/items")
def read_items():
    # 这里返回大列表时,速度会飞快
    return [{"id": i, "name": f"Item {i}", "created_at": "2024-01-01"} for i in range(5000)]

1. 标准库 json 的做法:层层扒皮#

假设你要把一个 Python 字典 { "name": "Jacin" } 发给前端。标准库是这么干的:

  1. Python 对象:拿到字典。
  2. 翻译成 Python 字符串 (最慢的一步)
  • 它会在内存里创建一个巨大的 Python str 对象。
  • Python 的字符串在内存里是非常“重”的(为了支持 Unicode,它有很多元数据)。
  • 结果:u'{"name": "Jacin"}' (存在内存里)。
  1. 编码成字节 (Encode)
  • 网络传输不能传 Python 对象,只能传 0101 的字节。
  • 所以它必须把上面那个巨大的字符串,再进行一次 UTF-8 编码。
  • 结果:b'{"name": "Jacin"}'
  1. 发送

缺点: 脱裤子放屁。中间那个“Python 字符串对象”不仅占内存,而且创建和销毁它非常耗时。

2. orjson 的做法:直达终点#

orjson 是用 Rust 写的,它极其“鸡贼”:

  1. Python 对象:拿到字典。
  2. 直接写内存 (Rust)
  • 它跳过了 Python 的 str 系统。
  • 它直接在内存里开辟一块空间,按照 JSON 规范,把 { "name": "Jacin" }UTF-8 字节码 填进去。
  • 结果:直接拿到 b'{"name": "Jacin"}'
  1. 发送

优势: 少了一次“全量数据”的内存拷贝和格式转换。 如果你的 JSON 有 10MB 大:

  • 标准库:先造 10MB 的字符串对象 -> 再转成 10MB 的字节流 -> 消耗 20MB 内存 + CPU 转换时间。

  • orjson:直接造 10MB 字节流 -> 消耗 10MB 内存 -> 结束。 B. 避免“回调 Python” (No GIL)

  • 标准库:遇到 datetime 时间格式,它不懂,它得停下来,去问 Python 解释器:“嘿,这个时间对象怎么转字符串?”(这涉及到 Python 函数调用,很慢)。

  • orjson:它在 Rust 代码里写死了对 datetime 的支持。它看到时间对象,直接在 Rust 层面就给格式化了,根本不通知 Python。这不仅快,还减少了 GIL(全局解释器锁)的争用。

评论

还没有评论,来发第一个吧