技术方案

产品级消息推送平台架构设计

从 Message Nest 的启发出发,设计一个面向企业级场景的多渠道消息推送平台完整架构。涵盖异步管道、多租户隔离、模板引擎、渠道插件体系、可观测性与 SDK 生态。

消息推送平台全景架构

一、产品定位

这不是一个”消息发送工具”,而是一个企业级消息推送中台。类比:消息推送领域的 API Gateway —— 上游业务只需要说”给用户发一条订单已发货”,平台负责决定通过什么渠道、什么格式、什么时机送达,并在送达失败时自动重试或降级。

核心价值主张:

业务方只关心”发什么”和”发给谁”,平台负责”怎么发”和”到没到”。

对标产品:OneSignal、阿里云消息推送、Firebase Cloud Messaging 的企业自建替代。


二、全景架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
                              ┌────────────────────────┐
│ Admin Dashboard │
│ (渠道管理/模板/监控/审计) │
└────────────┬───────────┘
│ gRPC
┌──────────┐ ┌──────────┐ ┌─────────────▼─────────────┐
│ Go SDK │ │.NET SDK │ │ API Gateway │
│ Python │ │ Java │ │ (鉴权 / 限流 / 路由) │
│ SDK │ │ SDK │ └─────────────┬───────────────┘
└────┬─────┘ └────┬─────┘ │
│ │ ┌───────▼───────┐
└──────┬──────┘ │ Message Bus │
│ │ (Pulsar) │
▼ └───┬───┬───┬───┘
┌───────────────────────┐ ┌───────┘ │ └───────┐
│ Template Engine │ │ │ │
│ - 模板版本管理 │ ▼ ▼ ▼
│ - 占位符校验 │ ┌──────┐ ┌──────┐ ┌──────────┐
│ - 预览/灰度 │ │Worker│ │Worker│ │Scheduler │
└───────────────────────┘ │ #1 │ │ #2 │ │ (Cron) │
└──┬───┘ └──┬───┘ └────┬─────┘
│ │ │
┌────────▼────────▼──────────▼────────┐
│ Channel Dispatcher │
│ (渠道路由 / 格式化 / 限流 / 重试) │
└───┬───┬───┬───┬───┬───┬───┬───┬───┘
│ │ │ │ │ │ │ │
┌───▼┐ ┌▼──┐┌▼──┐┌▼──┐┌▼──┐┌▼──┐┌▼──┐
│邮件│ │钉钉││企微││飞书││短信││Push││...│
└────┘ └───┘└───┘└───┘└───┘└───┘└───┘

┌──────────────────────────────────────────────────────────┐
│ Observability │
│ Prometheus + Grafana + Jaeger + ELK │
└──────────────────────────────────────────────────────────┘

架构分层

职责 技术选型
接入层 API Gateway + 鉴权 + 限流 + 租户隔离 Envoy / Ocelot / 自研 Gateway
总线层 异步消息解耦,削峰填谷,死信队列 Apache Pulsar (或 Kafka + RabbitMQ)
引擎层 模板渲染、渠道路由、格式化、重试策略 Go / .NET Worker Pool
渠道层 各渠道的协议适配、签名、限流、结果回写 Plugin 体系,每个渠道独立进程
调度层 定时消息、延迟发送、周期性推送 Cron + 延迟队列
数据层 模板存储、消息归档、审计日志、租户配置 PostgreSQL + Redis + S3/MinIO
可观测层 全链路追踪、推送漏斗、渠道健康度告警 Prometheus + Jaeger + Grafana

三、核心领域模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──────────────┐       ┌──────────────────┐
│ Tenant │ 1───N │ Channel │
│ (租户/团队) │ │ (渠道配置 + 凭证) │
└──────┬───────┘ └──────────────────┘

│ 1───N ┌──────────────────┐
│ │ Template │
│ │ (模板 + 版本) │
│ └────────┬─────────┘
│ │ 1───N
│ ┌────────▼─────────┐
│ │ TemplateInstance │
│ │ (模板×渠道 绑定) │
│ └──────────────────┘

│ 1───N ┌──────────────────┐
└───────│ MessageTask │
│ (发送任务) │
└────────┬─────────┘
│ 1───N
┌────────▼─────────┐
│ DeliveryRecord │
│ (送达记录) │
└──────────────────┘

关键实体

