暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Netflix案例分享-使用Cassandra存储每日数十亿条时间序数据

DataStax 2022-04-15
2810

    Cassandra非常适合时序数据的存储,比如 IOT数据(传感器数据),用户活动数据(浏览记录,操作事件,观看进度,交易记录等)。是因为Cassandra可以快速的根据数据的增长做线性扩容,同时极佳的写入性能可以满足每秒百万级别的写入。Netflix作为全球最大的流媒体提供商是Cassandra的重度用户,也是Apache Cassandra的核心贡献厂商。Netflix在全球范围内部署了上万个Cassandra 节点,存储数据多达数十PB。以下内容来自Netflix的博客:

    互联网的设备数量的增长带来了大量的访问类时序列数据。越来越多的公司致力于这些数据的挖掘从中获益,并基于数据做出决策。近年来随着技术的进步,大大提高了收集、存储和分析时间序列数据的效率,刺激了人们消费这些数据的欲望。然而,大多数的初始设计时间序列数据架构会随着数据的爆炸性增长而压

    Netflix作为一家以数据为基础的公司,对这些挑战并不陌生多年来,它已经优化管理增长的解决方案。在本文中我们将用两部分来分享并分析Netflix是如何通过多次增加规模来演变出一种时间序列数据存储架构的。

 

时间序列数据-历史记录的浏览

Netflix的用户每天观看超过1.4亿小时的内容。每个用户在查看视频标题时提供多个数据点,这些数据点会被存储为查看记录。Netflix分析浏览数据,并提供实时准确的书签和个性化推荐,如以下帖子所述:

  • 我们怎么知道某个视频你看到哪了

  • 帮助你找到想继续看的Netflix节目

查看历史记录的数据会沿着以下三个维度增加:

1.用户使用时间越长,储存的历史浏览数据越多

2.用户数量越多,储存的历史浏览数据越多

3.用户每个月使用时间越长,储存的历史浏览数据越多。

 

随着Netflix流媒体的用户在其头10年中1亿,浏览历史数据的数量也巨幅增加。在这篇文章中,将重点介绍Netflix是如何扩展存储来应对激增的历史浏览数据。

从简单开始

观看历史存储架构的第一个云原生版本使用的就是 Cassandra,原因如下:

 

Cassandra对时间序列数据建模有很好的支持,其中每行可以有动态的列数。

查看历史数据的读写比约为9:1。由于Cassandra的写操作效率很高,因此这种写操作繁重的工作负载非常适合Cassandra。

考虑到CAP定理,团队倾向于最终的一致性,而不是可用性的损失。Cassandra通过可调一致性支持这种权衡。


在最初的方法中,每个用户的查看历史记录都存储在Cassandra中的一行中,行键为:CustomerId。这种水平分区实现了用户增长的有效扩展,并使读取用户的整个查看历史记录的常见用例变得非常简单和高效。然而,随着用户数量的增加,更重要的是,每个用户流式传输越来越多的标题,行大小以及总体数据大小都增加了。随着时间的推移,这导致了较高的存储和操作成本,以及用户查看历史记录较多时的性能降低。


下图说明了初始数据模型的读写流程:

 


                                图1:单表数据模型

写流程

当一名用户点击标题时,一条浏览记录作为新列插入。该浏览记录在用户暂停或停止视频后更新。这种单列书写既快速又高效。

读流程

整行读取以检索某个用户的所有查看记录:当每个用户的记录数较少时,读取是有效的。随着一名会员点开了更多的标题,浏览记录的数量也随之增加。读取包含大量列的行会给Cassandra带来额外的压力,从而对读取延迟产生负面影响。


读取用户数据时间片的时间范围查询:这会导致与上述相同的不一致性能,具体取决于指定时间范围内查看记录的数量。


通过分页读取整行数据以解决大量的查看历史:这对Cassandra来说更好,因为它不需要等到所有数据都准备好后再发送回去。这也避免了客户端超时。然而就算这样,随着查看记录的数量增加,读取整行的总延迟也会增加。

减速原因

让我们看看Cassandra的一些内部结构,以了解为什么我们最初的简单设计会慢下来。随着数据的增长,sstable的数量也相应增加。由于内存中只有最近的数据,因此在许多情况下,必须读取memtables和SSTables才能检索查看历史。这对读取方面的延迟有负面影响。同样,随着数据大小的增加,压缩需要更多的IO和时间。随着行变宽,读取修复和整列修复变得更慢。

缓存层

Cassandra在编写查看历史数据时表现非常好,但需要减少读取方面的延迟。为了优化读取延迟,以增加写入路径期间的工作为代价我们在Cassandra存储前面添加了内存分片缓存层(EVCache)。缓存是一个简单的键值存储,键是CustomerId,值是把历史数据表现为压缩二进制形式。每次写入Cassandra都会产生额外的缓存查找,在缓存命中时,新数据会与现有值合并。查看历史记录读取首先由缓存提供服务。在缓存未命中时,从Cassandra读取条目,进行压缩,然后插入缓存。

由于添加了缓存层,这种单一的Cassandra表存储方法多年来一直运行良好。基于CustomerId的分区在Cassandra集群中具有良好的扩展性。到2012年,浏览记录Cassandra集群成为Netflix最大的专用Cassandra集群之一。为了进一步扩展,团队需要将集群规模扩大一倍。这意味着要打破旧时,冒险进入未知领域。与此同时,Netflix业务继续快速增长,包括不断增加的国际会员群和即将进行的原创内容投资。

