FastAPI 的 lifespan、startup 和 shutdown 有什么作用?
简化版
FastAPI 的生命周期钩子用于在应用启动和关闭时初始化或释放资源,比如连接池、模型、缓存客户端、定时任务等。新项目更推荐使用 lifespan 上下文管理器,而旧写法常见 @app.on_event("startup") 和 @app.on_event("shutdown")。
详细版
生命周期管理解决的是“应用级资源”的创建与销毁问题。
典型场景包括:
- 启动时创建数据库连接池;
- 启动时加载机器学习模型;
- 启动时初始化 Redis、消息队列客户端;
- 关闭时优雅释放连接;
- 关闭时停止后台任务。
推荐写法:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.redis = await create_redis()
yield
await app.state.redis.close()
app = FastAPI(lifespan=lifespan)
关键点:生命周期钩子管理的是应用级资源,不要把请求级资源放在全局生命周期里复用,比如每个请求独立的数据库 session。
完整版教学
一、为什么需要应用生命周期
一个 Web 应用不是只有“处理请求”这一个动作。它启动前可能要准备资源,关闭时也需要释放资源。如果没有统一生命周期管理,初始化逻辑可能散落在模块导入阶段,关闭逻辑可能完全缺失。
比如应用启动时要创建 Redis 连接池、加载配置、初始化日志、预热机器学习模型。应用关闭时要关闭连接池、停止后台协程、确保缓冲日志写完。生命周期钩子就是为这些场景准备的。
FastAPI 提供了启动和关闭阶段的入口,让资源管理更可控。
二、startup 和 shutdown 的传统写法
早期项目里常见写法是:
app = FastAPI()
@app.on_event("startup")
async def startup():
app.state.cache = await create_cache()
@app.on_event("shutdown")
async def shutdown():
await app.state.cache.close()
startup 在应用开始接收请求前执行,shutdown 在应用关闭时执行。app.state 是存放应用级共享对象的常见位置。
这种写法简单直观,老项目里仍然很多。但新版本 FastAPI 更推荐 lifespan 写法,因为它用一个上下文把资源创建和释放放在一起,可读性更好,也更符合现代 ASGI 生命周期管理方式。
三、lifespan 的推荐写法
lifespan 通常用 asynccontextmanager 实现:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.model = load_model()
app.state.redis = await create_redis_pool()
try:
yield
finally:
await app.state.redis.close()
app = FastAPI(lifespan=lifespan)
yield 前面的代码在启动阶段执行;yield 后面或 finally 中的代码在关闭阶段执行。这样资源的生命周期一眼就能看清楚。
对于复杂系统,lifespan 还可以集中处理多个资源的初始化顺序。如果某个资源初始化失败,应用可以直接启动失败,避免带着半残状态对外提供服务。
四、哪些资源适合放在 lifespan
适合放在 lifespan 的通常是“应用级、可复用、线程或协程安全”的资源。例如:
- 数据库连接池,而不是具体某个请求的 session;
- Redis 连接池;
- HTTP 客户端连接池;
- 机器学习模型对象;
- 配置中心客户端;
- 指标采集器;
- 后台任务调度器。
这些对象的共同特点是:创建成本较高,不应该每个请求都创建一次;并且可以被多个请求安全使用。
请求级资源则不适合直接放在 lifespan 里,比如 SQLAlchemy 的 Session。Session 通常包含事务状态,不应该跨请求共享,应该通过依赖注入在每个请求中创建和关闭。
五、机器学习服务中的典型场景
FastAPI 常被用于模型服务。模型加载可能很慢,如果每次请求都加载模型,接口会非常慢。正确做法是在应用启动时加载一次:
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.model = load_large_model("model.bin")
yield
app.state.model = None
请求处理时只读取 app.state.model:
@app.post("/predict")
def predict(request: PredictIn):
model = app.state.model
return model.predict(request.features)
这样能避免重复初始化的巨大开销。但如果模型推理本身是 CPU/GPU 密集型,还要考虑并发控制、任务队列、批处理和超时保护。
六、常见误区与追问
| 资源 | 适合放 lifespan 吗 | 理由 |
|---|---|---|
| Redis 连接池 | 适合 | 应用级复用,启动创建、关闭释放 |
| 大模型对象 | 适合 | 加载成本高,每请求加载会极慢 |
| SQLAlchemy Session | 不适合 | 请求级事务上下文,不能跨请求共享 |
| 当前登录用户 | 不适合 | 每个请求不同,应放依赖注入 |
启动阶段:加载配置 -> 建连接池 -> 加载模型 -> readiness=true
请求阶段:Depends 创建请求级资源 -> 路由处理 -> 释放请求级资源
关闭阶段:停止接流量 -> 等待请求结束 -> 关闭连接池和后台任务
易错点:lifespan 管应用级资源,Depends 管请求级资源,二者边界错了就容易出并发污染。
第一个误区是把所有初始化都写在模块顶层。模块导入阶段执行复杂初始化,会影响测试、命令行脚本和多进程启动,也不利于错误处理。
第二个误区是把请求级对象做成全局对象。例如把同一个数据库 session 放到 app.state,多个请求共用,会导致事务混乱和并发安全问题。
第三个误区是关闭阶段不处理资源释放。连接池、后台任务和异步客户端如果不关闭,可能在测试环境中出现资源泄漏警告,在生产环境中影响优雅退出。
- 误区:模块导入时初始化大资源最简单。 多进程启动、测试导入、脚本复用都会触发顶层代码,错误也难以纳入应用生命周期管理。
- 误区:把请求级 Session 放进
app.state复用。 Session 携带事务和对象状态,跨请求共享会造成并发安全问题。 - 误区:只写启动逻辑不写关闭逻辑。 Redis、HTTP 客户端、连接池、后台协程都需要在关闭阶段释放,否则影响优雅退出和测试稳定性。
- 追问:
lifespan和startup/shutdown怎么选? 新项目优先 lifespan,因为创建和释放写在一个上下文里更清晰;老项目看到on_event要能读懂。 - 追问:大模型服务为什么常把模型加载放 startup? 如果模型加载耗时 5 秒,每个请求加载都会把延迟放大到不可接受;启动加载一次能避免重复成本。
- 追问:readiness 和 liveness 有什么区别? liveness 只说明进程还活着,readiness 说明依赖资源和初始化已完成,可以接入真实流量。
七、加强记忆
记 FastAPI 生命周期:启动前准备应用级资源,关闭时释放应用级资源;新写法优先 lifespan,旧项目常见 startup/shutdown。连接池、模型、客户端适合放生命周期里;每个请求独立的 session、事务和用户上下文要放依赖注入里。