从一到无穷大 #46:探讨时序数据库Deduplicate与Compaction的设计权衡

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。

文章目录

  • 引言
  • Compaction Algorithms
  • Compact Execution Flow Based On Velox
    • LocalMergeSource的管道分离流式读取
    • 基于LoserTree的归并排序
    • Window算子实现流式计算
    • TableWrite的多路流式写入
  • 结束语

引言

时序数据库与关系型数据库一个比较大的功能差异为Deduplicate,时序数据库默认携带,而关系型数据库依赖于索引和查询时主动去重。

以Influxdb举例子阐述Deduplicate功能:

INSERT temperature,device_id=sensor1 v1=25,v2=25 1620000000000000000
INSERT temperature,device_id=sensor1 v1=26 1620000000000000000

一般有两种Deduplicate粒度

第一种为Field-Level Updates,上述数据会被合并为:

INSERT temperature,device_id=sensor1 v1=26,v2=25 1620000000000000000

其选择策略为tag+time相同的情况下,相同field的选择lsn大的,field取最后一个非空的

第二种为Row-Level Deduplication,上述数据会被合并为:

INSERT temperature,device_id=sensor1 v1=26 1620000000000000000

单纯的基于lsn行选择。

Compaction Algorithms

首先定义 Amplification:

  1. Write Amplification: 指写入存储设备的数据量与写入数据库的数据量的比率。例如向数据库写入 10 MB 数据,而观察到的磁盘写入为 30 MB,则写入放大率为 3。基于SSD的存储只能写入有限次数,因此写入放大会缩短SSD的使用寿命。
  2. Read Amplification:指每个查询的磁盘读取次数。例如如果需要读取 5 页来响应查询,则读取放大为 5。写入放大和读取放大的单位不同。写入放大衡量实际写入的数据量比应用程序认为的写入量多,而读取放大则指计算执行查询所需的磁盘读取次数比认为的多。
  3. Space Amplification:指磁盘上实际存储的物理数据量相对于用户数据的逻辑大小的放大倍数

名词定义:

  1. T:层之间的大小比值
  2. L:LSM树的层数
  3. B:SST文件大小

在这里插入图片描述

时序数据库中为了提高查询的效率,数据需要基于时间分shard,这种情况下全局看基本可以认为都是TWCS(Time Window Compaction Strategy)策略,因为老旧shard在经历FullCompact后有且只有一个Sort Runs,在活跃shard中基于不同的考量会有不同的实现方式。

在influxdb1.x中使用了Tiered,多层内均无序,Compact是在每一层选择可压缩的文件,CompactScheduler调度执行。

在GrepTimeDB中使用N-Level,只有两层,内部允许N个Sort Runs,在读写放大之间做权衡

在influxdb3.0中使用了Tiered+Leveled,L0无序,剩下的层数有序,Rocksdb的默认Compact策略也是这样的。

但是值得一提的是时序数据库的Timestamp是去重的主键列,不能简单的依赖时间去重,因为两次写入的时间也可能是重复的,合并的关键在于真实时间上后来的需要覆盖前面的,从实现上来讲一致性引擎的LSN就很合适。

但是这为Compaction的算法实现带来了一点复杂度,如果不按照LSN顺序进行合并,会导致字段覆盖逻辑错误,破坏数据一致性。举以下例子:

请添加图片描述

错误的合并顺序:
请添加图片描述
f1字段来自LSN=1的文件;f2字段来自LSN=3的文件。形成了LSN差值为1-3的混合记录请添加图片描述
由于合并结果中f1和f2的LSN分别为1和3,都不等于文件2的LSN=2, 按照LSN覆盖规则,LSN=2无法覆盖LSN=3的f2字段, 但LSN=2应该覆盖LSN=1的f1字段,导致逻辑混乱。

正确的合并顺序如下:请添加图片描述
这要求我们再Compact的时候需要合并 LSN 临近的文件。

