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

article/2025/8/10 8:50:08

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 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.hkcw.cn/article/YwKUIPUmOM.shtml

相关文章

突破DIFY沙箱限制,高效处理大文件

DIFY获取传入文件路径并处理文件内容 由于dify代码沙箱自身的安全限制,用户在沙箱环境下的代码无法实现对系统文件的写入和读取操作。如果想利用dify来处理文件数据,就不得不使用官方提供的文档提取器插件,但是使用该插件提取如.xlsx,.csv等…

比较云计算的四种部署模式:哪个是最佳选择?

在数字化转型浪潮中,企业面临的关键决策之一是如何选择云计算部署模式。公有云、私有云、社区云和混合云并非简单的技术选项,而是关乎业务架构的战略选择。每种模式都代表着不同的资源控制程度、成本结构和安全边界,理解其本质差异是制定有效…

云计算Linux Rocky day02(安装Linux系统、设备表示方式、Linux基本操作)

云计算Linux Rocky day02(安装Linux系统、设备表示方式、Linux基本操作) 目录 云计算Linux Rocky day02(安装Linux系统、设备表示方式、Linux基本操作)1、虚拟机VMware安装Rocky2、Linux命令行3、Linux Rocky修改字体大小和背景颜…

项目管理工具Maven

Maven的概念 什么是Maven 什么是依赖管理 对第三方依赖包的管理,可以连接互联网下载项目所需第三方jar包。 对自己开发的模块的管理,可以像引用第三方依赖包一样引用自己项目的依赖包。 什么是项目构建 一、项目构建的定义 项目构建是将源代码经过编…

使用原生前端技术封装一个组件

封装导航栏 navbar-template.html <header><nav><ul><li><a href"index.html"><i class"fas fa-home"></i> 主页</a></li><li><a href"#"><i class"fas fa-theate…

mac mini m4命令行管理员密码设置

附上系统版本图 初次使用命令行管理员&#xff0c;让输入密码&#xff0c;无论是输入登录密码还是账号密码&#xff0c;都是错的&#xff0c;百思不得其解&#xff0c;去网上搜说就是登录密码啊 直到后来看到了苹果官方的文档 https://support.apple.com/zh-cn/102367 https…

使用Vditor将Markdown文档渲染成网页(Vite+JS+Vditor)

1. 引言 编写Markdown文档现在可以说是程序员的必备技能了&#xff0c;因为Markdown很好地实现了内容与排版分离&#xff0c;可以让程序员更专注于内容的创作。现在很多技术文档&#xff0c;博客发布甚至AI文字输出的内容都是以Markdown格式的形式输出的。那么&#xff0c;Mar…

黑马k8s(十七)

一&#xff1a;高级存储 1.高级存储-pv和pvc介绍 2.高级存储-pv 3.高级存储-pvc 最后一个改成5gi pvc3是没有来绑定成功的 pv3没有绑定 删除pod、和pvc&#xff0c;观察状态&#xff1a; 4.高级存储-pc和pvc的生命周期 二&#xff1a;配置存储 1.配置存储-ConfigMap 2.配…

【ABAP 基本数据类型】

ABAP 基本数据类型 一、数值类型 1.1 整数类型 类型关键字长度值范围示例代码标准整型I4字节-2,147,483,648 到 2,147,483,647DATA lv_int TYPE i VALUE 100.短整型INT22字节-32,768 到 32,767DATA lv_short TYPE int2 VALUE -500.无符号整型INT11字节0 到 255DATA lv_flag T…

LearnOpenGL-笔记-其十一

Normal Mapping 又到了介绍法线贴图的地方&#xff0c;我感觉我已经写了很多遍了... 法线贴图用最简单的话来介绍的话&#xff0c;就是通过修改贴图对应物体表面的法线来修改光照效果&#xff0c;从而在不修改物体实际几何形状的前提下实现不同于物体几何形状的视觉效果。 因…

Scratch节日 | 粽子收集

