本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
工作上有一段攻坚时期,使得距离上一次写文章已经过了两个月,终于把拖了很久的《TimeUnion: An Efficient Architecture with Unified Data Model for Timeseries Management Systems on Hybrid Cloud Storage》学习完,并有时间记录一下,结合昨天的国足比赛,有一种吃了两颗屎味巧克力的感觉。
但是辩证的看,也不全然是坏消息,分辨屎味以及说出为什么这是一个屎味的巧克力也同样重要,这也是这篇文章诞生的原因。
现有的开源时间序列数据库(比如 Prometheus,InfluxDB)的设计大多借鉴了LSM-tree的理念,以追求高写入吞吐,数据按照时间进行shard分区,首先在内存中分批处理写入数据,然后刷新到磁盘,此外,每个shard都自包含倒排索引和数据本身。在设计中,有几个假设:
基于如上假设,在云上部署现有系统以管理大量时间序列时,存在以下几个关键问题。
为了引出主要贡献,作者设计了两个实验:
作者认为这篇文章的贡献主要集中在:
在我的角度来看,这篇文章可以看出作者科研实力较强,但是论文提及的系统本身工程意义不大,原因如下:
作者讨论了块存储与对象存储的成本与读写性能,这个值其实参考意义不是特别大,各大公有云厂商的具体值都不太一样,且与实例所属地域也有很大的关系,建议对这方面数据较为敏感的同学还是自己测试。
这里提一下为什么16kb一下对象存储的时延不变,现有公有云对象存储的架构鼻祖基本上是《Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency》,其中Partition Layer为对象表,Stream Layer则为实际的存储模块,一个大的对象在对象存储内部会被分为多个分块,并在对象表中记录大对象的所有分块在存储中的元数据(生命周期,索引等),所以单个分片阈值以下时延是稳定的,在有些实现中当一个对象小于这个分块阈值时kv只存在对象表,对象表的实现一般是一个range kv系统,存储都是SSD的,所以小对象存储会很快,看起来s3没有使用这种优化。
从结果可以看到大的指导方向就是:块存储贵性能高,对象存储便宜性能差
对象存储是文件系统/存储的一种简化替代:牺牲了强一致性、目录管理,访问时延等功能属性,以换取廉价的成本与海量伸缩的能力。它提供了一个简单的、高延迟、高吞吐扁平 KV 存储服务,从标准的存储服务中剥离出来。作为云服务之锚,对象存储优势还有很多:
块存储作为当然优势也有很多,但是价格真的非常高昂:
逻辑视图的核心思想在于每一个时间序列都归属于一个group,在向group中添加新的时间序列时,group tags会被提取出来,而其他tags则用于唯一标识group内的时间序列,且在构建倒排索引时使用group id,这样可以使得倒排索引中的发布列表长度大幅度减少(如下图),但是带来的问题就是如何快速索引组内的时间序列。
我对这里独立时间序列和group分离的理解是这种组织形式支持两种索引方式,普通的独立时间序列和目前influxdb的组织形式类似,而group是论文提出的组织形式,论文设计了两种方式的组织方式,最终都会被转化为Elastic time-partitioned LSM-tree的SSTable。
我认为设计两种组织形式的原因于论文中形式化证明的结果有关,具体可参考论文3.1节的Grouping analysis
。
这里优化的动机来自于实验1,实验本身没有问题,但是优化本身的现实意义有待商榷,我认为可以聊的具体有两个优化点:
这里优化的动机来自于实验2,优化的本质是需要重新设计原有的压缩机制,以减少压缩过程中的低效数据读取。
可以想到如果写入的时间戳永远单调递增,写入可以理解为内存到L0永远不存在合并,但是处理乱序数据会打破这个约束;其次因为不同层处于不同的介质,如何提升压缩率也是需要考虑的问题;
所以Elastic time-partitioned LSM-tree解决的问题就是如何平衡写入放大和增加数据压缩率。
分别使用大端编码将时间序列/组 ID 和时间序列/组块的起始时间戳存储在第一和第二个 64 位中,这样的设计可以使得同一个时间序列的数据存储在一起,同时根据时间戳排序,这样的key格式同时可以利用到key的前缀压缩功能,使得时间序列本身被压缩。
这样的设计利好于数据压缩,但是不利好于查询,因为时间序列的指定实际上是随机的,这样的key format使得一次查询涉及到的key遍布引擎的整个key namespace,从而使得查询性能极差。
这也是influxdb1.x原生引擎就存在的问题。
LSM 树只维护三个级别,第 0 层和第 1 层管理块存储中相对较少的时间序列数据(即最近 2 小时的数据),较早的数据存储在对象存储(第二层),以避免不必要的压缩(速度较慢)。由于时间序列数据是按时间戳排序的,因此我们根据不同的时间范围将 SSTables 划分到不同的级别,以控制每个级别的大小。对于第 0 级和第 1 级,时间分区长度从一个相对较小的值开始(如 30 分钟),该值将根据预定义的快速存储大小限制进行动态调整。特定时间分区的 SSTables 中数据块的数据样本严格受分区时间范围的限制。当 SST 表压缩到第 2 级时,几个分区会合并以创建更大的分区(如 2 小时)
为了减少不可变 MemTable 刷新时的插入阻塞,TimeUnion扩展了一个不可变 MemTable 队列,允许同时进行多次刷新,这是一个比较常见的优化方式,在底层写入较慢时是一个很好的缓冲方法。
当第 0 层中累积的时间分区超过阈值(即 2)时,将触发从第 0 层到第 1 层的压缩,以压缩第 0 层中最旧的时间分区。在快速存储中保留两个级别有两个考虑因素。
这个流程和正常的LSM压缩没有什么区别
当 0 级时间分区的总体时间跨度超过 2 级分区时间长度(例如最初为 2 小时)时,将触发从 1 级到 2 级的压缩。
这里有一点比较有意思,即patches
,用于第 1 级与第 2 级出现重叠时,为了不重写对象存储中的SST,会将重叠的地方生成一个patches,写入第二层
只在块存储上存储两个小时的数据显然不太合适,在快速存储上存的越多肯定是越利好于查询的,考虑到块存储的成本相对较高,需要考虑在快存储有限的可用容量上存储尽可能多的数据,但是时间维度不能很好的表示数据的多少,所以引入Dynamic size control。
如果level 1 的总时间跨度足够大但总大小小于阈值(例如,数据样本稀疏,或者时间序列数量少),会增加分区长度。此外,为了便于compaction期间的时间分区对齐,在动态控制期间将可调的时间分区长度乘/除以 2。这里最小也得是2,否则无法对齐。
这里我有一个疑问,目前来看这么多复杂设计就是为例基于时间做冷热数据分离,如果只是最后一层的数据沉降,key中也不携带时间,好像也没什么问题,还可以更好的控制块存储中的大小。
思路上patches
有借鉴意义,可以有效的减少写入放大,在混合云场景下很有意义。但是整体来看还是一个玩具,生产中基本无法使用。
参考: