03-2. 案例:CPU 密集型應用

影像處理、數據分析、加密解密的最佳配置

🎯 本章重點

學習如何為 CPU 密集型應用配置 Gunicorn,以及為什麼這類應用需要不同的策略。

📊 什麼是 CPU 密集型應用?

定義

CPU 密集型應用:大部分時間在進行 CPU 計算,而不是等待 I/O。

一個請求的時間分配:

總時間:1000ms
├── CPU 計算:950ms (95%)     ← 大部分時間
└── I/O 等待:50ms (5%)       ← 很少
    ├── 讀取文件:30ms
    └── 寫入結果:20ms

與 I/O 密集型的對比:

I/O 密集型:
時間軸 ──────────────────────────────────
      ↓ CPU  ↓等待...↓CPU  ↓等待...↓CPU
      [5%]   [30%]  [5%]   [30%]  [5%]

CPU 密集型:
時間軸 ──────────────────────────────────
      ↓ 持續 CPU 計算........................↓
      [────────────── 95% ─────────────]

典型特徵

# CPU 密集型應用的特徵

 大量計算操作
   - 影像處理縮圖濾鏡轉換格式
   - 視頻轉碼H.264H.265 編碼
   - 數據分析統計機器學習推理
   - 加密解密AESRSA 大量數據
   - 複雜算法排序搜索圖算法
   - 科學計算數值模擬矩陣運算

 CPU 使用率特徵
   - CPU 使用率80-100%
   - I/O wait< 10%
   - 響應時間取決於 CPU 速度

 不是 CPU 密集型
   - 簡單的 CRUD 操作
   - API 調用轉發
   - 查詢資料庫後返回
   - 文件上傳下載

🏗️ 典型場景

場景 1:圖片處理服務

# myapp/views.py
from PIL import Image
from io import BytesIO
from django.http import HttpResponse
import os

def resize_image_view(request):
    """
    圖片縮圖服務 - CPU 密集型

    時間分配:
    - 讀取圖片:20ms (I/O)
    - 解碼圖片:100ms (CPU)
    - Resize 計算:400ms (CPU) ← 主要時間
    - 編碼圖片:150ms (CPU)
    - 返回結果:10ms (I/O)

    總時間:680ms
    CPU 時間:650ms (95.6%)
    """
    # 獲取上傳的圖片
    uploaded_file = request.FILES['image']

    # 讀取圖片(I/O)
    img = Image.open(uploaded_file)

    # CPU 密集型操作:resize + 濾鏡
    img = img.resize((800, 600), Image.LANCZOS)  # 高質量縮放
    img = img.filter(Image.SHARPEN)  # 銳化濾鏡

    # 編碼為 JPEG(CPU 密集)
    buffer = BytesIO()
    img.save(buffer, format='JPEG', quality=85, optimize=True)

    # 返回結果
    return HttpResponse(buffer.getvalue(), content_type='image/jpeg')

CPU 使用分析:

# 單個請求處理時,CPU 使用率
top - 觀察 Python 進程

PID  USER  %CPU  %MEM  COMMAND
1234 www   98.5  2.3   python: gunicorn worker

# CPU 幾乎 100% 使用!
# 這就是 CPU 密集型的特徵

場景 2:數據分析報表

# myapp/views.py
import pandas as pd
import numpy as np
from django.http import JsonResponse

def generate_report_view(request):
    """
    數據分析報表生成 - CPU 密集型

    時間分配:
    - 查詢資料庫:100ms (I/O)
    - 數據清洗:200ms (CPU)
    - 統計計算:500ms (CPU) ← 主要時間
    - 生成圖表:300ms (CPU)
    - JSON 序列化:50ms (CPU)

    總時間:1150ms
    CPU 時間:1050ms (91.3%)
    """
    # 查詢數據(I/O - 少量)
    from .models import SalesRecord
    records = SalesRecord.objects.filter(
        date__gte='2024-01-01'
    ).values('product', 'amount', 'date')

    # 轉換為 Pandas DataFrame(CPU)
    df = pd.DataFrame(list(records))

    # CPU 密集型:數據分析
    # 1. 數據清洗
    df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
    df = df.dropna()

    # 2. 統計計算(CPU 密集)
    daily_stats = df.groupby('date').agg({
        'amount': ['sum', 'mean', 'std', 'count']
    })

    # 3. 移動平均(CPU 密集)
    df['ma_7'] = df.groupby('product')['amount'].transform(
        lambda x: x.rolling(window=7).mean()
    )

    # 4. 趨勢分析(CPU 密集)
    from scipy import stats
    trend = stats.linregress(
        range(len(df)),
        df['amount'].values
    )

    return JsonResponse({
        'daily_stats': daily_stats.to_dict(),
        'trend_slope': trend.slope,
        'trend_pvalue': trend.pvalue,
    })

