NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

规模化时间序列数据存储 Part1

  • 2018-03-18
  • 本文字数:4485 字

    阅读完需:约 15 分钟

引言

因特网互联设备的发展,提供了大量易于访问的时序数据。越来越多的公司有兴趣去挖掘这类数据,意图从中获取一些有意义的洞悉,并据此做出决策。技术的最新进展提高了时序数据的收集、存储和分析效率,激发了人们对如何处理此类数据的考量。然而,大多数现有时序数据体系结构的处理能力,可能无法跟上时序数据的爆发性增长。

作为一家根植于数据的公司,Netflix 已习惯于面对这样的挑战,多年来一直在推进应对此类增长的解决方案。该系列博客文章分为两部分发表,我们将分享 Netflix 在改进时序数据存储架构上的做法,如何很好地应对数据规模的成倍增长。

时序数据:会员视频观看记录

每天,Netflix 的全部会员会观看合计超过 1.4 亿小时的视频内容。观看一个视频时,每位会员会生成多个数据点,存储为视频观看记录。Netflix 会分析这些视频观看数据,实时准确地记录观看书签,并为会员提供个性化推荐。具体实现可参考如下帖子:

视频观看的历史数据将会在以下三个维度上取得增长:

  1. 随时间的推进,每位会员会生成更多需要存储的视频观看数据。
  2. 随会员数量的增长,需要存储更多会员的视频观看数据。
  3. 随会员每月观看视频时间的增加,需要为每位会员存储更多的视频观看数据。

Netflix 经过近十年的发展,全球用户数已经超过一亿,视频观看历史数据也在大规模增长。这篇博客帖子将聚焦于其中的一个重大挑战,就是我们的团队是如何解决视频观看历史数据的规模化存储的。

基本架构的初始设计

最初,Netflix 的云原生存储架构使用了 Cassandra 存储观看历史数据。团队是出于如下方面的考虑:

  • Cassandra 对时序数据的建模提供了很好的支持,支持一行中的列数动态可变。
  • 在观看历史数据上,读操作和写操作的数量比大约为 1:9。因为 Cassandra提供了非常高效的写操作,特别适用于此类写密集的工作负载。
  • CAP 定理方面考虑,相对于可用性而言,团队更侧重于实现最终一致性。Cassandra 支持可调整的一致性,有助于实现 CAP 上的权衡。

在最初的架构中,使用 Cassandra 存储所有会员的观看历史记录。其中,每位会员的观看记录存储为一行,使用CustomerId标识。这种水平分区设计支持数据存储随会员数量的增长而有效扩展,并支持简单并高效地读取会员的完整观看历史数据。这一读取操作是历史数据存储上最频繁发生的操作。然而,随着会员数量的持续增长,尤其是每位会员观看的视频流越来越多,存储的数据行数和整体数据量也日益膨胀。随着时间的推移,这将导致存储和操作的成本增大。而且对于观看了大量视频的会员而言,性能会严重降低。

下图展示了最初使用的数据模型中的读操作和写操作流。


图 1:单表数据模型

写操作流

当一位会员开始播放视频时,一条观看记录会以一个新列的方式插入。当会员暂停或停止观看视频流时,观看记录会做更新。在 Cassandra 中,对单一列值的写操作是快速和高效的。

读操作流

为检索一位会员的所有观看记录,需要读取整行记录。如果每位会员的观看记录数量不大,这时读操作是高效的。如果一位会员观看了大量的视频,那么他的观看记录数量将会增加,即记录的列数增加。读取一个具有大量列的数据行,会对 Cassandra 造成了额外压力,进而对读操作延迟产生负面影响。

要读取一段时间内的会员数据,需要做一次时间范围查询。这同样会导致上面介绍的性能不一致问题。因为查询性能依赖于给定时间范围内的观看记录数量。

