介绍
Database Warm-up
🧠 一句话理解
数据库 是在应用启动阶段,提前建立数据库连接 或 执行轻量 SQL 操作 ,从而 加快首个请求的响应速度 的一种优化手段
🎯 为什么需要数据库预热?
当 FastAPI 或其他 Web 服务刚启动时:
• 你虽然配置了数据库连接池(比如 SQLAlchemy、asyncpg);
• 但其实它 并不会立即创建数据库连接;
• 第一个真实的请求进来时,才会懒加载连接;
• 这个首次 handshake 连接建立 + TLS 认证等操作,可能耗时 几百毫秒甚至几秒;
• 所以:首个请求会变得异常缓慢 ⏳
⚠️ 这在性能敏感系统(比如对外开放 API)中可能引起问题。
⚙️ 数据库预热做了什么?
典型的预热操作如下:
python
async with AsyncSessionLocal() as session:
await session.execute(text("SELECT 1"))
步骤 | 描述 |
---|---|
1️⃣ | 创建异步数据库连接池(第一次真的连数据库) |
2️⃣ | 从连接池获取一个连接 |
3️⃣ | 发送一个轻量 SQL(通常是 SELECT 1) |
4️⃣ | 等待数据库返回结果,确认连接成功 |
✅ | 整个过程完成后,连接池中已存在可复用连接 |
框架 | 推荐方式 | 示例 |
---|---|---|
FastAPI | 使用 lifespan 生命周期钩子 | app = FastAPI(lifespan=lifespan_manager) |
Flask | 用 before_first_request 钩子 | @app.before_first_request |
Django | 通常在 AppConfig.ready() 或 middleware 中做 |
🔍 是否必要?
场景 | 是否推荐 |
---|---|
⛺️ 开发环境 | ✅ 推荐(方便调试,避免首个请求卡顿) |
🚀 生产环境 | ✅ 推荐(改善首请求响应时间,提升体验) |
🧪 单元测试 | 可选(一般会显式创建连接) |
数据库预热是通过在应用启动时提前"探路"数据库,确保连接池中已有活跃连接,首个请求过慢
连接与握手
-
FastAPI 起服务后是否和 PostgreSQL 建立了"管道"?
-
TLS 连接握手是在什么时候发生的?
-
是否每次请求都要握手?是否可以复用?
-
这些握手服务器是否有记录?
-
有没有专门的"连接协议"来优化这件事?
• 是的 ,应用一旦通过 SQLAlchemy 创建连接池,就与 PostgreSQL 建立了 TCP/TLS 连接。
• TLS 握手只发生在连接建立时 (第一次连接时),后续复用连接不会再进行 TLS 握手。
• 如果你使用了连接池,那么连接是"长连接",可以避免重复握手、认证。
• PostgreSQL 服务器和客户端(如 asyncpg)都会记录连接状态和握手信息。
• Postgres 的连接是通过 PostgreSQL Wire Protocol(私有协议)完成的,包括认证、SSL 握手等。
🧪 一次完整连接流程(含 TLS)
1. 客户端连接到 PostgreSQL TCP 端口(默认 5432)
2. 客户端发送 StartupMessage 请求开启连接
3. 如果配置为 sslmode=require 或 verify-full,则:
• 服务端发送 SSLRequest 响应
• 双方协商 TLS(证书交换 + 加密算法)
• TLS 握手完成后,连接进入加密通道状态
4. 客户端使用用户名密码进行认证(如 MD5、SCRAM-SHA-256)
5. 服务端认证通过,连接建立完成
✅ 此连接被连接池维护和复用,后续请求不会再次握手。
✔️ **这个"管道"就是 持久 TCP + TLS 连接 ,只要没有超时或被关闭,就可以反复使用,握手不会再来一次。
虽然 PostgreSQL 使用的是私有协议,但在连接池层面存在专门优化方案:
技术 | 描述 |
---|---|
pgbouncer | 轻量级 PostgreSQL 连接池代理,支持连接复用 |
asyncpg + SQLAlchemy | 内建连接池,可控制连接生命周期和大小 |
keepalive | OS 层 TCP 连接保活设置,防止连接过早断开 |
重启后之所以需要重新握手,本质上是因为"客户端连接池已被清空,原有加密连接(TLS 会话)丢失了"。
无论是 SQLAlchemy、asyncpg 还是 pgbouncer,连接池的目的就是为了复用 TCP/TLS 连接,避免每次都握手。
但注意:
⚠️连接池是存在于内存中的!
• 应用一重启,连接池(和里面的连接)都会被清空
• 再次发起数据库请求,就只能重新创建连接(包含 TLS 握手)
场景 | 建议做法 |
---|---|
重启后第一次请求慢 | ✔️ 使用 lifespan 钩子做数据库"预热"连接(你已经做了) |
多服务场景 | ✔️ 使用 pgbouncer 这类连接池中间件,在服务器端管理连接 |
请求敏感的场景 | ✔️ 在服务初始化脚本中做一个"健康请求",先手动 warm up |
TLS 每次新建连接性能不理想 | ✔️ 关闭 TLS(内网可用)或使用 TLS session reuse(PostgreSQL 暂不支持) |
预热操作
FastAPI 应用生命周期钩子实现
python
@asynccontextmanager
async def lifespan_manager(_app: FastAPI) -> AsyncGenerator[None, None]:
# ✅ 应用启动时执行,预热数据库连接池,避免首次请求时连接池未预热导致请求变慢
try:
async with AsyncSessionLocal() as session:
await session.execute(text("SELECT 1"))
logger.info("✅ 数据库连接池预热成功")
except Exception as e:
logger.error(f"❌ 数据库连接池预热失败: {e}")
yield
try:
await engine.dispose()
logger.info("🛑 数据库连接池已关闭")
except Exception as e:
logger.warning(f"⚠️ 关闭数据库连接池时出错: {e}")
✅ 正确解释:FastAPI 只有在"优雅退出"时才会运行 yield 后的逻辑!
python
CTRL+C 或 kill -TERM
才会触发 yield 之后 的代码。
❌ 不会触发 lifespan 关闭的情况:
情况 | 是否触发 lifespan 关闭逻辑 |
---|---|
🔁 热重载(--reload 模式) | ❌ 不会(因为子进程被强杀) |
💥 crash / 进程强制终止 | ❌ 不会(没有机会优雅退出) |
🧪 单元测试意外中断 | ❌ 不会(除非框架做了兼容) |