02-3. 進階配置技巧
⏱️ 閱讀時間: 12 分鐘 🎯 難度: ⭐⭐⭐ (中等)
🎯 本篇重點
掌握 Gunicorn 的進階配置技巧:preload_app、graceful_timeout、Hook functions、日誌優化等。
📋 進階參數總覽
| 參數 | 作用 | 默認值 | 使用場景 |
|---|---|---|---|
preload_app | 預載入應用 | False | 加快啟動、節省記憶體 |
graceful_timeout | 優雅關閉超時 | 30 秒 | 平滑重載 |
worker_tmp_dir | Worker 臨時目錄 | None | 效能優化 |
limit_request_line | 請求行長度限制 | 4094 | 安全防護 |
limit_request_fields | 請求頭數量限制 | 100 | 安全防護 |
🚀 preload_app(預載入應用)
什麼是 preload_app?
在 Master 進程啟動時就載入應用代碼,然後 fork 出 Workers,而不是每個 Worker 獨立載入。
工作原理對比
沒有 preload_app(默認)
Master 進程啟動
↓
Master: 我要創建 4 個 workers
↓
├─ Worker 1 啟動 → 載入 Django 應用(100MB)→ 就緒
├─ Worker 2 啟動 → 載入 Django 應用(100MB)→ 就緒
├─ Worker 3 啟動 → 載入 Django 應用(100MB)→ 就緒
└─ Worker 4 啟動 → 載入 Django 應用(100MB)→ 就緒
總記憶體:100MB × 4 = 400MB
啟動時間:慢(每個 worker 都要載入)有 preload_app = True
Master 進程啟動
↓
Master: 載入 Django 應用(100MB)
↓
Master: 用 fork() 複製出 4 個 workers
↓
├─ Worker 1(複製 Master)→ 就緒(快!)
├─ Worker 2(複製 Master)→ 就緒(快!)
├─ Worker 3(複製 Master)→ 就緒(快!)
└─ Worker 4(複製 Master)→ 就緒(快!)
總記憶體:約 150MB(共享記憶體)
啟動時間:快(只載入一次)配置方式
# gunicorn.conf.py
# 啟用預載入
preload_app = True優缺點
✅ 優點
加快啟動速度
沒有 preload: 每個 worker 啟動需要 5 秒 × 4 = 20 秒 有 preload: Master 載入 5 秒 + workers fork 1 秒 = 6 秒 提升:3.3 倍節省記憶體(Copy-on-Write)
Linux 的 fork() 使用 Copy-on-Write 機制: Master 載入的代碼(唯讀)→ 所有 workers 共享 只有修改的數據才會複製 節省記憶體:30-50%一致性更好
所有 workers 使用完全相同的代碼版本 避免某些 workers 載入了舊代碼
❌ 缺點
重載需要重啟所有 workers
# 沒有 preload(優雅重載) kill -HUP <master_pid> → 逐個重啟 workers,服務不中斷 # 有 preload(需要完全重啟) kill -HUP <master_pid> → 必須重啟 Master 和所有 workers → 會有短暫停機時間不能在 worker 中打開資料庫連接
# ❌ 錯誤:在模塊層級打開連接 import psycopg2 # 這個連接在 Master 中創建 conn = psycopg2.connect(...) # 危險! # Fork 後所有 workers 共享同一個連接 # 會導致連接競爭和錯誤 # ✅ 正確:在 post_fork hook 中打開 def post_fork(server, worker): # 每個 worker 創建自己的連接 import psycopg2 worker.conn = psycopg2.connect(...)全局變量問題
# ❌ 危險:全局變量會被所有 workers 共享 cache = {} # 在模塊層級 # Master 載入時,cache = {} # Fork 後,所有 workers 共享這個 cache # 但修改時會觸發 Copy-on-Write,導致不一致 # ✅ 正確:使用 Redis 等外部快取 import redis r = redis.Redis()
最佳實踐
場景 1:適合使用 preload_app
# gunicorn.conf.py
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'uvicorn.workers.UvicornWorker'
# 啟用預載入
preload_app = True
# Hook: 在 fork 後初始化資源
def post_fork(server, worker):
"""每個 worker fork 後執行"""
# 重新建立資料庫連接
from django import db
db.connections.close_all()
# 重新連接 Redis
from django.core.cache import cache
cache.close()
worker.log.info(f"Worker {worker.pid} initialized")
# Hook: 在 worker 退出前清理
def worker_exit(server, worker):
"""Worker 退出前執行"""
from django import db
db.connections.close_all()
worker.log.info(f"Worker {worker.pid} exited")適用場景:
- 大型 Django 應用(啟動慢)
- 記憶體有限的環境
- 不需要頻繁重載代碼
- 生產環境穩定版本
場景 2:不適合使用 preload_app
# gunicorn.conf.py
# 開發環境:需要頻繁重載
preload_app = False
# 理由:
# - 代碼經常修改
# - 需要快速重載
# - 啟動速度不是問題⏰ graceful_timeout(優雅關閉超時)
什麼是 graceful_timeout?
Worker 收到關閉信號後,給予的完成當前請求的時間。超過這個時間,強制殺掉。
工作原理
收到重載信號(SIGHUP 或 SIGTERM)
↓
Worker: 停止接收新請求
Worker: 繼續完成當前請求
↓
開始計時(graceful_timeout = 30)
↓
├─ 30 秒內完成 → ✅ 優雅退出
└─ 超過 30 秒 → ❌ 強制殺掉(SIGKILL)配置方式
# gunicorn.conf.py
# 請求處理超時
timeout = 30
# 優雅關閉超時
graceful_timeout = 30
# 建議:graceful_timeout >= timeout場景對比
場景 1:短請求(API 服務)
# gunicorn.conf.py
timeout = 30
graceful_timeout = 30 # 等於 timeout
# 理由:
# - 請求很快(< 1 秒)
# - 30 秒足夠完成所有請求
# - 不需要太長的等待時間場景 2:長請求(報表生成)
# gunicorn.conf.py
timeout = 300 # 5 分鐘
graceful_timeout = 60 # 1 分鐘
# 理由:
# - 正常請求可能需要 2-3 分鐘
# - 但重載時只給 1 分鐘
# - 超過 1 分鐘的請求會被強制中斷
# - 避免重載時等待太久場景 3:WebSocket / 長連接
# gunicorn.conf.py
timeout = 0 # 無限超時
graceful_timeout = 120 # 2 分鐘
# 理由:
# - WebSocket 連接可能持續很久
# - 但重載時給 2 分鐘通知客戶端斷開
# - 客戶端可以重新連接平滑重載流程
# gunicorn.conf.py
workers = 4
timeout = 30
graceful_timeout = 30
# 重載過程:
# 1. 發送信號
# kill -HUP <master_pid>
#
# 2. Master 收到信號
# → 啟動新的 Worker 1'
# → 發送 SIGTERM 給舊的 Worker 1
#
# 3. 舊 Worker 1
# → 停止接收新請求
# → 完成當前請求(最多 30 秒)
# → 優雅退出
#
# 4. 重複步驟 2-3,逐個替換所有 workers
#
# 優點:
# - 服務不中斷
# - 用戶無感知
# - 平滑過渡📁 worker_tmp_dir(臨時目錄優化)
什麼是 worker_tmp_dir?
Worker 用於心跳檢測的臨時文件目錄。默認使用 /tmp,可以改為記憶體文件系統以提升效能。
工作原理
Worker 心跳機制:
Worker 每隔一段時間更新一個臨時文件的時間戳
↓
Master 定期檢查這個文件
↓
├─ 文件時間戳更新了 → Worker 還活著 ✅
└─ 文件時間戳太舊 → Worker 可能掛了 → 重啟配置方式
# gunicorn.conf.py
# 默認(使用磁盤)
worker_tmp_dir = None # 使用 /tmp
# 優化(使用記憶體文件系統)
worker_tmp_dir = '/dev/shm' # Linux
# worker_tmp_dir = '/run/gunicorn' # 也可以效能對比
測試:1000 個 workers,心跳頻率 1 秒
使用 /tmp(磁盤):
- I/O 操作:1000 次/秒
- 磁盤負載:高
- 延遲:1-5 ms
使用 /dev/shm(記憶體):
- I/O 操作:1000 次/秒
- 記憶體負載:極低(只是時間戳)
- 延遲:< 0.1 ms
效能提升:10-50 倍最佳實踐
# gunicorn.conf.py
# Linux 環境(推薦)
worker_tmp_dir = '/dev/shm'
# Docker 環境
worker_tmp_dir = '/dev/shm'
# 需要在 docker run 時掛載:
# docker run --shm-size=512m ...
# macOS 環境
worker_tmp_dir = None # macOS 沒有 /dev/shm,使用默認
# Windows 環境
worker_tmp_dir = None # Windows 使用默認🛡️ 安全配置參數
1. limit_request_line(請求行長度限制)
# gunicorn.conf.py
# 限制 HTTP 請求行的長度(包括方法、URL、HTTP 版本)
limit_request_line = 4094 # 默認 4KB
# 範例請求行:
# GET /api/users?name=John&age=30&city=NYC HTTP/1.1
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 場景 1:一般 Web 應用
limit_request_line = 4094 # 4KB,默認值足夠
# 場景 2:API 服務(複雜查詢參數)
limit_request_line = 8190 # 8KB
# 場景 3:嚴格限制(防止攻擊)
limit_request_line = 2048 # 2KB為什麼需要限制?
# ❌ 攻擊案例:超長 URL
GET /api/search?q=AAAA...(100MB)...AAAA HTTP/1.1
# 問題:
# - 消耗大量記憶體解析
# - 可能導致 DoS 攻擊
# - 緩衝區溢出風險
# ✅ 有限制後:
# → 請求被拒絕(400 Bad Request)
# → 保護服務器2. limit_request_fields(請求頭數量限制)
# gunicorn.conf.py
# 限制 HTTP 請求頭的數量
limit_request_fields = 100 # 默認 100 個
# 範例請求頭:
# Host: example.com
# User-Agent: Mozilla/5.0
# Accept: text/html
# Cookie: session=xxx
# ...
# 場景 1:一般 Web 應用
limit_request_fields = 100 # 默認值
# 場景 2:嚴格限制
limit_request_fields = 50
# 場景 3:允許大量自定義頭
limit_request_fields = 2003. limit_request_field_size(請求頭大小限制)
# gunicorn.conf.py
# 限制單個請求頭的大小
limit_request_field_size = 8190 # 默認 8KB
# 範例:
# Cookie: session_id=very_long_string...(8KB)
# 場景 1:一般應用
limit_request_field_size = 8190 # 8KB
# 場景 2:大 Cookie(多個 session)
limit_request_field_size = 16380 # 16KB
# 場景 3:嚴格限制
limit_request_field_size = 4096 # 4KB🪝 Hook Functions(生命週期鉤子)
什麼是 Hook Functions?
在 Gunicorn 生命週期的特定時間點執行的自定義函數。
可用的 Hooks
# gunicorn.conf.py
# ============================================
# Server Hooks(服務器級別)
# ============================================
def on_starting(server):
"""服務器啟動時(Master 進程)"""
print(f"Starting Gunicorn with {server.cfg.workers} workers")
# 初始化全局資源(如連接池)
def on_reload(server):
"""配置重載時"""
print("Reloading configuration")
def when_ready(server):
"""服務器就緒時(可以接受請求)"""
print("Server is ready. Spawning workers")
def pre_fork(server, worker):
"""fork worker 之前"""
pass
def post_fork(server, worker):
"""fork worker 之後(每個 worker 執行)"""
print(f"Worker {worker.pid} spawned")
# 重新初始化資料庫連接
from django import db
db.connections.close_all()
def post_worker_init(worker):
"""worker 初始化完成後"""
print(f"Worker {worker.pid} initialized")
def worker_int(worker):
"""worker 收到 SIGINT 或 SIGQUIT"""
print(f"Worker {worker.pid} received interrupt signal")
def worker_abort(worker):
"""worker 被強制終止(超時)"""
print(f"Worker {worker.pid} aborted (timeout)")
def pre_exec(server):
"""重新執行 master 之前"""
print("Re-executing master")
def pre_request(worker, req):
"""處理請求之前"""
worker.log.debug(f"{req.method} {req.path}")
def post_request(worker, req, environ, resp):
"""處理請求之後"""
worker.log.debug(f"{req.method} {req.path} - {resp.status}")
def child_exit(server, worker):
"""worker 退出時"""
print(f"Worker {worker.pid} exited")
def worker_exit(server, worker):
"""worker 退出時(清理資源)"""
print(f"Cleaning up worker {worker.pid}")
# 關閉資料庫連接
from django import db
db.connections.close_all()
def nworkers_changed(server, new_value, old_value):
"""workers 數量改變時"""
print(f"Workers changed: {old_value} → {new_value}")
def on_exit(server):
"""服務器退出時"""
print("Gunicorn shutting down")實用 Hook 範例
範例 1:監控 Worker 記憶體
# gunicorn.conf.py
def post_request(worker, req, environ, resp):
"""每個請求後檢查記憶體"""
import psutil
import os
process = psutil.Process(os.getpid())
mem_mb = process.memory_info().rss / 1024 / 1024
# 記憶體超過 500MB 時警告
if mem_mb > 500:
worker.log.warning(
f"Worker {worker.pid} memory high: {mem_mb:.2f}MB"
)
def worker_exit(server, worker):
"""Worker 退出時記錄記憶體使用"""
import psutil
import os
try:
process = psutil.Process(worker.pid)
mem_mb = process.memory_info().rss / 1024 / 1024
worker.log.info(
f"Worker {worker.pid} exited. "
f"Peak memory: {mem_mb:.2f}MB"
)
except:
pass範例 2:請求計時
# gunicorn.conf.py
import time
def pre_request(worker, req):
"""請求開始時記錄時間"""
worker.log.debug(f"→ {req.method} {req.path}")
# 將開始時間存儲到 worker 屬性
worker.request_start_time = time.time()
def post_request(worker, req, environ, resp):
"""請求結束時計算耗時"""
duration = time.time() - worker.request_start_time
# 慢請求警告(超過 1 秒)
if duration > 1.0:
worker.log.warning(
f"Slow request: {req.method} {req.path} "
f"took {duration:.2f}s"
)
else:
worker.log.debug(
f"← {req.method} {req.path} "
f"{resp.status} ({duration:.2f}s)"
)範例 3:健康檢查端點
# gunicorn.conf.py
def when_ready(server):
"""服務器就緒時創建健康檢查文件"""
with open('/tmp/gunicorn_ready', 'w') as f:
f.write('ready')
server.log.info("Health check file created")
def on_exit(server):
"""服務器退出時刪除健康檢查文件"""
import os
try:
os.remove('/tmp/gunicorn_ready')
server.log.info("Health check file removed")
except:
pass
# Kubernetes 可以檢查這個文件
# readinessProbe:
# exec:
# command:
# - cat
# - /tmp/gunicorn_ready範例 4:數據庫連接管理
# gunicorn.conf.py
def post_fork(server, worker):
"""fork 後重新建立資料庫連接"""
from django import db
from django.core.cache import cache
# 關閉從 Master 繼承的連接
db.connections.close_all()
# 清除快取連接
cache.close()
worker.log.info(f"Worker {worker.pid}: DB connections reset")
def worker_exit(server, worker):
"""Worker 退出前關閉連接"""
from django import db
db.connections.close_all()
worker.log.info(f"Worker {worker.pid}: DB connections closed")
# 如果使用 preload_app = True,這些 hooks 必不可少!📊 日誌配置進階
1. 自定義日誌格式
# gunicorn.conf.py
# 訪問日誌格式
access_log_format = (
'%(h)s %(l)s %(u)s %(t)s ' # 客戶端信息
'"%(r)s" %(s)s %(b)s ' # 請求和響應
'"%(f)s" "%(a)s" ' # Referer 和 User-Agent
'%(D)s %(L)s' # 響應時間(微秒和秒)
)
# 格式說明:
# %(h)s - 客戶端 IP
# %(l)s - 遠程邏輯用戶名(通常是 -)
# %(u)s - 遠程用戶(HTTP 認證)
# %(t)s - 時間戳
# %(r)s - 請求行(方法 + URL + HTTP 版本)
# %(s)s - HTTP 狀態碼
# %(b)s - 響應大小(字節)
# %(f)s - Referer
# %(a)s - User-Agent
# %(D)s - 響應時間(微秒)
# %(L)s - 響應時間(秒,小數)
# %(p)s - Worker PID
# %(M)s - 響應時間(毫秒)範例輸出
# 默認格式
127.0.0.1 - - [30/Oct/2025:10:30:15 +0800] "GET /api/users HTTP/1.1" 200 1234
# 自定義格式(含響應時間)
127.0.0.1 - - [30/Oct/2025:10:30:15 +0800] "GET /api/users HTTP/1.1" 200 1234 "Mozilla/5.0" 123456 0.123
↑ ↑
微秒 秒2. 日誌輸出目標
# gunicorn.conf.py
# 輸出到標準輸出/錯誤(Docker 推薦)
accesslog = '-' # stdout
errorlog = '-' # stderr
# 輸出到文件
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
# 禁用訪問日誌(高流量時)
accesslog = None # 不記錄(由 Nginx 記錄)
errorlog = '/var/log/gunicorn/error.log' # 只記錄錯誤
# 輸出到 syslog
accesslog = 'syslog:server=localhost:514,facility=local0'
errorlog = 'syslog:server=localhost:514,facility=local0'3. 日誌級別
# gunicorn.conf.py
# 日誌級別
loglevel = 'info' # debug, info, warning, error, critical
# 不同環境的建議:
# 開發環境
loglevel = 'debug' # 顯示所有日誌
# 測試環境
loglevel = 'info' # 顯示一般信息
# 生產環境(低流量)
loglevel = 'info' # 顯示一般信息
# 生產環境(高流量)
loglevel = 'warning' # 只顯示警告和錯誤
accesslog = None # 禁用訪問日誌4. 日誌輪轉(Logrotate)
# /etc/logrotate.d/gunicorn
/var/log/gunicorn/*.log {
daily # 每天輪轉
rotate 14 # 保留 14 天
compress # 壓縮舊日誌
delaycompress # 延遲一天壓縮
missingok # 文件不存在不報錯
notifempty # 空文件不輪轉
create 0640 www-data www-data # 創建新文件的權限
sharedscripts # 所有日誌輪轉完再執行腳本
postrotate
# 通知 Gunicorn 重新打開日誌文件
if [ -f /var/run/gunicorn.pid ]; then
kill -USR1 `cat /var/run/gunicorn.pid`
fi
endscript
}🎤 面試常見問題
Q1: preload_app 有什麼好處和風險?
完整答案:
好處:
- 加快啟動速度:應用只載入一次,然後 fork 給所有 workers
- 節省記憶體:利用 Copy-on-Write 機制,共享唯讀代碼
- 一致性更好:所有 workers 使用完全相同的代碼版本
風險:
- 重載需要完全重啟:不能使用優雅重載,會有短暫停機
- 資源共享問題:不能在模塊層級打開資料庫連接
- 需要配合 Hook:必須在
post_forkhook 中重新初始化連接使用建議:
# 生產環境(穩定版本) preload_app = True # 開發環境(頻繁修改) preload_app = False
Q2: graceful_timeout 和 timeout 有什麼區別?
完整答案:
timeout:
- Worker 處理單個請求的最大時間
- 超過時間,Master 會殺掉 Worker 並重啟
- 用於防止請求卡住
graceful_timeout:
- Worker 收到關閉信號後,完成當前請求的最大時間
- 超過時間,強制殺掉 Worker
- 用於平滑重載和關閉
關係:
正常請求流程: 請求到達 → 處理中 → timeout 時間內完成 → 返回 重載流程: 收到信號 → 停止接收新請求 → graceful_timeout 時間內完成當前請求 → 退出建議:
# graceful_timeout >= timeout timeout = 30 graceful_timeout = 30
Q3: 為什麼要使用 /dev/shm 作為 worker_tmp_dir?
完整答案:
原因:
/dev/shm 是記憶體文件系統(tmpfs)
- 直接在記憶體中操作,不涉及磁盤 I/O
- 速度極快(< 0.1ms vs 1-5ms)
Worker 心跳機制需要頻繁更新文件時間戳
- 每個 worker 每秒更新一次
- 100 workers = 100 次/秒的文件操作
- 使用記憶體可以減少磁盤負載
效能提升明顯
- 特別是在 workers 數量多時
- 減少不必要的磁盤 I/O
配置:
# Linux worker_tmp_dir = '/dev/shm' # Docker(需要掛載) # docker run --shm-size=512m ...
Q4: Hook functions 有哪些常見用途?
完整答案:
Hook functions 允許在 Gunicorn 生命週期的特定時間點執行自定義代碼。
常見用途:
資源管理
def post_fork(server, worker): # 重新建立資料庫連接 from django import db db.connections.close_all()監控和日誌
def post_request(worker, req, environ, resp): # 記錄慢請求 if duration > 1.0: worker.log.warning(f"Slow: {req.path}")健康檢查
def when_ready(server): # 創建就緒標記文件 with open('/tmp/ready', 'w') as f: f.write('ok')清理工作
def worker_exit(server, worker): # 關閉連接,釋放資源 cleanup_resources()
✅ 重點回顧
核心進階參數
preload_app
- 預載入應用,加快啟動,節省記憶體
- 需要配合 Hook functions 重新初始化資源
- 生產環境推薦使用
graceful_timeout
- 優雅關閉的超時時間
- 建議 >= timeout
- 確保平滑重載
worker_tmp_dir
- 使用
/dev/shm提升效能 - 減少磁盤 I/O
- 特別適合多 workers 環境
- 使用
安全參數
- limit_request_line:請求行長度限制
- limit_request_fields:請求頭數量限制
- limit_request_field_size:請求頭大小限制
Hook Functions
- post_fork:初始化資源(資料庫連接)
- worker_exit:清理資源
- pre/post_request:監控和日誌
- when_ready:健康檢查
日誌配置
- 自定義日誌格式
- 根據環境選擇日誌級別
- 高流量時禁用訪問日誌
- 配置日誌輪轉
📚 接下來
現在你掌握了 Gunicorn 的進階配置技巧!下一篇我們會學習:
- 不同場景的完整配置文件
- 開發/測試/生產環境配置
- Docker 部署配置
- Kubernetes 部署配置
🤓 小測驗
preload_app = True 時,為什麼不能在模塊層級打開資料庫連接?
graceful_timeout 和 timeout 應該如何設置?
為什麼高流量生產環境要禁用訪問日誌?
post_fork hook 主要用於做什麼?
上一篇: 02-2. 基礎配置參數 下一篇: 02-4. 配置文件範例
相關閱讀:
- 01-8. 現代方案:Gunicorn + Uvicorn Workers - ASGI 的進階配置
- 02-1. Workers 數量計算 - 配合本章參數使用
最後更新:2025-10-30