如果要查看的历史数据规模很大,需要做分页才能进行整行读操作。分页对 Cassandra 更好,因为查询不需要等待所有数据都就绪,就能返回给用户。分页也避免了客户超时问题。但是,随着观看记录的增长,分页增加了读取整行的整体延迟。

延迟的原因

下面介绍一些 Cassandra 的内部机制,进而理解为什么我们最初的简单设计会产生性能下降。随着数据的增长, SSTable 的数量也随之增加。因为只有最近的数据是维护在内存中的,因此在很多情况下,检索观看历史记录时需要同时读取内存表和SSTable 。这对于读取延迟具有负面影响。同样,随着数据的增长,合并(Compaction)操作将占用更多的IO 和时间。此外,随着一行记录越来越宽,读修复(Read repair)全列修复(Full column repair)也会变慢。

缓存层

Cassandra 可以很好地对观看数据执行写操作,但是需要改进读操作上的延迟。为优化读操作延迟,我们考虑以增加写路径上的工作为代价,在 Cassandra 存储前增加了一个内存中的分片缓存层(即 EVCache )。缓存实现为一种基本的键 - 值存储,键是CustomerId,值是观看历史数据的二进制压缩表示。每次 Cassandra 的写操作,将额外生成一次缓存查找操作。一旦缓存命中,直接给出缓存中的已有值。对于观看历史记录的读操作,首先使用缓存提供的服务。一旦缓存没有命中,再从 Cassandra 读取条目,压缩后插入到缓存中。

在添加了缓存层后,多年来 Cassandra 单表存储方法一直工作很好。在 Cassandra 集群上, 基于CustomerId的分区提供了很好的扩展。到 2012 年,查看历史记录的 Cassandra 集群成为了 Netflix 的最大专用 Cassandra 集群之一。为进一步实现存储的规模化,团队需要实现集群的规模翻番。这意味着,团队需要冒险进入 Netflix 在使用 Cassandra 上尚未涉足的领域。同时,Netflix 业务也在持续快速增长,其中包括国际会员的增长,以及企业即将推出的自制节目业务。

重新设计:实时存储和压缩存储

很明显,为适应未来五年中企业的发展,团队需要尝试多种不同的方法去实现存储的规模化。团队分析了数据的特征和使用模式,重新定义了观看历史存储。团队给出了两个主要目标:

  • 更小的存储空间;
  • 考虑每位会员观看视频的增长情况,提供一致的读写性能。

团队将每位会员的观看历史数据划分为两个数据集:

  • 实时 / 近期观看历史记录(LiveVH):一小部分频繁更新的近期观看记录。LiveVH 数据以非压缩形式存储,详细设计随后介绍。
  • 压缩 / 归档观看历史记录(CompressedVH):大部分很少更新的历史观看记录。该部分数据将做压缩,以降低存储空间。压缩观看历史作为一列,按键值存储在一行中。

为提供更好的性能,LiveVH 和 CompressedVH 存储在不同的数据库表中,并做了不同的优化。考虑到 LiveVH 更新频繁,并且涉及的观看记录数量不大,因此可对 LiveVH 做频繁的 Compaction 操作。并且为了降低 SSTable 数量和数据规模,可以设置很小的 gc_grace_seconds 。为改进数据的一致性,也可以频繁执行读修复全列族修复(full column family repair)。而对于 CompressedVH,由于该部分数据很少做更新操作,因此为了降低 SSTable 的数量,偶尔手工做完全 Compaction 即可。在偶尔执行的更新操作中,会检查数据一致性,因此也不必再做读修复以及全列族修复。

写操作流

对于新的观看记录,使用同上的方法写入到 LiveVH。

读操作流

为有效地利用新设计的优点,团队更新了观看历史 API,提供了读取近期数据和读取全部数据的选项。

  • 读取近期观看历史:在大多数情况下,近期观看历史仅需从 LiveVH 读取。这限制了数据的规模,进而给出了更低的延迟。
  • 读取完整观看历史:实现为对 LiveVH 和 CompressVH 的并行读操作。

