“纽约时报”从1851年开始出版。起初,我们每天在报纸上发表几十篇文章,然后逐渐增加照片、食谱和评论。当我们转向互联网时,我们增加了幻灯片、视频、互动和播客。我们将这些项目统称为“发布资产”。经过近170年的出版,我们已经积累了超过5900万的资产。而且这个数字还在不断增长。
为了处理时报产生的资产数量,我们在2017年使用Apache Kafka开源软件创建了一个发布渠道。那一年,我们开始将每项资产的每一次修订都存储在一个我们称为Monolog的单分区Kafka主题上。
可以把这个独白想象成一个长长的、不断增长的列表。发布新资源时,会将其添加到独白日志的末尾。许多时报应用程序-如网站、移动应用程序和搜索索引-不断监控新资产的独白,以便在有新内容可用时进行刷新。
为了在网上展示一篇文章,我们的工程团队必须将多个资产组合在一起,比如一篇文章、一张图片和一名记者的简介(我们称之为“人的资产”)。
在数据库术语中,独白日志上的数据是标准化的,但是当出于显示目的组合数据时,它变得非规范化。为了渲染文章,图像和人物资源需要在文章资源之前出现在独白上。
关于独白的数据从1851年的第一篇文章开始,按时间顺序一直延续到今天,这是有意义的。与软件工程中的大多数事情一样,实际情况略有不同。
当我们在2017年推出独白时,我们立即开始添加新的资产,因为它们在网上发布。此后不久,我们在网上添加了几篇2000年至2016年的历史文章,其中包括2001年一篇纪念时报五周年的文章。然后我们添加了从1950年到2000年的精选文章,然后是从1851年到2000年添加文章的三个主要的批量档案再版。为了纠正数据错误并引入其他增强功能,我们偶尔进行规模小得多的批量重新发布。
时间顺序紊乱。如果有人想要找到任何给定年份的所有资产,他们将不得不处理所有5900万资产,这可能需要24小时以上。这对于机器学习或个性化项目来说尤其成问题,这些项目只需要过去几年的数据。
拓扑紊乱。文章经常引用其他资产,例如图像、食谱和人物。因为我们将2017年前发布的资产分批添加到专题库中,所以相互引用的资产通常位于专题库的不同位置。这些缺少的引用导致在对资产(例如缺少图像的文章或缺少食谱的食谱审查)进行非规范化时视图不完整。
复制性障碍。由于诸如重新发布和-嗯哼-bug之类的原因,在独白日志上通常会有同一资产的多个副本。在一个案例中,有超过90万个副本。这些副本占用空间,并强制进行不必要的处理。
很明显,独白的排序方式是不可持续的。我的团队认为解决这个问题的最好方法是创建一个新的按时间和拓扑顺序排序的独白日志。按时间顺序排列独白日志意味着我们可以使用Kafka功能使用时间戳跳到特定的偏移量,这意味着我们可以精确地从特定的时间范围内获取资产,而不需要处理整个事情。按拓扑顺序排列单值映射意味着我们可以消除不完整的非正规化资产。由于我们将查看所有资产,因此可以删除所有重复项。
问题是:我们如何做到这一点?这是一个需要使用Hadoop或Spark等工具来解决的大数据问题吗?一个更传统的数据库能满足要求吗?或者,解决方案会是其他的吗?
团队首先将每个资产的元数据插入到SQL数据库中。但是,需要纠正数据问题,特别是缺少引用,这意味着对数据进行简单排序是不够的。这种方法导致了一系列日益复杂的查询,运行时间长得令人遗憾(在某些情况下,我们说的是数周)。但更重要的是,这些查询没有产生正确排序的独白。
我们需要一个快速高效的解决方案。我是“纽约时报”的新手,我的团队要求我重新审视订购问题。
当我回顾以前的工作并努力理解这些查询时,我觉得SQL不是适合这项工作的工具-它阻碍了进度。所以我停了下来,退了一步,寻找另一种方法。
虽然5900万资产占用330 GiB的磁盘空间是很大的,但它不是大数据大数据,它通常落在数十亿条记录或PB级的数据范围内。
我的新笔记本电脑有16GiB的内存,我想知道它是否能在内存中容纳全部5900万资产。我做了计算:16GiB除以5900万大约是300字节。通过一些巧妙的编码,这似乎是可能的。
我仍然需要持久化数据,于是我开始考虑SQL的替代方案。我有一个啊哈的瞬间。一个算法开始在我的脑海中出现。
键值数据库对于某些数据访问模式非常有效。我设想了一种利用这种效率的多步骤算法。
第一步将消耗整个独白,并将每个资产转换成仅保留基本信息的小型编码格式,例如唯一ID、发布日期、对其他资产的引用以及用于检测重复的数据指纹。然后,该编码的资产将被插入到键值数据库中,其中密钥是唯一的发布日期,值是编码的资产。
多个资产可能共享相同的发布日期,因为它们是在同一天发布的-这主要是一个在泰晤士报有网站之前发布的资产的问题。为了确保每个资产都有唯一的日期,我们在日期中添加了纳秒。
第二步将查找并解决丢失的引用。该算法将按时间顺序检查每个资产,并标记到该时间点尚未看到的其他资产的引用。
稍后,当看到标记的引用时,我们将通过创建引用的副本并通过添加纳秒将发布日期设置为恰好在找到的资产之后来解决问题。然后可以删除原始引用中对丢失资产的引用。
尽管这一解决方案将捏造从未发生过的资产修订,但它将以一种不会实质性改变历史的方式做到这一点。它只是简单地修正了数据,以反映资产首次发布时应该发生的情况。此解决方案类似于现实生活中的场景,即编辑将图像添加到已发布的文章中,然后重新发布图像和文章。
一旦所有丢失的引用都已修复,第三步将查找重复资产。同样,将按时间顺序检查每项资产,并记录该资产以前版本的指纹。如果指纹相同(包括删除的引用),则后一个将被删除。
第四步将最后一次迭代资产以获取原始资产,并针对更改的引用或发布日期进行调整。然后,所有内容都将发布到一个新的排序的独白卡夫卡主题。
该算法驱动了键值数据库的几个关键需求。它必须能够按时间顺序高效地迭代键,并且必须在可用内存的范围内工作。
并不是所有的键值数据库都满足这些要求,但有两种类型可以满足这些要求:LSM树和B树。因为“纽约时报”的许多团队都使用GO,所以我开始搜索GO键值数据库,并选择了BadgerDB,这是一个正在积极维护的开源LSM-Tree数据库。
对于算法的第一步,我平均能够将每个资产编码为233个字节。这意味着大部分数据可以同时保存在内存中。使用BadgerDB,提取所有5900万资产大约需要4个小时,但大部分时间是由于与Kafka交谈的网络延迟。
剩下的步骤需要我们遍历所有5900万资产。事实证明,这些操作也非常快,根据任务的不同,需要7到15分钟(每秒高达140,000个资产)。这是一场巨大的胜利。
通过拥有我们这边的速度优势,失败的成本很低,并且我们可以试验恰到好处地获得复杂的实现细节。最后,花了大约8周时间才得出最终解决方案(这仍然比SQL方法花了5个月的时间要快)。
经过数百次迭代和调整,算法得到了改进和最终确定。生产版在云中的虚拟机上运行,与我的笔记本电脑没有太大不同。从卡夫卡提取数据后,只需90分钟左右即可完成整个订购过程。将订购的资产重新发布到新的卡夫卡主题还需要一天半的时间。
通过删除重复项,我们将资产数量减少到略高于3500万。抵销和出版日期现在同步增加,我们可以很容易地从特定年份调出所有资产。
当当今的许多技术问题都需要海量数据时,解决一个可以在一台笔记本电脑上解决的问题可能看起来并不尖端或酷。但是,找到一个复杂问题的解决方案是令人满意的,无论规模有多大。
道格·多诺霍(Doug Donohoe)是“纽约时报”的高级软件工程师,住在宾夕法尼亚州匹兹堡。他已经去过所有七大洲,并期待着很快再次探索世界。看看他的推特,在那里你可能会瞥见他的狗德克斯特。