01-5. Gevent Worker 詳解

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


⚠️ 重要提醒:技術定位

Gevent 是一個成熟但相對過時的技術,主要用於維護舊專案或 WSGI 應用的過渡階段。

現代 Python 異步方案

新專案應該優先考慮:

  • ASGI + Uvicorn/Daphne:Python 官方的異步標準
  • Django 3.0+ async views:Django 原生 async/await 支持
  • FastAPI:現代異步 Web 框架
# 現代方案(推薦)
pip install uvicorn
uvicorn myproject.asgi:application --workers 4

# Gevent 方案(舊專案維護)
pip install gunicorn gevent
gunicorn myproject.wsgi:application --worker-class gevent

何時使用 Gevent?

適合使用:

  • 維護舊的 WSGI Django 專案
  • 暫時無法遷移到 ASGI 的專案
  • 需要快速提升舊應用的並發能力

不建議使用:

  • 新專案:直接用 ASGI + asyncio
  • Django 3.0+ 專案:使用原生 async views
  • 需要長期維護的項目:避免技術債務

🎯 本篇重點

理解 Gevent Worker(協程 Worker)如何用一個進程處理成千上萬個並發請求,以及為什麼它逐漸被 asyncio 取代。


🤔 什麼是 Gevent Worker?

一句話解釋

Gevent Worker = 使用協程(Coroutine)的 Worker,一個進程可以同時處理數千個請求。


🎪 用雜技演員來比喻

Sync Worker = 普通演員

演員 A:
1. 拋球 1 → 等球落下 → 接住 → 完成 ✅
2. 拋球 2 → 等球落下 → 接住 → 完成 ✅
3. 拋球 3 → 等球落下 → 接住 → 完成 ✅

一次只能處理一個球

Gevent Worker = 雜耍演員

演員 B(雜耍大師):
1. 拋球 1 ↗️(球在空中飛)
2. 趁球 1 還在空中,拋球 2 ↗️
3. 趁球 1、2 還在空中,拋球 3 ↗️
4. 接住球 1 ✅
5. 拋球 4 ↗️
6. 接住球 2 ✅
...

同時處理多個球!

關鍵: 利用「等待時間」做其他事情!


💻 Gevent Worker 的工作原理

啟動 Gevent Worker

# 1. 先安裝 gevent
pip install gevent

# 2. 啟動 Gunicorn with Gevent
gunicorn myproject.wsgi:application \
    --workers 4 \
    --worker-class gevent \
    --worker-connections 1000 \
    --bind 0.0.0.0:8000

重點參數:

  • --worker-class gevent:使用協程模式
  • --worker-connections 1000:每個 Worker 最多 1000 個並發連接

總並發數 = workers × worker-connections = 4 × 1000 = 4000


協程的運作方式

# Gevent Worker 的內部邏輯(簡化版)

class GeventWorker:
    def __init__(self):
        self.greenlets = []  # 協程列表
    
    def run(self):
        while True:
            # 1. 接收新請求,創建協程
            request = self.accept_request()
            greenlet = gevent.spawn(self.handle_request, request)
            self.greenlets.append(greenlet)
            
            # 2. 在所有協程間切換
            # 當一個協程在等待 I/O 時,自動切換到其他協程
            gevent.joinall(self.greenlets, timeout=0.01)
    
    def handle_request(self, request):
        # 處理單個請求
        response = self.process(request)
        self.send_response(response)

核心概念:

  • Greenlet(綠色線程):超輕量的協程
  • 自動切換:等待 I/O 時自動切換到其他協程
  • 非阻塞:不會傻傻等待

📊 實際運作範例

場景:處理 3 個外部 API 請求

# views.py
import requests

def fetch_api(request):
    # 呼叫外部 API(需要 2 秒)
    response = requests.get('https://api.example.com/data')
    return JsonResponse(response.json())

Sync Worker(1 個 Worker)

