专栏名称: 高可用架构
高可用架构公众号。
目录
相关文章推荐
架构师之路  ·  高可用架构:fail-over的三种经典模式 ... ·  2 天前  
奇舞精选  ·  vercel是如何做微前端迁移的 ·  3 天前  
奇舞精选  ·  vercel是如何做微前端迁移的 ·  3 天前  
架构师之路  ·  MySQL必知必会(再版上架,送10本) ·  6 天前  
架构师之路  ·  中国程序员最大的悲哀!(1100W+阅读) ·  1 周前  
51好读  ›  专栏  ›  高可用架构

大选期间完成25亿推送:美国移动push平台Urban Airship架构解密

高可用架构  · 公众号  · 架构  · 2016-11-17 08:48

正文

导读:移动时代,推送系统的重要性不言而喻。Urban Airship 是国外专注于推送服务的 SaaS 厂商。在美国大选期间,Urban Airship 的消息推送量达到 25 亿次每天。Urban Airship 的技术团队是如何支持如此巨大的消息推送量?他们使用了什么开源技术?核心系统的架构又是怎样的?本文带你一窥全豹。


Urban Airship 被大量希望通过推送实现用户增长的企业所信赖。Urban Airship 是一个七岁的 SaaS 公司,更多信息可访问官网:


www.urbanairship.com


Urban Airship 现在平均每天发送超过 10 亿推送通知。本文介绍 2016 年美国大选 Urban Airship 推送使用情况,为你解密 Core Delivery Pipeline 系统的架构,这个系统为各大媒体提供数十亿的实时通知。


2016 年美国大选的推送数据


在选举日的 24 小时内,Urban Airship 发送了 25 亿次通知 - 这是迄今为止最高的发送量。 这相当于美国每人收到 8 个通知或世界上每个有效智能手机收到 1 个通知。


虽然 Urban Airship 支持超过 45,000 个应用程序,但是分析选举使用数据显示超过 400 个媒体应用程序占用了60% 的记录,即在一天内发送 15 亿通知。




总统选举结束时,通知量达到峰值。




Urban Airship API 的 HTTPS 入口流量在选举期间达到每秒近 75K 的峰值。大多数流量来自与 Urban Airship API 通信的 Urban Airship SDK。




推送量一直在快速增长。最近的主要驱动因素是英国的退欧公投 Brexit,奥运会和美国大选。到 2016 年 10 月,每月通知量年比年(YOY)同比增长 150%。




Core Delivery Pipeline 系统架构解密


Core Delivery Pipeline(CDP)是 Urban Airship 的核心系统,负责存储来自客户的设备信息并提供推送通知。 我们发送的所有通知都要求极低的延迟,无论他们同时推送数千万用户,或按定位进行多个复杂选项,或者包含个性化内容。 下面介绍下 CDP 的架构,以及我们从中学到的经验。


最初的版本:Python + Postgres


在 2009 年,最初的版本是作为 webapp 开发,逐渐一些模块转变为面向服务的架构(SOA)。当旧系统的一些部分开始发生规模问题时,我们便将它们提取为一个或多个新服务,这些服务虽然提供的是相同的功能,但可以支持更大规模,以及性能更好。


我们的许多原始 API 和 worker 是用 Python 开发,我们将这些功能逐渐移植到高并发的 Java 服务中。我们最初将设备数据存储在一组分片的(sharding)postgres 数据库中,但是访问规模的增加速度超过了我们添加新数据库分片的能力,因此我们将数据库迁移到 HBase 和 Cassandra。

CDP 是用于处理推送通知投递的一系列服务。这些服务都用于处理相同类型数据的请求,但是出于性能原因,每个服务以非常不同的方式索引数据。例如,我们有一个系统负责处理广播消息(向所有注册到同一个应用的设备推送同样的消息),此服务及数据存储的设计,与我们的个性化通知服务的设计是非常不同。

我们把所有长时间运行的进程都理解成服务。这些长时间进程在监测,配置和日志记录方面遵循我们的通用规范模板,以便部署和运维。


通常,我们的服务有两种类型:RPC 服务或队列消费者服务。


RPC服务(非常类似于 gRPC 的内部框架)提供一些功能命令和内部服务同步交互,而队列消费服务则处理来自 Kafka 的消息并且对那些消息执行服务特定操作。




数据库


为了满足我们的性能和规模要求,我们非常依赖 HBase 和 Cassandra 来满足数据存储需求。虽然 HBase 和 Cassandra 都是 NoSQL 存储,但是它们有非常不同的 tradeoff,并影响我们在什么场景该使用哪个存储。

HBase 高吞吐的扫描查找并返回较多数据,而 Cassandra 擅长较低的基数查找(返回的数据较少)。两者都允许大量的写吞吐量,这是我们的要求,因为来自用户手机的所有元数据更新都是实时的。

他们如何应对失败的策略也各不相同。HBase 在失败的情况下倾向于保证一致性和分区容错性(C + P),而 Cassandra 则有利于可用性和分区容错性(A + P)。每个 CDP 服务具有非常具体的情况,因此被设计为便于所需的访问模式。


作为一个通用规则,每个数据库仅由单个服务访问,该服务负责通过提供专用的接口来满足其他服务的数据访问的需求。