实体 说明
Tenant 多租户隔离的基本单位。每个租户有独立的渠道配置、模板、发送配额。
Channel 渠道定义 + 凭证管理。凭证加密存储(KMS 或 Vault),API 不返回明文。
Template 消息模板,支持版本管理(v1 → v2 → v3)。每个版本独立存储,可灰度切换。
TemplateInstance 模板在某渠道上的绑定配置。一个模板可以有多个实例(企微 + 钉钉 + 邮件),各自独立启停。
MessageTask 一次发送请求的完整记录。包含原始 payload、渲染后的消息、目标渠道列表。
DeliveryRecord 每个渠道的送达详情:状态、时间戳、渠道返回的原始响应、重试次数。

四、异步处理管道

这是整个平台最核心的设计——发送请求绝不同步等待渠道返回

管道流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
API 接收请求


┌──────────────┐
│ 1. 校验 │ 租户配额、模板存在性、必填字段、占位符完整性
└──────┬───────┘


┌──────────────┐
│ 2. 入库 │ MessageTask(status=pending) + DeliveryRecord × N
└──────┬───────┘


┌──────────────┐
│ 3. 入队 │ 投递到 Pulsar topic: message-delivery
└──────┬───────┘

▼ 立即返回 202 Accepted + task_id

▼ (异步)
┌──────────────┐
│ 4. Worker 消费│ 从队列拉取 → 模板渲染 → 逐渠道投递
└──────┬───────┘

├── 成功 → DeliveryRecord(status=delivered)

├── 限流 → 延迟 30s 重试 (retry_count++)

├── 网络错误 → 指数退避重试 (最多 5 次)

└── 永久失败 → DeliveryRecord(status=failed) → 死信队列

重试策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
retry_policy:
max_attempts: 5
backoff:
strategy: exponential
initial: 1s
max: 300s
multiplier: 2
retryable_errors:
- timeout
- rate_limited
- server_error_5xx
non_retryable_errors:
- invalid_credential
- template_rejected
- recipient_invalid

死信队列

永久失败的消息进入死信队列(DLQ),同时触发告警。运营人员可以在管理后台一键重放死信消息——例如修复渠道配置后,把积压的 500 条失败消息重新入队。


五、渠道插件体系

每个渠道作为一个独立的 Plugin,通过 gRPC 与核心引擎通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
service ChannelPlugin {
rpc Send(SendRequest) returns (SendResponse);
rpc ValidateConfig(ConfigCheckRequest) returns (ConfigCheckResponse);
rpc HealthCheck(HealthRequest) returns (HealthResponse);
}

message SendRequest {
string tenant_id = 1;
string message_id = 2;
bytes config = 3; // 加密的渠道配置
MessageContent content = 4;
repeated string recipients = 5;
}

插件隔离的好处

好处 说明
故障隔离 钉钉渠道 OOM 不会影响邮件渠道
独立扩缩 短信渠道 QPS 高就多起几个实例
语言无关 飞书 SDK 只有 Go 版本好?用 Go 写。邮件有现成的 .NET 库?用 .NET 写
热加载 新增渠道不需要重启核心引擎,注册 Plugin 即可

渠道健康度模型

每个渠道实例定期上报健康度:

1
2
3
4
5
6
channel_health = {
success_rate: 0.998, // 最近 5 分钟成功率
avg_latency_ms: 320, // 平均延迟
rate_limit_remaining: 45, // 剩余配额
circuit_breaker_state: "closed" // 熔断器状态
}

success_rate < 0.95 或连续 10 次失败时,熔断器打开,该渠道的发送请求直接降级——不再尝试发送,而是标记为 degraded 并通知管理员。


六、模板引擎设计

模板生命周期

1
2
3
4
5
Draft → Review → Approved → Published → (Gray 10% → 50% → 100%) → Archived
│ │
│ ├── Rollback (即时回滚到上一版本)
│ │
└───────────────────────────┘

模板版本管理

每个模板有独立的版本链:

1
2
3
4
template: "order-notification"
├── v1 (archived)
├── v2 (active, 90% traffic)
└── v3 (gray, 10% traffic)

灰度发布时,v3 先承接 10% 流量,观察 30 分钟内送达成功率无异常后,逐步提升至 100%。

占位符系统

