Netflix 如何设计一个能满足 5 倍增长量的时序数据存储新架构?

阅读数:1485 2018 年 12 月 4 日

2016 年 1 月,Netflix 在全球范围内扩展业务。越来越多的会员、越来越多的语言和越来越多的视频回放将时间序列数据存储架构扩展到了它的临界点(详见第 1 部分文章《Netflix 实战指南:规模化时序数据存储》)。在第 2 部分中,我们将探索该架构的局限性,并介绍我们如何为下一个演进阶段重新构建架构。

2016 年 1 月,Netflix 在全球范围内扩展业务,向另外 130 个国家开放服务,总共支持 20 种语言。2016 年晚些时候,在浏览体验中加入了视频预览。越来越多的会员、越来越多的语言和越来越多的视频回放让 Netflix 的时间序列数据存储架构达到了临界点。在这篇文章中,我们将探索该架构的局限性,并介绍我们如何为下一个演进阶段重新构建架构。

临界点

之前的架构将所有的观看数据同等对待,而不考虑类型(完整观看或视频预览)或时间(多久之前观看过)。随着该功能扩展到更多设备,预览与完整观看的比例正迅速增长。到 2016 年底,数据存储在一个季度内增长了 30%;由于对数据存储的潜在影响,视频预览功能被推迟。最简单的解决方案是扩展底层的 Cassandra 集群以适应这种增长,但是,它已经是正在使用中的一个最大的集群,并且已接近集群规模限制。我们必须做点什么,而且要快。

重新思考我们的设计

我们开始重新思考我们的方法,并设计出一个至少能满足 5 倍增长量的方法。我们可以重用之前架构中的模式,但这还不够,我们需要新的模式和技术。

分析

我们首先分析数据集的访问模式。我们分析出三种不同类型的数据:

  • 完整视频播放
  • 视频预览播放
  • 语言首选项(即播放哪种字幕 / 配音)

对于每一类数据,我们发现了另外一种模式——大多数数据访问都是针对最近的数据。数据越旧,其详细级别就应该降得越低。把这些见解与我们同数据消费者的交流相结合,我们就可以判定哪些数据需要在什么样的详细级别上存储多长时间。

存储效率问题

对于增长最快的数据集——视频预览和语言信息,我们的合作伙伴只需要最近的数据。非常短的视频预览被我们的合作伙伴过滤掉,因为它们不能反映出会员对内容的观看意向。此外,我们发现,对于他们观看的完整视频,大多数会员选择了相同的字幕 / 配音语言。为每个观看记录存储相同的语言首选项会导致大量的数据重复。

客户端复杂性

我们研究的另一个限制因素是观看数据服务的客户端库如何满足调用者在特定时间段内对特定数据的特殊需求。调用者可以通过指定以下条件来检索观看数据:

  • 视频类型 — 完整视频或视频预览;
  • 时间范围— 过去 X 天 /X 月 /X 年,其中 X 会随不同使用场景而变化;
  • 详细级别 —完整或摘要;
  • 是否包含字幕 / 配音信息。

对于大多数场景,这些过滤器会在从后端服务获取到完整数据后将其应用于客户端。你可能想到了,这会导致大量不必要的数据传输。此外,对于较大的观看数据集,性能会迅速下降,导致 99 百分位读取延迟出现巨大变化。

重新设计

我们的目标是设计一个能够扩展到 5 倍增长量的解决方案,并且具有合理的成本效率以及更可预测的延迟。通过对上述问题的分析和了解,我们进行了此次重大的重新设计。以下是我们的设计准则:

数据类别

  • 按数据类型分片;

  • 减少数据字段,仅保留基本元素。

数据年龄

  • 按数据年龄分片;

  • 对于最近数据,在设定好的 TTL 之后
    过期;

  • 对于历史数据,汇总并转入归档集群。

性能

  • 并行化读取为近期数据和历史数据提供了统一的抽象。

集群分片

之前,我们将所有数据组合到一个集群中,并使用一个客户端库根据类型 / 年龄 / 详细级别过滤数据。现在,我们颠倒了这种方法,我们按照类型 / 年龄 / 详细级别对集群进行分片,每个数据集的增长率就可以互相分离,简化了客户端,并改善了读取延迟。

存储效率

对于增长最快的数据集——视频预览和语言信息,我们能够与合作伙伴保持一致,只保留最近的数据。我们不存储非常短的预览视频播放数据,因为它们不能很好地说明会员对内容感兴趣程度。此外,我们现在也存储了初始语言首选项,然后只存储后续播放的增量。这意味着对于大多数会员,我们只存储一个语言首选项记录,从而节省了大量存储空间。我们降低了预览视频和语言首选项数据的 TTL,因而,与完整视频数据相比,它们的过期策略会更激进。

在必要时,我们将应用“动态(live)”和压缩技术,其中可配置数量的近期观看记录以未压缩的形式存储,其余的记录以压缩的形式存储在单独的表中。对于存储较旧数据的集群,我们完全以压缩形式存储数据,在访问时在较低的存储成本和较高的计算成本之间做出权衡。

最后,我们使用较少的几列把摘要视图存储在一个单独的表中,而不是存储完整视频播放的所有细节。这个摘要视图也被压缩,以进一步优化存储成本。

总体上,我们的新架构看起来像下面这样:

image

如上所示,观看数据是按类型进行分片的——有单独的集群用于完整播放数据、预览播放数据和语言首选项信息。对于完整播放数据,存储是按年龄进行分片的。对于最近的观看数据(过去几天)、过去的观看数据(几天到几年)和历史查看数据,都有单独的集群。最后,对于历史观看数据,只有摘要视图,而没有详细记录。

image

数据流

写入

数据写入到最近的集群中。在输入之前会应用过滤器,例如不存储非常短的视频预览播放,或将所播放的字幕 / 配音与以前的首选项进行比较,只在与以前的行为相比存在变化时才存储。

读取

对最新数据的请求直接发送到最近的集群。当请求更多的数据时,并行读取可以更有效地获取数据。

最近几天的观看数据:对于大多数需要几天完整播放记录的场景,只从“最近(Recent)”集群中读取数据。在集群中并行读取 LIVE 表和 COMPRESSED 表。继续利用动态和压缩数据集模式,当从 LIVE 读取的记录数量超出配置的阈值时,记录会被汇总,并在压缩后写入 COMPRESSED 表,作为一个具有相同 row key 的新版本。

此外,如果需要语言首选项信息,就并行读取“语言首选项”集群。类似地,如果需要预览播放信息,那么就并行读取“预览播放”集群中的 LIVE 表和 COMPRESSED 表。与完整观看数据类似,如果 LIVE 表中的记录数超过了配置的阈值,那么这些记录将作为具有相同 row key 的新版本汇总、压缩并写入 COMPRESSED 表中。

过去几个月的完整播放数据需要并行读取“最近的”和“过去的”集群。

汇总观看数据通过并行读取“最近”、“过去”和“历史”集群获得。然后,将数据整合在一起,得到完整的汇总视图。为了减少存储大小和成本,“历史”集群中的汇总视图不包含最近几年会员观看的更新数据,因此,需要通过汇总来自“最近”和“过去”集群的观看数据来进行增强。

数据流转

对于完整播放记录,不同年龄数据的集群之间的数据移动是异步进行的。在从“最近”集群中读取会员的观看数据时,如果确定有比配置的天数更长的记录,则会排队等待将该会员的相关记录从“最近”集群移动到“过去”集群。在执行任务时,相关记录与“过去”集群中 COMPRESSED 表中的现有记录相组合。然后,将组合的记录集压缩并作为新版本存储在 COMPRESSED 表中。一旦新版本写入成功,以前的版本记录将被删除。

如果压缩后的新版本记录集的大小大于配置的阈值,则将记录集分块,并并行写入这些块。这些从一个集群到另一个集群的记录传输是成批进行的,这样,它们就不会在每次读取时都触发。

image

在从“过去”集群读取数据时,也完成了记录到“历史”集群的移动。相关记录会被重新处理,使用现有汇总记录创建新的汇总记录。然后,将它们压缩并作为新版本写入“历史”集群中的 COMPRESSED 表。新版本写入成功后,删除先前的版本记录。

性能优化

与之前的架构类似,LIVE 记录和 COMPRESSED 记录存储在不同的表中,并进行不同的调优以获得更好的性能。由于 LIVE 表会频繁更新,而且仅包含少量的观看记录,所以需要经常进行压缩,并设置较小的 gc_grace_seconds ,以便减少 SSTable 的数量。为了提高数据一致性,会频繁进行读修复和“全列族修复( full column family repair )”。由于 COMPRESSED 表的更新频率较低,所以通过低频的手动全量压缩足以减少 SSTable 的数量。在极少的更新执行期间检查数据的一致性,从而避免进行读修复和全列族修复。

缓存层的变化

由于我们对 Cassandra 的大数据块进行大量的并行读取,使用缓存层会带来很大的好处。为了模拟后端存储架构, EVCache 缓存层架构也做了修改,如下图所示。所有的缓存都有接近 99% 的命中率,并且在减少对 Cassandra 层的读请求数量方面非常有效。

image

缓存和存储架构之间的一个区别是,“汇总”缓存集群存储完整播放观看数据的压缩汇总。在大约 99% 的缓存命中率下,只有一小部分请求到达了 Cassandra 层,这一层会并行读取 3 个表,把记录拼接在一起,然后创建整个观看记录的汇总。

迁移:初步结果

这些变更已经完成了一半以上。能够从按照数据类型进行分片的集群中受益的场景已经迁移完毕。因此,虽然我们还不能分享完整的成果,但以下是我们获得的初步结果和经验教训:

  • 仅基于按数据类型分片的集群就获得了 Cassandra 操作特性(压缩、GC 压力和延迟)的巨大改进。
  • 巨大的完整观看数据 Cassandra 集群动态余量可以应对至少 5 倍的规模增长。
  • 得益于更激进的数据压缩和数据 TTL,节省了大量成本。
  • 架构重构是向后兼容的。现有的 API 将继续有效,预计会有更好、更可预测的延迟。为访问数据子集而新创建的 API 将带来额外的延迟优势,但需要修改客户端。这使得独立于客户端推出服务器端变更变得更加容易,并且可以根据客户的使用带宽在不同的时间使用不同的客户端。

小结

在过去几年里,观看数据存储架构已经取得了长足的进步。我们逐步演变成使用一种动态和压缩数据的模式,可以并行读取观看数据,并将这种模式重新用于团队内其他的时间序列数据存储上。最近,我们对存储集群进行了分片,以满足不同场景的独特需求,并对一些集群使用了动态和压缩数据模式。我们扩展了动态和压缩数据的流转模式,以便在按年龄分片的集群之间移动数据。

设计这些可扩展的构建块可以简单而高效地扩展我们的存储层。当我们以 5 倍增长规模为目标进行重新设计时,我们知道,Netflix 的产品体验在持续发生变化和改进。我们正密切关注可能需要进一步演进的地方。

查看英文原文: https://medium.com/netflix-techblog/scaling-time-series-data-storage-part-ii-d67939655586?br=ro&

image

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论