重新设计规划:实时和压缩存储方法

很明显,需要一种不同的方法来实现未来5年的预期增长。该团队分析了数据特征和使用模式,并重新设计了浏览记录存储,其中考虑了两个主要目标:

1.更小的存储空间。

2.随着每个用户浏览量的增加,读/写性能保持一致。

对于每个用户,查看历史数据分为两组:

实时或近期查看历史记录(LiveVH):少量近期查看记录,并经常更新。数据以未压缩的形式存储,如上面详述的简单设计所示。

压缩或存档查看历史记录(CompressedVH):大量较旧的查看记录,很少更新。数据被压缩以减少存储空间。压缩的查看历史记录存储在每行键的一列中。

LiveVH和CompressedVH存储在不同的表中,并进行不同的调整,以实现更好的性能。由于LiveVH具有频繁的更新和少量的查看记录,因此压缩会频繁运行,gc_grace_seconds会很小,以减少SSTables的数量和数据大小。需要经常运行读取修复和全列族修复提高数据一致性。由于很少对 CompressedVH 进行更新,因此手动和不频繁的完全压缩足以减少 SSTable 的数量。在罕见的更新期间检查数据的一致性这消除了读取修复以及完整列族修复的需要。


写流程

使用与前面描述的相同方法将新的查看记录写入LiveVH。

读流程

为了突出新设计的优势,查看历史API被更新为:读取最近或读取完整的数据:

最近的查看历史:在大多数情况下,这会导致仅从LiveVH读取数据,从而限制数据大小,从而降低延迟。

完整查看历史记录:实现为LiveVH和CompressedVH的并行读取。由于数据压缩和CompressedVH具有较少的列,因此读取较少的数据,从而显著加快读取速度。

压缩VH更新流程

在从LiveVH读取查看历史记录时,如果记录数超过可配置的阈值,则最近的查看记录将通过后台任务汇总、压缩并存储在CompressedVH中。数据被存储在一个新的CustomerId行中。对新汇总进行版本控制,并在写入后读取以检查一致性。只有在验证新版本的一致性后,才会删除旧版本的汇总数据。为了简单起见,在汇总期间没有锁定,Cassandra负责解决非常罕见的重复写入(即最后一写入获胜)。

 


                              图2: 实时和压缩数据模型

 


如图2所示,CompressedVH中的汇总行还存储元数据信息,如最新版本、对象大小和分块信息(稍后将详细介绍)。version列存储引用最新版本汇总数据,以便CustomerId的读取始终只返回最新的汇总数据。汇总数据存储在一列中,以降低压实压力。为了最大限度地减少频繁查看模式的用户的汇总频率,在汇总后,只有最后几天值得查看的历史记录保存在LiveVH中,其余记录在汇总期间与CompressedVH中的记录合并。

通过分块自动缩放

对于大多数用户来说,将其整个查看历史记录存储在一行压缩数据中可以在读取流期间获得良好的性能。对于一小部分具有非常大查看历史记录的用户,由于第一个体系结构中描述的类似原因,从一行读取CompressedVH的速度开始减慢。对于这种罕见的情况,需要有一个读写延迟的上限,但不能对常见情况下的读写延迟产生负面影响。

为了解决这个问题,如果数据大小大于可配置的阈值,我们将汇总的压缩数据分成多个块。这些块存储在不同的Cassandra节点上。即使对于非常大的查看数据,这些数据块的并行读写也会使读写延迟拥有上限。

 


            图 3: 通过分块自动缩放


写流程

如图3所示,根据可配置的块大小,将汇总的压缩数据拆分为多个块。所有块都是用行键:CustomerId$Version$ChunkNumber并行写入不同的行。成功写入分块数据后,元数据将使用row key:CustomerId写入自己的行。这将写入延迟限制为两次写入,以汇总非常大的查看数据。在这种情况下,元数据行有一个空的数据列,可以快速读取元数据。

为了快速处理常见情况(压缩的查看数据小于可配置的阈值),将元数据与同一行中的查看数据相结合,以消除元数据查找开销,如图2所示。

读流程

首先使用CustomerId作为键读取元数据行。对于常见情况,区块计数为1,元数据行还包含最新版本的汇总压缩查看数据。对于罕见的情况,有多个压缩的查看数据块。使用版本和区块计数等元数据信息,生成区块的不同行键,并并行读取所有区块。这将读取延迟限制为两次读取。

缓存层更改

内存缓存层得到了增强,以支持大条目的分块。对于具有大量查看历史记录的用户,不可能将整个压缩的查看历史记录放在一个EVCache条目中。与CompressedVH模型类似,每个大的查看历史缓存条目被分解为多个块,元数据与第一个块一起存储。

结论:

通过利用并行性、压缩和改进的数据模型,团队能够实现所有目标:

1.通过压缩缩小存储空间。

2.通过分块和并行读/写实现一致的读/写性能。对于常见情况,延迟绑定到一次读取和一次写入;对于罕见情况,延迟绑定到两次读取和两次写入。

                                            图 4: 结论


该团队将数据大小减少了约6倍,将用于Cassandra维护的系统时间减少了约13倍,平均读取延迟减少了约5倍,平均写入延迟减少了约1.5倍。更重要的是,它为团队提供了一个可扩展的体系结构和净空,以适应Netflix浏览数据的快速增长。


文章转载自DataStax,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论