1
2
3
4
5
6
7
8
9
10
{
"title": "订单 {{status_text}}通知",
"body": "您的订单 {{order_id}} 已于 {{time}} {{status_text}}。",
"placeholders": {
"order_id": { "type": "string", "required": true, "max_length": 32 },
"status_text": { "type": "enum", "required": true, "values": ["已发货", "已签收", "已退款"] },
"time": { "type": "datetime","required": false, "default": "now", "format": "YYYY-MM-DD HH:mm" },
"amount": { "type": "decimal", "required": false, "min": 0 }
}
}

调用方发送消息时,平台自动校验占位符的完整性、类型和枚举值。校验不通过直接拒绝,不浪费渠道配额。

渲染管线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Raw API Request


┌─────────────────┐
│ Placeholder │ 校验类型、必填、枚举值、长度
│ Validator │
└────────┬────────┘
│ ✓

┌─────────────────┐
│ Pre-processor │ 执行模板函数: {{amount|currency}} → $99.00
│ │ {{time|timezone:Asia/Shanghai}}
└────────┬────────┘


┌─────────────────┐
│ Channel Adaptor │ 根据渠道调整格式:
│ │ 钉钉 → Markdown, 邮件 → HTML, 短信 → 纯文本
└────────┬────────┘


最终发送内容

七、多租户隔离

隔离维度

1
2
3
4
5
6
7
8
Tenant
├── 渠道配额: 每天最多 10000 条短信,50000 条 App Push
├── 发送速率: 邮件 ≤ 100/s, 短信 ≤ 10/s
├── 模板数量: 最多 50 个活跃模板
├── API Key: 每租户最多 5 对 AK/SK,支持轮转
├── 回调 URL: 每租户独立的事件回调地址
├── 数据存储: 物理隔离 (database-per-tenant) 或逻辑隔离 (tenant_id 分区)
└── Dashboard: 仅可见本租户的模板、发送记录、统计数据

凭证管理

渠道 API Key / Token 使用租户级 KMS 加密存储:

1
2
3
存储:  ciphertext = AES-256-GCM(plaintext, tenant_master_key)
解密: 仅在 Worker 发送前解密到内存,用完立即清零
日志: 所有日志自动脱敏 —— token=sk-xxxx → token=sk-***x

八、事件回调与 Webhook

事件类型

事件 触发时机 Payload
message.delivered 任一渠道送达成功 {task_id, channel, recipient, ts}
message.failed 所有渠道均失败 {task_id, failures: [{channel, error}]}
message.degraded 渠道熔断,降级处理 {task_id, channel, reason}
template.published 模板新版本发布 {template_id, version, publisher}
channel.alert 渠道健康度异常 {channel, metric, threshold, current}

回调配置

租户在管理后台配置 Webhook URL + 签名密钥。平台推送时附带 X-Signature: HMAC-SHA256(payload, secret),接收方验证签名防止伪造。


九、SDK 生态

多语言支持

语言 SDK 形式 核心能力
Go go get 全功能:Send / SendAsync / Template / Callback Server
.NET NuGet 包 全功能 + ASP.NET Core 集成
Python pip install Send / SendAsync / Flask 回调验证
Java Maven 依赖 Send / Spring Boot Starter
JavaScript npm 包 Send / Next.js 集成
CLI 单二进制 pigeon send --template order --data '{"id":"123"}'

SDK 标准接口

所有 SDK 遵循统一 API 规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Go SDK
client := push.NewClient(push.Config{
BaseURL: "https://push.your-company.com",
APIKey: "pk_live_xxx",
APISecret: "sk_live_xxx",
})

task, err := client.Send(push.SendRequest{
TemplateID: "order-notification",
Recipients: []push.Recipient{
{UserID: "u-001", Channel: push.ChannelWeCom, Address: "zhangsan"},
},
Placeholders: map[string]interface{}{
"status_text": "已发货",
"order_id": "ORD-20260613-001",
},
})

十、可观测性

推送漏斗

1
2
3
4
5
6
7
8
9
10
11
请求接收 ─────────────────────────── 100%

模板渲染成功 ──────────────────────── 99.8%

渠道路由分配 ──────────────────────── 99.8%

Worker 开始处理 ───────────────────── 99.7%

渠道 API 调用成功 ─────────────────── 98.5%

渠道确认送达 ──────────────────────── 97.2%

漏斗每一层都作为 Prometheus Counter 上报,Grafana 面板实时展示。

核心指标