時間軸:
0秒  → 請求 A 到達,開始呼叫 API
       等待 API 回應... ⏸️(Worker 閒置)
       請求 B 到達,等待中... ⏳
       請求 C 到達,等待中... ⏳

2秒  → 請求 A 完成 ✅
       開始處理請求 B
       等待 API 回應... ⏸️

4秒  → 請求 B 完成 ✅
       開始處理請求 C
       等待 API 回應... ⏸️

6秒  → 請求 C 完成 ✅

總時間:6 秒
CPU 利用率:幾乎 0%(都在等待)

Gevent Worker(1 個 Worker)

時間軸:
0秒  → 請求 A 到達,發送 API 請求
       ↓(不等待!立即切換)
       請求 B 到達,發送 API 請求
       ↓(不等待!立即切換)
       請求 C 到達,發送 API 請求
       ↓
       現在有 3 個 API 請求在飛行中...

2秒  → 請求 A 的 API 回應了 ✅
       請求 B 的 API 回應了 ✅
       請求 C 的 API 回應了 ✅

總時間:2 秒
並發處理:3 個請求同時進行!

效能提升:3 倍!


🔍 Gevent 的核心技術

1. Monkey Patching(猴子補丁)

Gevent 需要 “Monkey Patch” 才能正常工作:

# gunicorn_config.py
import gevent.monkey
gevent.monkey.patch_all()

# 這會替換 Python 的標準庫:
# - socket → gevent.socket(非阻塞)
# - time.sleep → gevent.sleep(非阻塞)
# - threading → gevent(協程)

為什麼需要?

# 沒有 Monkey Patch
import time
time.sleep(2)  # 阻塞整個 Worker ❌

# 有 Monkey Patch
import time
time.sleep(2)  # 自動切換到其他協程 ✅

2. 事件循環(Event Loop)

# Gevent 的事件循環(簡化概念)

event_loop = EventLoop()

# 協程 1:處理請求 A
def handle_request_a():
    print("請求 A:發送 API 請求")
    response = requests.get('https://api.com')  # 等待 I/O
    # ↑ 在這裡自動切換到協程 2!
    print("請求 A:收到回應")

# 協程 2:處理請求 B
def handle_request_b():
    print("請求 B:發送 API 請求")
    response = requests.get('https://api.com')  # 等待 I/O
    # ↑ 在這裡自動切換到協程 3!
    print("請求 B:收到回應")

# 事件循環管理所有協程
event_loop.add(handle_request_a)
event_loop.add(handle_request_b)
event_loop.run()

輸出:

請求 A:發送 API 請求
請求 B:發送 API 請求  ← 立即切換!
請求 A:收到回應       ← 哪個先回應就先處理
請求 B:收到回應

3. Greenlet(輕量級協程)

# Greenlet 的特性

# 記憶體占用對比:
Thread:   8 MB     線程
Greenlet: 4 KB     協程輕量 2000!)

# 創建速度對比:
Thread:   1 ms     線程
Greenlet: 0.001 ms  協程1000!)

# 因此可以創建大量協程:
1 個 Gevent Worker = 1000-10000 個協程 

⚖️ Gevent Worker 的優缺點

✅ 優點

優點說明數據
高並發單個 Worker 可處理上千連接1 worker = 1000+ 連接
記憶體效率協程比線程輕量 2000 倍1 greenlet = 4 KB
I/O 友好自動利用等待時間CPU 利用率 > 80%
程式碼簡單不需要 async/await 語法用同步的方式寫異步代碼

❌ 缺點

缺點說明影響
需要 Monkey Patch可能影響某些第三方庫兼容性問題
不適合 CPU 密集協程無法利用多核計算任務慢
除錯困難協程切換不可見錯誤難追蹤
C 擴展可能阻塞某些 C 庫不支援協程仍會阻塞

🎯 Gevent Worker 的適用場景

✅ 非常適合

