使用Gunicorn时Pandas apply函数比Uvicorn慢10倍的原因排查
问题:Gunicorn+UvicornWorker运行Pandas处理比纯Uvicorn慢10倍
我用FastAPI+Uvicorn开发Python API服务,切换为Gunicorn(搭配UvicornWorker)后,处理大型DataFrame的函数速度骤降。例如处理含88000行日期字符串的dummy.csv,将YYYY-MM-DD格式转为YYYY-MM时,使用Pandas的apply()和strftime(),Uvicorn仅需0.23秒,Gunicorn却耗时3.28秒,速度慢了10倍。已知Pandas apply本身效率不算高,但不清楚Gunicorn与Uvicorn为何存在如此大的耗时差异,想了解背后原因及自身可能存在的问题。
代码实现
import time import pandas as pd from fastapi import FastAPI app = FastAPI() @app.get("/pd") def panda(): df = pd.read_csv("./dummy.csv") df["date"] = pd.to_datetime(df.date) print(df) start = time.time() df["date"] = df["date"].apply(lambda x: x.strftime("%Y-%m")) end = time.time() print("[TIME]", end - start) print(df)
启动命令
Uvicorn启动
uvicorn main:app
Gunicorn启动
gunicorn -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000
依赖包版本
python==3.10.6 fastapi==0.95 pandas==1.4.2 uvicorn==0.19.0 uvloop==0.17.0 gunicorn==20.1.0 ...
测试结果
Uvicorn运行耗时
0.23秒
date 0 2010-01-04 1 2010-01-04 ... ... 88286 2019-12-31 [88287 rows x 1 columns] [TIME] 0.23171710968017578 date 0 2010-01 ... ... 88286 2019-12 [88287 rows x 1 columns]
Gunicorn运行耗时
3.28秒
date 0 2010-01-04 ... ... 88286 2019-12-31 [88287 rows x 1 columns] [TIME] 3.2823679447174072 date 0 2010-01 ... ... 88286 2019-12 [88287 rows x 1 columns]
原因分析与解决方案
1. 进程模型与资源分配差异
- Uvicorn默认单进程运行,启动时会集中分配资源给单个工作进程,依赖初始化开销仅发生一次;而Gunicorn默认启动与CPU核心数一致的多Worker进程,每个Worker都需要单独初始化Pandas、加载数据,首次请求的初始化开销被放大。如果你的测试是首次请求,这个差异会格外明显。
- 多进程环境下,CPU、内存资源被多个Worker分摊,Pandas底层依赖的NumPy线程优化会因进程间资源竞争导致效率下降。
2. Uvloop事件循环的使用差异
Uvicorn默认启用uvloop作为事件循环(性能优于标准库事件循环),但Gunicorn+UvicornWorker的组合可能未正确加载uvloop,或多进程环境下uvloop的优势无法充分发挥。
3. Pandas apply的低效性(差异放大诱因)
apply()是Python层面的逐行循环,本身就远慢于Pandas的矢量化操作。单进程环境下,单个进程的资源集中使用掩盖了部分开销;但多进程环境下,多个Worker的Python解释器会竞争GIL(全局解释器锁),进一步降低逐行循环的效率。
解决建议
- 替换apply为矢量化操作:这是最有效的优化方式,完全规避Python循环开销。将日期格式转换代码修改为:
矢量化操作由Pandas底层C语言实现,速度比apply快数倍,且多进程环境下的性能差异会大幅缩小。df["date"] = df["date"].dt.strftime("%Y-%m") - 控制Gunicorn Worker数量:如果必须使用多进程,先尝试设置
--workers 1,验证是否与Uvicorn速度一致(确认是否为多进程资源竞争导致)。后续根据实际负载调整Worker数量,避免过度竞争。 - 强制启用uvloop:在Gunicorn启动命令中通过环境变量显式指定事件循环:
UVICORN_LOOP=uvloop gunicorn -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000 - 预热Worker资源:在FastAPI的启动事件中提前完成数据加载或Pandas初始化,避免首次请求的开销:
@app.on_event("startup") def startup_event(): global df df = pd.read_csv("./dummy.csv") df["date"] = pd.to_datetime(df.date)
内容的提问来源于stack exchange,提问作者Wapar
相关产品推荐
相关产品推荐