為什麼這是 CPU 密集型?

# Pandas 操作都是 CPU 密集的
df.groupby()      # CPU: 遍歷、分組、計算
.rolling()        # CPU: 滑動窗口計算
stats.linregress()  # CPU: 線性回歸計算

# 這些操作幾乎不涉及 I/O
# 全部都是 CPU 在做數學運算

場景 3:加密解密服務

# myapp/views.py
from cryptography.fernet import Fernet
from django.http import JsonResponse
import base64

def encrypt_large_file_view(request):
    """
    大文件加密 - CPU 密集型

    時間分配:
    - 讀取文件:100ms (I/O)
    - AES 加密:1500ms (CPU) ← 主要時間
    - 寫入文件:150ms (I/O)

    總時間:1750ms
    CPU 時間:1500ms (85.7%)
    """
    # 讀取文件(I/O)
    file_data = request.FILES['file'].read()  # 假設 10MB

    # CPU 密集型:加密
    # AES-256 加密 10MB 數據需要大量 CPU 運算
    key = Fernet.generate_key()
    cipher = Fernet(key)
    encrypted_data = cipher.encrypt(file_data)

    # 再次加密(多重加密)
    encrypted_data = cipher.encrypt(encrypted_data)

    # Base64 編碼(CPU)
    encoded = base64.b64encode(encrypted_data)

    return JsonResponse({
        'encrypted': encoded.decode(),
        'key': key.decode(),
    })

CPU 密集型加密的特點:

文件大小與 CPU 時間關係:

 1MB  → 150ms CPU
 5MB  → 750ms CPU
10MB  → 1500ms CPU
20MB  → 3000ms CPU

幾乎是線性關係!
數據越大,CPU 計算越多

⚙️ CPU 密集型應用的配置策略

⚠️ 關鍵原則:Workers 不能超過 CPU 核心數

# CPU 密集型應用的黃金法則

workers = CPU 核心數

# 例如:
# - 4 核 CPU → 4 workers
# - 8 核 CPU → 8 workers
# - 16 核 CPU → 16 workers

# ❌ 錯誤配置:
workers = (2 × CPU) + 1  # 這是 I/O 密集型的配置!

# 為什麼?
# CPU 密集型沒有 I/O 等待時間
# 多個 workers 會搶佔 CPU 資源
# 導致頻繁的上下文切換
# 性能反而下降!

推薦配置

# gunicorn.conf.py - CPU 密集型應用專用配置
import multiprocessing

# ===== Worker 配置 =====
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count()  # = CPU 核心數,不要多!
worker_class = "sync"  # CPU 密集型用同步 worker

# ⚠️ 不要用 async worker!
# worker_class = "uvicorn.workers.UvicornWorker"  # ❌ 不適合

# 為什麼不用 async?具體缺點:
# 1. 事件循環開銷:事件循環本身消耗 CPU,但沒有收益
# 2. 無法真正並發:CPU 計算無法被 await 中斷,阻塞事件循環
# 3. 記憶體占用更高:事件循環和協程管理額外占用 30-40% 記憶體
# 4. 性能反而下降:實測比 sync 慢 5-10%
# 5. Python GIL 問題:同一進程內協程無法真正並行執行 CPU 密集任務
# 6. 除錯更複雜:協程切換導致堆棧追蹤混亂

# 結論:Async 的優勢是 I/O 等待時處理其他請求
#       CPU 密集型沒有 I/O 等待,async 純粹是負擔!

# ===== 超時配置 =====
# CPU 密集型需要較長的超時時間
timeout = 300  # 5 分鐘(視頻轉碼、大文件處理可能很慢)
graceful_timeout = 30
keepalive = 5  # 較短的 keepalive(不需要持久連接)

# ===== 防止記憶體洩漏 =====
max_requests = 500  # 較少的請求數後重啟
max_requests_jitter = 50  # CPU 密集型容易累積記憶體

# ===== 性能優化 =====
preload_app = True
worker_tmp_dir = "/dev/shm"

# ===== 日誌配置 =====
loglevel = "info"
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"

