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  # ✅ 多進程/多線程,適合生產環境

三大功能:

  1. 進程管理:啟動多個 Worker 進程處理並發請求
  2. 負載均衡:自動分配請求到不同的 Worker
  3. 故障恢復: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 有什麼區別?

標準答案:

特性WSGIASGI
全名Web Server Gateway InterfaceAsynchronous Server Gateway Interface
同步/異步同步異步
支援協議HTTPHTTP、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 4

Master 的主要職責:

# 簡化的 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 都會停止,整個應用會下線。因此生產環境通常使用 SystemdSupervisor 來管理 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 完成後

關鍵點:

  1. 遇到 await 才會切換,否則持續執行
  2. 單線程,同一時間只執行一個協程
  3. 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 result

3. 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 如何實現?

答:使用 --reloadkill -HUP 信號:

# 方法 1:使用 HUP 信號
kill -HUP <master_pid>

# 流程:
# 1. Master 啟動新的 Workers(使用新代碼)
# 2. 舊 Workers 優雅關閉(完成當前請求後退出)
# 3. 新 Workers 接管所有請求
# ✅ 整個過程無停機時間

# 方法 2:使用 systemd
systemctl reload myapp-gunicorn

4. 預加載與記憶體

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 優化記憶體

答題技巧:

  1. 先回答核心概念
  2. 給出代碼示例
  3. 說明適用場景
  4. 補充注意事項

記住:面試官不只看你知道答案,更看你理解原理、能實際應用

0%