
一、产品定位
这不是一个”消息发送工具”,而是一个企业级消息推送中台。类比:消息推送领域的 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
| 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 保障时再迁移到产品级方案。
十三、总结
这个架构设计的核心思路是:
- 异步优先 —— 发送请求立刻返回,所有重试/降级/死信在异步管道中处理
- 渠道即插件 —— 故障隔离、独立扩缩、语言无关、热加载
- 模板即产品 —— 版本管理、灰度发布、占位符强校验,把消息内容从代码中剥离
- 可观测内建 —— 推送漏斗、渠道健康度、死信告警,出问题 5 分钟内定位
- 多租户真隔离 —— 配额、速率、数据、凭证全部按租户独立
如果你正在评估自建消息推送中台,可以先以 Message Nest 快速验证场景,然后按这个架构图逐步演进。不需要一步到位——先把异步管道和渠道插件体系搭起来,模板灰度、多租户配额是锦上添花。