纽约时报是如何使用Apache Kafka替代数据库存储?


该文是介绍在纽约时报如何使用Kafka实现内容生产和内容消费分离的基于日志的架构(一种事件溯源Event Sourcing/CQRS的读写分离架构)。文章还阐述了使用Kafka替代传统数据库作为事实存储的原因。

原文大意如下:

“纽约时报”有许多不同的系统用于制作发布内容,我们有好几个内容管理系统CMS,我们会使用第三方数据编写文章。 此外还有,161年历史的新闻和21年历史的在线发布内容,我们拥有庞大的内容历史存档,它们仍然需要在线提供,还需要能被搜索。

这些都是我们所说的发布内容的来源;这些内容也是已经编写好的、被准备好进行公众消费的。

另一方面,我们有广泛的服务和应用程序需要访问这些发布的内容 - 有搜索引擎,个性化服务,Feed生成器以及所有不同的前端应用程序,如网站和本机应用程序。每当发布文章时,需要同时对所有这些系统提供非常低的延迟,且没有数据丢失。

本文介绍了一种基于由Apache Kafka提供的基于日志架构(banq注:实则是事件溯源Event Sourcing)来解决此问题,这是一种新方法。 我们称之为出版管道 。 本文的重点将放在后端系统上。 具体来说,我们将介绍如何使用Kafka来存储“纽约时报”发表的所有文章,以及如何使用Kafka和Streams API将发布的内容实时提供给各种应用程序和读者,新架构总结在下图中,我们将在本文的其余部分深入研究架构。

基于API方法的问题

访问已发布内容的不同后端系统具有非常不同的要求:

1.我们有一个服务:为网站和本机应用程序提供实时内容。 该服务需要在发布文章后立即提供文章内容,但它只需要文章的最新版本。

2.另外的一个服务是提供内容列表。 这些列表中的一些是手动策划的,有些是基于查询的。 对于基于查询的列表,每当发布w文章正好符合查询时,那么所有对该列表的请求都需要包含新的文章。 类似地,如果发布的文章进行了修改更新,导致文章不再符合查询内容时,则应将其从列表中删除。 我们还必须支持更改查询本身以及创建新列表,这需要访问以前发布的内容(重新)生成列表。

3.我们有一个Elasticsearch集群提供站点全文搜索。 这里的延迟要求不太高 - 如果文章在发布之后需要一两分钟才能通过搜索找到它,这通常不是一件大事。 然而,搜索引擎需要轻松访问以前发布的内容,因为我们需要在Elasticsearch模式定义更改时重新编辑所有内容,或者当我们改变搜索摄取管道时也要这么做。

4.我们有个性化推荐系统,只关心最近的内容,但是每当个性化算法改变时,都需要重新处理这些内容。

我们以前为所有不同消费者提供访问已发布内容的方法是通过构建API实现的。 内容的制作者提供访问该内容的API,并且还可以订阅正在发布的新文章。 其他后端系统也就是内容消费者,然后会调用这些API来获取他们需要的内容。


这种相当典型的基于API的架构有一些问题。

由于不同的API在不同时间由不同的团队开发,所以通常以非常不同的方式工作。 可用的实际端点是不同的,它们具有不同的语义,并且采用不同的参数。 当然这可能是固定的,但需要协调一些团队。

更重要的是,他们都有自己的隐含定义的模式。 一个CMS中的字段名称与另一个CMS中的名称相同字段可能不同,同一字段名称在不同系统中可能意味着不同内容。

这意味着每个访问内容的系统必须知道所有这些不同的API及其特性,然后他们需要以不同模式处理。

另外一个问题是很难访问以前发布的内容。 大多数系统没有提供有效地流式传输内容存档的方法,而且它们用于存储的数据库也不会支持这一功能(下一节更详细)。 即使您拥有所有已发布文章的列表,进行单个API调用来检索每个单独的文章也将花费很长时间,并对API造成大量不可预测的负担。

基于日志的架构

本文描述的解决方案是使用基于日志的架构。 在“设计数据密集型应用程序”有更详细的描述。 日志作为通用数据结构:“每个软件工程师应该知道实时数据的统一抽象” 。 在我们的例子中,日志就是Kafka,所有已发布的内容按时间顺序追加到Kafka主题。 其他服务通过消费日志访问它。

传统上,数据库已经被用作许多系统的真实来源。尽管有很多明显的好处,但长期来看,数据库可能难以管理。 首先,更改数据库的架构通常很棘手。 添加和删除字段不是太难,但更基础的结构更改在无需停机前提下就难以实现了。 更深层次的问题是数据库现在变得难以替代。 大多数数据库系统没有适合流变化(Stream Change)的良好API; 您可以使用快照,但快照会立即过时。 这意味着创建派生存储也很难,例如我们用来为nytimes.com和本机应用程序提供网站搜索的搜索索引 - 这些索引需要包含每一篇有史以来发表的文章,同时也有正在出版最新的新文章。 解决方法通常最终是客户端同时写入多个存储,导致当其中一个写入成功并且另一个失败时出现一致性问题。

正因为如此,数据库作为状态的长期维护者往往最终成为复杂的单个整体,试图为每个人做一切事情。

基于日志的架构通过将日志作为真相的根源来解决这个问题。 数据库通常存储某些事件的结果,日志却是存储事件本身,因此日志成为系统中发生的所有事件的有序表示。 使用此日志,您可以创建任意数量的自定义数据存储。 这些存储实际成为日志的物化视图。 如果要更改这样的数据存储库中的数据表结构,可以根据使用要求创建一个新的数据表结构,然后从头开始再次读取消费日志,直到将新数据表填满新的结构数据。

使用日志作为真相的来源,不再需要所有系统必须使用的单个共同的数据库了。 相反,每个系统都可以创建自己的数据存储(数据库或数据表结构) - 它自己的物化视图 - 仅以其所需的数据,以及以适合使用者的形式存储。 这大大简化了架构中数据库的作用,并使它们更适合于每个应用程序(消费内容的应用程序)的需要。

此外,基于日志的架构简化了访问内容流。 在传统的数据存储中,访问完整转储数据(即快照)和访问“实时”数据(即Feed)是不同的操作方式。 使用日志消费方式的一个重要方面就是消除了这种区别。 您可以开始在某些特定的偏移位置(Kafka的队列指针)读取日志 - 可以从开始到尾部或任何中间点暂停,然后再次继续读取。 这意味着如果要重新创建数据存储,您只需在一开始时从头读取日志。 在某个时候你最终会赶上当前实时流,这些对日志的消费者来说是透明的。

因此,日志消费者“总是可以重播”。

基于日志的架构在部署系统方面也提供了很多好处。 通过始终从头开始重新部署新的实例,而不是修改一个新的实例,以前相关整个问题就会消失。 以日志作为真相的根源,我们现在可以不间断地部署有状态的系统。 由于可以从日志中重新创建任何数据存储,因此我们可以在每次部署更改时从头开始创建它们,而不是在现场更改任何内容 - 这在本文后面将会给出一个实际的例子。

为什么Google PubSub或AWS SNS / SQS / Kinesis不适用于此问题

Apache Kafka通常用于解决两个截然不同的用例。

目前最常见的是Apache Kafka作为消息代理。 这可以涵盖分析和数据集成案例。 卡夫卡在这方面可以说有很多优势,但像Google Pub / Sub , AWS SNS / AWS SQS和AWS Kinesis这样的服务也提供其他方式的问题解决。 这些服务全部让多个消费者订阅多个生产商发布的消息,跟踪他们拥有和未见过的消息,并适时处理消费者停机时间,而不会丢失数据。 这种应用需求只是Kafka日志架构的一种实现细节而已。

日志架构特点:

1. 我们需要日志来永久保留所有事件,否则无法从头重新创建数据存储。
2. 我们需要订购日志消费。 如果因果关系事件处理不当,结果将是错误的。

只有卡夫卡同时支持这两个要求。

Monlog

Monolog是我们发布内容的新的真相来源。 创建内容的每个系统在准备发布时都会将其新内容写入Monolog,以追加方式附加。 实际写入是通过网关服务进行的,该服务验证发布的文章是否符合我们的结构。


Monolog包含自1851年以来发行的每项文章资产。它们根据发布时间完全排序。 这意味着消费者可以选择要开始使用的时间点。 需要所有内容的消费者可以从一开始就在开始时间(即在1851年)进行读取,其他消费者应用如果只想要某个时间以后的更新,那么就从那个时间开始读取。

著作内容资产以规范化的形式发布到Monolog,也就是说,每个独立的内容都作为单独的消息写入Kafka。 例如,图像独立于文章,因为多个文章可能包含相同的图像。


这与关系数据库中的归一化模型非常相似,内容资产之间存在多对多关系。

内容资产本身被作为protobuf二进制文件发布到Monolog。

在Apache Kafka中,Monolog被实现为Kafka的单分区主题。 为什么呢?因为我们希望维护总排序 - 具体来说,我们希望确保在使用日志时,在资产内容产执行引用其他资源之前,您始终会看到所引用的资源。 这确保顶级内容资产的内部一致性 - 如果我们在给添加文章引用的图像时,不希望这个被引用图像在Kafka存储中还没有。

作为日志消费者,您可以轻松地构建物化的日志视图,因为您知道引用的内容资产的版本始终是您在日志中看到的最后一个版本。

采用Kafka单分区主题,这是Kafka存储分区的方式,带来的约束是必须将其存储在单个磁盘上。 这对我们来说实际上并不是一个问题,因为我们所有的内容都是由人类制作的文本 - 我们的总体语料库现在不到100GB,磁盘的可扩展增长速度可以比我们记者写文章速度要快。