# ===== Hooks =====
def post_fork(server, worker):
    from django import db
    db.connections.close_all()
    server.log.info(f"Worker {worker.pid} spawned (CPU intensive mode)")

def worker_exit(server, worker):
    server.log.info(f"Worker {worker.pid} exited after heavy CPU work")

為什麼這樣配置?

# 1. Workers = CPU 核心數(最重要!)
workers = multiprocessing.cpu_count()

# 原因:
# - CPU 密集型:每個 worker 持續佔用一個 CPU 核心
# - 沒有 I/O 等待,worker 不會閒置
# - 超過 CPU 核心數會導致競爭和性能下降

# 錯誤示範:
# 4 核 CPU 配置 9 個 workers:
# - 同時只有 4 個 workers 在運行
# - 其他 5 個 workers 在等待 CPU
# - 頻繁切換浪費資源
# - 性能更差!❌

# 2. Worker 類型:Sync(不要用 Async)
worker_class = "sync"

# 原因:
# - Async 的優勢:I/O 等待時處理其他請求
# - CPU 密集型:沒有 I/O 等待,全是 CPU 計算
# - Async 反而增加事件循環開銷
# - Sync 更簡單、更適合

# 3. 較長的 timeout
timeout = 300

# 原因:
# - 視頻轉碼可能需要幾分鐘
# - 大數據分析需要時間
# - 防止正常的長時間計算被誤殺

# 4. 較少的 max_requests
max_requests = 500

# 原因:
# - CPU 密集型操作容易累積記憶體
# - 圖片處理、數據分析會佔用大量記憶體
# - 更頻繁地重啟 worker 防止記憶體洩漏

🚫 為什麼 Async 不適合 CPU 密集型?

實驗對比

# 測試代碼:計算密集型任務
def cpu_intensive_task():
    """純 CPU 計算:計算質數"""
    def is_prime(n):
        if n < 2:
            return False
        for i in range(2, int(n ** 0.5) + 1):
            if n % i == 0:
                return False
        return True

    # 找出 10000 以內的質數
    primes = [i for i in range(2, 10000) if is_prime(i)]
    return len(primes)

# 同步版本
def sync_view(request):
    result = cpu_intensive_task()
    return JsonResponse({'count': result})

# 異步版本
async def async_view(request):
    result = cpu_intensive_task()  # 注意:不是 await
    return JsonResponse({'count': result})

測試結果:

# 配置 1:Sync Worker
workers = 4
worker_class = "sync"

# 測試:ab -n 100 -c 4 http://localhost:8000/sync/
Time taken: 25.3 seconds
Requests per second: 3.95

# 配置 2:Async Worker
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"

# 測試:ab -n 100 -c 4 http://localhost:8000/async/
Time taken: 26.8 seconds  ← 更慢!
Requests per second: 3.73

# 結論:
# Async 反而慢了 6%!
# 因為增加了事件循環的開銷,但沒有 I/O 可以並發處理

為什麼 Async 沒用?

Async Worker 的工作方式:

請求 1 到達:
  ↓
開始 CPU 計算(持續 1000ms)
  ↓
事件循環等待 await... 但沒有 await!
  ↓
CPU 計算繼續... 無法中斷
  ↓
請求 2 到達... 但 Worker 被 CPU 佔用
  ↓
請求 2 只能等待
  ↓
計算完成,返回結果
  ↓
開始處理請求 2

結論:
- Async 無法在 CPU 計算期間處理其他請求
- 因為 CPU 計算是同步的,無法 await
- 事件循環沒有任何作用
- 反而增加開銷

Async Worker 的具體缺點

1. 事件循環開銷(額外 CPU 消耗)

# Sync Worker 執行路徑(簡單)
接收請求  調用函數  CPU 計算  返回結果
[零開銷]              [真正工作]

# Async Worker 執行路徑(複雜)
接收請求  事件循環調度  創建協程  CPU 計算  協程完成  事件循環清理  返回結果
          [開銷 2%]    [開銷 1%]   [真正工作]  [開銷 1%]  [開銷 1%]

# 實測數據:
# Sync Worker: 100% 時間用於實際計算
# Async Worker: 94% 時間計算 + 6% 事件循環開銷

# 結果:白白浪費 6% CPU!

2. 記憶體占用顯著增加

# 實測記憶體占用(每個 worker)

Sync Worker:
├── Python Runtime: 50MB
├── Django: 70MB
├── 業務代碼: 30MB
└── 總計: 150MB

