はじめに
レート制限は適切な速度で送信するのに役立ちますが、数百万の受信者を効果的に処理したい場合は、作業を分割して作業者に均等に分配し、キャンペーンの途中で問題が発生した場合に再開できるようにする必要があります。
この記事では、ワーカー プール アーキテクチャと運用ワークロードのバッチ オーケストレーションについて説明します。
1. 各メールを個別に処理してみてはいかがでしょうか?
各受信者が最初から別個のジョブである場合、システムは次のような問題に直面します。
- キューが大きすぎます。バックログのトピックが膨大です。
- DB ルックアップが何度も繰り返されました。
- 各メッセージのオーバーヘッドシリアライズ/デザー。
- 進捗状況の追跡は散発的であり、再開は困難です。
合理的なアプローチ
- 受信者の キャンペーン スナップショットを作成します。
- スナップショットを 安定したバッチに分割します。
- 各バッチはワーカー プールに割り当てられます。
- バッチでは、ワーカーは現在のクォータに従ってレンダリングおよび送信します。
- 進行状況を定期的に更新します。必要がない場合は、電子メールを送信するたびに DB を更新しないでください。
2. 受信者リストのチャンク戦略
バッチサイズの選択基準
|バッチサイズ |利点 |デメリット | |を使用する場合 |---------------|-----------|---------------|----------| | 100 |リトライしやすく、高い平滑性 |オーバーヘッドキューが大きい |トランザクションバースト | | 500 |バランス |適切な進捗管理が必要 |最も人気のある | | 1,000 |バルク API の方が効果的 |再試行はより高価になります |マーケティング キャンペーン | | 5,000以上 |ネットワーク通話を最適化する |正確に再開するのは難しい |前処理に使用 |
推奨
- SES/SendGrid などの一括 API を使用して、500 ~ 1000 人の受信者の 論理バッチ を作成します。
- SMTP または重いテンプレートのレンダリングでは、バッチを 200 ~ 500 未満に保ちます。
- パーソナライゼーションが複雑な場合は、メモリの急増を避けるためにバッチを小さくする必要があります。
スナップショットの受信者
CREATE TABLE campaign_recipients_snapshot (
campaign_id TEXT NOT NULL,
recipient_id BIGINT NOT NULL,
email TEXT NOT NULL,
locale TEXT,
timezone TEXT,
template_variables JSONB,
batch_no INT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
PRIMARY KEY (campaign_id, recipient_id)
);
CREATE INDEX idx_campaign_batch_status
ON campaign_recipients_snapshot (campaign_id, batch_no, status);
スナップショットは、ソース システムのユーザー セグメントが継続的に更新される場合に、キャンペーンが途中で変更されるのを防ぐのに役立ちます。
3. ワーカープールのアーキテクチャ
Campaign Planner
│
├── Create recipient snapshot
├── Split to batch jobs
└── Publish batch jobs
│
▼
Batch Queue / Topic
│
├── Worker Group A: critical
├── Worker Group B: high
└── Worker Group C: bulk marketing
│
▼
Send Adapter Layer
│
▼
ESP / SMTP / Webhooks
ワークロードごとにワーカーを分離する
| 労働者団体 | トラフィックの種類 | 数量 | 特長 |
|---|---|---|---|
| クリティカルワーカー | OTP、パスワードをリセット | 数は少ないですが、いつでもご利用いただけます | 低遅延 |
| 標準労働者 | 通常のトランザクション | 平均 | 安定した |
| バルクワーカー | マーケティング キャンペーン | 強い弾力性 | 高スループット |
ワーカー グループを分離すると、マーケティング キャンペーンで重要なトランザクション フローが不足することを防ぐことができます。
4. メモリを節約する方法でデータをフェッチする
よくある間違い
recipients = db.query("SELECT * FROM recipients WHERE campaign_id = ?", campaign_id)
for recipient in recipients:
process(recipient)
数百万のレコードがあるため、この方法ではワーカーが簡単に数ギガバイトの RAM を消費してしまう可能性があります。
ストリーミング/ページネーションの方が正確です
def fetch_batch(db, campaign_id: str, batch_no: int, limit: int = 500):
return db.fetch_all(
"""
SELECT recipient_id, email, locale, timezone, template_variables
FROM campaign_recipients_snapshot
WHERE campaign_id = %s
AND batch_no = %s
AND status IN ('pending', 'retry')
ORDER BY recipient_id
LIMIT %s
""",
(campaign_id, batch_no, limit),
)
def process_batch(batch_job):
rows = fetch_batch(db, batch_job.campaign_id, batch_job.batch_no)
rendered = [render_email(row) for row in rows]
send_results = email_provider.send_bulk(rendered)
persist_results(batch_job.campaign_id, batch_job.batch_no, send_results)
運用ルール
- 一度に 1 つのバッチのみをメモリに保持します。
- レンダリング後、すぐに送信します。キャンペーン全体は RAM に蓄積されません。
- バッチまたはマイクロバッチでステータスを記録し、書き込み増幅を軽減します。
5. Kubernetes による動的スケーリング
HPA はキューの深さに基づいています
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: bulk-email-workers
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: bulk-email-workers
minReplicas: 4
maxReplicas: 80
metrics:
- type: External
external:
metric:
name: kafka_consumer_lag
selector:
matchLabels:
consumer_group: bulk-workers
target:
type: AverageValue
averageValue: "5000"
自動スケーリングは CPU のみに基づいて行うべきではありません
CPU が低いからといって、必ずしもキューが健全であるとは限りません。ワーカーは、レート リミッターによってブロックされているか、プロバイダーの応答が遅いためにアイドル状態になっている可能性があります。キューのラグ、保留中のバッチ、平均送信遅延は、ビジネスに近いシグナルです。
6. 正常なシャットダウンと再開可能な処理
ワーカーが電子メールを送信し、デプロイまたは自動スケーリングの途中で強制終了されるのは通常のことです。デザインは事前に計算する必要があります。
原則
- ワーカーはバッチ ジョブを受け取ります。
- バッチを次のようにマークします
processingリースタイムアウトあり。 - 処理中、定期的にハートビートが発生します。
- ワーカーが死亡すると、リースが期限切れになり、バッチはキューに戻ります。
- 電子メールの送信は、次に従って冪等である必要があります。
message_id。
リーステーブルの例
CREATE TABLE batch_leases (
campaign_id TEXT NOT NULL,
batch_no INT NOT NULL,
worker_id TEXT NOT NULL,
leased_until TIMESTAMPTZ NOT NULL,
heartbeat_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (campaign_id, batch_no)
);
重複したメールを避ける
- 各受信者/メッセージには 1 つ必要です
message_id安定しています。 - 固有の制約を使用して送信ログを記録する
message_id。 - リースを再試行または再利用する場合は、再度送信する前に送信状態を確認してください。
7. 大規模なキャンペーンの進捗状況の追跡
リアルタイムで確認する必要があるメトリクス
| メトリクス | 意味 |
|---|---|
| 受信者の合計 | スナップショット内の受信者の総数 |
| キューに入れられたバッチ | バッチ保留中 |
| 処理バッチ | バッチ実行中 |
| 送信数 | ESP に電子メールが送信されました |
| 配信数 | メール配信確認済み |
| 失敗数 | 永久に失敗する |
| eta_分 | 完成予定日 |
集約状態テーブル
CREATE TABLE campaign_progress (
campaign_id TEXT PRIMARY KEY,
total_recipients BIGINT NOT NULL,
sent_count BIGINT NOT NULL DEFAULT 0,
failed_count BIGINT NOT NULL DEFAULT 0,
processing_batches INT NOT NULL DEFAULT 0,
queued_batches INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
単純なETA
ETA = remaining_recipients / effective_send_rate_per_second
effective_send_rate 紙上の理論上の速度ではなく、メトリクスから受け取った実際の速度である必要があります。
8. 実際の戦闘の最適化
最適化チェックリスト
- 行ごとに更新するのではなく、バッチ更新によって DB 書き込みを収集します。
- テンプレートのコンパイル結果をキャッシュします。
- 必要に応じて、CPU バウンドのレンダリングを IO バウンドの送信から分離します。
- サービス間の大きなペイロードには gzip/圧縮を使用します。
- 可能であれば、すべての重いテンプレート変数をキューに詰め込まず、参照のみを送信してください。
レンダリング サービスはいつ分離する必要がありますか?
- テンプレートが複雑すぎるか、パーソナライゼーションで多くの余分なデータが使用されます。
- 個別の A/B テスト、ローカリゼーション、コンテンツ検証が必要です。
- 線形スケーリングのために、ワーカーの送信は非常にコンパクトに保つ必要があります。
概要
バッチ処理とワーカー プールは、何百万もの受信者のリストを制御可能な、再試行、再開、および自動スケールの作業単位に変換するレイヤーです。正しく設計されていれば、大規模なキャンペーンは 1 つの巨大なリスクのブロックではなく、多数の小さな独立した問題として機能します。
次の記事: 戦略的再試行からデッドレターキュー、安全な再処理まで、障害状況に対処します。