非规范化日志和Kafka的Streams API

Monolog对于希望对数据实现规范化视图的消费者来说非常棒。 对于一些不是这样类型的消费者 例如,为了在Elasticsearch中索引数据,您需要对数据进行非规范化视图,因为Elasticsearch不支持对象之间的多对多关系。 如果您想要通过匹配图像标题来搜索文章,那么这些图像标题在必须出现在文章对象中。

为了支持这种数据视图,我们还有一个非规范化的日志。 在非正规化日志中,构成顶级内容资产的所有组件都会一起发布。 对于上面的示例,当文章1发布时,我们向非规范化日志写入消息,其中包含文章及其所有依赖关系以及单个消息。


推送给数据Elasticsearch 的卡夫卡消费者可以从日志中选择此消息,将内容资产重组为Elasticsearch 所需形状,并推送到索引。 当文章2出版时,所有依赖关系再次捆绑在一起,包括已经为文章1中发布的依赖关系:

如果依赖关系更新,整个内容资产将被重新发布。 例如,如果图像2更新,则所有涉及到图像2的文章1都会再次出现在日志上:


称为非规范化的组件用于创建非规范化日志。这个非规范化的组件组件是一个使用Kafka Streams API的Java应用程序。 它消费Monolog,并维护了每个年内容资产的最新版本的本地存储以及对该内容资产的引用。 当内容资产出版时,该本地存储不断更新。 当发布顶级内容资产时,反规范化器将从本地存储收集该内容资产的所有依赖关系,并将其作为捆绑包写入非规范日志。 如果由顶级内容资产引用的内容资产已发布,则“反规范化”将重新发布将其作为依赖引用的所有顶级内容资产。

由于这个日志是非规范化的,它不再需要完全排序。 我们现在只需要确保同一顶级内容资产的不同版本的顺序正确。 这意味着我们可以使用分区日志,并且有多个客户端可并行消费日志。 我们使用Kafka Streams进行此操作,扩展从非规范化日志读取的应用程序实例数量的能力使得我们能够对整个发布历史进行非常快速的重播 。

弹性搜索示例

以下草图显示了此设置如何对后端搜索服务进行端到端的示例。 如上所述,我们使用Elasticsearch为NYTimes.com上的网站搜索提供支持:


数据流程如下:

1.内容资产由CMS发布或更新。
2.内容资产作为protobuf二进制文件写入网关。
3.网关验证内容资产,并将其写入Monolog。
4.非规范化器消费读取了Monolog的内容资产。 如果这是一个顶级内容资产,它将从其本地存储中收集其所有依赖项,并将它们一起写入非规范化日志。 如果该资产是其他顶级资产的依赖,则所有顶级资产都将写入非规范化日志。
5.Kafka分区器根据顶级资产的URI将资产分配给分区。
6.搜索采集节点都使用Kafka Streams访问非规范化日志。 每个节点读取一个分区,创建我们要在Elasticsearch中进行索引的JSON对象,并将它们写入特定的Elasticsearch节点。 在重播期间,关闭Elasticsearch复制功能,使索引更快。


此发布管道运行在Google Cloud Platform / GCP上 。 我们的设置的细节超出了本文的范围。 我们在GCP Compute实例上运行Kafka和ZooKeeper、 Gateway、所有Kafka复制器,使用Kafka Streams API构建的Denormalizer应用程序等所有其他进程都运行在GKE / Kubernetes上的容器中。 我们使用gRPC / Cloud Endpoint作为我们的API,以及相互SSL身份验证/授权来保护Kafka本身的安全。


结论

我们已经在这个新的出版架构上工作了一年多了。 也就是新架构进入产品期了,但还有很多事要做,我们还有很多系统需要转移到这个新的出版管道。

我们已经看到很多优点。 所有内容都通过相同的渠道实现,这简化了我们的软件开发流程,无论是前端应用程序还是后端系统。 部署也变得更简单 - 例如,当我们对分析器或模式进行更改时,我们现在重新进行全面重播到新的Elasticsearch索引,而不是尝试对现有索引进行就地更改,我们发现这容易出错。 此外,我们也正在建立一个更好的系统来监测已发布的资产如何通过这套系统进行。 通过网关发布的所有资产均被分配唯一的消息ID,并将此ID提供给发布者,并通过Kafka和消费应用程序传递,从而允许我们跟踪并监视每个系统中每个单独更新的处理方式,一直到最终用户应用程序。 这对于跟踪性能和在出现问题时精确定位问题非常有用。

最后,这是构建应用程序的一种新方式,习惯于使用数据库和传统的pub /sub模型的开发人员需要一个心理转移。 为了充分利用这一设置,我们需要以这样一种方式构建应用程序,即可以轻松部署使用重放的新实例来重新创建日志的物化视图,我们正在投入大量的精力来提供工具和基础设施,使重放变得更容易。


Publishing with Apache Kafka at The New York Times