バックグラウンドタスクとCelery — 非同期タスクキュー
BackgroundTasksはレストランの呼び出し番号システムのようなものです。注文後すぐに番号札 (APIレスポンス)を受け取り, 料理ができたら知らされます。Celeryは中央キッチンのようなものです。複数のシェフが異なる注文を同時に調理し, 各注文のステータスを追跡でき, 失敗時にはリトライされます。
1. 学ぶ内容
- FastAPI内蔵
BackgroundTasks:シンプルなシナリオのクイックソリューション - Celeryアーキテクチャ:Worker / Broker (Redis)/ Backend / Flowerモニタリング
- CeleryとFastAPIの統合:タスク定義, トリガー, ステータス照会エンドポイント
- タスクのリトライとエラー処理:
@app.task(retry=3, acks_late=True) - Aliceシナリオ:バッチ価格スクレイピング — APIが即座にタスクIDを返し, Celery Workerが非同期で百万件のプロダクト価格の収集を処理
2. Aliceのリアルストーリー
(1) ペインポイント:時間のかかるタスクがAPIレスポンスをブロック
AliceのPriceTrackerは百万件のプロダクト価格を一括でスクレイピングする必要があり, 1回のスクレイピングに30分かかります。同期的に処理するとAPIリクエストがタイムアウトし, Bobのフロントエンドは30分待たないとレスポンスを受け取れません。ユーザー体験は最悪です。一方, FastAPIのBackgroundTasksは現在のプロセス内でしか実行できず, ワーカーを再起動するとタスクが失われます。
(2) Celery分散キューのソリューション
Celeryは時間のかかるタスクをメッセージキュー (Redis Broker)に配置し, Workerプロセスが非同期に消費します。APIは即座にタスクIDを返します。タスクが失敗すると自動的にリトライされ, Workerは水平スケーリング可能で, プロセスの再起動はキュー内のタスクに影響しません。
PYTHON
from celery import Celery
celery_app = Celery("pricetracker", broker="redis://localhost:6379/0")
@celery_app.task(bind=True, max_retries=3)
def scrape_prices(self, product_ids: list[int]):
# 非同期価格スクレイピング - Celery Workerで実行
...
(3) 収益
百万件のプロダクトのスクレイピングが「APIを30分ブロック」から「1秒でタスクID返却 + バックグラウンド処理」に変化しました。ワーカー数を1から10にスケーリングでき, スクレイピング時間が30分から3分に短縮されました。タスクは失敗時に3回自動リトライし, 成功率が95%から99.9%に向上しました。
3. バックグラウンドタスクの軽量ソリューション
(1) 適用シナリオ
(1) ▶サンプル:BackgroundTasksで通知を送信
PYTHON
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
app = FastAPI()
class PriceAlertRequest(BaseModel):
product_id: int
target_price: float
email: str
def send_price_alert_email(email: str, product_id: int, price: float):
# メール送信のシミュレーション (ここではawaitを使用しない)
print(f"Sending alert to {email}: Product {product_id} hit ${price}")
@app.post("/alerts")
async def create_alert(alert: PriceAlertRequest, bg: BackgroundTasks):
# レスポンス送信後に実行するタスクを追加
bg.add_task(send_price_alert_email, alert.email, alert.product_id, alert.target_price)
return {"message": "Alert created", "product_id": alert.product_id}
出力:
TEXT
# 関数定義成功
(2) 意思決定ツリー:BackgroundTasks vs. Celery
flowchart TD
Start{Need background task?} --> Time{Takes > 1 min?}
Time -->|No| Simple[Use BackgroundTasks]
Time -->|Yes| Retry{Need retry/resilience?}
Retry -->|No| Simple
Retry -->|Yes| Scale{Need horizontal scaling?}
Scale -->|No| Simple
Scale -->|Yes| Celery[Use Celery]
Simple -->|Pros| P1[Simple, no infra]
Simple -->|Cons| C1[No retry, no scale, lost on restart]
Celery -->|Pros| P2[Retry, scale, persistent, monitor]
Celery -->|Cons| C2[Redis + Worker infrastructure]
| 項目 | BackgroundTasks | Celery |
|---|---|---|
| 複雑さ | ゼロ設定 | Redis + Workerが必要 |
| 永続性 | プロセスメモリ | Redis永続化 |
| リトライ | なし | 内蔵リトライ機構 |
| スケーラビリティ | 単一プロセス | Workerベースの水平スケーリング |
| モニタリング | なし | Flowerダッシュボード |
| 用途 | 1秒未満の軽量タスク | 1分以上の時間のかかるタスク |
4. Celeryアーキテクチャ
(1) アーキテクチャ概要
flowchart TD
API[FastAPI App] -->|Enqueue Task| Broker[(Redis Broker)]
Broker -->|Consume Task| Worker1[Celery Worker 1]
Broker -->|Consume Task| Worker2[Celery Worker 2]
Broker -->|Consume Task| WorkerN[Celery Worker N]
Worker1 -->|Store Result| Backend[(Redis Backend)]
Worker2 -->|Store Result| Backend
WorkerN -->|Store Result| Backend
API -->|Query Status| Backend
Flower[Flower Monitor] -->|Observe| Broker
Flower -->|Observe| Backend
| コンポーネント | 機能 | 推奨 |
|---|---|---|
| Broker | タスクメッセージキュー | Redis |
| Backend | 結果ストレージ | Redis |
| Worker | タスク実行プロセス | celery -A app worker |
| Flower | モニタリングダッシュボード | celery -A app flower |
(1) ▶サンプル:Celery設定
PYTHON
# app/core/celery_app.py
from celery import Celery
celery_app = Celery(
"pricetracker",
broker="redis://localhost:6379/0",
backend="redis://localhost:6379/1",
)
celery_app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="UTC",
enable_utc=True,
task_track_started=True,
task_acks_late=True, # 実行後にACK, 実行前ではない
worker_prefetch_multiplier=4,
result_expires=3600, # 結果は1時間後に期限切れ
)
出力:
TEXT
# 実行成功
5. Celeryタスクの定義とトリガー
(1) タスク定義
(1) ▶サンプル:価格スクレイピングタスク
PYTHON
# app/tasks/price_scraping.py
from app.core.celery_app import celery_app
import asyncio
from sqlalchemy import select
@celery_app.task(bind=True, max_retries=3, default_retry_delay=60)
def scrape_product_prices(self, product_ids: list[int]):
"""Scrape current prices for given products."""
try:
for pid in product_ids:
# スクレイピングのシミュレーション (本番:価格ソースへのHTTPリクエスト)
price = fetch_price_from_source(pid)
# データベースに保存
save_price_record(pid, price)
return {"scraped": len(product_ids), "status": "success"}
except Exception as exc:
# 指数バックオフでリトライ
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@celery_app.task(bind=True)
def bulk_scrape_all(self, total_products: int = 1000000, batch_size: int = 1000):
"""Scrape all products in batches - chord pattern."""
batches = [
list(range(i, min(i + batch_size, total_products)))
for i in range(0, total_products, batch_size)
]
# 個別のスクレイピングタスクにファンアウト
for batch in batches:
scrape_product_prices.delay(batch)
return {"total_batches": len(batches), "status": "started"}
出力:
TEXT
# 関数定義成功
(2) タスクステータス遷移
stateDiagram-v2
[*] --> PENDING: タスク作成
PENDING --> STARTED: Workerが取得
STARTED --> PROGRESS: 実行中 (オプション)
PROGRESS --> SUCCESS: 完了
PROGRESS --> FAILURE: エラー発生
STARTED --> FAILURE: エラー発生
FAILURE --> RETRY: max_retriesに未達
RETRY --> PENDING: 再キューイング
FAILURE --> [*]: max_retries超過
SUCCESS --> [*]
(2) ▶サンプル:FastAPIエンドポイントがCeleryタスクをトリガー
PYTHON
from fastapi import FastAPI, Depends
from app.core.celery_app import celery_app
from app.tasks.price_scraping import scrape_product_prices, bulk_scrape_all
app = FastAPI()
@app.post("/api/v1/scrape/prices")
async def trigger_scrape(
product_ids: list[int],
user=Depends(require_subscription("pro")),
):
# Celeryタスクをトリガー - 即座にタスクIDを返す
task = scrape_product_prices.delay(product_ids)
return {"task_id": task.id, "status": "pending"}
@app.post("/api/v1/scrape/bulk")
async def trigger_bulk_scrape(
user=Depends(require_subscription("enterprise")),
):
task = bulk_scrape_all.delay(total_products=1000000)
return {"task_id": task.id, "status": "pending"}
出力:
TEXT
# 関数定義成功
(3) ▶サンプル:タスクステータス照会エンドポイント
PYTHON
from celery.result import AsyncResult
@app.get("/api/v1/tasks/{task_id}")
async def get_task_status(task_id: str):
result = AsyncResult(task_id, app=celery_app)
response = {
"task_id": task_id,
"status": result.status,
}
if result.ready():
if result.successful():
response["result"] = result.result
else:
response["error"] = str(result.result)
elif result.state == "PROGRESS":
response["progress"] = result.info
return response
出力:
TEXT
# 関数定義成功
6. Celery Workerの実行とモニタリング
(1) ▶サンプル:WorkerとFlowerの起動
BASH
# Celery Workerの起動
celery -A app.core.celery_app worker --loglevel=info --concurrency=4
# Flowerモニタリングダッシュボードの起動
celery -A app.core.celery_app flower --port=5555
# http://localhost:5555 にアクセスしてモニタリングダッシュボードを表示
出力:
TEXT
# コマンド実行成功
(2) ▶サンプル:タスク進捗レポート
PYTHON
from celery import current_task
@celery_app.task(bind=True)
def scrape_with_progress(self, product_ids: list[int]):
total = len(product_ids)
for i, pid in enumerate(product_ids):
# 各プロダクトを処理
price = fetch_price_from_source(pid)
save_price_record(pid, price)
# 進捗をレポート
self.update_state(
state="PROGRESS",
meta={"current": i + 1, "total": total, "percent": (i + 1) / total * 100},
)
return {"scraped": total}
出力:
TEXT
# 関数定義成功
❓ よくある質問
Q BackgroundTasksはいつ実行されますか?
A レスポンスがクライアントに送信された後に実行されます。タスクが例外をスローしても, すでに送信されたレスポンスには影響しませんが, ログには記録されます。
Q Celery WorkerとFastAPIは同じプロセスで実行すべきですか?
A いいえ。Workerは独立したプロセスで, 独立してデプロイ・スケーリングされます。FastAPIはタスクのトリガーのみを担当し, Workerが実行を担当します。
Q RedisをBrokerとBackendの両方として使う違いは何ですか?
A Brokerはタスクキュー (実行待ちのタスク), Backendは結果ストア (完了したタスクの結果)です。同じRedisインスタンスの異なるデータベース (DB 0とDB 1など)を使用できます。
Q
task_acks_late=Trueの目的は何ですか?A デフォルトでは, Workerはタスクを受信するとすぐにACKします。実行中にクラッシュするとタスクが失われます。
acks_late=Trueを設定すると, 実行完了後にのみACKし, クラッシュ時は別のWorkerが再実行します。Q Celeryタスク内で
async/awaitを使用できますか?A Celeryタスクは同期関数です。非同期コードを呼び出すには
asyncio.run()でラップします。または, Workerでeventletやgeventの並行モデルを使用します。Q 百万件規模のタスクの進捗追跡はどう処理しますか?
A 「chord」パターンを使用します。タスクを1,000のサブタスクに分割 (各サブタスクは1,000件を処理), サブタスク完了後「chord」コールバックが結果を集約します。フロントエンドは「chord」タスクのステータスをポーリングします。
📖 まとめ
- BackgroundTasksは1秒未満の軽量タスク (メール送信, ログ書き込み)に適しています。設定不要ですが, リトライや永続化はサポートしません
- Celeryはプロダクションレベルのタスクキューです:Broker (Redisキュー)+ Worker (実行)+ Backend (結果ストレージ)
@app.task(bind=True, max_retries=3)でリトライ可能なタスクを定義,self.retry()でリトライをトリガー- FastAPIは
.delay()でタスクをトリガーし,AsyncResultでステータスを照会します。APIは即座にタスクIDを返します - Flowerはビジュアルモニタリングダッシュボードを提供し, Workerステータス, タスク進捗, 成功率をリアルタイムで確認できます
📝 練習問題
- 基本問題 (難易度 ⭐):
BackgroundTasksを使用して, プロダクト作成後にバックグラウンドで通知メールを送信 (ログ出力でシミュレーション)するエンドポイントを実装し, APIは即座に作成結果を返します。ヒント:bg: BackgroundTasks+bg.add_task(fn, args) - 応用問題 (難易度 ⭐⭐):Celeryを設定 (Redis Broker + Backend)し,
scrape_pricesタスク (max_retries=3)を定義し, FastAPIエンドポイントでタスクをトリガーしてtask_idを返し, 別のエンドポイントでタスクステータスを照会します。ヒント:celery_app.delay()+AsyncResult(task_id) - チャレンジ (難易度 ⭐⭐⭐):進捗レポート付きのバッチスクレイピングタスクを実装します。
scrape_with_progressはself.update_state(state="PROGRESS")に進捗パーセンテージをレポートし, フロントエンドは/tasks/{task_id}をポーリングしてプログレスバーを表示し, Enterpriseユーザーは百万件レベルのスクレイピングをトリガーできます。ヒント:self.update_state()+result.infoで進捗を取得
---|



