簡介
我們以一個現實問題結束本系列:一家電子商務公司需要在 4 小時內發送 1000 萬封閃購電子郵件,同時仍保持交易流程正常運作。本文將先前的所有部分組合成一個可立即投入生產的設計。
1. 問題與輸入假設
業務需求
- 最多 4 小時內發送 1000 萬封電子郵件。
- 有基本的個人化名稱、折扣代碼、區域設定。
- 有取消訂閱、開啟追蹤、點選追蹤。
- 不影響OTP和訂單確認。
容量目標
10,000,000 / 14,400 giây ≈ 694 emails/giây
Peak headroom x2 -> thiết kế cho ~1,400 emails/giây
營運架構
- 1 個主要提供者:Amazon SES。
- 1 備份提供者:SendGrid。
- 2 個獨立的工作人員池:交易型和批量行銷型。
- 1 個 Redis 集群用於速率限制和調度。
- 1 個 Kafka 叢集作為事件驅動的主幹。
- 1 個 PostgreSQL 主副本 + 用於元資料和分析攝取的唯讀副本。
2.整體生產架構
Admin UI / Campaign API
│
▼
Campaign Planner
│
├── Recipient Snapshot Service
├── Batch Planner
└── Kafka topics
│
▼
Bulk Worker Pool
│
┌───────┴────────┐
▼ ▼
Amazon SES SendGrid Fallback
│ │
└───────┬────────┘
▼
Webhook Ingestion
│
▼
Status Aggregator
│
▼
PostgreSQL + Grafana/Prometheus
主要成分
| 成分 | 角色 |
|---|---|
| 活動規劃 | 建立快照和批次作業 |
| 卡夫卡 | 生產者/消費者解耦 |
| Redis | 限制器、重試計畫、分散式鎖 |
| 散裝工人 | 渲染 + 發送行銷活動流量 |
| 事務性工作者 | 確保關鍵流量 |
| Webhook 攝取 | 接收遞送/退回/投訴事件 |
3.推薦的 Kubernetes 基礎設施
依命名空間和部署分離工作負載
apiVersion: apps/v1
kind: Deployment
metadata:
name: bulk-email-workers
spec:
replicas: 12
selector:
matchLabels:
app: bulk-email-workers
template:
metadata:
labels:
app: bulk-email-workers
spec:
containers:
- name: worker
image: ghcr.io/xdev/notification-workers:2026.04.01
env:
- name: WORKER_GROUP
value: bulk
- name: KAFKA_CONSUMER_GROUP
value: bulk-workers
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
readinessProbe:
httpGet:
path: /health/ready
port: 8080
初始尺寸建議
| 服務 | 數量 | 筆記 |
|---|---|---|
| API / 規劃器 | 3 莢 | 基礎血壓 |
| 散裝工人 | 12-40 莢 | 依滯後自動縮放 |
| 事務性工作者 | 4-8 莢 | 預留容量 |
| Webhook 處理器 | 3-6 個豆莢 | 透過回呼突發擴展 |
| Redis | 3 個節點 | 哨兵/叢集 |
| 卡夫卡 | 3 經紀人 | 複製因子 3 |
4. CI/CD 與發布策略
管道應該在那裡
- 模板渲染、限制器、提供者適配器的單元測試。
- 與Kafka、Redis、PostgreSQL的整合測試。
- 冒煙測試透過沙箱提供者發送電子郵件。
- Canary 部署到新的工作版本。
- 如果發送失敗率增加,請快速回滾。
為什麼工人需要金絲雀?
渲染器或提供者適配器中的一個小錯誤可能會將 1000 萬封電子郵件變成 1000 萬個錯誤。金絲雀 1-5% 的流量有助於在活動受到廣泛影響之前檢測到回歸情況。
5. 使用 k6 進行負載測試
如果不對活動協調部分進行負載測試,就不可能測試生產流量。
行銷活動 API 的 k6 範例
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
create_campaigns: {
executor: 'ramping-vus',
startVUs: 1,
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 100 },
{ duration: '2m', target: 0 },
],
},
},
};
export default function () {
const payload = JSON.stringify({
campaign_id: `camp-${__VU}-${__ITER}`,
template_id: 'flash_sale_v2',
segment_id: 'active_users_30d',
priority: 'normal',
});
const response = http.post('https://api.example.com/campaigns', payload, {
headers: { 'Content-Type': 'application/json' },
});
check(response, {
'campaign accepted': (r) => r.status === 202,
});
sleep(1);
}
除了 API 還需要測試什麼
- 當提供者節流時佇列積壓成長。
- 當延遲增加時工作人員會自動縮放。
- Redis 在同時存取較高時限制延遲。
- 提供者刷新事件後 Webhook 攝取爆發。
6. 應模擬混沌場景
| 情況 | 期望 |
|---|---|
| SES 支付 429 持續 15 分鐘 | 減速+部分故障轉移到SendGrid |
| Redis 增加延遲 | 工人退化但不會大規模複製 |
| 20% 的工蜂群被殺 | 批量租約回收並恢復 |
| Webhook 處理器停機 | 事件被緩衝,沒有狀態遺失 |
| 活動中的範本錯誤 | 活動已暫停,其他流量仍然安全 |
混沌測試的目標
不是為了證明系統是不朽的,而是為了確認當系統發生故障時,它會以受控、可觀察和可恢復的方式發生故障。
7. 初步成本分析
成本構成較大
| 類別 | 估計 |
|---|---|
| Amazon SES 發送 1000 萬封電子郵件 | 約 1,000 美元 |
| SendGrid 後備儲備 | 幾百到幾千美元,取決於計劃 |
| Kubernetes 運算 | 取決於雲端和自動縮放視窗 |
| 卡夫卡/Redis/PostgreSQL | 固定基礎成本 |
| 可觀察性 | Prometheus/Grafana 託管或自架 |
成本優化
- 使用 SES 作為大容量工作負載的主要提供者。
- 僅在足以應對災難場景的層級啟用後備提供者。
- 將繁重的分析分離到非同步管道中,而不強制主 PostgreSQL 承擔整個負載。
- 優化模板渲染快取以減少工作CPU。
8. 從生產中學到的經驗教訓
正確的決定
- 從一開始就將事務性工作人員和散裝工作人員分開。
- 標識訊息
message_id穩定到冪等。 - 在進行大型活動之前建立儀表板活動預計到達時間和投訴率。
- 比最初預期更仔細地預熱網域名稱/IP。
痛苦但寶貴的教訓
1.worker的理論吞吐量並不像通過provider的實際吞吐量那麼重要。 2. 如果使用相同的網域/IP,糟糕的行銷活動甚至可能會損害交易流量的聲譽。 3. 沒有抖動的重試很快就會變成自殘的DDoS。 4. 需要進行 Webhook 協調才能了解實際發送了哪些電子郵件。
9. 1000 萬封電子郵件活動的上線清單
- SPF、DKIM、DMARC 域已通過身份驗證和對齊。
- 段已清理,已套用抑制清單。
- 根據配置的提供者/域/IP 進行速率限制。
- 儀表板、警報和操作手冊已準備就緒。
- 後備提供者經過測試。
- 小型金絲雀戰役成功進行。
- 待命輪調準確了解頁面閾值以及如何暫停活動。
總結
發送 1000 萬封電子郵件的問題不僅僅是規模工人的問題。這是事件驅動架構、可交付性、速率控制、系統監控和操作程序的同時問題。當這些層被設計在一起時,大型活動就變成了可預測、可控制的工作負載,而不是一場賭博。
您已經完成了設計大型電子郵件通知平台的核心知識鏈,從高層設計到生產部署。