網頁應用程式的訊息佇列與背景工作
為何你的網頁應用程式需要背景工作
當使用者點擊「註冊」或「下單」時,他們期望快速回應。但該點擊觸發的許多操作——發送歡迎電子郵件、產生 PDF 發票、調整上傳圖片大小,或與第三方 API 同步資料——可能需要數秒甚至數分鐘。如果你在請求期間同步執行這些任務,使用者必須等待,而你的伺服器也會佔用資源。背景工作透過將工作移出請求-回應週期來解決這個問題。
訊息佇列是背景工作系統的骨幹。它們讓你的網頁應用程式將工作加入佇列並立即返回回應,而獨立的工作者程序則取出工作並以非同步方式執行。這將網頁層與處理層解耦,提升回應性、可靠性與可擴展性。
核心概念:佇列、生產者與消費者
最簡單來說,訊息佇列是一個緩衝區,用於保存訊息直到消費者取出它們。其組成部分包括:
- 生產者 (Producer):你的網頁應用程式(或任何服務),負責建立訊息並將其推入佇列。
- 佇列 (Queue):保存訊息的儲存機制。它可以是記憶體內(如 Redis)或專用代理(如 RabbitMQ)。
- 消費者 (Consumer/Worker):一個獨立程序,監聽佇列、取出訊息並執行工作。
此模式通常稱為生產者-消費者或發布-訂閱(如果多個消費者可以對同一訊息採取行動)。關鍵好處是解耦:生產者不需要知道誰處理工作或需要多長時間。
背景工作的常見使用案例
背景工作非常適合任何不需要在向使用者發送回應之前完成的任務。典型例子包括:
- 電子郵件發送:歡迎郵件、密碼重設、電子報。
- 圖片與影片處理:縮圖產生、壓縮、浮水印。
- 報告產生:PDF 發票、CSV 匯出、分析儀表板。
- 第三方 API 呼叫:付款處理、運費查詢、CRM 同步。
- 資料清理:刪除舊記錄、封存日誌、重新計算統計資料。
- 排程任務:每日摘要、快取預熱、資料庫備份。
如果一個任務可以延遲幾秒而不損害使用者體驗,它就是背景工作的候選者。
為工作選擇合適的工具
正確的訊息佇列取決於你的規模、可靠性需求與現有技術堆疊。以下是常見選項的比較:
| 工具 | 最適合 | 持久性 | 複雜度 |
|---|---|---|---|
| Redis(搭配 RQ、Bull、Celery) | 簡單、快速的佇列;中小規模 | 選用(可持久化至磁碟) | 低 |
| RabbitMQ | 複雜路由、保證送達、高可靠性 | 是 | 中 |
| Apache Kafka | 高吞吐量事件串流、日誌聚合 | 是 | 高 |
| AWS SQS | 全託管、無伺服器、按用量付費 | 是 | 低 |
| 資料庫支援(例如 PostgreSQL SKIP LOCKED) | 簡單、無需額外基礎設施 | 是 | 低 |
對許多網頁應用程式而言,從 Redis 或資料庫支援的佇列開始就足夠了。隨著規模成長,你可以遷移到更強大的代理,如 RabbitMQ 或 Kafka。
實作背景工作:逐步指南
讓我們使用 Python、Celery 和 Redis 走過基本實作。相同的原則適用於其他技術堆疊(例如 Node.js 搭配 Bull、Ruby 搭配 Sidekiq、Go 搭配 Machinery)。
1. 設定 Redis 與 Celery
安裝 Redis 與 Celery 函式庫。設定 Celery 使用 Redis 作為代理與結果後端。
# Install dependencies
pip install celery redis
# Start Redis server (if not already running)
redis-server
2. 定義 Celery 應用程式
建立一個檔案 tasks.py,初始化 Celery 並定義背景任務。
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379/0')
@app.task
def send_welcome_email(user_id):
# Simulate sending an email
print(f"Sending welcome email to user {user_id}")
# In production, integrate with an email service
return f"Email sent to user {user_id}"
3. 從你的網頁應用程式將工作加入佇列
在你的網頁框架中(例如 Flask、Django),以非同步方式呼叫任務。.delay() 方法會將工作加入佇列並立即返回。
from tasks import send_welcome_email
@app.route('/signup', methods=['POST'])
def signup():
# ... create user in database ...
send_welcome_email.delay(user_id=123)
return {"status": "success"}, 202
4. 執行工作者程序
啟動一個或多個工作者程序,監聽佇列並執行任務。
celery -A tasks worker --loglevel=info
現在,當使用者註冊時,網頁應用程式立即返回 202 Accepted 回應,而工作者則在背景發送電子郵件。
可靠背景工作的最佳實務
背景工作會引入新的失敗模式。遵循以下實務讓你的系統保持穩健:
- 冪等性 (Idempotency):設計任務使其可以多次執行而不產生副作用。例如,在再次發送電子郵件之前檢查是否已發送過。
- 帶退避的重試:為暫時性失敗(例如網路逾時)設定自動重試。使用指數退避以避免壓垮外部服務。
- 死信佇列:在達到最大重試次數後,將失敗的訊息路由到單獨的佇列以供手動檢查。
- 監控與警示:追蹤佇列長度、工作成功/失敗率與工作者健康狀態。Flower(用於 Celery)或 Prometheus 等工具可以提供協助。
- 優雅關閉:確保工作者在終止前完成當前工作,以避免遺失工作。
- 速率限制:對呼叫外部 API 的工作進行節流,以維持在配額內。
擴展你的背景工作系統
隨著應用程式成長,你需要同時擴展佇列與工作者。策略包括:
- 水平擴展:增加更多工作者程序或機器。大多數佇列支援多個消費者。
- 優先級佇列:將高優先級工作(例如密碼重設)與低優先級工作(例如分析)分開。
- 批次處理:將類似的工作分組以減少開銷。
- 分片:如果達到吞吐量限制,將佇列分散到多個代理。
請記住,增加工作者會提高並行度,這可能對資料庫或外部 API 造成壓力。監控資源使用情況並相應調整。
常見問題
訊息佇列與背景工作有何不同?
訊息佇列是傳輸訊息的基礎設施,而背景工作是由訊息表示的工作單元。你將工作(訊息)加入佇列,工作者則處理它。
我需要像 RabbitMQ 這樣的獨立訊息代理嗎?
不一定。對許多網頁應用程式而言,Redis 甚至資料庫表格都可以作為簡單的佇列。當你需要進階路由、保證送達或高吞吐量時,才使用專用代理。
如何處理失敗的背景工作?
實作帶指數退避與最大重試次數的重試。之後,將工作移至死信佇列以供手動審查。始終記錄失敗並附上足夠的上下文以便除錯。
準備好優化你的網頁應用程式效能了嗎?從卸載你的第一個背景工作開始。當你在這些工作中需要處理 PDF 或圖片時,請查看我們的 PDF 壓縮工具,在將檔案發送給使用者之前縮小檔案大小。