1. 大量外部 API 呼叫

# 範例:聚合多個數據源
def dashboard(request):
    # 需要呼叫 5 個外部 API
    
    # ❌ Sync Worker:串行執行(10 秒)
    weather = requests.get('https://api.weather.com')    # 2秒
    news = requests.get('https://api.news.com')          # 2秒
    stocks = requests.get('https://api.stocks.com')      # 2秒
    social = requests.get('https://api.twitter.com')     # 2秒
    analytics = requests.get('https://api.analytics.com') # 2秒
    
    # ✅ Gevent Worker:並發執行(2 秒)
    # 所有請求幾乎同時發出,等待時間重疊!
    
    return JsonResponse({...})

效能提升:5 倍!


2. 高並發 Web 應用

# 範例:社交媒體 API
def get_feed(request):
    user = request.user
    
    # 查詢資料庫(I/O)
    posts = Post.objects.filter(
        author__in=user.following.all()
    )[:50]
    
    # 對每篇貼文獲取讚數(Redis)
    for post in posts:
        post.likes = cache.get(f'post:{post.id}:likes')
    
    return JsonResponse(...)

# 同時 5000 個用戶訪問:
# - Sync Worker(4 workers):只能同時處理 4 個 ❌
# - Gevent Worker(4 workers, 1000 connections):可處理 4000 個 ✅

3. 爬蟲或數據抓取

# 範例:批量抓取網頁
def scrape_websites(request):
    urls = [
        'https://example1.com',
        'https://example2.com',
        # ... 100 個 URLs
    ]
    
    # ❌ Sync Worker:串行抓取(100 秒)
    results = []
    for url in urls:
        response = requests.get(url)  # 每個 1 秒
        results.append(response.text)
    
    # ✅ Gevent Worker:並發抓取(2-3 秒)
    # 使用 gevent 的協程池
    import gevent
    from gevent import monkey
    monkey.patch_all()
    
    jobs = [gevent.spawn(requests.get, url) for url in urls]
    gevent.joinall(jobs)
    results = [job.value.text for job in jobs]
    
    return JsonResponse({'count': len(results)})

效能提升:30-50 倍!


4. WebSocket 或長連接

# 範例:即時通知系統
def notification_stream(request):
    def event_stream():
        while True:
            # 檢查新通知(Redis)
            notification = get_new_notification(request.user)
            if notification:
                yield f"data: {notification}\n\n"
            gevent.sleep(1)  # 等待 1 秒(不阻塞其他連接)
    
    return StreamingHttpResponse(event_stream())

# 1000 個用戶同時連接:
# - Sync Worker:需要 1000 個 workers ❌(不可能)
# - Gevent Worker:1 個 worker 就夠 ✅

❌ 不適合

1. CPU 密集型任務

# ❌ 不適合:複雜計算
def calculate_primes(request):
    n = 1000000
    primes = []
    for num in range(2, n):
        is_prime = True
        for i in range(2, int(num ** 0.5) + 1):
            if num % i == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return JsonResponse({'count': len(primes)})

# 問題:
# - 沒有 I/O 等待,協程無法切換
# - 一個計算密集的請求會阻塞所有其他請求
# - 無法利用多核 CPU

解決方案: 使用 Sync Worker 或 多進程


2. 不兼容 Monkey Patch 的庫

# ❌ 某些 C 擴展庫可能不支援
import psycopg2  # PostgreSQL 驅動

# 可能導致問題:
# - 資料庫連接阻塞
# - 協程無法正常切換

# ✅ 解決方案:使用支援協程的庫
import psycopg2.pool  # 使用連接池
# 或
from psycogreen.gevent import patch_psycopg  # 打補丁
patch_psycopg()

🔧 Gevent Worker 最佳實踐

1. 完整配置

# gunicorn_config.py
import gevent.monkey
gevent.monkey.patch_all()

import multiprocessing