指标 类型 说明
push_requests_total Counter 总请求量
push_delivery_duration_seconds Histogram 端到端送达延迟
push_channel_success_rate Gauge 渠道成功率
push_retry_total Counter 重试次数
push_dlq_size Gauge 死信队列积压量
push_template_render_errors Counter 模板渲染失败数
push_tenant_quota_usage Gauge 租户配额使用率

告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
alerts:
- name: ChannelSuccessRateLow
expr: push_channel_success_rate < 0.95
for: 5m
severity: warning
annotation: "渠道 {{ channel }} 成功率降至 {{ value }}"

- name: DLQBacklog
expr: push_dlq_size > 100
for: 10m
severity: critical
annotation: "死信队列积压 {{ value }} 条,需人工介入"

- name: TenantQuotaNearLimit
expr: push_tenant_quota_usage > 0.85
severity: info
annotation: "租户 {{ tenant }} 配额使用率 {{ value }}"

十一、部署拓扑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
                           ┌─────────────┐
│ Cloud LB │
└──────┬──────┘

┌─────────────┼─────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐
│ API GW #1 │ │ API GW │ │ API GW #3 │
│ │ │ #2 │ │ │
└─────┬─────┘ └────┬────┘ └─────┬─────┘
│ │ │
└─────────────┼─────────────┘

┌─────────────▼─────────────┐
│ Pulsar Cluster │
│ (3 Broker + 3 Bookie) │
└─────────────┬───────────────┘

┌───────────────────┼───────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Worker │ │ Worker │ │ Worker │
│ Pool × 5 │ │ Pool × 5 │ │ Pool × 5 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────────┼───────────────────┘

┌───────────────────┼───────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Channel │ │ Channel │ │ Channel │
│ Plugin #1 │ │ Plugin #2 │ │ Plugin #3 │
│ (邮件) │ │ (钉钉) │ │ (飞书) │
└───────────┘ └───────────┘ └───────────┘

┌────────────────────────────────────────────────────────────┐
│ Data & Infra │
│ PostgreSQL × 3 (主从) + Redis Cluster + S3/MinIO │
└────────────────────────────────────────────────────────────┘

资源估算

组件 规格 数量 说明
API Gateway 2C4G 3 无状态,水平扩展
Pulsar Broker 4C8G 3 消息持久化,三副本
Worker Pool 4C8G 3×5 Pods 按渠道隔离,独立扩缩
Channel Plugin 2C2G 按渠道 轻量 gRPC 服务
PostgreSQL 8C32G 3 一主两从
Redis Cluster 4C16G 6 缓存 + 限流
S3/MinIO 模板附件、日志归档

日处理 100 万条消息所需资源约 40–50 核 / 100GB 内存


十二、与 Message Nest 的差异

维度 Message Nest 本方案
部署 单二进制 微服务集群
消息模型 同步 HTTP → 渠道 异步 Pulsar 管道 + 重试 + DLQ
渠道扩展 代码内注册 gRPC Plugin,热加载
多租户 数据库级隔离 + 配额
模板 单版本 版本链 + 灰度发布
可观测性 控制台日志 Prometheus + Jaeger + Grafana
SDK Go only 5 种语言
适用规模 < 10 万条/天 > 100 万条/天
运维复杂度 5 分钟部署 需 K8s 集群 + 运维团队

本质区别:Message Nest 解决了”我来帮你发“的问题,本方案解决的是”企业用它做消息基础设施“的问题。两者不冲突——小团队用 Message Nest 足够,当业务增长到需要 SLA 保障时再迁移到产品级方案。


十三、总结

这个架构设计的核心思路是:

  1. 异步优先 —— 发送请求立刻返回,所有重试/降级/死信在异步管道中处理
  2. 渠道即插件 —— 故障隔离、独立扩缩、语言无关、热加载
  3. 模板即产品 —— 版本管理、灰度发布、占位符强校验,把消息内容从代码中剥离
  4. 可观测内建 —— 推送漏斗、渠道健康度、死信告警,出问题 5 分钟内定位
  5. 多租户真隔离 —— 配额、速率、数据、凭证全部按租户独立

如果你正在评估自建消息推送中台,可以先以 Message Nest 快速验证场景,然后按这个架构图逐步演进。不需要一步到位——先把异步管道和渠道插件体系搭起来,模板灰度、多租户配额是锦上添花。