1. Overview
2. 单表数据模型
- 数据结构
- 写操作
- 读操作
- 读延迟
- 缓存层
3. 实时数据与历史数据分离模型
- 数据结构
- LiveVH
- 写操作
- 读操作
- CompressedVH
- 写操作
- 读操作
- 分块实现自动扩展
- 写操作
- 读操作
- 改进缓存层
4. 结果对比
Overview
Netflix 的云原生存储架构使用了 Cassandra存储观看历史数据,考虑如下,
- 支持对时序数据的建模
- 在当前业务数据上,读写操作比是1:9。而Cassandra提供了高效写操作API,适用于当前的写密集型业务应用
- 权衡,当前业务更偏向于C。而Cassandra支持可调整的一致性,有助于实现CAP上的权衡
演进路线总结,
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`, record1, record2, record3, ..., recordN)
- 。随着时间的推移,这将导致存储和操作的成本增大。
写操作
。。在 Cassandra 中,对单一列值的写操作是快速和高效的。
读操作
。。。读取一个具有大量列的数据行,会对 Cassandra 造成了额外压力,进而对读操作延迟产生负面影响。
时间范围
查询。这同样会导致上面所说的性能不一致问题。因为查询性能依赖于给定时间范围内的观看记录数/列数。
如果要查看的历史数据规模很大,需要做分页才能进行整行读操作。分页对 Cassandra 更好,因为查询不需要等待所有数据都就绪,就能返回给用户。分页也避免了客户超时问题。但是,随着观看记录的增长,分页增加了读取整行的整体延迟。
读延迟
缓存层
为优化读操作延迟,考虑了以增加写路径上的工作为代价,在Cassandra存储前增加了一个内存中的分片缓存层(即EVCache)。缓存实现为一种基本的键-值存储,键是customerId,值是观看历史数据的二进制压缩表示。每次Cassandra的读操作,将额外
生成一次缓存查找操作。一旦缓存命中,直接给出缓存中的已有值。对于观看历史记录的读操作,首先使用缓存提供的服务。一旦缓存没有命中,再从Cassandra读取条目,压缩后插入到缓存中。
实时数据与历史数据分离模型(Version2)
数据结构
为进一步实现存储的规模化,分析了数据的特征和使用模式,重新定义了观看历史存储。给出了两个主要目标,
- 更小的存储空间
- 实时/近期观看历史记录(LiveVH,Live or Recent Viewing History):一小部分频繁更新的近期观看记录。LiveVH 数据以非压缩形式存储
- 历史/归档观看历史记录(CompressedVH,Compressed or Archival Viewing History):大部分很少更新的历史观看记录。该部分数据将做压缩,以降低存储空间。
压缩观看历史作为一列
,按键值存储在一行中
为提供更好的性能,LiveVH 和 CompressedVH 存储在不同的数据库表中,并做了不同的优化。
冷数据太多,单机memory放不下,就将冷数据打包,然后切片分段,缓存到不同的单机memory上。在查找时再根据meta data的routing来查。
LiveVH
写操作
。
读操作
读取实时/近期观看历史:在大多数情况下,近期观看历史仅需从LiveVH读取。这限制了数据的规模,进而给出了更低的延迟。
CompressedVH
写操作
在从LiveVH读取观看历史记录时,如果记录数量超过了一个预设的阈值,那么最近观看记录将由后台任务打包(roll up)、压缩并存储在CompressedVH 中。
打包数据存储在一个行标识为 customerId 的新行中。新打包的数据在写入后会给出一个版本,用于读操作检查数据的一致性。只有验证了新版本的一致性后,才会删除旧版本的打包数据。
CompressedVH的打包行中还存储了元数据信息,其中包括最新版本信息
、对象规模
和分块信息
。
新行记录中具有一个版本列,指向最新版本的打包数据。这样,读取 customerId 总是会返回最新打包的数据。
为降低存储的压力,只使用了一个列
存储归档数据。
。
打包后,其余的记录在打包期间会与 CompressedVH中已有的记录归并。
读操作
读取完整观看历史:实现为对 LiveVH 和CompressVH的并行读操作(实时与历史同时读,与下文的分块的并行不一样)。考虑到数据是压缩的,并且CompressedVH 具有更少的列,因此读取操作涉及更少的数据,这显著地加速了读操作。
分块实现自动扩展
。
。即从一行中读取CompressedVH的性能很低。
为解决这个问题,如果数据规模大于一个预先设定的阈值,就将打包的压缩数据切分为多个分块,并存储在不同的 Cassandra节点中。并行读写也会将读写延迟控制在设定的上限内。
数据分块实现自动扩展(类似join倾斜的prefix)写操作
打包压缩数据基于一个预先设定的分块大小切分为多个分块。各个分块使用标识CustomerId$Version$ChunkNumber
并行写入到不同的行中。
在成功写入分块数据后,元数据
会写入一个标识为 customerId 的单独行中。
对非常大的归档观看数据,这一做法将写延迟限制为两次写操作。这时,元数据行为一个不具有数据列的行,这种实现支持对元数据的快速读操作。
读操作
在读取时,首先会使用行标识customerId
读取元数据行
,
- 对于通常情况,分块数是1,元数据行中包括了打包压缩观看数据的最新版本
- 对于罕见情况,存在多个压缩观看数据的分块。使用了元数据信息(例如版本和分块数)对不同分块生成不同的行标识即
CustomerId$Version$ChunkNumber
,并行读取所有的分块。这将读延迟限制为两次读操作。