考虑到数据是压缩的,并且 CompressedVH 具有更少的列,因此读取操作涉及更少的数据,这显著地加速了读操作。

CompressedVH 更新流

在从 LiveVH 读取观看历史记录时,如果记录数量超过了一个预设的阈值,那么最近观看记录将由后台任务打包(roll up)、压缩并存储在 CompressedVH 中。打包数据存储在一个行标识为CustomerId的新行中。新打包的数据在写入后会给出一个版本,用于读操作检查数据的一致性。只有验证了新版本的一致性后,才会删除旧版本的打包数据。出于简化的考虑,在打包中没有考虑加锁,由 Cassandra 负责处理非常罕见的重复写问题(即以最后写入的数据为准)。


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

如图 2 所示,CompressedVH 的打包行中还存储了元数据信息,其中包括最新版本信息、对象规模和分块信息,细节稍后介绍。记录中具有一个版本列,指向最新版本的打包数据。这样,读取CustomerId总是会返回最新打包的数据。为降低存储的压力,我们使用一个列存储打包数据。为最小化具有频繁观看模式的会员的打包频率,LiveVH 中仅存储最近几天的观看历史记录。打包后,其余的记录在打包期间会与 CompressedVH 中的记录归并。

通过分块实现自动扩展

通常情况是,对于大部分的会员而言,全部的观看历史记录可存储在一行压缩数据中,这时读操作流会给出相当不错的性能。罕见情况是,对于一小部分具有大量观看历史的会员,由于最初架构中的同一问题,从一行中读取 CompressedVH 的性能会逐渐降低。对于这类罕见情况,我们需要对读写延迟设置一个上限,以避免对通常情况下的读写延迟产生负面影响。

为解决这个问题,如果数据规模大于一个预先设定的阈值,我们会将打包的压缩数据切分为多个分块,并存储在不同的 Cassandra 节点中。即使某一会员的观看记录非常大,对分块做并行读写也会将读写延迟控制在设定的上限内。


图 3:通过数据分块实现自动扩展

写操作流

如图 3 所示,打包压缩数据基于一个预先设定的分块大小切分为多个分块。各个分块使用标识CustomerId$Version$ChunkNumber并行写入到不同的行中。在成功写入分块数据后,元数据会写入一个标识为CustomerId的单独行中。对非常大的打包观看数据,这一做法将写延迟限制为两次写操作。这时,元数据行实现为一个不具有数据列的行。这种实现支持对元数据的快速读操作。

为加快对通常情况(即经压缩的观看数据规模小于预定的阈值)的处理,我们将元数据与观看数据合并为一行,消除查找元数据的开销,如图 2 所示。

读操作流

在读取时,首先会使用行标识CustomerId读取元数据行。对于通常情况,分块数是 1,元数据行中包括了打包压缩观看数据的最新版本。对于罕见情况,存在多个压缩观看数据的分块。我们使用元数据信息(例如版本和分块数)对不同分块生成不同的行标识,并行读取所有的分块。这将读延迟限制为两次读操作。

改进缓存层

为了支持对大型条目的分块,我们还改进了内存中的缓存层。对于存在大量观看历史的会员,整个压缩的观看历史可能无法置于单个 EVCache 条目中。因此,我们采用类似于对 CompressedVH 模型的做法,将每个大型缓存条目分割为多个分块,并将元数据存储在首个分块中。

结果

在引入了并行读写、数据压缩和数据模型改进后,团队达成了如下目标:

  1. 通过数据压缩,实现了占用更少的存储空间;
  2. 通过分块和并行读写,给出了一致的读写性能;
  3. 对于通常情况,延迟限制为一次读写。对于罕见情况,延迟限制为两次读写。


图 4:运行结果