# Worker 配置
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'gevent'
worker_connections = 1000

# 超時配置
timeout = 60  # Gevent 可以處理更長的連接

# 連接配置
keepalive = 5

# 日誌
loglevel = 'info'
accesslog = '-'
errorlog = '-'

2. 使用協程池

# 批量處理時使用協程池

from gevent.pool import Pool

def batch_process(request):
    urls = request.POST.getlist('urls')
    
    # 創建協程池(最多同時 50 個)
    pool = Pool(50)
    
    # 並發處理
    results = pool.map(fetch_url, urls)
    
    return JsonResponse({'results': results})

def fetch_url(url):
    response = requests.get(url)
    return response.json()

優點:

  • 限制並發數,避免過載
  • 自動管理協程生命週期

3. 錯誤處理

import gevent
from gevent import Timeout

def fetch_with_timeout(request):
    url = request.GET.get('url')
    
    # 設置超時(5 秒)
    timeout = Timeout(5)
    timeout.start()
    
    try:
        response = requests.get(url)
        return JsonResponse(response.json())
    except Timeout:
        return JsonResponse({'error': 'Request timeout'}, status=408)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)
    finally:
        timeout.close()

4. 監控協程數量

# 查看當前協程數量
import gevent

def status(request):
    greenlets = len(gevent._greenlet.greenlets)
    
    return JsonResponse({
        'active_greenlets': greenlets,
        'max_connections': 1000
    })

📊 效能對比實測

測試場景:呼叫外部 API

# views.py
import requests
import time

def api_call(request):
    # 模擬外部 API(1 秒延遲)
    response = requests.get('http://httpbin.org/delay/1')
    return JsonResponse(response.json())

壓力測試

# 測試:1000 個請求,100 並發

# Sync Worker(4 workers)
ab -n 1000 -c 100 http://localhost:8000/api/

結果:
- Requests per second: 4 [#/sec]  ← 一次只能處理 4 個
- Time taken: 250
# Gevent Worker(4 workers, 1000 connections)
ab -n 1000 -c 100 http://localhost:8000/api/

結果:
- Requests per second: 95 [#/sec]  ← 幾乎全部並發
- Time taken: 10.5 秒

效能提升:23.8 倍!🚀

🎤 面試常見問題

Q1: Gevent 如何實現高並發?

答案:

Gevent 使用**協程(Coroutine/Greenlet)**實現高並發:

  1. 輕量級:每個協程只佔用 4KB 記憶體(線程需要 8MB)
  2. 自動切換:當協程等待 I/O 時,自動切換到其他協程
  3. 非阻塞:利用等待時間處理其他請求
  4. 單進程:在一個進程內通過事件循環管理上千協程

一個 Gevent Worker 可以同時處理 1000+ 個連接,而 Sync Worker 只能處理 1 個。


Q2: 什麼是 Monkey Patching?為什麼需要?

答案:

Monkey Patching 是指在運行時動態替換 Python 標準庫的功能。

為什麼需要:

# 標準的 socket 是阻塞的
import socket
s = socket.socket()
s.connect(...)  # 阻塞!

# Gevent 需要替換成非阻塞版本
import gevent.monkey
gevent.monkey.patch_all()
# 現在 socket 變成 gevent.socket(非阻塞)

這樣第三方庫(如 requests)無需修改就能支援協程。


Q3: Gevent Worker 適合什麼場景?

答案:

Gevent Worker 適合 I/O 密集型場景:

適合:

  • 大量外部 API 呼叫
  • 高並發 Web 應用
  • 爬蟲和數據抓取
  • WebSocket、長連接
  • 微服務間的 RPC 呼叫

不適合:

  • CPU 密集型計算(無法利用多核)
  • 需要原生線程的場景
  • 不兼容 Monkey Patch 的庫

Q4: Gevent vs Sync Worker,如何選擇?

答案:

使用 Gevent Worker 當:

  • 應用主要是等待(API、資料庫、網絡)
  • 需要處理大量並發連接
  • 每個請求處理時間較長(> 500ms)

使用 Sync Worker 當:

  • 應用主要是計算(數據分析、圖像處理)
  • 每個請求很快(< 100ms)
  • 並發需求不高(< 100 連接)

快速判斷:

if 請求中有大量等待時間:
    use_gevent()
elif 請求中主要是計算:
    use_sync()

🆚 Gevent vs 現代 Asyncio

技術對比

特性Gevent (舊)ASGI + Asyncio (新)
標準支持❌ 第三方庫✅ Python 官方標準
語法同步風格async/await 明確
Monkey Patch⚠️ 需要,有兼容性風險✅ 不需要
生態系統⚠️ 逐漸減少✅ 積極發展
Django 支持WSGI onlyDjango 3.0+ 原生支持
除錯⭐⭐ 困難⭐⭐⭐⭐ 較容易
學習曲線低(看起來像同步)高(需要理解 async)
長期維護⚠️ 技術債務✅ 推薦

代碼對比

# Gevent 方式(舊)
import gevent.monkey
gevent.monkey.patch_all()
import requests

def fetch_data(request):
    # 看起來是同步,實際是異步
    response = requests.get('https://api.example.com')
    return JsonResponse(response.json())

# ASGI + Asyncio 方式(新)
import httpx

async def fetch_data(request):
    # 明確的 async/await 語法
    async with httpx.AsyncClient() as client:
        response = await client.get('https://api.example.com')
        return JsonResponse(response.json())

遷移建議

如果你正在使用 Gevent:

  1. 短期:繼續使用沒問題,但要了解它的限制
  2. 中期:計劃遷移到 ASGI(Django 3.0+ 支持)
  3. 長期:新功能用 async views,舊代碼逐步重構

如果你要開新專案:

# 直接使用現代方案
django-admin startproject myproject
pip install uvicorn

# 運行 ASGI 服務器
uvicorn myproject.asgi:application --workers 4

✅ 重點回顧

Gevent Worker 的核心

  • 使用協程(Greenlet)實現高並發
  • 一個 Worker = 1000+ 並發連接
  • 需要 Monkey Patch 改造標準庫
  • 自動在 I/O 等待時切換協程

技術定位

  • ⚠️ 舊專案維護:適合快速提升 WSGI 應用的並發能力
  • 新專案開發:應使用 ASGI + asyncio
  • 🔄 過渡方案:等待遷移到 ASGI 的階段

適用場景

  • ✅ 維護舊的 WSGI Django 專案
  • ✅ 暫時無法遷移到 ASGI
  • ✅ 需要快速提升舊應用並發
  • ❌ 新專案(用 ASGI)
  • ❌ Django 3.0+ 新功能(用 async views)

效能優勢

  • 記憶體:協程比線程輕量 2000 倍
  • 並發:1 worker 可處理 1000+ 連接
  • 效能:I/O 密集場景可提升 10-50 倍

配置要點

  • 設定 --worker-class gevent
  • 設定 --worker-connections 1000
  • 在配置文件中加入 gevent.monkey.patch_all()

📚 接下來

現在你完全理解 Gevent Worker 了!下一篇我們會學習最後一種 Worker:

01-6. Gthread Worker 詳解

  • 什麼是線程(Thread)
  • Gthread Worker 如何結合進程與線程
  • 適合混合型任務的場景

🤓 小測驗

  1. Gevent Worker 的核心技術是什麼?

  2. 為什麼 Gevent 適合 I/O 密集型任務?

  3. 4 個 Gevent Workers,每個 1000 connections,總並發是多少?

  4. 為什麼 Gevent 不適合 CPU 密集型任務?


上一篇: 01-4. Sync Worker 詳解
下一篇: 01-6. Gthread Worker 詳解


最後更新:2025-10-30

0%