端午节怎么过&#xff1f;当然是收粽子啦&#xff01;这款 粽子收集 小游戏&#xff0c;让你一秒沉浸节日氛围&#xff0c;轻松收集粽子&#xff0c;收获满满快乐&#xff01; &#x1f3ae; 玩法介绍f 开始游戏&#xff1a;点击开始按钮&#xff0c;游戏正式开始&#xff01;…

Linux(9)——进程(控制篇——下)

三、进程等待 1&#xff09;进程等待的必要性 之前提过子进程退出&#xff0c;父进程如果不读取子进程的退出信息&#xff0c;就可能造成“僵尸进程”的问题&#xff0c;从而造成内存泄漏的问题。再者&#xff0c;一旦子进程进入了僵尸状态&#xff0c;那就连kill -9都杀不亖…

Scratch节日 | 六一儿童节

六一儿童节到啦&#xff01;快来体验这款超简单又超好玩的 六一儿童节 小游戏吧&#xff01;只需要一只鼠标&#xff0c;就能尽情释放你的创意&#xff0c;绘出属于你自己的缤纷世界&#xff01; &#x1f3ae; 玩法介绍 鼠标滑动&#xff1a;在屏幕上随意滑动鼠标&#xff0c…

高原户外制氧机的优势特点有哪些

一、便携设计&#xff0c;突破环境限制 高原户外制氧机以轻量化、紧凑化设计为核心&#xff0c;重量普遍控制在3kg以内&#xff0c;可轻松装入背包或车载空间。采用折叠式吸氧管与一体化电源设计&#xff0c;支持太阳能充电板或车载供电&#xff0c;满足徒步、骑行、自驾等多元…

harmonyos实战关于静态图片存放以及image图片引入

编写demo轮播图的时候&#xff0c;用到图片&#xff0c;想到静态资源具体存放在哪里&#xff1f;&#xff1f; 官网工程目录结构解释&#xff1a;文档中心 Resource资源 使用资源格式可以跨包/跨模块引入图片&#xff0c;resources文件夹下的图片都可以通过$r资源接口读取到并…

LINUX530 rsync定时同步 环境配置

rsync定时代码同步 环境配置 关闭防火墙 selinux systemctl stop firewalld systemctl disable firewalld setenforce 0 vim /etc/selinux/config SELINUXdisable设置主机名 hostnamectl set-hostname code hostnamectl set-hostname backup设置静态地址 cd /etc/sysconfi…

超低延迟与高稳定性的行业领先直播解决方案

随着实时音视频技术的不断发展&#xff0c;直播行业的需求逐渐从单一的流媒体传输向更高效、更稳定、更加智能化的方向转变。作为一家创新型技术公司&#xff0c;上海视沃信息科技有限公司凭借其自研的大牛直播SDK&#xff0c;为各行业提供了一款极致体验的、低延迟、高稳定的音…

华为云Flexus+DeepSeek征文 | 基于Dify和DeepSeek-R1开发企业级AI Agent全流程指南

作者简介 我是摘星&#xff0c;一名专注于云计算和AI技术的开发者。本次通过华为云MaaS平台体验DeepSeek系列模型&#xff0c;将实际使用经验分享给大家&#xff0c;希望能帮助开发者快速掌握华为云AI服务的核心能力。 目录 1. 前言 2. 环境准备 2.1 华为云资源准备 2.1 实…

quasar electron mode如何打包无边框桌面应用程序

预览 开源项目Tokei Kun 一款简洁的周年纪念app&#xff0c;现已发布APK&#xff08;安卓&#xff09;和 EXE&#xff08;Windows&#xff09; 项目仓库地址&#xff1a;Github Repo 应用下载链接&#xff1a;Github Releases Preparation for Electron quasar dev -m elect…

Linux 中应用层自定义协议与序列化 -- 自定义协议概述,序列化和反序列化,Jsoncpp

目录 1. 应用层自定义协议概述 2. 序列化和反序列化 3. 重新理解 read, write, recv, send 以及 TCP 为什么支持全双工 4.Jsoncpp 4.1 Jsoncpp 的特性 4.2 使用 Jsoncpp 的序列化和反序列化 4.3 Json::Value 的介绍 4.3.1 构造函数 4.3.2 访问元素 4.3.3 类型检查 …