当前位置: 首页 > 科技观察

美国大选期间,Urban Airship如何将系统扩展至发送25亿个通知的规模-

时间:2023-03-21 14:27:39 科技观察

UrbanAirship如何扩展其系统以在美国大选期间发送25亿条通知?如今,UrbanAirship受到成千上万希望与移动技术一起成长的公司的信赖。UrbanAirship是一家拥有7年历史的SaaS公司,采用免费增值业务模式。UrbanAirship现在平均每天发送超过10亿条推送通知。本文介绍了UrbanAirship在2016年美国大选期间对通知的使用,探讨了其核心交付管道系统的架构,该系统向新闻媒体交付了数十亿条实时通知。在2016年美国大选选举日之前的24小时内,UrbanAirship发送了25亿条通知——这是迄今为止的最高单日通知量。这相当于美国每人8条通知或全球每部活跃智能手机1条通知。虽然UrbanAirship支持超过45,000个应用程序,涵盖每个垂直行业,但对选举使用数据的分析表明,400多个媒体应用程序占了这一记录发送量的60%,每个人都在跟踪和报告选举结果时,发送了1.5一天内有10亿条通知。通知量在总统选举结束时趋于平稳并达到峰值。UrbanAirshipAPI的HTTPS入站流量在选举期间达到每秒近75K的峰值。大部分流量来自与UrbanAirshipAPI通信的UrbanAirship软件开发工具包(SDK)。推送通知的数量一直在快速增长。最近的主要驱动因素是英国脱欧、奥运会和美国大选。2016年10月的月度通知同比增长150%。CoreDeliveryPipeline架构解密CoreDeliveryPipeline(CDP)是UrbanAirship的核心系统,负责使用受众选择器工具确定设备地址并发送通知。它发送的所有通知都必须是低延迟的,无论它们是同时推送给数千万用户、发送给多个复杂的子组、包含个性化内容,还是其他什么。以下是系统架构的概述以及公司吸取的一些经验教训。他们是如何开始的在2009年,CDP最初只是一个Web应用程序,带有一些工作模块,后来变成了面向服务的架构(SOA)。随着遗留系统的一部分开始出现规模问题,我们将它们提取到一个或多个旨在提供相同功能集但规模更大且性能更好的新服务中。我们的许多原始API和worker都是用Python编写的,将它们提取为高度并发的Java服务。最初,设备数据存储在一组Postgres数据库分片中,但公司增长速度超过了添加新数据库分片的能力,系统迁移到使用HBase和Cassandra的多数据库架构。CDP是一组负责处理分段和推送通知的服务。这些服务提供相同类型的数据来响应请求,但出于性能原因,每个服务都以完全不同的方式为数据编制索引。例如,我们有一个系统可以处理广播消息并将相同的通知内容推送到每个已注册相关应用程序的设备。该服务和底层数据存储的设计方式与我们负责根据位置或用户配置文件属性发送通知的服务完全不同。我们认为任何长时间运行的进程都是服务。这些长时间运行的流程遵循一个通用模板,包括指标、配置和日志,以便于部署和操作。通常,我们的服务属于两组之一:RPC服务或消费者服务。RPC服务提供这些命令以使用内部库与服务同步交互,很像GRPC,而消费者服务处理来自Kafka流的消息并对这些消息执行特定于服务的操作。数据库为了满足我们的性能和规模需求,我们高度依赖HBase和Cassandra来满足数据存储需求。虽然HBase和Cassandra都是列式NoSQL存储系统,但是它们有着完全不同的权衡,影响着我们使用哪种存储系统以及我们使用它来做什么。HBase非常擅长高吞吐量扫描,其中响应的预期基数非常高,而Cassandra擅长低基数查询,其中预期响应仅包含少数结果。两者都允许大量的写入吞吐量,这对我们来说是一个要求,因为来自用户手机的所有元数据更新都是实时完成的。它们的故障特征也不同。在发生故障时,HBase支持一致性和分区容错性,而Cassandra支持可用性和分区容错性。每个CDP服务都有一个非常具体的用例,因此有一个非常专用的数据库模式(schema),旨在促进所需的访问模式并限制存储空间。一般来说,每个数据库只被一个服务访问,它负责通过一个不太专用的接口来满足数据库对其他服务的访问需求。在服务与其后端数据库之间实现这种1:1的关系有很多优势:我们通过将服务的后端数据存储视为实现细节而不是共享资源来获得灵活性。·只要更改服务的代码,我们就可以调整服务的数据模型。?使用跟踪更简单、更直观,使容量规划更容易。·故障排除更容易。有时是服务代码的问题,有时是后台数据库的问题。将服务和数据库作为一个逻辑单元大大简化了故障排除。我们不必弄清楚“还有谁可以访问该数据库以使其以这种方式运行?”相反,我们可以依赖服务本身的应用层指标,而只需担心一组访问模式。·由于只有一个服务与数据库交互,我们可以在不停机的情况下执行几乎所有的维护活动。繁重的维护任务变成了服务级别问题:数据修复、数据库架构迁移,甚至切换到完全不同的数据库都可以在不中断服务的情况下执行。诚然,当我们将应用程序分解为更小的服务时,性能可能会受到影响。然而,我们发现我们可以灵活地满足我们的高可扩展性和高可用性要求,性能损失是值得的。数据建模我们的大多数服务都处理相同的数据,只是使用不同的格式。一切都必须一致。为了使所有这些服务的数据保持最新,我们严重依赖Kafka。Kafka非常快速和可靠。速度带来了某些缺点。Kafka消息只保证至少传递一次,但不保证它们按顺序到达。我们如何处理这个?我们将所有变量路径建模为可交换的:操作可以以任何顺序执行,结果相同。它们也是幂等的。这有一个很好的副作用:我们可以为一次性数据修复作业、回填甚至迁移重放Kafka流。为此,我们充分利用了HBase和Cassandra中都存在的“单元版本控制”概念。它通常是一个时间戳,但可以是您喜欢的任何数字(有一些例外;像MAX_LONG可能会导致一些奇怪的行为,具体取决于HBase或Cassandra的版本以及您的数据库模式如何处理删除)。对我们来说,这些单元的一般规则是它们可以有多个版本,我们根据提供的时间戳对版本进行排序。考虑到这种行为,我们可以将入站消息分解为一组特定的列,并将此布局与墓碑的自定义应用程序逻辑结合起来,同时考虑时间戳。这允许在保持数据完整性的同时盲写底层数据存储。盲目地将更改的部分写入Cassandra和HBase并非没有问题。一个典型的例子就是在重放的情况下重复写入相同的数据。虽然由于我们努力确保记录是幂等的,数据的状态不会改变,但必须压缩重复数据。在最极端的情况下,这些额外的记录会导致严重的压缩延迟和备份。由于这个细节,我们密切关注压缩时间和队列深度,因为压缩操作会在Cassandra和HBase中引起严重的问题。通过确保来自数据流的消息遵循一套严格的规则,并通过设计消费服务来处理乱序和重复的消息,我们可以确保大量异步服务同步,更新只需一秒或两个滞后。服务设计我们的大部分服务都是用Java编写的,但风格非常独特和现代。我们在设计Java服务时牢记一组基本准则:?做一件事,并做好它——在设计服务时,它应该只承担一个责任。实施者决定这是哪种责任,但她或他需要准备好在代码审查期间对此负责。·无共享操作状态——在设计服务时,假设至少有三个实例始终在运行。服务需要能够处理任何其他实例可以处理的相同请求,而无需任何外部协调。熟悉Kafka的人会特别指出Kafka消费者在外部协调topic:group对的分区所有权。本指南旨在针对特定服务进行外部协调,而不是利用可能进行外部协调的图书馆或客户。限制队列——我们在所有服务中都使用队列,这是批处理请求的好方法。所有队列都应该有限制。但是,限制队列确实会引发一些问题:△队列满了生产者怎么办?他们会阻止吗?异常处理?还是掉?△我的队列应该有多大?要回答这个问题,假设队列总是满的帮助。△如何干净利落地关闭队列?△对于这些问题,每个服务都会根据具体用例给出不同的答案。·命名自定义线程池,并注册UncaughtExceptionHandler-如果我们绝对创建自己的线程池,我们使用Executors的构造函数或帮助方法,允许我们提供ThreadFactory。有了这个ThreadFactory,我们就可以正确命名线程,设置守护进程状态,并注册UncaughtExceptionHandler来处理让它进入堆栈顶部的异常。这些措施将大大简化服务的调试,让我们免于沮丧的熬夜。优先使用不可变数据对象而不是可变状态——可变状态在高度并发的环境中可能很危险。通常,我们使用可以在内部子系统和队列之间传递的不可变数据对象。使不可变对象成为子系统之间通信的主要形式使得并发使用的设计更加直观地简单,并使故障排除更加容易。下一步是什么?凭借UrbanAirship通过手机钱包通行证发送通知的能力,增加了对Web通知和AppleNews通知的支持,以及其向任何平台、设备或营销渠道发送通知的开放渠道,我们预计通知推送量将呈指数级增长。为满足这一需求,我们继续大力投资核心交付管道架构、服务、数据库和基础设施。原标题:UrbanAirshipHowUrbanAirshipScaledto25BillionNotificationsDuringtheU.S.Election,作者:ToddHoff