FastAPI 生命周期事件深入解析:在启动前初始化、关闭时释放资源的完整方案
生命周期事件(Lifespan Events)是 FastAPI 应用管理"全局共享资源"的官方入口:它允许你在应用开始接收请求之前恰好执行一次启动逻辑,并在应用处理完所有请求关闭之前执行一次收尾逻辑。本指南以 docs/es/docs/advanced/events.md(对应官方英文文档 events.md)为主线,结合本仓库 docs_src/events 中的完整示例与 tests 中的测试代码,系统讲解现代推荐的 lifespan 写法、Python 异步上下文管理器原理、已废弃的 startup/shutdown 事件方案,以及它们在子应用与测试环境下的真实行为。读完你将能正确管理数据库连接池、共享机器学习模型、GPU 或内存等需要在应用进程生命周期内共享与清理的资源。
为什么需要"应用启动前 / 关闭后"的执行时机
正常情况下,FastAPI 的接口代码只覆盖请求到来之后的这段时间。但你经常需要处理这样两类逻辑:
- 应用启动时:创建全局共享资源,例如建立数据库连接池、加载一个所有请求共用的机器学习模型。
- 应用关闭时:释放这些资源,例如关闭连接、释放 GPU/内存、写入收尾日志。
这类代码应当在应用开始接收请求之前执行一次,并在可能处理了成千上万个请求之后、进程退出前执行一次。由于它正好覆盖应用从启动到关闭的完整时间段,官方文档用 "lifespan"(生命周期)这个词来称呼它。
典型使用场景:共享机器学习模型
想象一个最典型的需求场景:你有若干机器学习模型需要用来处理请求,所有请求共享同一份模型(不是每请求一个、每用户一个)。加载模型可能需要从磁盘读取大量数据、耗时较长,显然不能为每个请求重复加载。
一个直觉做法是在模块/文件顶层直接加载:
# 不推荐:模块顶层加载
model = load_model() # 无论是否需要处理请求,导入模块就会执行
但这样做有个严重副作用:只要你导入这个模块,模型就会被加载。如果此时你只想跑一个与该模型无关的独立自动化测试,导入过程就会被迫等待模型加载完成,测试因此被拖慢。文档原话指出:加载模型应当在请求被处理之前发生,但只应在应用即将开始接收请求的那个时刻,而不是代码被加载的时刻。这正是 lifespan 机制要解决的问题。
现代推荐写法:通过 lifespan 参数 + async context manager
文档强调:定义 startup 与 shutdown 逻辑的推荐方式是使用 FastAPI() 构造时的 lifespan 参数,并搭配一个"上下文管理器"。
先看本仓库 docs_src/events/tutorial003_py310.py 提供的完整示例:
from contextlib import asynccontextmanager
from fastapi import FastAPI
def fake_answer_to_everything_ml_model(x: float):
return x * 42
ml_models = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# 加载 ML 模型(模拟昂贵的启动操作)
ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
yield
# 清理 ML 模型并释放资源
ml_models.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/predict")
async def predict(x: float):
result = ml_models"answer_to_everything"
return {"result": result}
逐段理解这个示例:
yield之前的部分:在启动阶段执行。示例把"模型"塞进ml_models字典,模拟一次昂贵的模型加载。它会在应用开始接收请求之前运行。yield之后的部分:在应用处理完所有请求、即将 shutdown 时执行。示例调用ml_models.clear()把模型移出内存,实际工程里这里通常做释放内存、GPU 显存或关闭连接的收尾工作。- shutdown 发生的时机是停止应用的时候,例如发布新版本需要重启服务,或你主动结束运行。文档还给了个幽默注释:"或者你只是厌倦了运行它 🤷"。
yield 异步生成器与 yield 依赖的相似性
注意一个关键点:这里定义的 lifespan() 是一个带 yield 的 async def 函数,与 FastAPI 的"带 yield 的依赖项"(Dependencies with yield)在形态上非常接近。本质上 FastAPI 把这类函数当作一个生成器来驱动:
yield之前的分支在应用开始前执行;yield之后的分支在应用结束后执行。
装饰器 @asynccontextmanager:把函数变成 async context manager
示例中的函数被 @asynccontextmanager 装饰,从而转换为一种被称为 async context manager(异步上下文管理器) 的对象。
普通 Python 开发者都熟悉同步 context manager,例如 open() 可以用在 with 语句中:
with open("file.txt") as file:
file.read()
较新版本的 Python 还支持异步上下文管理器,配合 async with 使用:
async with lifespan(app):
await do_stuff()
无论是 context manager 还是 async context manager,其执行契约是相同的:进入 with/async with 代码块之前,先执行 yield 之前的代码;退出代码块时,执行 yield 之后的代码。
在我们的示例里并没有直接手写 async with lifespan(app)——我们把 lifespan 这个 async context manager 整体交给 FastAPI,由框架在合适时机自动进入/退出它。FastAPI 应用的 lifespan 参数正是接收一个 async context manager,因此可以像第 22 行那样直接传 lifespan=lifespan。
从源码角度看,fastapi/applications.py 中 FastAPI.__init__ 的 lifespan 参数(约 L527-L538)的类型标注为 Lifespan[AppType] | None,其文档字符串明确说明:"一个 Lifespan 上下文管理器处理器,用一个 context manager 取代 startup 和 shutdown 函数"。该值随后与 on_startup、on_shutdown 一起透传给底层的路由/ASGI 应用构造。
用测试代码验证 startup / shutdown 的执行时序
仓库 tests/test_tutorial/test_events/test_tutorial003.py 用 TestClient 严格验证了上述时序:
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from docs_src.events.tutorial003_py310 import (
app,
fake_answer_to_everything_ml_model,
ml_models,
)
def test_events():
assert not ml_models, "ml_models should be empty"
with TestClient(app) as client:
assert ml_models["answer_to_everything"] == fake_answer_to_everything_ml_model
response = client.get("/predict", params={"x": 2})
assert response.status_code == 200, response.text
assert response.json() == {"result": 84.0}
assert not ml_models, "ml_models should be empty"
这段测试精确印证了文档描述的整个生命周期:
- 进入
with TestClient(app) as client之前,ml_models为空——说明此时启动逻辑尚未执行(这也是使用 lifespan 而非模块顶层加载的好处:不进入应用上下文就不会加载模型); - 进入
with块、TestClient 启动应用后,ml_models中已存在模型——yield前的 startup 逻辑已运行; client.get("/predict", params={"x": 2})正常返回{"result": 84.0}(2 * 42),请求期间共享资源可用;- 退出
with块后,ml_models再次为空——yield后的 shutdown 清理逻辑已被执行。
顺带一提:该文件中的 test_openapi_schema 证明 lifespan 的存在不会影响 OpenAPI schema 的生成,/predict 接口照常出现在 /openapi.json 中。
已废弃的替代方案:startup 与 shutdown 事件
文档专门用一整节说明:存在一种更古老的写法——通过 @app.on_event("startup") 和 @app.on_event("shutdown") 注册事件处理器。但这套方案已经废弃(deprecated),源码中 FastAPI.on_event(fastapi/applications.py 约 L4653 起)与 APIRouter.on_event(fastapi/routing.py 约 L6423 起)的文档字符串均带有 @deprecated 装饰器与明确的弃用说明:"on_event is deprecated, use lifespan event handlers instead(on_event 已弃用,请改用 lifespan 事件处理器)"。测试目录中凡使用 on_event 的地方也都需要显式屏蔽 DeprecationWarning,例如 tests/test_router_events.py。
文档特别给出警告:推荐方式只有上面介绍的 lifespan 参数。如果提供了 lifespan,则 startup/shutdown 事件处理器将不再被调用。二选一,不能同时使用。(原文:"It's all lifespan or all events, not both.")如果你刚开始接触 FastAPI,文档建议直接跳过本节。
警告
- lifespan 与 startup/shutdown 事件互斥:提供了 lifespan 就不要再注册事件处理器
- on_event 在当前源码中已被 @deprecated 标记
- 事件处理器可以用 async def 或普通 def 声明
尽管如此,了解这套旧写法仍然有价值——大量存量项目还在使用它,遇到老代码时你需要能读懂。
startup 事件示例
注册一个在应用启动前执行的函数,事件类型传 "startup":
from fastapi import FastAPI
app = FastAPI()
items = {}
@app.on_event("startup")
async def startup_event():
items["foo"] = {"name": "Fighters"}
items["bar"] = {"name": "Tenders"}
@app.get("/items/{item_id}")
async def read_items(item_id: str):
return items[item_id]
这段代码来自 docs_src/events/tutorial001_py310.py。这里 startup 处理器向作为内存"数据库"的 items 字典预填了两条数据。文档补充了两个重要行为约定:
- 可以注册多个
startup事件处理器; - 应用不会开始接收请求,直到所有
startup处理器都执行完毕。
shutdown 事件示例与同步/异步的选择
注册关闭时执行的函数,事件类型传 "shutdown"。示例来自 docs_src/events/tutorial002_py310.py:
from fastapi import FastAPI
app = FastAPI()
@app.on_event("shutdown")
def shutdown_event():
with open("log.txt", mode="a") as log:
log.write("Application shutdown")
@app.get("/items/")
async def read_items():
return [{"name": "Foo"}]
这里 shutdown 处理器向 log.txt 追加一行 "Application shutdown"。文档借此引出一个非常实用的选型要点:
open()的mode="a"表示 append(追加),新行会接在文件已有内容之后,不会覆盖旧内容;- 这里使用的是标准 Python 同步
open(),它涉及磁盘 I/O、"等待"数据落盘,但open()本身不基于async/await; - 因此处理器声明为普通
def shutdown_event(),而非async def——当处理器内部调用的是同步阻塞型 I/O 函数时,用普通def声明即可,FastAPI 会在后台线程池中运行它,避免阻塞事件循环。
为什么旧方案最终让位于 lifespan
文档点明了旧方案的结构性缺陷:真实项目里 startup 与 shutdown 的逻辑往往是配对的——启动某物就要结束它、获取资源就要释放它。把这些配对逻辑拆成两个互相隔离的函数,只能通过模块级全局变量或类似技巧在两者间传递状态,非常别扭。
而 lifespan 用一个函数把"启动→运行→关闭"完整地收拢在同一个作用域内:yield 之前初始化、yield 之后清理,中间需要共享的变量天然就是函数局部变量或闭包状态,无需全局变量。这正是它被推荐为现代唯一写法的根本原因。
生命周期事件与子应用 / 挂载路由的边界
文档在最后专门用 🚨 提醒一个极易踩坑的边界:lifespan 事件(startup 与 shutdown)只会针对主应用执行,不会为 Sub Aplicaciones(子应用)(通过 mount 挂载的 ASGI 应用)执行。
也就是说,如果你的 FastAPI 应用用 app.mount("/other", other_app) 挂载了另一个独立的 ASGI 应用,那个子应用自身注册的 lifespan/startup/shutdown 逻辑在父应用的生命周期里不会被触发——挂载的子应用本质上是被当作一个普通路由/中间件处理对象接入的,其进程级生命周期由父应用统一管理。如果你确实需要子应用初始化资源,应在父应用的 lifespan 中统一处理,或让子应用自行惰性初始化资源。
需要区分的一个概念是:mount 挂载"独立 ASGI 子应用"与 include_router 包含"同一应用内的路由"并不相同。从本仓库测试 tests/test_router_events.py 可以看到,通过 include_router 纳入的路由器上注册的 on_event("startup")/on_event("shutdown") 处理器(test_router_events),甚至路由器级 APIRouter(lifespan=...) 的启动/关闭逻辑(test_router_nested_lifespan_state 等),都会在应用生命周期中被合并执行——测试用 app_startup / router_startup / sub_router_startup 等状态标志逐阶段断言了这些回调的执行顺序,并验证了多级 lifespan 的 yield 值会按"父级优先、子级覆盖同键"的规则合并。也就是说,本文讨论的"只作用于主应用"限制专指 mount 挂载的独立子应用,而 include_router 引入的路由(含其事件)归属于主应用,会正常生效。
技术细节:这一切底层的 ASGI Lifespan 协议
最后是文档留给好奇者的技术彩蛋:在 ASGI 技术规范层面,这套机制对应 Lifespan Protocol(生命周期协议),它定义了 startup 与 shutdown 两种事件。当 ASGI 服务器(如 Uvicorn)启动应用进程时,会向应用发送 lifespan 消息,驱动我们上面编写的 context manager 完成初始化;服务器优雅停机时,再发送对应的关闭消息,触发 yield 之后的清理逻辑。FastAPI 文档建议进一步阅读 Starlette 官方的 lifespan 处理文档,其中包括如何借助 lifespan 向应用各处注入共享状态(FastAPI 的 Request.state 等机制即与之相关)。从本仓库的 APIRouter 多级 lifespan 测试可以看出,当前实现已支持把 yield 吐出的状态字典逐层合并后暴露给请求处理代码,这是做全链路依赖注入时非常有用的进阶能力。
小结:一张图掌握选择依据
| 关注点 | 结论 |
|---|---|
| 推荐做法 | FastAPI(lifespan=async_context_manager),yield 前启动、yield 后清理 |
| 适用资源 | 数据库连接池、共享 ML 模型、GPU/内存、长连接客户端等全应用共享资源 |
| 旧写法 | @app.on_event("startup"/"shutdown"),已废弃,源码中带 @deprecated 标记 |
| 互斥规则 | 提供了 lifespan 后,startup/shutdown 事件不再执行,二者只能选其一 |
| 覆盖范围 | 只覆盖主应用;mount 挂载的独立子应用不会被触发 |
| 测试方式 | with TestClient(app) 进入/退出即完整驱动 startup 与 shutdown(见 tests/test_tutorial/test_events/test_tutorial003.py) |
| 底层协议 | ASGI Lifespan Protocol 的 startup / shutdown 消息 |
参考实现与证据文件:完整代码示例见 docs_src/events,lifespan 参数与 on_event 弃用标记见 fastapi/applications.py 与 fastapi/routing.py,生命周期时序验证见 tests/test_tutorial/test_events/test_tutorial003.py 及 tests/test_router_events.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00