在服务及其后台数据库之间实现这种 1:1 关系,具有许多优点。


  • 通过将服务的后端数据存储作为实现细节,而不是共享资源,我们获得了灵活性。

  • 我们可以调整服务的数据模型,而只更改服务的代码。

  • 使用跟踪更直接,这使得容量规划更容易。

  • 故障排除更容易。将服务和数据库打包作为逻辑单元极大地简化了故障排除过程。我们不必想知道“还有谁可以访问这个数据库,使其以这种方式运行?”相反,我们可以依赖于来自服务本身的应用程序级监测,并且只关注一组访问模式。

  • 因为只有一个服务与数据库交互,我们可以执行几乎所有的维护活动,而不会停机。重型维护任务成为一个服务级别的问题:数据修复,模式迁移甚至切换到完全不同的数据库,而不会中断服务。


确实,当将应用程序分解为较小的服务时,可能会有一些性能损失。但它带来的可扩展性和高可用性方面获得的灵活性,超过了性能降低带来的损失。


数据建模


我们的大多数服务处理相同的数据,只是使用不同的格式。为了保持所有这些服务的数据更新,我们非常依赖 Kafka。 Kafka 速度非常快,也牢靠。速度快来自于一些 tradeoff。 Kafka 消息只保证至少发送一次,并且不保证它们按顺序到达。

我们如何处理这个问题呢?我们已将所有可变路径建模为可交换的:操作可以按任何顺序应用,并以相同的结果结束。他们也是幂等的。这带来一个很好的副作用,我们可以重放 Kafka 流来进行数据修复工作,甚至迁移数据。

为此,我们使用了“cell version”的概念,它存在于 HBase 和 Cassandra 中,它通常是一个时间戳,但也可以使用其他数字(有一些例外,例如 MAX_LONG 可能会导致一些奇怪的行为,这取决于 HBase 或 Cassandra 的版本以及你的 schema 如何处理删除)。

对我们来说,这些单元格的一般规则是它们可以有多个版本,我们定义版本方式是通过时间戳。考虑到这种行为,我们可以将传入的消息存入为不同的列中,并通过自定义的应用程序以及合适的时间戳来取出这些数据。这样就允许对底层数据存储进行盲写,同时保持数据的完整性。

只是盲目地写更改 Cassandra 和 HBase 并非没有问题。一个很好的例子是在队列重放的情况下,系统会重复写入相同的数据。虽然由于我们保持记录幂等性的设计,使得数据的状态不会改变,但是重复的数据必须被压缩。在最极端的情况下,这些额外的记录可能导致显着的压缩延迟和备份。由于这个细节,我们密切监控我们的压缩时间和队列长度,因为在 Cassandra 和 HBase 中,压缩都会导致严重的问题。

通过确保来自数据流的消息遵循严格的规则,并且消费服务能够处理无序和重复的消息,我们可以保持大量的异步服务同步,通常仅有一到两秒更新滞后。


服务设计


我们的大部分服务使用 Java 开发,但是具有非常好的审美和现代风格。我们在开发 Java 服务时的指导原则如下:


  • 单一责任:只做一件事,并且做好。在设计服务时,它应该有一个责任。由实施者决定什么包括在一个责任里,但她或他将需要准备好在 code review 时来说明这一点。

  • 没有共享操作状态:在设计服务时,假设总是至少有三个实例正在运行。服务需要能够在没有任何其他实例外部协调的情况下处理相同请求。熟悉 Kafka 的人会注意到,Kafka 消费者从外部协调一个主题的分区所有权:组对(group pair)。本指南指的是特定于服务的外部协调,而不是利用可能在外部协调的库或客户端。

  • 限制队列的长度:我们在所有服务中都使用队列,这是进行批处理请求的好方法。所有队列都应该有边界。然而,限制队列确实引发了一些问题:

    • 当队列满时生产者会发生什么?他们应该阻塞吗?或者摘除掉?

    • 队列有多大?要回答这个问题,它有助于假设队列总是充满。

    • 如何优雅的停止队列?

    • 每个服务将有不同的答案,这些问题取决于具体的用例。

  • 命名自定义线程池,并注册一个UncaughtExceptionHandler - 如果我们最终创建自己的线程池,我们使用来自Executors的构造函数或帮助方法来允许我们提供一个ThreadFactory。有了ThreadFactory,我们可以正确命名我们的线程,设置它们的守护状态,并注册一个UncaughtExceptionHandler来处理异常。这些步骤使调试服务变得更加容易,并避免了我们在深夜沮丧(译者注:看来老外也是半晚上爬起来处理 BUG)。

  • 优先选用不可变数据类型(immutable data objects):在高度并发环境中,可变状态可能是危险的。一般来说,我们使用可以在内部子系统和队列之间传递的不可变数据对象。不可变对象是子系统之间的主要通信形式,它使得并发使用更加直接,并且让故障排除更容易。


未来规划


Urban Airship 有通过移动设备发送通知的能力,也支持新的 Web 通知以及 Apple News 通知,包括其他大量平台设备或营销渠道发送通知的能力,我们预计推送量将呈指数增长。 为了满足这种需求,我们将继续在我们的 CDP 系统架构,服务,数据库和基础设施方面进行大量投资。


本文由高可用架构志愿者翻译,原文地址

http://highscalability.com/blog/2016/11/14/how-urban-airship-scaled-to-25-billion-notifications-during.html


欢迎通过公众号菜单「联系我们」进行投稿,也欢迎最新优秀技术文章的译稿。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。


高可用架构

改变互联网的构建方式


长按二维码 关注「高可用架构」公众号


高可用架构主办 GIAC 全球互联网架构大会特别推出「高压下的架构演进」专题,大型互联网网站架构专家李智慧出品,议题方向包括百度、蚂蚁金服、今日头条等公司应对压力的演进历程。现在购买还可以享受 7 折早鸟票,双日套票最低仅需 1,260 元,点击阅读原文进入购买页面。