从实现的角度上讲,GrepTimedb和influxdb3.0的引擎是类似的,适用于时间线爆炸的场景,而influxdb1.x在时间线较少的场景下依然有较强的竞争力,Compaction的设计上也有区别。

时序数据库Compact最在意什么呢?不同的业务场景有不同的答案:TTL短的业务,写入量大,查询量大,存储量相对少,可以Tired+Level均衡读写;TTL长的业务,相对不是特别关心实时数据查询性能,TWCS冷shard可以理解为Level,所以活跃shard可以采用Tired这样的写入友好的策略;

回到实现的角度,自然Tiered是最简单的,因为Compaction的时候只需要维护LSN的区间有序就可以,compact的时候需要保证LSN连续的文件执行合并。influxdb1.x引入了一个Generations的概念,代表一次compact的输出,其文件名组成为000001-01.tsm,前面是Generations,后面是按文件大小切分的chunk,合并的时候只允许Generations连续的执行合并,这其实就是LSN相邻的文件合并,因为数据中没有存储LSN(TSI+SeriesFile+TSM,没有行的概念,无法支持行去重),只能在Compact上下功夫,这样的坏处自然就是事实数据查询性能差。

如果是Level策略,且只支持行级别去重,文件的行中存储一个LSN,这样Compact策略就比较自由,因为文件本身内部附带LSN信息,在合并的过程中建立WindowBuild内部可以基于LSN作去重,对于Compact的策略没有影响。

但是要是Level策略+支持Field级别去重,这就比较麻烦了,就像上面举的例子,事实上因为文件内部记录的是行级别的LSN,但是合并后可能存在不同LSN的数据被合并到一行,如果只记录一个LSN这会导致其中有些列的LSN被强行升级了,这会导致去重出现错误的数据覆盖。
这个时候有三种解决问题的思路:
第一种方法是简单的记录field级别LSN,在宽列场景下几乎不可用,因为存储空间占用太大

第二种方法是记录添加一个额外的列,记录出现Field覆盖时Field的LSN,这样可以大大减少冗余的LSN存储,毕竟列覆盖是极少数情况,但是需要给原始文件再加一列,因为Parquet等文件格式支持复杂类型,这样做也是可以的

第三种是Compact在选择文件时不仅仅考虑SortKey,而且需要考虑LSN的连续,具体的实现是在L0沉降L1时执行下述操作,是最优选择:

  1. L0为Tired,选择需要被合并的文件,要求LSN连续,计算其LSN区间,为区间1
  2. 选择L1中和L0文件SortKey重叠的文件,计算其LSN区间,为区间2
  3. 查找L1中区间1与区间2的空洞文件,计算其LSN区间,为区间3
  4. 三部分文件组成一个Compact任务,合并后更新Compact文件LSN区间,如果目标文件较大,可以拆分成多个,但是LSN区间是一样的

所以采用特殊的Compact策略可以实现低成本Field/Row Level 的Deduplicate。

Compact Execution Flow Based On Velox

这一节和文章题目没有关系,单纯的记录一下

基于执行引擎做Compact已经不是什么稀奇的事情了,毕竟一个好的执行引擎库基本上可以认为是AP的基础库了,而且Compact可以被抽象为算子的组合,在Velox中,我们可以把Compact抽象为TableScan+LocalMerge+Window+TableWrite的算子组合。

其实现的技术要点如下:

LocalMergeSource的管道分离流式读取

在Velox的LocalMerge操作中,PlanBuilder阶段传入的TableScan算子并不是直接转换为LocalMergeSource,而是通过管道分离和数据流重定向过程实现的。

管道 0 (Producer Pipeline):
TableScanNode → CallbackSink → LocalMergeSource[0]

管道 1 (Producer Pipeline):
TableScanNode → CallbackSink → LocalMergeSource[1]

管道 2 (Producer Pipeline):
TableScanNode → CallbackSink → LocalMergeSource[2]

管道 3 (Consumer Pipeline):
LocalMergeOperator ← LocalMergeSource[0,1,2]

