引言
在当今这个信息爆炸的时代,通知系统已经成为了现代应用程序中不可或缺的重要组成部分。无论是突发新闻的即时推送、产品更新的及时告知、促销活动的精准触达,还是用户交互的实时反馈,通知都扮演着至关重要的角色。一个高效、可靠、可扩展的通知系统,不仅能够提升用户体验,增强用户粘性,还能有效地传递关键信息,驱动业务增长。
本文将深入探讨如何设计一个可扩展的通知系统,涵盖了从需求分析、高层设计到详细设计的各个环节,并着重强调了系统的可靠性、可扩展性、安全性以及其他关键的设计考量。
第一步:理解问题并确定设计范围
在任何系统设计的初期阶段,最重要的一步都是深刻理解问题和明确设计范围。对于通知系统而言,其功能看似简单——发送通知,但实际上,要构建一个能够发送数百万条通知的可扩展系统,其背后涉及到诸多复杂的技术细节和架构选择。
- 通知类型: 系统需要支持推送通知 (移动端 - iOS, Android, 桌面端 - 笔记本/台式机)、短信 和 电子邮件 三种主流通知形式。这三种形式各有特点,适用场景也不同,例如,推送通知更适合实时性要求高的场景,短信和邮件则更适用于非实时但需要保证送达的场景。
- 实时性: 系统被定义为 软实时系统。这意味着系统应尽可能快速地发送通知,但允许在系统高负载情况下出现轻微延迟。这种定义平衡了实时性需求和系统性能的考量,为后续设计提供了灵活性。
- 支持设备: 系统需要覆盖 iOS 设备、Android 设备以及 笔记本电脑/台式机。这要求系统需要能够兼容不同的平台和设备,并针对不同平台的特性进行适配。
- 触发方式: 通知可以由 客户端应用程序 触发,也可以在 服务器端 触发。这表明通知系统需要支持多种触发机制,以满足不同的业务场景需求。例如,用户行为可以触发客户端通知,而定时任务或系统事件可以触发服务器端通知。
- 用户选择退出: 系统必须允许用户 选择退出 接收通知。这体现了对用户隐私和选择权的尊重,也是现代应用程序设计的基本原则。选择退出机制需要在设计中予以充分考虑,并确保用户能够方便地管理自己的通知偏好。
- 日均发送量:1000万条移动推送通知、100万条短信和 500万封电子邮件。这些指标直接关系到系统的容量规划、性能优化和成本控制。高并发、大吞吐量是可扩展通知系统需要重点解决的问题。
通过以上问题的梳理,我们对通知系统的需求和设计范围有了清晰的认识,这为后续的高层设计奠定了坚实的基础。
第二步:提出高层设计并获得认可
在高层设计阶段,我们需要勾勒出系统的整体架构,明确各个组件的功能和交互关系。高层设计方案,清晰地展示了支持各种通知类型 (iOS 推送通知、Android 推送通知、短信和电子邮件) 的基本框架。其核心结构可以概括为以下三个方面:
1. 不同类型的通知
首先从技术层面剖析了各种通知类型的工作原理。
iOS 推送通知 (APNs): iOS 推送通知依赖于 Apple 推送通知服务 (APNs)。发送流程涉及三个关键组件:
- 提供者 (Provider): 负责构建通知请求,包括设备令牌 (Device Token) 和负载 (Payload)。负载是一个 JSON 字典,可以包含通知的标题、内容、徽章 (Badge) 等信息。
- APNs: 苹果官方提供的远程服务,负责将推送通知通过 “Apple to device” 协议传播到 iOS 设备。
- iOS 设备: 最终接收并展示推送通知的终端设备。
Android 推送通知 (FCM): Android 推送通知流程与 iOS 类似,但通常使用 Firebase 云消息传递 (FCM) 作为推送服务,而非 APNs。FCM 提供了跨平台的消息传递解决方案,也支持 Web 和 iOS 平台。
短信 (SMS): 短信服务通常会借助于第三方服务提供商,例如 Twilio、Nexmo 等。这些服务商提供了 API 接口,方便开发者集成短信发送功能。选择第三方服务可以降低自建短信网关的复杂度和成本,并通常能获得更好的送达率和功能支持。
电子邮件 (Email): 电子邮件服务同样可以自建邮件服务器,但更多公司倾向于选择商业电子邮件服务,如 SendGrid、Mailchimp 等。商业邮件服务在送达率、反垃圾邮件、数据分析等方面通常更具优势。
2. 联系信息收集流程
要成功发送通知,首先需要收集用户的联系信息,包括移动设备令牌、电话号码和电子邮件地址。
当用户首次安装或注册应用程序时,API 服务器负责收集用户联系信息,并将这些信息存储到数据库中。
这是一个简化的数据库表结构,用于存储联系信息。user
表存储用户的电子邮件地址和电话号码,device
表则存储设备令牌。一个用户可以拥有多个设备,这意味着可以向用户的所有设备发送推送通知。这种一对多的关系模型,支持多设备场景下的通知触达。
3. 通知发送/接收流程 (高层设计)
该设计方案的核心是一个 通知系统 组件,作为整个通知流程的中心枢纽。
- 服务 1 到 N: 代表各种需要发送通知的业务服务,例如,计费服务、电商平台、社交应用等。这些服务通过调用通知系统提供的 API 来触发通知发送。
- 通知系统: 核心组件,负责接收来自各个服务的通知请求,并将其转发给相应的第三方服务。初始设计中,所有通知处理逻辑都集中在一个通知服务器上。
- 第三方服务: 负责实际的通知发送,例如 APNs, FCM, 短信服务商, 邮件服务商等。
初始设计的局限性
虽然初始设计方案简洁明了,但也存在一些明显的局限性:
- 单点故障 (SPOF): 所有通知相关的组件都集中在一个服务器上,一旦该服务器发生故障,整个通知系统将瘫痪。
- 难以扩展: 随着业务增长,通知量不断增加,单一服务器在数据库、缓存和通知处理组件的扩展方面都面临挑战。垂直扩展 (Scale-Up) 终究有瓶颈,水平扩展 (Scale-Out) 在这种架构下也较为困难。
- 性能瓶颈: 通知处理和发送本身是资源密集型任务,例如构建 HTML 邮件、等待第三方服务响应等。在高并发场景下,单一服务器容易成为性能瓶颈,导致系统过载。
高层设计 (改进)
改进的核心在于引入了 消息队列、缓存 和 独立的数据库,并采用了 水平扩展 的策略。
- 消息队列 (Message Queue): 引入消息队列 (如 Kafka, RabbitMQ) 作为系统组件之间的缓冲层,实现了服务之间的 解耦。消息队列可以应对突发流量,平滑峰值,并提高系统的 异步处理能力 和 可靠性。不同类型的通知事件 (iOS PN, Android PN, SMS, Email) 可以分别进入不同的消息队列,实现更精细化的管理和隔离,避免单一类型的通知服务故障影响全局。
- 缓存 (Cache): 引入缓存 (如 Redis, Memcached) 用于存储用户信息、设备信息、通知模板等 高频访问但相对静态的数据。缓存可以显著提升数据读取速度,降低数据库负载,提高系统性能。
- 独立的数据库 (DB): 将数据库从通知服务器中分离出来,并可以采用 集群部署 或 分库分表 等技术,提升数据库的 可扩展性 和 可靠性。
- 通知服务器集群 (Notification Servers): 采用 水平扩展 策略,部署多个通知服务器实例,并通过 负载均衡 (Load Balancer) 将请求分发到不同的服务器实例。这显著提升了系统的 并发处理能力 和 可用性。
- Worker: 引入 Worker 组件,负责从消息队列中消费通知事件,并调用相应的第三方服务发送通知。Worker可以水平扩展,根据消息队列的积压情况动态调整Worker数量,实现 弹性伸缩。
改进后的高层设计流程
- 服务调用通知服务器 API: 业务服务通过调用通知服务器提供的 API 发送通知请求。API 设计需要考虑安全性,例如采用 内部 API 或 认证机制,防止恶意请求或垃圾邮件。
- 通知服务器元数据提取: 通知服务器接收到请求后,首先进行 基本验证 (例如,验证邮箱格式、电话号码格式等),然后从 缓存 或 数据库 中提取渲染通知所需的元数据,例如用户信息、设备令牌、通知设置等。
- 事件推送至消息队列: 通知服务器将通知事件推送到相应的 消息队列 (例如,iOS PN 队列, SMS 队列等)。
- Worker消费队列事件: Worker 组件从消息队列中拉取通知事件。
- Worker调用第三方服务发送通知: Worker根据事件类型,调用相应的 第三方服务 (APNs, FCM, 短信/邮件服务商) 发送通知。
第三步:设计深入
在高层设计的基础上,我们需要进一步深入探讨系统的细节设计,包括可靠性、附加组件以及其他重要的设计考量。
可靠性
可靠性是通知系统设计的核心要素之一。用户期望能够及时、准确地收到重要通知,任何数据丢失或延迟都可能影响用户体验甚至业务运营。
如何防止数据丢失?: 为了确保通知数据不丢失,系统需要做到 数据持久化 和 实现重试机制。
数据持久化: 将通知数据 (如图11 所示的 “通知日志数据库” 中的数据) 存储到数据库中,即使系统发生故障,数据也不会丢失,可以用于后续的重试或审计。
重试机制: 当通知发送失败时 (例如,第三方服务故障、网络异常等),系统需要具备 自动重试 的能力。重试机制需要考虑重试策略 (例如,指数退避、最大重试次数等),避免无限重试导致系统压力过大。
每个接收者是否恰好接收一次通知?: 在分布式系统中,由于网络延迟、消息重复等因素,很难保证 精确的一次性交付 (Exactly-Once Delivery)。通知系统 无法保证每个接收者恰好接收一次通知,但可以通过 去重机制 (Deduplication) 来 减少重复通知的发生。
去重机制: 一种简单的去重逻辑是 基于事件 ID。当通知事件触发时,首先检查事件 ID 是否已经处理过。如果已经处理过,则丢弃该事件;否则,发送通知并记录事件 ID。更复杂的去重机制可能需要借助分布式锁或状态管理系统。
附加组件和考量因素
除了核心的发送流程,一个完善的通知系统还需要考虑许多附加组件和因素,以提升用户体验、系统效率和可维护性。
通知模板 (Notification Templates): 对于大型通知系统,每天需要发送数百万条通知,其中很多通知的格式和结构是相似的。引入 通知模板 可以实现 模板复用,避免重复构建相似的通知,提高效率,并保持通知格式的一致性。
短信通知模板的示例:
你梦想的[ITEM NAME]回来了——仅此[DATE]。 CTA: 现在下单,或保存我的[ITEM NAME]。
通知模板可以使用占位符 (例如
[ITEM NAME]
,[DATE]
) 来表示动态参数,在发送通知时,将这些占位符替换为实际的内容。通知设置 (Notification Settings): 用户通常会收到大量的通知,过多的通知容易引起用户的反感。因此,为用户提供 精细化的通知设置选项 至关重要。用户可以根据自己的偏好,选择接收哪些类型的通知,以及通过哪些渠道接收通知。
通知设置表的字段示例:
user_id
: 用户 IDchannel
: 通知渠道 (推送通知, 电子邮件, 短信)opt_in
: 用户是否选择接收该渠道的通知 (Boolean)rate_limiting
: 频率限制设置 (可选)
系统在发送通知前,需要 首先检查用户的通知设置,尊重用户的选择。
安全推送通知 (Secure Push Notifications): 对于推送通知,安全性至关重要。为了防止未授权的访问和滥用,需要采取安全措施。对于 iOS 和 Android 应用,可以使用
apnscert
和appsecret
进行 API 鉴权,只有经过身份验证的客户端才能通过 API 发送推送通知。监控队列通知 (Monitoring Queue Notifications): 监控 是保证系统稳定运行的关键手段。对于通知系统,队列积压情况 是一个重要的监控指标。如果队列积压量过大,意味着通知事件处理速度跟不上产生速度,可能导致通知延迟。监控队列长度,并根据情况 动态调整Worker数量,可以有效避免通知延迟。
事件跟踪 (Event Tracking): 为了了解通知的 效果 和 用户行为,需要进行 事件跟踪。例如,追踪通知的 打开率、点击率、用户参与度 等指标。这些数据对于分析用户行为、优化通知内容和策略都非常有价值。通知系统需要与 分析服务 集成,将事件数据上报给分析服务进行处理和展示。
速率限制 (Rate Limiting): 为了防止系统被滥用 (例如,恶意发送大量垃圾邮件或短信),以及保护用户免受过多的通知打扰,需要实施 速率限制。速率限制可以针对不同的维度进行,例如,限制单个用户在单位时间内接收的通知数量,限制单个 IP 地址在单位时间内发送的通知请求数量等。
最终设计
整合所有上述组件和考量因素后,最终的通知系统设计。与之前的设计相比,最终设计更加完善和健壮,考虑了可靠性、安全性、监控、用户设置和速率限制等关键方面。
总结
构建一个可扩展的通知系统,需要重点关注以下几个方面:
- 需求明确: 深入理解业务需求,明确通知类型、实时性要求、支持设备、触发方式、用户偏好以及性能指标等。
- 架构合理: 采用分布式架构,引入消息队列、缓存、独立的数据库和通知服务器集群,提升系统的可扩展性、可靠性和性能。
- 可靠性保障: 实施数据持久化和重试机制,最大程度地减少数据丢失,并采用去重机制降低重复通知的概率。
- 功能完善: 提供通知模板、用户通知设置、安全推送、队列监控和事件跟踪等附加组件,提升用户体验和系统管理能力。
- 安全性考量: 采用 API 鉴权等安全措施,防止系统被滥用。
- 用户体验至上: 尊重用户选择,提供精细化的通知设置,并实施速率限制,避免过度打扰用户。
参考资料
ByteByteGo