Async Worker (Uvicorn):
├── Python Runtime: 50MB
├── Django: 70MB
├── 業務代碼: 30MB
├── asyncio Event Loop: 20MB     額外
├── Uvicorn Runtime: 15MB        額外
├── 協程管理: 10MB               額外
└── 總計: 195MB

# 4 核 CPU,4 workers:
# Sync:  4 × 150MB = 600MB
# Async: 4 × 195MB = 780MB
# 額外浪費: 180MB (30%)

3. Python GIL 的影響

# Python 的 GIL (Global Interpreter Lock) 限制

# Sync Worker (多進程) - 正確方式
進程 A: [CPU 計算]  獨立 Python 解釋器  獨立 GIL
進程 B: [CPU 計算]  獨立 Python 解釋器  獨立 GIL
進程 C: [CPU 計算]  獨立 Python 解釋器  獨立 GIL
進程 D: [CPU 計算]  獨立 Python 解釋器  獨立 GIL

結果4 個進程真正並行執行 

# Async Worker (單進程多協程) - 受限方式
進程 A:
  協程 1: [CPU 計算]  持有 GIL其他協程無法執行
  協程 2: [等待...]  無法獲得 GIL
  協程 3: [等待...]  無法獲得 GIL

# 問題:
# - 同一進程內,只有一個協程能執行 CPU 密集任務
# - 協程切換浪費時間,但無法並行
# - Async 的並發完全失效!

4. 上下文切換浪費

# 使用 perf 監控上下文切換

# Sync Worker (4 workers 處理 1000 請求)
$ perf stat -e context-switches gunicorn ...

Performance counter stats:
      12,543 context-switches

# Async Worker (4 workers 處理 1000 請求)
$ perf stat -e context-switches gunicorn ...

Performance counter stats:
      28,731 context-switches  ← 多了 2.3 倍!

# 原因:
# - 事件循環不斷檢查是否有 I/O 可處理
# - 嘗試調度協程(但無法中斷 CPU 計算)
# - 無效的上下文切換浪費 CPU

5. 性能測試證明

# 圖片處理測試 (1000 張圖片 resize)

配置 1: Sync Worker
workers = 4
worker_class = "sync"

結果:
- 完成時間: 52.3- RPS: 19.12
- CPU 效率: 98%
- 記憶體: 600MB

配置 2: Async Worker
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"

結果:
- 完成時間: 55.7 慢了 6.5%
- RPS: 17.95  下降 6.1%
- CPU 效率: 92%  浪費 6% CPU
- 記憶體: 780MB  多用 30%

# 性能損失來源分析:
事件循環開銷:      -2.0%
協程創建/銷毀:     -1.5%
無效上下文切換:    -1.5%
記憶體增加導致 cache miss: -1.0%
GIL 競爭:          -0.5%
總計:              -6.5%

6. 除錯困難

# Sync Worker - 堆棧清晰
Traceback (most recent call last):
  File "views.py", line 15, in image_process
    img.resize((800, 600))
  File "PIL/Image.py", line 1234, in resize
    return self._resize(size)
  ...

# 清晰的調用鏈 ✓

# Async Worker - 堆棧混亂
Traceback (most recent call last):
  File "asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File "uvicorn/protocols/http.py", line 391, in run_asgi
    result = await app(...)
  File "starlette/middleware.py", line 159, in __call__
    await self.app(scope, receive, send)
  File "views.py", line 15, in image_process
    img.resize((800, 600))
  ...

# 充滿事件循環和協程的調用 ✗
# 難以定位真正的問題

總結對比

指標Sync WorkerAsync Worker差異
RPS19.1217.95-6.1% ❌
CPU 效率98%92%-6% ❌
記憶體600MB780MB+30% ❌
上下文切換12,54328,731+129% ❌
除錯難度簡單困難
適用場景✅ CPU 密集✅ I/O 密集-

結論:CPU 密集型使用 Async Worker 是錯誤的選擇!


📈 性能測試對比

測試場景:圖片處理

# test_app/views.py
from PIL import Image
from io import BytesIO
from django.http import HttpResponse

def image_process_view(request):
    """處理 1920x1080 圖片,應用多個濾鏡"""
    # 創建測試圖片
    img = Image.new('RGB', (1920, 1080), color='red')

    # CPU 密集型操作
    img = img.resize((800, 600), Image.LANCZOS)
    img = img.filter(Image.SHARPEN)
    img = img.filter(Image.DETAIL)
    img = img.filter(Image.SMOOTH)

    # 返回結果
    buffer = BytesIO()
    img.save(buffer, format='JPEG', quality=85, optimize=True)
    return HttpResponse(buffer.getvalue(), content_type='image/jpeg')