数据生产阶段:每个TableScan管道中的数据流

TableScanOperator::getOutput()

CallbackSink::addInput(RowVectorPtr input)

consumer(input, future) // 这是LocalMergeSource的enqueue函数

LocalMergeSource::enqueue(input, future)

LocalMergeSourceQueue::enqueue(input) // 数据进入队列

数据消费阶段:LocalMerge管道中的数据流

LocalMerge::getOutput()

TreeOfLosers::next() // 从多个source中选择最小值

LocalMergeSource::next() // 从队列中取数据

LocalMergeSourceQueue::next()

返回排序后的RowVector

这样做有以下好处:

  1. 不同的并行性要求:生产者管道 (TableScan): 需要多线程并行处理,充分利用 I/O 带宽;消费者管道 (LocalMerge): 必须单线程执行,保证排序的正确性
  2. 数据流控制:生产者和消费者解耦,以支持背压控制(backpressure),生产者可以快速写入缓冲区,消费者按需读取
  3. 内存管理
    1. LocalMergeSource提供缓冲队列,支持流式处理,当缓冲区满时,生产者会被阻塞,防止内存溢出
    2. 可以基于缓冲队列实现精确的内存控制
  4. 容错性:管道间独立,单个管道失败不影响其他
    1. 生产者管道故障:不会直接影响消费者管道,可以独立重试
    2. 消费者管道故障:不会影响生产者的数据生成
    3. 部分失败处理:某个生产者失败时,其他生产者可以继续工作

基于LoserTree的归并排序

在Velox Window操作的LocalMerge场景中,需要处理多个已排序(基于提前指定的timestamp || measurement || tags作为排序key)的数据流并将其合并为一个全局有序的结果。

假设待排序列数为 N,待排元素总个数为 n,复杂度分析:

LoserTree堆排序
空间复杂度O(N)O(N)
单次调整时间复杂度O(n*logN)O(2n*logN)
整体排序完成时间复杂度O(logN)O(2*logN)

在调整LoserTree时,由于只需比较和更新对应叶子节点的路径上的节点,无需比较兄弟节点,因此在最坏情况下,单次调整败者树的时间复杂度为 O(logN)。

而堆排单次调整则需要比较兄弟节点,这里有常数级别的优化。

Window算子实现流式计算

认为每个窗口函数都通过一个 OVER 子句来运行,规定了 Window 函数的聚合方式。

窗口函数支持:

  1. 排名函数:ROW_NUMBER、RANK、DENSE_RANK
  2. 聚合函数:SUM、COUNT、AVG、MIN、MAX等作为窗口函数
  3. 分析函数:LAG、LEAD、FIRST_VALUE、LAST_VALUE等

RANGE框架边界类型:

  1. kUnboundedPreceding:之前的全部
  2. kPreceding:之前的N个
  3. kCurrentRow:当前行
  4. kFollowing:之后的N个
  5. kUnboundedFollowing:之后的全部

样例sql:

  1. sum(value) over (partition by partition_key order by order_key rows between unbounded preceding and current row) as running_sum
  2. avg(value) over (partition by partition_key order by order_key rows between 2 preceding and 2 following) as moving_avg
  3. row_number() over (partition by partition_key order by order_key) as rn
  4. c0, c1, c2, first_value(c0) OVER (PARTITION BY c1 ORDER BY c2 DESC NULLS FIRST {})

window的区间划分类有三种
请添加图片描述

TableWrite的多路流式写入

A[TableWriter] --> B[HiveDataSink]
B --> C[ParquetWriter]
C --> D[WriteFileSink]
D --> E[S3WriteFile/LocalFile]
|
F[Input Data] --> A
A --> G[addInput]
G --> H[appendData]
H --> I[write method]
I --> J[Row Group Buffer]
J --> K[Flush Policy Check]
K --> L[Write Row Group]
L --> M[Footer Writing]

