Netflix单key存储的架构演进

2018-02-16  本文已影响49人  chenfh5

记录一下看了Netflix实战指南:规模化时序数据存储后关于其中的单key存储的架构演进理解,目录如下,

1. Overview
2. 单表数据模型
    - 数据结构
    - 写操作
    - 读操作
      - 读延迟
      - 缓存层
3. 实时数据与历史数据分离模型
    - 数据结构
    - LiveVH
      - 写操作
      - 读操作
    - CompressedVH
      - 写操作
      - 读操作
    - 分块实现自动扩展
      - 写操作
      - 读操作
      - 改进缓存层
4. 结果对比

Overview

Netflix 的云原生存储架构使用了 Cassandra存储观看历史数据,考虑如下,

演进路线总结,

v1:一个用户一个key,key后面跟一连串的用户观影记录信息data。当data少时,可以快速定位<userId, data[record1, ..., recordN]>;水平扩展性好。但是当data很大,查询需要低效的O(N)来遍历整个data。LRU缓存可以改善查询低效,但是空间换时间。

v2:既然v1的问题是累积data的太大引起,那么可以根据自定义阈值T1将data切分为两部分,一部分是实时数据;一部分是历史数据。小的实时数据可以按照v1的方式一列一个record;大的历史数据都压缩在一起成为一列(多个历史列合并成一个列)。如果合并压缩后的历史数据还是太大,那么依照阈值T2对其切分。

个人感觉是分库分表/MapReduce/冷热数据分离的思想,将一个读操作或者一个写操作的数据模型,切开成多个小的数据模型,然后在其上实现并发读写。


单表数据模型(Version1)

数据结构

每位会员的所有观看记录存储为一行,使用customerId为主键,每一次观看记录为一列,即,

(`customerId`, record1, record2, record3, ..., recordN)
单表数据模型/v1模型的读、写操作

写操作

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

读操作

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

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

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

读延迟

原因,只有最近的数据是维护在内存中的(LRU),因此在很多情况下,检索观看历史记录时需要同时读取内存表和 SSTable。这对于读取延迟具有负面影响。同样,随着数据的增长,合并(Compaction)操作将占用更多的 IO 和时间。此外,随着一行记录越来越宽,读修复(Read repair)和全列修复(Full column repair)也会变慢。

缓存层

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


实时数据与历史数据分离模型(Version2)

数据结构

为进一步实现存储的规模化,分析了数据的特征使用模式,重新定义了观看历史存储。给出了两个主要目标,

最后决定将每位会员的观看历史数据划分为两个数据集,

为提供更好的性能,LiveVH 和 CompressedVH 存储在不同的数据库表中,并做了不同的优化。

冷数据太多,单机memory放不下,就将冷数据打包,然后切片分段,缓存到不同的单机memory上。在查找时再根据meta data的routing来查。

LiveVH

写操作

如果一位会员观看了大量的视频,那么他的观看记录数量将会增加,即记录的列数增加(跟version1的写操作一样)。

读操作

读取实时/近期观看历史:在大多数情况下,近期观看历史仅需从LiveVH读取。这限制了数据的规模,进而给出了更低的延迟。

CompressedVH

写操作

在从LiveVH读取观看历史记录时,如果记录数量超过了一个预设的阈值,那么最近观看记录将由后台任务打包(roll up)、压缩并存储在CompressedVH 中。

打包数据存储在一个行标识为 customerId 的新行中。新打包的数据在写入后会给出一个版本,用于读操作检查数据的一致性。只有验证了新版本的一致性后,才会删除旧版本的打包数据。

CompressedVH的打包行中还存储了元数据信息,其中包括最新版本信息对象规模分块信息

实时数据与历史数据/v2模型的读、写操作

新行记录中具有一个版本列,指向最新版本的打包数据。这样,读取 customerId 总是会返回最新打包的数据。
为降低存储的压力,只使用了一个列存储归档数据。
为最小化具有频繁观看模式的会员的打包频率,LiveVH中仅存储最近几天的观看历史记录。
打包后,其余的记录在打包期间会与 CompressedVH中已有的记录归并

读操作

读取完整观看历史:实现为对 LiveVH 和CompressVH的并行读操作(实时与历史同时读,与下文的分块的并行不一样)。考虑到数据是压缩的,并且CompressedVH 具有更少的列,因此读取操作涉及更少的数据,这显著地加速了读操作。

分块实现自动扩展

通常情况是,对于大部分的会员而言,全部的观看历史记录可存储在一行压缩数据中。

罕见情况是,对于一小部分具有大量观看历史的会员,与v1架构中的问题一样,单行记录太长。即从一行中读取CompressedVH的性能很低。

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

数据分块实现自动扩展(类似join倾斜的prefix)

写操作

打包压缩数据基于一个预先设定的分块大小切分为多个分块。各个分块使用标识CustomerId$Version$ChunkNumber并行写入到不同的行中。

在成功写入分块数据后,元数据会写入一个标识为 customerId 的单独行中。

对非常大的归档观看数据,这一做法将写延迟限制为两次写操作。这时,元数据行为一个不具有数据列的行,这种实现支持对元数据的快速读操作。

读操作

在读取时,首先会使用行标识customerId读取元数据行

改进缓存层

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


结果对比

v1 vs. v2
上一篇 下一篇

猜你喜欢

热点阅读