介紹
Database Warm-up
🧠 一句話理解
數據庫是在應用啟動階段,提前建立數據庫連接 或 執行輕量 SQL 操作,從而 加快首個請求的響應速度 的一種優化手段
🎯 為什么需要數據庫預熱?
當 FastAPI 或其他 Web 服務剛啟動時:
? 你雖然配置了數據庫連接池(比如 SQLAlchemy、asyncpg);
? 但其實它 并不會立即創建數據庫連接;
? 第一個真實的請求進來時,才會懶加載連接;
? 這個首次 handshake 連接建立 + TLS 認證等操作,可能耗時 幾百毫秒甚至幾秒;
? 所以:首個請求會變得異常緩慢 ?
?? 這在性能敏感系統(比如對外開放 API)中可能引起問題。
?? 數據庫預熱做了什么?
典型的預熱操作如下:
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 應用生命周期鉤子實現
@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 后的邏輯!
CTRL+C 或 kill -TERM
才會觸發 yield 之后 的代碼。
? 不會觸發 lifespan 關閉的情況:
情況 | 是否觸發 lifespan 關閉邏輯 |
---|---|
🔁 熱重載(–reload 模式) | ? 不會(因為子進程被強殺) |
💥 crash / 進程強制終止 | ? 不會(沒有機會優雅退出) |
🧪 單元測試意外中斷 | ? 不會(除非框架做了兼容) |