RowGroup默认刷新大小:

  1. rowsInRowGroup: 1’024 * 1’024
  2. bytesInRowGroup: 128 * 1’024 * 1’024

Compact需要基于sortkey和文件大小在compact的过程中切分输出文件

TableWrite运算符目前无法做到,只能通过Partition和bucket来分区,这种情况在分区时采用hash来选择排序key所属的文件,而不是range,需要修改PartitionIdGenerator,实现range形式的partitionID分配。

结束语

想明白上述问题以后Compact就剩下工程问题了,需要聚焦在Compact任务的调度(分池,并发限制),与Catalog的交互,Garbage Collector的设计。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/82323.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大厂前端研发岗位设计的30道Webpack面试题及解析

文章目录 一、基础核心二、配置进阶三、性能优化四、Loader原理五、Plugin机制六、高级应用七、工程化实战八、原理深挖九、异常处理十、综合场景一、基础核心 Webpack的核心概念是什么? 解析:入口(entry)、输出(output)、加载器(loader)、插件(plugins)、模式(mode)。Loader…

pytest 常用命令参数

以下是 pytest 常用命令参数 的整理,涵盖测试运行、过滤、调试、报告等常见场景,方便你高效使用 pytest: 1. 基本测试运行 命令说明pytest运行当前目录及子目录下所有测试(test_*.py 或 *_test.py)pytest path/to/tes…

利用openwrt路由器和随身WIFI搭建CPE

背景: 最近5GCPE挺火,各种硬件层出不穷,包括DY上很多商家在推的AX3000叠加展锐RM500 5G模块,自己组装CPE,成本也在300 看了下开源硬件,其实就是一个开源的openwrt系统,硬件上5G模块通过usb协议…

Python中使用pandas

使用Pandas进行数据处理和分析 Pandas是Python中最流行的数据处理和分析库之一。下面我将介绍Pandas的基本使用方法。 安装Pandas pip install pandas 基本数据结构 1. Series - 一维数组 import pandas as pd# 创建Series s pd.Series([1, 3, 5, 7, 9]) print(s) 2. D…

ISO18436-2 CATII级振动分析师能力矩阵

ISO18436-2021是当前针对针对分析师的一个标准,它对振动分析师的能力和知识体系做了4级分类,这里给出的是一家公司响应ISO18436的CATII级标准,做的一个专题培训的教学大纲。摘自: 【振動噪音產學技術聯盟】04/19-23 ISO 18436-2…

Qt实现的水波进度条和温度进度条

一.效果 二.原理 1.水波 要模拟波浪,就要首先画出一条波浪线,正弦余弦曲线就很适合。 y=A*sin(ω*x+φ)+k y=A*cos(ω*x+φ)+k 这是正弦余弦曲线的公式,要想实现水波效果,那需要两条曲线,一条曲线的波峰对着另外一条曲线的波谷,要实现这样的曲线效果,只有让正弦曲线前移…

《Python 应用中的蓝绿部署与滚动更新:持续集成中的实践与优化》

《Python 应用中的蓝绿部署与滚动更新:持续集成中的实践与优化》 引言 在现代软件开发中,持续集成与持续部署(CI/CD)已成为标准实践。面对频繁发布与升级需求,蓝绿部署和滚动更新两种策略为 Python 应用提供了稳定、安全的发布方式。本文将深入探讨这两种策略的原理、适…

4.2.2 Spark SQL 默认数据源

在本实战概述中,我们探讨了如何在 Spark SQL 中使用 Parquet 格式作为默认数据源。首先,我们了解了 Parquet 文件的存储特性,包括其二进制存储方式和内嵌的 Schema 信息。接着,通过一系列命令,我们演示了如何在 HDFS 上…

当前用户的Git本地配置情况:git config --local --list

通过config命令可以查询当前用户的本地配置情况。这些配置项定义了 Git 在当前仓库中的行为,包括文件权限处理、符号链接处理以及大小写敏感性等。 git config --local --list core.repositoryformatversion0 指定 Git 仓库的格式版本。版本 0 是最初的格式。 cor…