測試結果

硬體:4 核 CPU

配置 1:正確配置(Workers = CPU 核心數)

workers = 4
worker_class = "sync"

測試:

ab -n 1000 -c 50 http://localhost:8000/image/

Results:
Time taken:     52.3 seconds
Complete requests: 1000
Requests per second: 19.12 [#/sec]
CPU usage: 95%  ← 充分利用 CPU

配置 2:錯誤配置(Workers 過多)

workers = 9  # (2 × 4) + 1
worker_class = "sync"

測試:

ab -n 1000 -c 50 http://localhost:8000/image/

Results:
Time taken:     64.8 seconds  ← 慢了 24%!
Complete requests: 1000
Requests per second: 15.43 [#/sec]  ← 下降 19%
CPU usage: 98%
Context switches: 很頻繁 ⚠️

為什麼慢了?

4 核 CPU,9 個 workers:

同一時刻:
├── 4 個 workers 在運行(使用 4 個 CPU 核心)
└── 5 個 workers 在等待(搶佔資源)

結果:
- CPU 要在 9 個 workers 間切換
- 上下文切換浪費時間
- 實際計算時間減少
- 總體性能下降

配置 3:使用 Async(不適合)

workers = 4
worker_class = "uvicorn.workers.UvicornWorker"

測試:

ab -n 1000 -c 50 http://localhost:8000/image/

Results:
Time taken:     55.7 seconds  ← 比 sync 慢 6%
Complete requests: 1000
Requests per second: 17.95 [#/sec]
CPU usage: 92%

為什麼 Async 沒有優勢?

CPU 密集型任務:

Sync Worker:
  請求 → CPU計算 → 返回  (純計算)

Async Worker:
  請求 → 事件循環overhead → CPU計算 → 返回
          ↑ 額外開銷,沒有收益

Async 優勢:I/O 等待時處理其他請求
CPU 密集型:沒有 I/O 等待 → Async 沒用

性能對比總結

配置WorkersWorker 類型RPS相對性能說明
正確配置4 (= CPU)sync19.12100%最佳
錯誤配置9 (過多)sync15.4381%性能下降 19%
不適合配置4async17.9594%輕微性能損失

結論:CPU 密集型應用,Workers = CPU 核心數 + Sync Worker 是最佳配置!


🎯 實戰案例:視頻縮圖服務

業務場景

視頻平台的縮圖生成服務:
├── 用戶上傳視頻
├── 提取第 1 秒的幀作為縮圖
├── Resize 為多個尺寸(大、中、小)
└── 保存並返回 URL

要求:
- 支援多種格式(MP4、AVI、MOV)
- 生成 3 種尺寸縮圖
- 平均處理時間:< 5 秒

應用代碼

# myapp/views.py
import subprocess
from PIL import Image
from django.http import JsonResponse
from django.core.files.storage import default_storage
import uuid
import os

def generate_thumbnail_view(request):
    """
    視頻縮圖生成 - CPU 密集型

    時間分配(假設 30秒視頻):
    - 上傳視頻:500ms (I/O)
    - FFmpeg 提取幀:2000ms (CPU 密集) ← 主要時間
    - Resize 3 種尺寸:1500ms (CPU 密集)
    - 保存圖片:200ms (I/O)

    總時間:4200ms
    CPU 時間:3500ms (83.3%)
    """
    # 獲取上傳的視頻
    video_file = request.FILES['video']

    # 保存視頻到臨時位置
    temp_video = f'/tmp/{uuid.uuid4()}.mp4'
    with open(temp_video, 'wb') as f:
        f.write(video_file.read())

    # CPU 密集型:使用 FFmpeg 提取第 1 秒的幀
    temp_frame = f'/tmp/{uuid.uuid4()}.jpg'
    subprocess.run([
        'ffmpeg',
        '-i', temp_video,
        '-ss', '00:00:01',  # 第 1 秒
        '-vframes', '1',     # 只取 1 幀
        temp_frame
    ], check=True)

    # CPU 密集型:生成多個尺寸的縮圖
    thumbnails = {}
    sizes = {
        'large': (1280, 720),
        'medium': (640, 360),
        'small': (320, 180),
    }

    img = Image.open(temp_frame)

    for size_name, (width, height) in sizes.items():
        # CPU 密集型:Resize
        resized = img.resize((width, height), Image.LANCZOS)

        # 保存
        thumb_path = f'thumbnails/{uuid.uuid4()}_{size_name}.jpg'
        buffer = BytesIO()
        resized.save(buffer, format='JPEG', quality=85)

        # 上傳到存儲(I/O)
        default_storage.save(thumb_path, buffer)
        thumbnails[size_name] = default_storage.url(thumb_path)

    # 清理臨時文件
    os.remove(temp_video)
    os.remove(temp_frame)

    return JsonResponse({
        'thumbnails': thumbnails,
        'status': 'success'
    })

Gunicorn 配置

# gunicorn.conf.py
import multiprocessing

# 伺服器規格:8 核 CPU,16GB RAM
bind = "0.0.0.0:8000"

# CPU 密集型:workers = CPU 核心數
workers = 8  # = CPU 核心數
worker_class = "sync"  # 同步 worker

# 超時配置(視頻處理可能較慢)
timeout = 600  # 10 分鐘(大視頻可能需要時間)
graceful_timeout = 60
keepalive = 5

# 防止記憶體洩漏(CPU 密集型容易累積記憶體)
max_requests = 300  # 較少的請求後重啟
max_requests_jitter = 50

# 性能優化
preload_app = True
worker_tmp_dir = "/dev/shm"

# 日誌
loglevel = "info"
accesslog = "/var/log/gunicorn/thumbnail-access.log"
errorlog = "/var/log/gunicorn/thumbnail-error.log"

# Hooks
def post_fork(server, worker):
    from django import db
    db.connections.close_all()
    server.log.info(f"Worker {worker.pid} ready for CPU intensive video processing")

def worker_exit(server, worker):
    # CPU 密集型 worker 退出時,可能累積了很多記憶體
    import gc
    gc.collect()
    server.log.info(f"Worker {worker.pid} exited, memory cleaned")

性能測試

# 使用 Apache Bench 測試
ab -n 100 -c 8 -p video.dat -T "multipart/form-data" \
   http://localhost:8000/api/thumbnail/

# 結果:
Concurrency Level:      8
Time taken for tests:   52.3 seconds
Complete requests:      100
Failed requests:        0
Requests per second:    1.91 [#/sec]
Time per request:       4184 [ms]

# CPU 監控:
# 8 個 workers 持續 100% CPU 使用率
# 每個 worker 佔用一個 CPU 核心
# 完美利用所有 CPU 資源 ✅

⚠️ CPU 密集型的正確做法:異步任務隊列

問題:Web Request 處理 CPU 密集型任務的弊端

# ❌ 錯誤做法:在 View 中直接處理
def bad_video_process_view(request):
    video = request.FILES['video']

    # 這個處理可能需要 5 分鐘!
    result = process_video(video)  # CPU 密集型

    return JsonResponse(result)

# 問題:
# 1. 用戶要等待 5 分鐘才能看到響應
# 2. Worker 被佔用 5 分鐘,無法處理其他請求
# 3. 如果超時,前端工作白費
# 4. 資源利用率低

✅ 正確做法:使用 Celery 異步任務隊列

# myapp/tasks.py
from celery import shared_task
from .models import VideoProcessJob

@shared_task
def process_video_task(video_path, job_id):
    """
    Celery 任務:處理視頻
    這個任務會在背景 worker 中執行
    """
    import subprocess
    from PIL import Image

    job = VideoProcessJob.objects.get(id=job_id)
    job.status = 'processing'
    job.save()

    try:
        # CPU 密集型處理
        # 1. 提取幀
        subprocess.run(['ffmpeg', '-i', video_path, ...])

        # 2. 生成縮圖
        img = Image.open(frame_path)
        img.resize((1280, 720))
        # ...

        job.status = 'completed'
        job.result_url = thumbnail_url
        job.save()

    except Exception as e:
        job.status = 'failed'
        job.error = str(e)
        job.save()

# myapp/views.py
from django.http import JsonResponse
from .models import VideoProcessJob
from .tasks import process_video_task

def submit_video_view(request):
    """
    提交視頻處理任務 - 立即返回
    """
    video = request.FILES['video']

    # 保存視頻
    video_path = save_video(video)

    # 創建任務記錄
    job = VideoProcessJob.objects.create(
        status='pending',
        video_path=video_path
    )

    # 提交到 Celery 異步處理
    process_video_task.delay(video_path, job.id)

    # 立即返回任務 ID
    return JsonResponse({
        'job_id': job.id,
        'status': 'pending',
        'message': 'Video processing started'
    })

def check_job_status_view(request, job_id):
    """
    查詢任務狀態
    """
    job = VideoProcessJob.objects.get(id=job_id)

    response = {
        'job_id': job.id,
        'status': job.status,  # pending/processing/completed/failed
    }

    if job.status == 'completed':
        response['result_url'] = job.result_url
    elif job.status == 'failed':
        response['error'] = job.error

    return JsonResponse(response)

Celery 配置

# celery_app.py
from celery import Celery

app = Celery('myapp')
app.config_from_object('django.conf:settings', namespace='CELERY')

# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'

# CPU 密集型任務配置
CELERY_WORKER_CONCURRENCY = 4  # = CPU 核心數
CELERY_WORKER_PREFETCH_MULTIPLIER = 1  # 一次只取一個任務
CELERY_TASK_TIME_LIMIT = 3600  # 1 小時超時

啟動 Celery Workers

# 啟動 Celery Worker(專門處理 CPU 密集型任務)
celery -A myapp worker \
    --concurrency=4 \
    --loglevel=info \
    --max-tasks-per-child=50

# Gunicorn 只處理 HTTP 請求(輕量)
gunicorn -w 8 -k sync myapp.wsgi:application

架構對比

❌ 錯誤架構:在 Gunicorn 中處理

用戶 → Nginx → Gunicorn (8 workers)
                   ↓
              直接處理視頻(CPU 密集)
              ↓ Worker 被佔用 5 分鐘
              ↓ 其他請求等待...
              ✗ 糟糕的用戶體驗


✅ 正確架構:Celery 異步處理

用戶 → Nginx → Gunicorn (8 workers)
                   ↓ 提交任務(立即返回)
              Redis Queue
                   ↓
              Celery Workers (4 workers)
                   ↓ 背景處理 CPU 密集型任務
              完成後更新資料庫

優勢:
✓ Gunicorn 立即返回,不被阻塞
✓ Celery 專門處理 CPU 密集型任務
✓ 用戶可以查詢進度
✓ 失敗可以重試
✓ 更好的資源利用

💡 面試常見問題

Q1:什麼是 CPU 密集型應用?與 I/O 密集型有什麼區別?

完整答案:

CPU 密集型應用是指大部分時間在進行 CPU 計算,而不是等待外部資源的應用。

典型場景:

  • 影像處理(resize、濾鏡、轉換格式)
  • 視頻轉碼(編碼、解碼)
  • 數據分析(統計計算、機器學習)
  • 加密解密(大量數據的加密運算)

與 I/O 密集型的對比:

特性CPU 密集型I/O 密集型
主要操作CPU 計算等待 I/O
CPU 使用率80-100%20-50%
I/O wait< 10%> 30%
時間分配95% CPU + 5% I/O10% CPU + 90% I/O
典型應用圖片處理、視頻轉碼API 調用、資料庫查詢

判斷方法:

# 運行應用後觀察 top
top

# CPU 密集型:
# - %CPU 接近 100%
# - wa (I/O wait) < 10%

# I/O 密集型:
# - %CPU < 50%
# - wa (I/O wait) > 20%

Q2:CPU 密集型應用如何配置 Gunicorn?為什麼不能用 Async Worker?

完整答案:

CPU 密集型配置原則:

workers = CPU 核心數  # 不能多!
worker_class = "sync"  # 不要用 async
timeout = 300  # 較長的超時
max_requests = 500  # 較頻繁的重啟

為什麼 Workers = CPU 核心數?

  • CPU 密集型沒有 I/O 等待
  • 每個 worker 持續佔用一個 CPU 核心
  • 超過 CPU 核心數會導致競爭和性能下降

為什麼不用 Async Worker?

Async 的優勢是處理 I/O 等待:

# I/O 密集型(Async 有優勢)
async def view():
    data1 = await fetch_api()  # 等待時可處理其他請求
    data2 = await query_db()   # 等待時可處理其他請求

# CPU 密集型(Async 沒用)
async def view():
    result = heavy_compute()  # 持續 CPU 計算,無法中斷
    # 沒有 await,事件循環無法切換
    # Async 反而增加開銷

實測對比:

  • Sync Worker: 19.12 RPS
  • Async Worker: 17.95 RPS(慢 6%)

結論:CPU 密集型用 Sync Worker + Workers = CPU 核心數


Q3:為什麼 CPU 密集型不適合放在 Web 請求中處理?應該怎麼做?

完整答案:

不適合的原因:

  1. 阻塞 Worker

    • CPU 密集型任務可能需要幾分鐘甚至更久
    • Worker 被佔用,無法處理其他請求
    • 導致其他用戶等待
  2. 超時風險

    • Gunicorn 有 timeout 限制(通常 30-300 秒)
    • 超時會被強制終止
    • 已完成的工作白費
  3. 用戶體驗差

    • 用戶要等待很久才能看到結果
    • 網頁可能顯示轉圈或無響應
    • 容易導致重複提交

正確做法:使用異步任務隊列(Celery)

# 1. View 只負責提交任務
def submit_task_view(request):
    # 創建任務記錄
    job = Job.objects.create(status='pending')

    # 提交到 Celery
    process_video_task.delay(video_path, job.id)

    # 立即返回任務 ID
    return JsonResponse({'job_id': job.id})

# 2. Celery 任務背景處理
@shared_task
def process_video_task(video_path, job_id):
    # CPU 密集型處理
    result = heavy_processing(video_path)

    # 更新任務狀態
    job = Job.objects.get(id=job_id)
    job.status = 'completed'
    job.result = result
    job.save()

# 3. 前端輪詢查詢狀態
def check_status_view(request, job_id):
    job = Job.objects.get(id=job_id)
    return JsonResponse({
        'status': job.status,
        'result': job.result if job.status == 'completed' else None
    })

架構優勢:

  • ✅ Gunicorn 立即返回,不被阻塞
  • ✅ Celery 專門處理 CPU 密集型任務
  • ✅ 可以顯示進度、支援重試
  • ✅ 更好的資源利用和擴展性

Q4:如何優化 CPU 密集型應用的性能?

完整答案:

優化策略分為四個層次:

1. 配置層優化

# 正確配置 Workers
workers = CPU_cores  # 不要超過!
worker_class = "sync"
max_requests = 500  # 防止記憶體累積

2. 算法優化(最重要!)

# 優化算法效率
# 例如:圖片 resize

# ❌ 低效算法
img.resize((800, 600), Image.NEAREST)  # 速度快但質量差

# ✅ 平衡方案
img.resize((800, 600), Image.LANCZOS)  # 質量好,速度可接受

# 使用更快的庫
# PIL → Pillow-SIMD(利用 SIMD 指令加速)
pip install pillow-simd

3. 使用異步任務隊列

# 移出 HTTP 請求處理流程
# 使用 Celery 背景處理
@shared_task
def cpu_intensive_task():
    # 不阻塞 Web Worker
    pass

4. 使用專門的服務/硬體

# 選項 1:使用 GPU 加速
# 例如:視頻轉碼用 NVIDIA GPU
subprocess.run([
    'ffmpeg',
    '-hwaccel', 'cuda',  # 使用 GPU
    '-i', input_video,
    output_video
])

# 選項 2:使用專門的微服務
# 例如:圖片處理用 ImageMagick 服務
# 視頻轉碼用專門的轉碼服務

優化優先級:

  1. 算法優化:10-100x 提升 ⭐⭐⭐
  2. 使用 Celery:提升用戶體驗 ⭐⭐⭐
  3. 正確配置:10-20% 提升 ⭐⭐
  4. 硬體加速:5-50x 提升 ⭐⭐⭐

✅ 重點回顧

CPU 密集型應用特徵

✓ 大量 CPU 計算(圖片、視頻、數據分析)
✓ CPU 使用率 80-100%
✓ I/O wait < 10%
✓ 沒有等待時間

配置原則

# 黃金法則
workers = CPU 核心數  # 不要超過!
worker_class = "sync"  # 不要用 async
timeout = 300  # 較長的超時
max_requests = 500  # 防止記憶體洩漏

為什麼 Async 不適合?

Async 優勢:I/O 等待時處理其他請求
CPU 密集型:沒有 I/O 等待 → Async 沒用
反而增加事件循環開銷

正確做法

❌ 在 Web 請求中處理 CPU 密集型任務
✅ 使用 Celery 異步任務隊列
   - 立即返回
   - 背景處理
   - 可查詢進度
   - 支援重試

📚 接下來

下一篇:03-3. 案例:混合型應用

  • 如何處理既有 I/O 又有 CPU 計算的應用
  • 如何選擇最佳配置
  • 實戰案例

相關章節:


最後更新:2025-10-31

0%