用 FastAPI 封装离线模型服务时,超时和队列积压通常是同一件事的两面:请求已经到达服务端,但模型执行不过来,后续请求只能在队列里等待。FastAPI 默认把同步接口放进线程池执行,但这个线程池只解决“不阻塞事件循环”的问题,不保证并发请求都能在客户端超时时间内完成。调参之前先确认超时是发生在排队阶段还是推理阶段,否则加大队列只会把问题往后推。
判断摘要:先区分“单请求慢”和“并发后超时”。单请求正常、并发后才超时,优先检查 worker 进程数、线程池上限和模型是否串行推理;队列积压只是结果不是原因。调参顺序建议:先定并发处理模型,再调节队列容量;每次只改一个参数,用响应时间分布和排队时长验证,不要一次性加大队列,否则只会掩盖超时。
先确认超时发生在哪一段
在改参数之前,先让每个请求带上两个时间:一个是从请求进入到响应返回的总时长,一个是模型开始执行的时间。中间件里可以取到前者,业务函数开头可以取到后者。
@app.middleware("http")
async def log_total_time(request, call_next):
start = time.time()
response = await call_next(request)
logger.info("total=%.3f pid=%s", time.time() - start, os.getpid())
return response用日志比较这两个时间:
- 请求进入后很快返回,说明服务端没接到请求或客户端/网关超时设置过短。
- 日志里 total 很长,但业务函数一开始就执行了,说明超时主要发生在模型推理阶段。
- 业务函数迟迟不执行,等了一段时间才进入推理,说明请求在线程池或队列里排队。
FastAPI 并发参数的调节方向
离线模型场景下,主要调节三个地方:worker 进程数、线程池上限、客户端超时时间。FastAPI 默认线程池上限通常是 40,但这个数值对 CPU/GPU 密集的推理任务并不一定合适。
worker 进程可以由 Uvicorn 启动:uvicorn main:app `--workers` 4。如果模型是 PyTorch 或 TensorFlow 且只跑 CPU,多进程可以让多个请求真正并行,但每个进程都会重新加载模型,内存开销会成倍增加;GPU 场景还要确认显存能放下多份模型。
线程池大小如果只调 FastAPI 默认线程池,可以这样改:
from anyio.to_thread import current_default_thread_limiter
@app.on_event("startup")
async def set_thread_pool():
limiter = current_default_thread_limiter()
# 从 CPU 核心数的 2-4 倍开始试,结合日志调整
limiter.total_tokens = 8注意,这个设置只影响 FastAPI 用它执行同步接口的线程池容量。模型推理如果占用 GIL,或者 GPU 上任务本身就是串行提交的,多线程不会带来线性收益。线程数超过合理范围后,线程切换开销会让平均响应时间更差。
客户端超时建议先看服务端单次推理时间的波动范围,再把超时设成“模型最坏推理时间 + 允许的排队时间”。不建议盲目设成 1 秒或 60 秒,前者在模型稍慢时就误报,后者会让一堆半死请求挂在连接上。
队列积压的观察方式
FastAPI 没有直接提供一个“队列长度”参数,积压体现为请求长时间处于等待状态。要观察它,可以在日志里分别记录“请求进入时间”和“推理开始时间”,两者之差就是排队时间。压测时可以用 hey -n 200 -c 20 http://127.0.0.1:8000/predict 这类命令,注意压测工具所在的机器本身也可能成为瓶颈,观察的重点是不同并发下服务端日志里的排队时间分布。
如果进程 CPU 已经打满,但请求仍然排队,说明处理能力到了上限,应该考虑多进程或换更快的推理方式;如果 CPU 还有空闲但请求排队,说明线程池上限或 worker 数设置偏小。
当服务端确实处理不过来时,有界队列配合快速失败比无限排队更可控。FastAPI 本身不直接提供队列长度参数,可以在网关或中间件层实现:当服务端积压请求达到阈值时,直接返回 503 或自定义错误,让客户端尽快重试,而不是一直占用连接。
参数调节顺序与风险边界
建议按下面的顺序操作,每一步都用日志验证再进入下一步:
- 先单独请求一次,确认模型推理时间和接口总耗时都稳定。
- 用低并发(2-4)测试,观察是否出现超时,如果出现,先看是排队还是推理。
- 逐个调整 worker 数和线程池上限,每次只改一个参数,记录日志中的排队时间、推理时间和失败请求数。
- 确认处理能力无法满足峰值时,设置明确的拒绝策略,而不是把队列长度调到很大。
风险边界:调大线程池上限不能解决 GIL 或 GPU 串行推理问题;增加 worker 数会带来多份模型的内存/显存开销;队列调大只是缓存请求,不提升处理速度;用无限超时掩盖排队只会让故障更难发现。最后的判断依据始终是日志里能看到的排队时间、请求开始执行时间和响应时间分布,而不是某个单一参数的大小。