网络互联设备的增长带来了大量易于访问的时间序列数据越来越多的公司对挖掘这些数据感兴趣, 从而获取了有价值的信息并做出了相应的数据决策
近几年技术的进步提高了收集, 存储和分析时间序列数据的效率, 同时也刺激了人们对这些数据的消费欲望然而, 这种时间序列的爆炸式增长, 可能会破坏大多数初始时间序列数据的体系结构
Netflix 作为一家以数据为驱导的公司, 对这些挑战并不陌生, 多年来致力于寻找如何管理日益增长的数据我们将分享 Netflix 如何通过多次扩展来解决时间序列数据的存储架构问题
时间序列数据会员观看记录
Netflix 会员每天观看超过 1.4 亿小时的内容而每个会员在点击标题时会产生几个数据点, 这些数据点将被作为观看记录进行存储 Netflix 通过分析这些观看数据, 为每位会员提供了实时准确的标签和个性化推荐服务, 如这些帖子中所述:
如何判断你在观看一个节目?
https://medium.com/netflix-techblog/netflixs-viewing-data-how-we-know-where-you-are-in-house-of-cards-608dd61077da
帮助你继续观看 Netflix 的节目
https://medium.com/netflix-techblog/to-be-continued-helping-you-find-shows-to-continue-watching-on-7c0d8ee4dab6
从以下 3 个维度累积历史观看记录:
随着时间的推移, 每个会员的更多观看记录数据将被存储
随着会员数量的增长, 更多会员的观看记录数据会被存储
随着会员每月观看时间的累积, 每个会员的更多观看记录将被存储
过去十年的发展, Netflix 已经在全球拥有 1 亿名会员, 其观看记录的数据亦是大幅增加在本篇博客中, 我们将重点讨论如何应对存储观看历史数据带来的巨大挑战
从简单的开始
观看记录的第一版原生云存储架构使用 Cassandra 的理由如下:
Cassandra 对时间序列数据建模提供了很好的支持, 其中每行都有动态的列数
观看记录数据的读写速度比约为 9:1 由于 Cassandra 的写入效率非常高, 因此 Cassandra 非常适合频繁写入操作的工作
根据 CAP 定理, 团队更倾向于最终的一致性 Cassandra 支持通过调整一致性进行权衡
在最初的方法中, 每个成员的观看历史记录都存储在 Cassandra 中, 并使用行键存储在一行中: CustomerId 这种水平分区的方式能够随着会员数量的增长而有效扩展, 并且使得浏览会员的整个观看记录的常见用例变得简单高效
然而随着会员数量的增加, 更重要的是, 每个会员的流量越来越多, 行数和整体数据量也越来越多随着时间的推移, 这导致了昂贵的操作成本, 对于读写具有海量观看记录的会员数据而言性能较差
下图说明了初始数据模型的读写流程:
图 1: 单个图表数据模型
写流程
当会员点击播放时, 一条观看记录将作为新的列插入点击暂停或停止后, 则该观看记录被更新可见对于单列的写入是迅速和高效的
读流程
通过整行读取来检索一个会员的所有观看记录: 当每个会员的记录数很少时, 读取效率很高但是随着一个会员点击更多标题产生更多的观看记录此时读取具有大量列的行数据会给 Cassandra 带来额外的压力, 并造成一定的读取延迟
通过时间范围查询读取会员数据的时间片: 将导致了与上面的性能不一致, 这取决于在指定的时间范围内查看记录的数量
通过分页整行读取大量观看记录: 这对于 Cassandra 来说是好的, 因为它并不需要等待所有的数据返回就可以加载同时也避免了客户端超时然而, 随着观看记录数量的增加, 整行读取的总延迟增加了
放缓原因
让我们来看看 Cassandra 的一些内部实现, 以了解为什么我们最初简单设计的性能缓慢随着数据的增长, SSTable 的数量相应增加
由于只有最近的数据在内存中, 所以在很多情况下, 必须同时读取 memtables 和 SSTable 才能检索观看记录这样就造成了读取延迟同样, 随着数据量的增加, 压缩需要更多的 IO 和时间由于行越来越宽, 读修复和全列修复因此变得更加缓慢
缓存层
虽说 Cassandra 在观看记录数据写入方面表现很好, 但仍有必要改进读取延迟为了优化读取延迟, 需要以牺牲写入路径上的工作量为代价, 我们通过在 Cassandra 存储之前增加内存分片缓存层 (EVCache) 来实现
缓存是一种简单的键值对存储, 键是 CustomerId, 值是观看记录数据的压缩二进制表示每次写入 Cassandra 都会发生额外的缓存查找, 并在缓存命中时将新数据与现有值合并
读取观看记录首先由缓存提供服务在高速缓存未命中时, 再从 Cassandra 读取条目, 压缩并插入高速缓存
多年来随着缓存层的增加, 这种单一的 Cassandra 表格存储方法表现良好基于 CustomerId 的分区在 Cassandra 集群中可扩展性亦较好
直到 2012 年, 观看记录 Cassandra 集群成为 Netflix 最大的 Cassandra 集群之一为进一步扩展, 团队决定将集群规模扩大一倍
这就意味着 Netflix 要冒险进入使用 Cassandra 的未知领域与此同时, 伴随着 Netflix 业务的快速增长, 包括不断增加的国际会员数和即将投入的原创内容
重新设计: 实时和压缩存储方法
显然, 需要采取不同的方法进行扩展来应对未来 5 年的预期增长团队分析了数据特征和使用模式, 重新设计了观看记录存储方式并实现了两个主要目标:
较小的存储空间
每个会员的观看记录增长与读写性能保持一致
对于每个会员, 观看记录数据被分成两个集合:
实时或近期观看记录(LiveVH): 频繁更新的最近观看记录数量较少这样的数据以非压缩形式存储, 如上面简单的设计中所述
压缩或存档观看历史记录(CompressedVH): 大量较早的观看记录很少更新 这样的数据将被压缩以减少存储空间压缩的观看历史记录存储在每行键的单个列中
LiveVH 和 CompressedVH 存储在不同的表格中, 并通过不同的调整以获得更好的性能由于 LiveVH 的频繁更新和拥有少量的观看记录, 因此压缩需频繁进行, 且保证 gc_grace_seconds 足够小以减少 SSTables 数量和数据大小
只读修复和全列修复频繁进行保证数据的一致性由于对 CompressedVH 的更新很少, 因此手动和不频繁的全面压缩足以减少 SSTables 的数量在不频繁更新期间检查数据的一致性这样做消除了读修复以及全列维修的需要
使用与前面所述相同的方法将新观看记录写入 LiveVH
写流程
使用与前面所述相同的方法将新观看记录写入 LiveVH
读流程
为了从新设计中获益, 观看历史记录的 API 已更新, 可以选择读取最近的或完整的数据:
最近观看记录: 对于大多数的用例, 只需从 LiveVH 中读取数据, 通过限制数据大小降低延迟
完整的观看记录: 作为 LiveVH 和 CompressedVH 的并行读取实现由于数据压缩和 CompressedVH 的列较少, 因此通过读取较少的数据就可以显著加速读取
CompressedVH 更新流程
当从 LiveVH 中读取观看历史记录时, 如果记录数量超过可配置的阈值, 那么最近的观看记录就被汇总一次, 压缩并通过后台任务存储在 CompressedVH 中然后使用行键(行关键字):CustomerId 将数据存储在新行中新的汇总是版本化的, 写入后会再次检查查数据的一致性只有在验证与新版本数据一致后, 旧版本的数据才会被删除为简单起见, 在汇总过程中没有加锁, Cassandra 负责解决极少的重复写入操作(即最后一个写入操作获胜)
图 2: 实时和压缩的数据模型
如上图所示, CompressedVH 中汇总的行也存储元数据信息, 如最新版本号, 对象大小和块信息 (稍后更多) 版本列存储对最新版本的汇总数据进行引用, 以便 CustomerId 的读取始终只返回最新的汇总数据
汇总起来的数据存储在一个单一的列中, 以减少压缩压力为了最大限度地减少频繁观看模式的会员的汇总频率, 最后几天查看历史记录的值将在汇总后保存在 LiveVH 中, 其余部分在汇总期间与 CompressedVH 中的记录合并
通过 Chunking 进行扩展
对于大多数会员来说, 将其整个观看记录存储在单行压缩数据中将在读取流程中提升性能对于一小部分具有大量观看记录的会员, 由于第一种体系结构中描述的类似原因, 从单行中读取 CompressedVH 速度缓慢不常见用例需要在读写延迟上设一个上限, 才不会对常见用例造成读写延迟
为了解决这个问题, 如果数据大小大于可配置的阈值, 我们将汇总起来的压缩数据分成多个块这些块存储在不同的 Cassandra 节点上即使对于非常大的观看记录数据, 对这些块的并行读取和写入也最多只能达到读取和写入延迟上限
图 3: 自动缩放通过组块
写流程
如图 3 所示, 根据可配置的块大小, 汇总起来的压缩数据被分成多个块所有块都通过行键: CustomerId $ Version $ ChunkNumber 并行写入不同的行在成功写入分块数据之后, 元数据通过行键: CustomerId 写入到自己的行
对于大量观看记录数据的汇总, 上述方法将写入延迟限制为两种写入在这种情况下, 元数据行具有一个空数据列, 以便能够快速读取元数据
为了使常见用例 (压缩观看记录小于可配置阈值) 被快速读取, 将元数据与同一行中的观看记录组合以消除元数据查找流程, 如图 2 所示
读流程
通过关键字 CustomerId 首次读取元数据行对于常见用例, 块数为 1, 元数据行也具有最新版本汇总起来的压缩观看记录对于不常见的用例, 有多个压缩的观看记录数据块使用版本号和块数等元数据信息生成块的不同行密钥, 并且并行读取所有块上述方法将读取延迟限制为两种读取
缓存层更改
内存缓存层的增强是为了支持对大型条目进行分块对于具有大量观看记录的会员, 无法将整个压缩的观看历史记录放入单个 EVCache 条目中与 CompressedVH 模型类似, 每个大的观看历史高速缓存条目被分成多个块, 并且元数据与第一块一起被存储
结果
利用并行, 压缩和改进的数据模型, 实现了所有目标:
通过压缩缩小存储空间
通过分块和并行的读 / 写操作保证读 / 写一致性常见用例的延迟受限于一次读操作和一次写操作, 以及不常见用例的延迟受限于两次读操作和两次写操作
图 4: 结果
数据大小减少了约 6 倍, 花费在 Cassandra 维护上的系统时间减少了约 13 倍, 平均读取延迟减少了约 5 倍, 平均写入延迟减少了约 1.5 倍更重要的是, 它为团队提供了可扩展的架构和空间, 可以适应 Netflix 观看记录数据的快速增长
来源: https://yq.aliyun.com/articles/534744