团队实现了数据规模缩减约 6 倍,Cassandra 维护时间降低约 13 倍,平均读延迟降低约 5 倍,平均写时间降低约 1.5 倍。更为重要的是,团队实现了一种可扩展的架构和存储空间,可适应 Netflix 观看数据的快速增长。

在该博客系列文章的第二部分中,我们将介绍存储规模化中的一些最新挑战。这些挑战推动了会员观看历史数据存储架构的下一轮更新。如果读者对解决类似问题感兴趣,可加入到我们的团队中

查看英文原文: Scaling Time Series Data Storage — Part I

感谢蔡芳芳对本文的审校。

公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2018-03-18 18:211881
用户头像

发布了 391 篇内容, 共 127.0 次阅读, 收获喜欢 256 次。

关注

评论

发布
暂无评论
发现更多内容

Webpack | 提升构建速度和体积优化的N种方式

梁龙先森

大前端 webpack 2月春节不断更

Go Modules v2 及后续版本

Rayjun

Go 语言

使用APICloud敏捷式开发总结,回顾开发一个完整APP过程。

孙叫兽

App 开发 APICloud 引航计划

股票配资系统开发

v16629866266

阿里架构师经验分享!写给互联网大厂员工的真心话,最全的BAT大厂面试题整理

欢喜学安卓

android 程序员 面试 移动开发

Python实现钉钉/企业微信自动打卡

sum56

Python python 爬虫 打卡

驱动力读书笔记之四

张老蔫

28天写作

大作业2-知识总结

arcyao

百度网盘限速解决方案

孙叫兽

解决方案 百度网盘 限速

产品 0 期 - 第四周作业

vipyinzhiwei

程序员养家活口接私活必备网站(顺便用技术改变世界)

孙叫兽

程序员 网站 私活

“五年饮冰,难凉热血”,一名专科生的求学历程

不脱发的程序猿

程序人生 心路历程 2月春节不断更 大学总结 2020年度总结

期末大作业一

心在那片海

探寻内部类的奥秘(上)

后台技术汇

2月春节不断更

机器学习笔记之:Matrix Matrix Multiplication

Nydia

史上最全的技术手册整理总结,编程小白都从这篇文章迅速成为大牛

孙叫兽

Java 大前端 技术手册 开发文档

大作业1-同城快递业务系统设计

arcyao

MyBatis专栏 - 进阶(引入外部配置文件, 类型参数设置)

小马哥

Java mybatis 七日更 2月春节不断更

什么是防火墙?

重磅发布 | 2021年OpenAtom XuperChain开源技术路径

开放原子开源基金会

区块链 百度 开源 开放原子开源基金会

让人“眼前一亮、不明觉厉”的互联网技术PPT

不脱发的程序猿

程序人生 PPT 2月春节不断更 互联网技术PPT 互联网工具

Linux Lab 进阶: Uboot 引导程序

贾献华

Linux bootloader Linux Kenel boot

OpenCV简介及其工程应用-游戏色块检测

行者AI

OpenCV

婚恋交友软件开发

luluhulian

Elasticsearch multi-index 搜索

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

2020年末总结,脚踏实地,一步一个脚印——致敬自己一年的心酸历程

孙叫兽

孙叫兽 年度报告 引航计划

话题讨论 |互联网软件技术培训,靠谱吗?

不脱发的程序猿

程序员 程序人生 话题讨论 互联网培训 技术培训

容器&服务:开篇,压力与资源

程序员架构进阶

容器 服务 七日更 28天写作 2月春节不断更

2 期架构师训练营 - 大作业(二)

云飞扬

架构师训练营第2期

学习总结之HTML5剑指前端(建议收藏,图文并茂)

我是哪吒

学习 程序员 面试 大前端 2月春节不断更

即使技术再精,面试时一问这个必挂!!

冰河

面试 类加载器 我要进大厂 Java类加载

规模化时间序列数据存储Part1_语言 & 开发_Netflix_InfoQ精选文章