FastAPI 效能與最佳化:在多核心環境下使用 Gunicorn + Uvicorn Workers
簡介
在現代的 Web 應用程式中,效能往往是使用者體驗與營運成本的關鍵因素。FastAPI 以其非同步(async)特性與自動產生 OpenAPI 文件的便利性,已成為 Python 生態系中最受歡迎的框架之一。但即使是最快的框架,若只跑在單一執行緒或單一核心上,也無法發揮硬體的全部潛能。
本單元將說明如何在 多核心 CPU 上部署 FastAPI,結合 Gunicorn(作為主控程式)與 Uvicorn workers(作為 ASGI 伺服器)來同時處理大量請求。透過正確的設定與調校,你的 API 可以在同一台機器上同時跑上 數十甚至上百個工作執行緒,大幅提升吞吐量與響應速度。
核心概念
1. 為什麼需要多個 Worker?
- CPU 核心利用率:單一 Uvicorn worker 只會佔用一個 CPU 核心。若伺服器有 8 核心,僅跑一個 worker 就只能使用 12.5% 的 CPU。
- 阻塞 I/O:即使使用
async,仍有可能因外部資源(資料庫、檔案系統)阻塞而導致 worker 空閒。多 worker 能在一個 worker 被阻塞時,讓其他 worker 繼續服務請求。 - 容錯與彈性:當某個 worker 發生未捕獲的例外並崩潰時,Gunicorn 會自動重新啟動它,確保服務不中斷。
2. Gunicorn 與 Uvicorn 的角色
| 元件 | 功能 | 為什麼要一起使用 |
|---|---|---|
| Gunicorn | WSGI/ASGI 的 process manager,負責啟動、監控、重啟 worker。 | 提供 pre‑fork 模式,使多個 worker 以獨立的 Python process 執行,避免 GIL(Global Interpreter Lock)的限制。 |
| Uvicorn | 高效能的 ASGI server,支援 asyncio、uvloop、httptools 等底層加速庫。 |
作為 Gunicorn 的 worker 程式,負責實際的 HTTP 請求處理與路由分發。 |
重點:Gunicorn 本身只能跑 WSGI 應用,若要跑 ASGI(如 FastAPI),必須透過
uvicorn.workers.UvicornWorker作為 worker 類別。
3. 計算適當的 Worker 數量
最常見的公式是:
workers = (cpu_cores * 2) + 1
cpu_cores:機器的實體核心數(不含超執行緒)。+1:確保即使所有核心皆忙碌,仍有一個備用 worker 可接受新連線。
當你的服務主要是 I/O 密集型(例如大量資料庫查詢、外部 API 呼叫),可以適度 增加 worker 數量;若是 CPU 密集型(例如大量影像處理),則不宜超過實體核心數。
4. Uvicorn Worker 的常用參數
| 參數 | 說明 | 範例 |
|---|---|---|
--workers / -w |
設定 worker 數量(由 Gunicorn 控制)。 | -w 5 |
--bind / -b |
綁定的 IP:Port。 | -b 0.0.0.0:8000 |
--log-level |
日誌等級(debug、info、warning、error)。 | --log-level info |
--timeout |
worker 無回應的最大秒數,超過則重啟。 | --timeout 30 |
--keep-alive |
HTTP keep‑alive 時間(秒)。 | --keep-alive 5 |
程式碼範例
以下示範三個常見的部署方式,從最簡單的單一 worker 到正式環境的 Docker + Systemd 配置。
範例 1:最簡單的本機測試(單一 worker)
# 直接使用 uvicorn(不需要 gunicorn)
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
說明:
--reload只適合開發環境,會在程式碼變更時自動重新載入。此指令只會啟動 一個 worker,適合測試功能。
範例 2:使用 Gunicorn + Uvicorn Workers(多核心)
# 假設 CPU 有 4 核心,依公式計算 workers = (4*2)+1 = 9
gunicorn main:app \
-w 9 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--log-level info \
--timeout 30 \
--keep-alive 5
重點:
-k uvicorn.workers.UvicornWorker告訴 Gunicorn 使用 Uvicorn 作為 ASGI worker。-w 9為 9 個獨立的 Python process,每個 process 內部仍然是單執行緒的 event loop。
範例 3:Docker 化部署(Dockerfile + docker‑compose)
Dockerfile
# 使用官方的 Python slim 版映像
FROM python:3.11-slim
# 安裝系統相依套件(例如 gcc、libpq-dev)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && rm -rf /var/lib/apt/lists/*
# 建立工作目錄
WORKDIR /app
# 複製需求檔與安裝
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 複製程式碼
COPY . .
# 設定環境變數
ENV PYTHONUNBUFFERED=1
# 使用 gunicorn 作為入口點
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]
docker-compose.yml
version: "3.9"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- TZ=Asia/Taipei
restart: unless-stopped
# 可根據宿主機 CPU 核心數調整 workers
command: >
gunicorn main:app
-w ${WORKER_COUNT:-4}
-k uvicorn.workers.UvicornWorker
-b 0.0.0.0:8000
--log-level info
說明:
WORKER_COUNT可以在執行docker-compose up -d前以環境變數方式覆寫。- Docker 容器內的 CPU 限制可以透過
docker run --cpus="2"或docker-compose.yml的deploy.resources.limits.cpus來限制,從而讓workers設定更貼近實際可用核心。
範例 4:使用 Systemd 管理服務(適合在實體 VM 或裸機上)
# /etc/systemd/system/fastapi.service
[Unit]
Description=FastAPI with Gunicorn/Uvicorn
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/fastapi
Environment="PATH=/opt/fastapi/venv/bin"
ExecStart=/opt/fastapi/venv/bin/gunicorn main:app \
-w 8 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--log-level info \
--timeout 30 \
--keep-alive 5
Restart=on-failure
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
# 啟用與啟動服務
sudo systemctl daemon-reload
sudo systemctl enable fastapi.service
sudo systemctl start fastapi.service
重點:Systemd 可以自動監控服務狀態,發生崩潰時會自動重啟,且支援
LimitNOFILE(檔案描述子上限)以避免因大量連線導致「Too many open files」錯誤。
範例 5:動態調整 Worker 數量(使用 preload_app 與 --preload)
在某些情況下,資料庫連線池或 大型模型(如機器學習模型)需要在所有 worker 之間共享初始化成本。Gunicorn 的 --preload 參數可以在 fork 前先載入應用程式。
gunicorn main:app \
-w 6 \
-k uvicorn.workers.UvicornWorker \
--preload \
-b 0.0.0.0:8000
注意:使用
--preload時,所有 worker 會共享同一個記憶體區段(寫時複製),若應用程式在啟動時建立了全域的資料庫連線,必須在startup事件中重新建立連線,否則會因連線在 fork 後失效而產生錯誤。
# main.py
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
app = FastAPI()
engine = None
SessionLocal = None
@app.on_event("startup")
def startup_event():
global engine, SessionLocal
# 在每個 worker 內部重新建立連線池
engine = create_engine("postgresql://user:pw@db:5432/mydb")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
忘記設定 workers |
只跑單一核心,效能低下。 | 依照 CPU 核心數使用公式 (cores * 2) + 1。 |
在 --preload 後使用全域 DB 連線 |
子進程繼承的連線失效,拋出 psycopg2.OperationalError。 |
在 startup 事件內部重新建立連線或使用 @app.on_event("shutdown") 正確關閉。 |
過度設定 workers 超過實體核心 |
產生過多上下文切換,CPU 使用率飆升,反而變慢。 | 觀察 htop/top,保持在合理範圍。 |
未調整 ulimit -n |
大量併發時出現 OSError: [Errno 24] Too many open files。 |
在 Systemd 或容器內設定 LimitNOFILE 或 ulimit -n 65535。 |
| 使用同步函式阻塞事件迴圈 | 單個 worker 被卡住,其他請求被延遲。 | 盡量使用 async 函式;若必須使用同步 I/O,將其包在 run_in_threadpool 或 starlette.concurrency.run_in_threadpool。 |
忽略 keep-alive 設定 |
連線頻繁建立與關閉,增加 TCP 三次握手開銷。 | 設定 --keep-alive 5(或根據需求調整)。 |
進階最佳實踐
- 使用
uvloop:在main.py內部載入uvloop,可提升事件迴圈效能。import uvloop import asyncio asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - 啟用
httptools:Uvicorn 預設已使用httptools,確保安裝時包含uvicorn[standard]。 - 設定適當的
timeout:對於長時間的背景任務,可考慮在 worker 中使用asyncio.wait_for或將任務交給 Celery、RQ 等佇列系統。 - 監控指標:整合 Prometheus + Grafana,透過
prometheus_fastapi_instrumentator收集request_latency_seconds,process_cpu_seconds_total等指標,幫助調校workers數量。 - 使用
--reload僅限開發:生產環境千萬不要使用--reload,會額外產生 file‑watcher 監控,浪費資源。
實際應用場景
| 場景 | 為什麼需要多 worker | 建議配置 |
|---|---|---|
| 高流量的公開 API(如金融行情、天氣資訊) | 每秒可能接收上千個請求,需要快速回應。 | workers = (cpu * 2) + 1,keep-alive = 5,timeout = 30,使用 uvloop + httptools。 |
| 內部微服務(資料驗證、認證) | 請求量較穩定,但對延遲極為敏感。 | workers = cpu,timeout = 10,配合 asyncpg(非同步 PostgreSQL)或 aioredis。 |
| 機器學習模型推論服務 | 每次請求會載入大型模型,CPU/GPU 為瓶頸。 | workers = cpu(避免過度分叉),將模型載入放在 --preload 並在每個 worker 中共享(使用 torch.multiprocessing 或 tensorflow 的多執行緒模式)。 |
| 長輪詢或 WebSocket(即時聊天、遊戲) | 需要保持大量長時間連線。 | workers = cpu,keep-alive 可調高,或使用 Uvicorn 的 --ws 參數,配合 redis Pub/Sub 做訊息分發。 |
| 批次資料處理 API(上傳 CSV、產生報表) | 請求會觸發 CPU 密集型運算。 | 考慮將此類任務交給背景工作者(Celery),API 本身只回應任務 ID,保持 workers 數量在 CPU 數量左右。 |
總結
FastAPI 本身已具備非同步的高效能基礎,而 Gunicorn + Uvicorn workers 則提供了在 多核心 CPU 上水平擴展的完整解決方案。透過正確的 worker 數量、preload、keep‑alive、timeout 與 系統資源限制 等設定,我們可以:
- 最大化硬體利用率,讓每個核心都能發揮作用。
- 提升服務彈性,在 worker 崩潰時自動重啟,確保 99.9% 的可用性。
- 減少阻塞與資源浪費,藉由非同步 I/O 與事件迴圈的結合,讓高併發請求保持低延遲。
在實務上,建議先 在本機或測試環境 用 gunicorn -w 2 -k uvicorn.workers.UvicornWorker 觀察 CPU、記憶體與 I/O 指標,再逐步調整至正式環境的 (cores*2)+1。配合 Prometheus 監控、Systemd 或 Docker Compose 的自動化管理,你的 FastAPI 服務將能在任何規模的部署中保持穩定、快速與可維護。
祝你在打造高效能 API 的路上,玩得開心、跑得更快! 🚀