系统设计::设计一个推送系统

设计一个推送系统

近年来,通知系统已经成为许多应用程序的一个非常流行的功能。通知会提醒用户一些重要的信息,如突发新闻、产品更新、事件、产品等。它已经成为我们日常生活中不可缺少的一部分。在本章中,你被要求设计一个通知系统。

一个通知不仅仅是移动推送通知。三种类型的通知格式是:移动推送通知、SMS 消息和电子邮件。图 10-1 显示了这些通知中的每一种的例子。

理解问题并确定设计范围

构建一个每天发送数百万条通知的可扩展系统并不是一件容易的事。它需要对通知生态系统有深刻的理解。面试问题被特意设计成开放式和模糊不清的,你有责任提出问题来澄清需求。

应聘者:系统支持哪些类型的通知?面试官。推送通知,短信,和电子邮件。
应聘者:这是一个实时系统吗?
面试官:让我们说这是一个软实时系统。我们希望用户能尽快收到通知。但是,如果系统处于高负荷工作状态,稍有延迟是可以接受的。
应聘者:支持的设备有哪些?
面试官:iOS 设备,安卓设备,以及笔记本/台式机。
应聘者:什么会触发通知?
面试官:通知可以由客户端应用程序触发。也可以在服务器端安排。
应聘者:用户是否可以选择退出?
面试官:是的,选择退出的用户将不会再收到通知。
应聘者:每天有多少通知被发送出去?
面试官:1000 万条移动推送通知,100 万条短信,500 万封电子邮件。

提出高层次的设计并获得认同

本节展示了支持各种通知类型的高层设计:iOS 推送通知、Android 推送通知、SMS 消息和电子邮件。它的结构如下。

  • 不同类型的通知
  • 联系信息收集流程
  • 通知的发送/接收流程

不同类型的通知

我们首先看一下每种通知类型在高层次上是如何工作的。

iOS 推送通知

我们主要需要三个组件来发送一个 iOS 推送通知。

  • 提供者。提供者构建并向苹果推送通知服务(APNS)发送通知请求。为了构建一个推送通知,提供者提供以下数据。
    • 设备令牌。这是一个用于发送推送通知的唯一标识符。
    • 有效载荷。这是一个 JSON 字典,包含通知的有效载荷。下面是一个例子。
  • APNS:这是苹果提供的一个远程服务,用于向 iOS 设备传播推送通知。
  • iOS 设备。它是终端客户,接收推送通知。

Android 推送通知

Android 也采用了类似的通知流程。通常不使用 APN,而是使用 Firebase Cloud Messaging(FCM)来向安卓设备发送推送通知。

SMS 消息

对于 SMS 消息,通常使用第三方 SMS 服务,如 Twilio[1]、Nexmo[2]和其他许多服务。它们中的大多数是商业服务。

电子邮件

虽然公司可以建立自己的电子邮件服务器,但许多公司选择了商业电子邮件服务。Sendgrid[3]和 Mailchimp[4]是最受欢迎的电子邮件服务之一,它们提供了更好的发送率和数据分析。

图 10-6 显示了包括所有第三方服务后的设计。

联系信息收集流程

为了发送通知,我们需要收集移动设备令牌、电话号码或电子邮件地址。如图 10-7 所示,当用户安装我们的应用程序或首次注册时,API 服务器会收集用户的联系信息并将其存储在数据库中。

图 10-8 显示了存储联系信息的简化数据库表。电子邮件地址和电话号码存储在用户表中,而设备令牌则存储在设备表中。一个用户可以有多个设备,表明推送通知可以被发送到所有的用户设备上。

通知发送/接收流程

我们将首先介绍初始设计;然后,提出一些优化方案。

高层设计

图 10-9 显示了设计,下面对每个系统组件进行解释。

1 到 N 服务:一个服务可以是一个微服务,一个 cron job,或者一个触发通知发送事件的分布式系统。例如,一个计费服务发送电子邮件提醒客户到期付款,或者一个购物网站通过短信告诉客户他们的包裹明天会被送到。通知系统。通知系统是发送/接收通知的中心环节。从简单的东西开始,只使用一个通知服务器。它为服务 1 到 N 提供 API,并为第三方服务建立通知有效载荷。