Flutter 包依赖升级指南:让项目保持最新状态

在 Flutter 开发过程中,依赖项管理是确保项目顺利运行和持续优化的关键环节。依赖项是项目中不可或缺的外部库,它们提供了各种功能,从 UI 组件到数据处理工具,帮助开发者快速构建应用。然而,随着时间的推移&#xff0c…

【深度学习】实验四 卷积神经网络CNN

实验四 卷积神经网络CNN 一、实验学时: 2学时 二、实验目的 掌握卷积神经网络CNN的基本结构;掌握数据预处理、模型构建、训练与调参;探索CNN在MNIST数据集中的性能表现; 三、实验内容 实现深度神经网络CNN。 四、主要实验步…

SpringBoot高校宿舍信息管理系统小程序

概述 基于SpringBoot的高校宿舍信息管理系统小程序项目,这是一款非常适合高校使用的信息化管理工具。该系统包含了完整的宿舍管理功能模块,采用主流技术栈开发,代码结构清晰,非常适合学习和二次开发。 主要内容 这个宿舍管理系…

Redis 难懂命令-- ZINTERSTORE

**背景:**学习的过程中 常用的redis命令都能快速通过官方文档理解 但是还是有一些比较难懂的命令 **目的:**写博客记录一下(当然也可以使用AI搜索) 在Redis中,ZINTERSTORE 是一个用于计算多个有序集合(So…

React 路由管理与动态路由配置实战

React 路由管理与动态路由配置实战 前言 在现代单页应用(SPA)开发中,路由管理已经成为前端架构的核心部分。随着React应用规模的扩大,静态路由配置往往难以满足复杂业务场景的需求,尤其是当应用需要处理权限控制、动态菜单和按需加载等高级…

【学习笔记】深度学习-梯度概念

一、定义 梯度向量不仅表示函数变化的速度,还表示函数增长最快的方向 二、【问】为什么说它表示方向? 三、【问】那在深度学习梯度下降的时候,还要判断梯度是正是负来更新参数吗? 假设某个参数是 w,损失函数对它的…

题海拾贝:P8598 [蓝桥杯 2013 省 AB] 错误票据

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》 欢迎点赞&#xff0c;关注&#xff01; 1、题…

webpack的安装及其后序部分

npm install原理 这个其实就是npm从registry下载项目到本地&#xff0c;没有什么好说的 值得一提的是npm的缓存机制&#xff0c;如果多个项目都需要同一个版本的axios&#xff0c;每一次重新从registry中拉取的成本过大&#xff0c;所以会有缓存&#xff0c;如果缓存里有这个…

百度golang研发一面面经

输入一个网址&#xff0c;到显示界面&#xff0c;中间的过程是怎样的 IP 报文段的结构是什么 Innodb 的底层结构 知道几种设计模式 工厂模式 简单工厂模式&#xff1a;根据传入类型参数判断创建哪种类型对象工厂方法模式&#xff1a;由子类决定实例化哪个类抽象工厂模式&#…

使用 HTML + JavaScript 实现图片裁剪上传功能

本文将详细介绍一个基于 HTML 和 JavaScript 实现的图片裁剪上传功能。该功能支持文件选择、拖放上传、图片预览、区域选择、裁剪操作以及图片下载等功能&#xff0c;适用于需要进行图片处理的 Web 应用场景。 效果演示 项目概述 本项目主要包含以下核心功能&#xff1a; 文…

GO+RabbitMQ+Gin+Gorm+docker 部署 demo

更多个人笔记见&#xff1a; &#xff08;注意点击“继续”&#xff0c;而不是“发现新项目”&#xff09; github个人笔记仓库 https://github.com/ZHLOVEYY/IT_note gitee 个人笔记仓库 https://gitee.com/harryhack/it_note 个人学习&#xff0c;学习过程中还会不断补充&…