02-3. 進階配置技巧

⏱️ 閱讀時間: 12 分鐘 🎯 難度: ⭐⭐⭐ (中等)


🎯 本篇重點

掌握 Gunicorn 的進階配置技巧:preload_app、graceful_timeout、Hook functions、日誌優化等。


📋 進階參數總覽

參數作用默認值使用場景
preload_app預載入應用False加快啟動、節省記憶體
graceful_timeout優雅關閉超時30 秒平滑重載
worker_tmp_dirWorker 臨時目錄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

優缺點

✅ 優點

  1. 加快啟動速度

    沒有 preload: 每個 worker 啟動需要 5 秒 × 4 = 20 秒
    有 preload:    Master 載入 5 秒 + workers fork 1 秒 = 6 秒
    
    提升:3.3 倍
  2. 節省記憶體(Copy-on-Write)

    Linux 的 fork() 使用 Copy-on-Write 機制:
    
    Master 載入的代碼(唯讀)→ 所有 workers 共享
    只有修改的數據才會複製
    
    節省記憶體:30-50%
  3. 一致性更好

    所有 workers 使用完全相同的代碼版本
    避免某些 workers 載入了舊代碼

❌ 缺點

  1. 重載需要重啟所有 workers

    # 沒有 preload(優雅重載)
    kill -HUP <master_pid>
    → 逐個重啟 workers,服務不中斷
    
    # 有 preload(需要完全重啟)
    kill -HUP <master_pid>
    → 必須重啟 Master 和所有 workers
    → 會有短暫停機時間
  2. 不能在 worker 中打開資料庫連接

    # ❌ 錯誤:在模塊層級打開連接
    import psycopg2
    
    # 這個連接在 Master 中創建
    conn = psycopg2.connect(...)  # 危險!
    
    # Fork 後所有 workers 共享同一個連接
    # 會導致連接競爭和錯誤
    
    # ✅ 正確:在 post_fork hook 中打開
    def post_fork(server, worker):
        # 每個 worker 創建自己的連接
        import psycopg2
        worker.conn = psycopg2.connect(...)
  3. 全局變量問題

    # ❌ 危險:全局變量會被所有 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 = 200

3. 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 有什麼好處和風險?

完整答案:

好處:

  1. 加快啟動速度:應用只載入一次,然後 fork 給所有 workers
  2. 節省記憶體:利用 Copy-on-Write 機制,共享唯讀代碼
  3. 一致性更好:所有 workers 使用完全相同的代碼版本

風險:

  1. 重載需要完全重啟:不能使用優雅重載,會有短暫停機
  2. 資源共享問題:不能在模塊層級打開資料庫連接
  3. 需要配合 Hook:必須在 post_fork hook 中重新初始化連接

使用建議:

# 生產環境(穩定版本)
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?

完整答案:

原因:

  1. /dev/shm 是記憶體文件系統(tmpfs)

    • 直接在記憶體中操作,不涉及磁盤 I/O
    • 速度極快(< 0.1ms vs 1-5ms)
  2. Worker 心跳機制需要頻繁更新文件時間戳

    • 每個 worker 每秒更新一次
    • 100 workers = 100 次/秒的文件操作
    • 使用記憶體可以減少磁盤負載
  3. 效能提升明顯

    • 特別是在 workers 數量多時
    • 減少不必要的磁盤 I/O

配置:

# Linux
worker_tmp_dir = '/dev/shm'

# Docker(需要掛載)
# docker run --shm-size=512m ...

Q4: Hook functions 有哪些常見用途?

完整答案:

Hook functions 允許在 Gunicorn 生命週期的特定時間點執行自定義代碼。

常見用途:

  1. 資源管理

    def post_fork(server, worker):
        # 重新建立資料庫連接
        from django import db
        db.connections.close_all()
  2. 監控和日誌

    def post_request(worker, req, environ, resp):
        # 記錄慢請求
        if duration > 1.0:
            worker.log.warning(f"Slow: {req.path}")
  3. 健康檢查

    def when_ready(server):
        # 創建就緒標記文件
        with open('/tmp/ready', 'w') as f:
            f.write('ok')
  4. 清理工作

    def worker_exit(server, worker):
        # 關閉連接,釋放資源
        cleanup_resources()

✅ 重點回顧

核心進階參數

  1. preload_app

    • 預載入應用,加快啟動,節省記憶體
    • 需要配合 Hook functions 重新初始化資源
    • 生產環境推薦使用
  2. graceful_timeout

    • 優雅關閉的超時時間
    • 建議 >= timeout
    • 確保平滑重載
  3. worker_tmp_dir

    • 使用 /dev/shm 提升效能
    • 減少磁盤 I/O
    • 特別適合多 workers 環境
  4. 安全參數

    • limit_request_line:請求行長度限制
    • limit_request_fields:請求頭數量限制
    • limit_request_field_size:請求頭大小限制

Hook Functions

  • post_fork:初始化資源(資料庫連接)
  • worker_exit:清理資源
  • pre/post_request:監控和日誌
  • when_ready:健康檢查

日誌配置

  • 自定義日誌格式
  • 根據環境選擇日誌級別
  • 高流量時禁用訪問日誌
  • 配置日誌輪轉

📚 接下來

現在你掌握了 Gunicorn 的進階配置技巧!下一篇我們會學習:

02-4. 配置文件範例

  • 不同場景的完整配置文件
  • 開發/測試/生產環境配置
  • Docker 部署配置
  • Kubernetes 部署配置

🤓 小測驗

  1. preload_app = True 時,為什麼不能在模塊層級打開資料庫連接?

  2. graceful_timeout 和 timeout 應該如何設置?

  3. 為什麼高流量生產環境要禁用訪問日誌?

  4. post_fork hook 主要用於做什麼?


上一篇: 02-2. 基礎配置參數 下一篇: 02-4. 配置文件範例


相關閱讀:


最後更新:2025-10-30

0%