第三方服务。第三方服务负责向用户发送通知。在与第三方服务集成时,我们需要额外注意可扩展性。良好的可扩展性意味着一个灵活的系统,可以很容易地插入或拔出第三方服务。另一个重要的考虑因素是,第三方服务可能在新的市场或在未来无法使用。例如,FCM 在中国是不可用的。因此,在那里使用替代的第三方服务,如 Jpush、PushY 等。

iOS, Android, SMS, Email: 用户在他们的设备上接收通知。

在这个设计中发现了三个问题。

  • 单点故障(SPOF)。单一的通知服务器意味着 SPOF。
  • 难以扩展。通知系统在一台服务器上处理所有与推送通知有关的事情。要独立扩展数据库、缓存和不同的通知处理组件是很有挑战性的。
  • 性能瓶颈。处理和发送通知可能是资源密集型的。例如,构建 HTML 页面和等待第三方服务的响应可能需要时间。在一个系统中处理所有的事情会导致系统过载,特别是在高峰期。

高层设计(改进) 在列举了最初设计中的挑战后,我们对设计进行了如下改进。

  • 将数据库和缓存移出通知服务器。
  • 增加更多的通知服务器,并设置自动横向扩展。
  • 引入消息队列来解耦系统组件。

图 10-10 显示了改进后的高层设计。

浏览上图的最佳方式是从左到右。

1 到 N 服务:它们代表不同的服务,通过通知服务器提供的 API 发送通知。

通知服务器。它们提供以下功能。

  • 为服务提供发送通知的 API。这些 API 只能由内部或经过验证的客户访问,以防止垃圾邮件。
  • 进行基本验证,以验证电子邮件、电话号码等。
  • 查询数据库或缓存以获取渲染通知所需的数据。
  • 将通知数据放到消息队列中进行并行处理。下面是一个发送电子邮件的 API 的例子。

POST https://api.example.com/v/sms/send

请求正文

缓存:用户信息、设备信息、通知模板被缓存起来。
数据库:它存储关于用户、通知、设置等的数据。
消息队列:它们消除了组件之间的依赖性。当大量的通知被发送出去时,消息队列充当缓冲器。每种通知类型都被分配了一个不同的消息队列,所以一个第三方服务的中断不会影响其他通知类型。
工作服务器:工作服务器是一个服务器列表,它从消息队列中提取通知事件并将其发送到相应的第三方服务。
第三方服务:在最初的设计中已经解释过了。
iOS, Android, SMS, Email:在最初的设计中已经解释过了。

接下来,让我们来看看每个组件是如何一起工作来发送通知的。

  1. 一个服务调用通知服务器提供的 API 来发送通知。
  2. 通知服务器从缓存或数据库中获取元数据,如用户信息、设备令牌和通知设置。
  3. 通知事件被发送到相应的队列中进行处理。例如,一个 iOS 推送通知事件被发送到 iOS PN 队列中。
  4. 工作服务器从消息队列中提取通知事件。
  5. 工作服务器向第三方服务发送通知。
  6. 第三方服务向用户设备发送通知。

深究设计

在高层设计中,我们讨论了不同类型的通知、联系人信息收集流程和通知发送/接收流程。我们将深入探讨以下问题。

  • 可靠性。
  • 额外的组件和注意事项:通知模板、通知设置、限流、重试机制、推送通知的安全性、监控排队通知和事件跟踪。
  • 更新设计

可靠性

在设计分布式环境的通知系统时,我们必须回答几个重要的可靠性问题。

如何防止数据丢失?

在一个通知系统中,最重要的要求之一是不能丢失数据。通知通常可以被延迟或重新排序,但绝不会丢失。为了满足这一要求,通知系统将通知数据持久化在一个数据库中,并实现重试机制。如图 10-11 所示,通知日志数据库包括了数据的持久性。

收件人会正好收到一次通知吗?

简短的回答是不会。尽管在大多数情况下,通知会被准确地传递一次,但分布式的性质可能会导致重复的通知。为了减少重复的发生,我们引入了一个删除机制,并仔细处理每个失败的案例。下面是一个简单的重复计算逻辑。

当一个通知事件第一次到达时,我们通过检查事件的 ID 来检查它是否以前被看过。如果它以前被看到过,它就被丢弃。否则,我们就会发出通知。有兴趣的读者可以探讨一下为什么我们不能有完全的一次发送,请参考参考资料[5]。

其他组件和考虑因素

我们已经讨论了如何收集用户的联系信息,发送和接收通知。一个通知系统远不止这些。在这里,我们讨论额外的组件,包括模板重用、通知设置、事件跟踪、系统监控、限流等。

通知模板

一个大型的通知系统每天会发出数百万条通知,其中许多通知都遵循类似的格式。通知模板的引入是为了避免从头开始建立每一个通知。通知模板是一个预先格式化的通知,通过定制参数、样式、跟踪链接等来创建你独特的通知。下面是一个推送通知的例子模板。

BODY

你梦想着它。我们敢于这样做。[ITEM NAME] 回来了–只到[DATE]。

CTA:

现在就订购。或者,保存我的[ITEM NAME]。

使用通知模板的好处包括:保持一致的格式,减少差错,以及节省时间。

通知设置

用户一般每天都会收到太多的通知,他们很容易感到不堪重负。因此,许多网站和应用程序让用户对通知设置进行细化控制。这些信息被存储在通知设置表中,有以下字段。

user_id bigInt
channel varchar # 推送通知、电子邮件或短信
opt_in boolean # 选择接收通知

在向用户发送任何通知之前,我们首先检查用户是否选择接收这种类型的通知。

限流

为了避免用户被太多的通知所淹没,我们可以限制用户可以收到的通知的数量。这一点很重要,因为如果我们发送得太频繁,接收者可能会完全关闭通知。

重试机制

当第三方服务发送通知失败时,该通知将被添加到消息队列中进行重试。如果问题持续存在,将向开发者发出警报。

推送通知的安全性

对于 iOS 或 Android 应用程序,appKey 和 appSecret 被用来保护推送通知的 API[6]。只有经过认证或验证的客户端才允许使用我们的 API 发送推送通知。有兴趣的用户应该参考参考资料[6]。

监控排队通知

要监控的一个关键指标是排队通知的总数量。如果数量很大,说明工作服务器处理通知事件的速度不够快。为了避免通知交付的延迟,需要更多的工作服务器。图 10-12(归功于[7])显示了一个待处理的排队消息的例子。

事件跟踪

通知指标,如打开率、点击率和参与度,对了解客户行为很重要。分析服务实现了事件跟踪。通常需要在通知系统和分析服务之间进行整合。图 10-13 显示了为分析目的可能被跟踪的事件的例子。

更新设计

把所有东西放在一起,图 10-14 显示了更新的通知系统设计。

在这个设计中,与之前的设计相比,增加了许多新的组件。

  • 通知服务器配备了两个更关键的功能:认证和限流。
  • 我们还增加了一个重试机制来处理通知失败。如果系统发送通知失败,它们会被放回消息队列中,工作者会重试预定的次数。
  • 此外,通知模板提供了一个一致和有效的通知创建过程。
  • 最后,监测和跟踪系统被添加到系统健康检查和未来改进中。

总结

通知是不可缺少的,因为它们让我们及时了解重要信息。它可能是关于你在 Netflix 上最喜欢的电影的推送通知,也可能是关于新产品折扣的电子邮件,或者是关于你在线购物付款确认的消息。

在这一章中,我们描述了一个可扩展的通知系统的设计,它支持多种通知格式。推送通知、SMS 消息和电子邮件。我们采用了消息队列来解耦系统组件。

除了高层次的设计,我们还深入挖掘了更多的组件和优化。

  • 可靠性。我们提出了一个强大的重试机制,以减少故障率。
  • 安全性。AppKey/appSecret 对被用来确保只有经过验证的客户才能发送通知。
  • 跟踪和监控。这些都是在通知流程的任何阶段实现的,以捕获重要的统计信息。
  • 尊重用户设置。用户可以选择不接收通知。我们的系统在发送通知之前首先检查用户的设置。
  • 速率限制。用户会欣赏他们收到的通知数量的频率上限。

恭喜你走到这一步! 现在给自己拍拍胸脯吧。干得好!

参考资料

[1] Twilio SMS
[2] Nexmo SMS
[3] Sendgrid
[4] Mailchimp
[5] You Cannot Have Exactly-Once Delivery
[6] Security in Push Notifications
[7] RadditMQ