Django 面試準備 05-1:面試常見問題(基礎)
Gunicorn 基礎概念面試題精選與詳解
05-1. 面試常見問題(基礎)
本章整理 Gunicorn 相關的基礎面試問題,涵蓋核心概念、Worker 類型、配置參數等主題。
1. Gunicorn 基礎概念
Q1:什麼是 Gunicorn?它的作用是什麼?
標準答案:
Gunicorn(Green Unicorn)是一個 WSGI/ASGI HTTP Server,用於在生產環境運行 Python Web 應用(如 Django、Flask)。
核心作用:
# Django 內建的開發服務器
python manage.py runserver # ❌ 只能處理一個請求,不適合生產環境
# Gunicorn 生產服務器
gunicorn myapp.wsgi:application # ✅ 多進程/多線程,適合生產環境三大功能:
- 進程管理:啟動多個 Worker 進程處理並發請求
- 負載均衡:自動分配請求到不同的 Worker
- 故障恢復:Worker 崩潰後自動重啟
架構:
客戶端請求
↓
主進程 (Master Process)
↓
Worker 1 → Django App
Worker 2 → Django App
Worker 3 → Django App
Worker 4 → Django App延伸問題:為什麼不直接用 Django 的 runserver?
答:runserver 是單線程、不穩定、沒有並發處理能力,只適合開發環境。生產環境需要 Gunicorn 的多進程管理和負載均衡能力。
Q2:WSGI 和 ASGI 有什麼區別?
標準答案:
| 特性 | WSGI | ASGI |
|---|---|---|
| 全名 | Web Server Gateway Interface | Asynchronous Server Gateway Interface |
| 同步/異步 | 同步 | 異步 |
| 支援協議 | HTTP | HTTP、WebSocket、HTTP/2 |
| Django 版本 | 所有版本 | Django 3.0+ |
| 適用場景 | 傳統 Web 應用 | 實時應用、高並發 I/O |
代碼示例:
# WSGI 應用
# myapp/wsgi.py
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# 啟動:
# gunicorn myapp.wsgi:application
# ASGI 應用
# myapp/asgi.py
from django.core.asgi import get_asgi_application
application = get_asgi_application()
# 啟動:
# gunicorn myapp.asgi:application -k uvicorn.workers.UvicornWorker關鍵差異:
# WSGI:同步處理
def wsgi_app(environ, start_response):
data = database_query() # 阻塞等待
start_response('200 OK', [('Content-Type', 'text/plain')])
return [data]
# ASGI:異步處理
async def asgi_app(scope, receive, send):
data = await database_query() # 不阻塞,可以處理其他請求
await send({
'type': 'http.response.start',
'status': 200,
})
await send({
'type': 'http.response.body',
'body': data,
})延伸問題:什麼時候應該使用 ASGI?
答:
- ✅ Django 3.0+ 的新專案
- ✅ I/O 密集型應用(大量資料庫查詢、API 呼叫)
- ✅ 需要 WebSocket 或實時功能
- ❌ Django 2.x 或更早版本(只支援 WSGI)
Q3:什麼是 Worker?Master Process 和 Worker Process 有什麼區別?
標準答案:
Worker 是實際處理 HTTP 請求的進程。
Master Process(主進程):
- 管理所有 Worker
- 監控 Worker 健康狀態
- 自動重啟崩潰的 Worker
- 接收信號(如 HUP 重新載入)
- 不處理請求
Worker Process(工作進程):
- 實際處理 HTTP 請求
- 執行 Django 應用程式碼
- 可以有多個 Worker 並行處理
# 查看進程結構
ps aux | grep gunicorn
# 輸出:
# USER PID PPID COMMAND
# www 1234 1 gunicorn: master [myapp] ← Master
# www 1235 1234 gunicorn: worker [myapp] ← Worker 1
# www 1236 1234 gunicorn: worker [myapp] ← Worker 2
# www 1237 1234 gunicorn: worker [myapp] ← Worker 3
# www 1238 1234 gunicorn: worker [myapp] ← Worker 4Master 的主要職責:
# 簡化的 Master Process 邏輯
class MasterProcess:
def run(self):
# 1. 啟動 Workers
for i in range(num_workers):
self.spawn_worker()
# 2. 監控 Workers
while True:
for worker in self.workers:
if not worker.is_alive():
# Worker 崩潰,重啟
self.spawn_worker()
# 3. 處理信號
if self.received_sighup:
self.reload_workers()延伸問題:如果 Master Process 崩潰會怎樣?
答:所有 Worker 都會停止,整個應用會下線。因此生產環境通常使用 Systemd 或 Supervisor 來管理 Gunicorn,確保 Master 崩潰後自動重啟。
2. Worker 類型
Q4:Gunicorn 有哪些 Worker 類型?分別適用於什麼場景?
標準答案:
Gunicorn 主要有 4 種 Worker 類型:
| Worker 類型 | 並發方式 | 適用場景 | Django 版本 |
|---|---|---|---|
| sync | 多進程(同步) | 舊專案、CPU 密集 | 所有版本 |
| gthread | 多進程 + 多線程 | 混合型應用 | 所有版本 |
| gevent | 協程(greenlet) | I/O 密集(舊版) | 所有版本 |
| uvicorn | 異步(asyncio) | I/O 密集(現代) | Django 3.0+ |
配置示例:
# gunicorn.conf.py
# 1. Sync Worker(預設)
workers = 9 # (2 × CPU) + 1
worker_class = 'sync'
# 2. Gthread Worker
workers = 4 # CPU 核心數
threads = 10 # 每個 worker 10 個線程
worker_class = 'gthread'
# 3. Gevent Worker
workers = 4
worker_class = 'gevent'
worker_connections = 1000
# 4. Uvicorn Worker(推薦)
workers = 4
worker_class = 'uvicorn.workers.UvicornWorker'選擇流程圖:
Django 版本?
├─ Django 3.0+ → ✅ 使用 Uvicorn Worker
└─ Django 2.x →
應用類型?
├─ I/O 密集 → Gevent Worker
├─ CPU 密集 → Sync Worker
└─ 混合型 → Gthread Worker延伸問題:為什麼 Django 3.0+ 推薦使用 Uvicorn?
答:因為 Django 3.0+ 原生支援 ASGI,Uvicorn 基於 asyncio,性能更好、支援 WebSocket、更適合現代異步應用。
Q5:Sync Worker 的工作原理是什麼?為什麼 CPU 密集型要用 Sync?
標準答案:
Sync Worker 原理:
# Sync Worker 的處理流程
def sync_worker():
while True:
# 1. 等待請求
request = wait_for_request()
# 2. 同步處理(阻塞)
response = handle_request(request) # 完全處理完才能接下一個
# 3. 返回響應
send_response(response)
# 4. 回到步驟 1特點:
- ✅ 簡單穩定
- ✅ 每個 Worker 一次只處理一個請求
- ❌ 遇到 I/O 會阻塞等待
為什麼 CPU 密集型要用 Sync?
因為 Python 的 GIL(Global Interpreter Lock):
# Python GIL 限制:
# 同一時間只有一個線程執行 Python bytecode
# Sync Worker 使用多進程:
# - 每個進程有獨立的 GIL
# - 可以真正並行利用多核 CPU
# Async/Thread Worker 使用單進程:
# - 共享同一個 GIL
# - CPU 密集任務無法並行,反而更慢示例:
# CPU 密集任務
def calculate_fibonacci(n):
if n <= 1:
return n
return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)
# Sync Worker(多進程):
# Worker 1: 計算 fib(40) → 使用 CPU 核心 1
# Worker 2: 計算 fib(40) → 使用 CPU 核心 2
# Worker 3: 計算 fib(40) → 使用 CPU 核心 3
# Worker 4: 計算 fib(40) → 使用 CPU 核心 4
# ✅ 真正的並行
# Async Worker(單進程 asyncio):
# Worker 1:
# - 計算 fib(40) → 持續占用 CPU,無法切換
# - 其他請求必須等待
# ❌ 無法並行延伸問題:如果 I/O 密集型用 Sync Worker 會怎樣?
答:會浪費大量時間等待 I/O,吞吐量低。例如:
- 4 個 Sync Workers,每個等待資料庫 100ms
- 只能同時處理 4 個請求
- 而 4 個 Async Workers 可以同時處理數百個請求
Q6:Async Worker 的事件循環是如何工作的?
標準答案:
事件循環(Event Loop) 是 Async Worker 的核心,負責調度協程(coroutines)。
工作原理:
# 事件循環的簡化邏輯
class EventLoop:
def __init__(self):
self.tasks = [] # 待執行的協程
self.waiting = {} # 等待 I/O 的協程
def run(self):
while True:
# 1. 檢查是否有 I/O 完成
ready = check_io_ready()
for task in ready:
self.tasks.append(self.waiting.pop(task))
# 2. 執行一個協程
if self.tasks:
task = self.tasks.pop(0)
try:
task.send(None) # 執行到下一個 await
except StopIteration:
pass # 協程完成
# 3. 回到步驟 1實際執行流程:
# 三個請求同時到達
async def request_1():
print("請求1:開始")
data = await db_query() # ← 暫停,加入 waiting
print("請求1:結束")
return data
async def request_2():
print("請求2:開始")
response = await api_call() # ← 暫停,加入 waiting
print("請求2:結束")
return response
async def request_3():
print("請求3:開始")
result = calculate() # 不是 async,無法暫停
print("請求3:結束")
return result
# 時間線:
# T0: 請求1 開始 → await db_query() → 暫停
# T0.1: 請求2 開始 → await api_call() → 暫停
# T0.2: 請求3 開始 → calculate() → 完成 → 請求3 結束
# T100: db_query() 完成 → 喚醒請求1 → 請求1 結束
# T200: api_call() 完成 → 喚醒請求2 → 請求2 結束輸出順序:
請求1:開始
請求2:開始
請求3:開始
請求3:結束 ← 沒有 I/O,立即完成
請求1:結束 ← I/O 完成後
請求2:結束 ← I/O 完成後關鍵點:
- 遇到
await才會切換,否則持續執行 - 單線程,同一時間只執行一個協程
- I/O 等待時不浪費 CPU,可以處理其他請求
延伸問題:如果協程中有 CPU 密集運算會怎樣?
答:會阻塞事件循環,導致其他請求都在等待:
async def bad_request():
# ❌ CPU 密集運算,沒有 await,無法切換
result = sum(range(100000000)) # 持續占用 CPU 5 秒
return result
# 這 5 秒內,事件循環無法處理其他請求
# 應該改用:
async def good_request():
# ✅ 使用 asyncio.to_thread 移到線程池
result = await asyncio.to_thread(sum, range(100000000))
return result3. Worker 配置
Q7:Worker 數量應該如何計算?
標準答案:
不同 Worker 類型有不同的計算公式:
1. Sync Worker:
workers = (2 × CPU 核心數) + 1
# 例如 4 核 CPU:
workers = (2 × 4) + 1 = 9
# 原因:
# - CPU 密集:使用 CPU 核心數(例如 4)
# - I/O 密集:等待 I/O 時 CPU 閒置,可以多開 Worker
# - +1:應對突發流量2. Async Worker (Uvicorn):
workers = CPU 核心數
# 例如 4 核 CPU:
workers = 4
# 原因:
# - 每個 Worker 使用事件循環處理數百個並發
# - 不需要開太多進程
# - 避免上下文切換開銷3. Gthread Worker:
workers = CPU 核心數
threads = 4-10 # 每個 Worker 的線程數
# 例如 4 核 CPU:
workers = 4
threads = 10
# 總併發:4 × 10 = 40
# 原因:
# - 多進程利用多核
# - 多線程處理 I/O 並發
# - 平衡進程和線程的數量實際配置示例:
# gunicorn.conf.py
import multiprocessing
# 方法 1:手動計算
workers = 9
worker_class = 'sync'
# 方法 2:自動計算
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'sync'
# 方法 3:根據記憶體限制
# 假設每個 Worker 占用 200MB,服務器有 4GB 可用記憶體
max_workers = 4 * 1024 / 200 # = 20
workers = min(max_workers, multiprocessing.cpu_count() * 2 + 1)延伸問題:Worker 數量是不是越多越好?
答:❌ 不是!
- 太多 Worker 會消耗大量記憶體
- 上下文切換開銷增加
- 資料庫連接數可能不足
- 建議:監控 CPU 使用率,如果 < 70%,不需要增加 Worker
Q8:timeout 參數的作用是什麼?如何設置合理的超時時間?
標準答案:
timeout 是 Worker 處理單個請求的最大時間,超過則 Worker 被強制殺死。
# gunicorn.conf.py
timeout = 30 # 預設 30 秒
# 超時後的行為:
# 1. 主進程檢測到 Worker 超時
# 2. 發送 SIGKILL 信號強制殺死 Worker
# 3. 啟動新的 Worker 替代
# 4. 用戶收到 502 Bad Gateway如何設置合理的超時時間:
# 根據應用類型設置
# 1. API 服務(快速回應)
timeout = 30 # 30 秒
# 2. 管理後台(可能有複雜查詢)
timeout = 60 # 1 分鐘
# 3. 報表系統(可能需要長時間處理)
timeout = 120 # 2 分鐘
# 4. 即時通訊(WebSocket 長連接)
timeout = 300 # 5 分鐘
keepalive = 5注意事項:
# ❌ 錯誤:遇到慢請求就增加 timeout
timeout = 300 # 5 分鐘
# 問題:
# - 慢請求可能持續占用 Worker
# - 其他請求無法被處理
# - 應該優化程式碼,而不是增加 timeout
# ✅ 正確:優化程式碼
def slow_endpoint(request):
# 錯誤:同步處理大文件
# data = process_large_file()
# 正確:使用 Celery 異步處理
task = process_large_file_task.delay()
return JsonResponse({'task_id': task.id})延伸問題:如果 timeout 設太短會怎樣?
答:
- Worker 可能在處理正常請求時被殺死
- 用戶收到 502 錯誤
- 應該監控慢請求日誌,找出合理的 timeout 值
Q9:什麼是 graceful timeout?與 timeout 有什麼區別?
標準答案:
| 參數 | 作用 | 觸發時機 | 信號 |
|---|---|---|---|
| timeout | 請求處理超時 | Worker 處理單個請求超時 | SIGKILL(強制殺死) |
| graceful_timeout | 優雅關閉超時 | Worker 收到重啟信號 | 先 SIGTERM,後 SIGKILL |
graceful_timeout 的用途:
# gunicorn.conf.py
timeout = 30
graceful_timeout = 30
# 場景:重新載入配置
# $ kill -HUP <master_pid>
# Worker 的關閉流程:
# 1. Master 發送 SIGTERM 給 Worker(優雅關閉)
# 2. Worker 停止接受新請求
# 3. Worker 繼續處理當前請求
# 4. 如果 graceful_timeout 秒內未完成:
# Master 發送 SIGKILL 強制殺死配置建議:
# 一般設置為相同值
timeout = 30
graceful_timeout = 30
# 或稍微長一點,給予更多時間完成當前請求
timeout = 30
graceful_timeout = 40
# 不要設太長,否則重啟會很慢延伸問題:Zero-downtime deployment 如何實現?
答:使用 --reload 或 kill -HUP 信號:
# 方法 1:使用 HUP 信號
kill -HUP <master_pid>
# 流程:
# 1. Master 啟動新的 Workers(使用新代碼)
# 2. 舊 Workers 優雅關閉(完成當前請求後退出)
# 3. 新 Workers 接管所有請求
# ✅ 整個過程無停機時間
# 方法 2:使用 systemd
systemctl reload myapp-gunicorn4. 預加載與記憶體
Q10:什麼是 preload_app?何時應該使用?
標準答案:
preload_app 決定是否在 Master 進程中預先載入 Django 應用。
# gunicorn.conf.py
preload_app = True # 預設為 False
# 影響:
# ┌────────────────────────────────────────┐
# │ preload_app = False(預設) │
# ├────────────────────────────────────────┤
# │ Master 啟動 │
# │ ↓ │
# │ Fork Worker 1 → 載入 Django(200MB) │
# │ Fork Worker 2 → 載入 Django(200MB) │
# │ Fork Worker 3 → 載入 Django(200MB) │
# │ Fork Worker 4 → 載入 Django(200MB) │
# │ 總記憶體:800MB │
# └────────────────────────────────────────┘
# ┌────────────────────────────────────────┐
# │ preload_app = True │
# ├────────────────────────────────────────┤
# │ Master 啟動 → 載入 Django(200MB) │
# │ ↓ │
# │ Fork Worker 1 → 共享記憶體 │
# │ Fork Worker 2 → 共享記憶體 │
# │ Fork Worker 3 → 共享記憶體 │
# │ Fork Worker 4 → 共享記憶體 │
# │ 總記憶體:250MB(因為 COW) │
# └────────────────────────────────────────┘Copy-on-Write (COW) 機制:
# Linux Fork 的 COW 機制:
# - Fork 時不複製記憶體,而是共享
# - 只有當 Worker 修改資料時才複製
# preload_app = True 時:
# - 只讀的程式碼和資料(大部分):共享
# - 需要修改的資料:複製
# - 總記憶體占用大幅降低何時使用:
# ✅ 應該使用 preload_app = True:
# 1. 生產環境
# 2. 記憶體受限
# 3. Worker 數量多(> 4)
# ❌ 不應該使用:
# 1. 開發環境(需要熱重載)
# 2. 應用有不安全的全局狀態
preload_app = True
# 配合使用的鉤子:
def on_starting(server):
"""Master 啟動時執行(preload_app = True)"""
print("Master 進程啟動,載入 Django")
def post_fork(server, worker):
"""Worker Fork 後執行"""
print(f"Worker {worker.pid} 已啟動")
# 重新初始化資料庫連接(不能共享)
from django.db import connections
connections.close_all()延伸問題:preload_app = True 有什麼缺點?
答:
- 熱重載不可用(修改代碼需要重啟)
- 如果有不安全的全局狀態可能導致問題
- 某些資源(如資料庫連接)不能在 Fork 前建立
小結
本章涵蓋了 Gunicorn 面試的基礎問題:
核心概念:
- Gunicorn 的作用和架構
- WSGI vs ASGI
- Master Process vs Worker Process
Worker 類型:
- Sync、Gthread、Gevent、Uvicorn
- 事件循環的工作原理
- 不同場景的選擇
配置參數:
- Worker 數量計算
- timeout 和 graceful_timeout
- preload_app 優化記憶體
答題技巧:
- 先回答核心概念
- 給出代碼示例
- 說明適用場景
- 補充注意事項
記住:面試官不只看你知道答案,更看你理解原理、能實際應用!