ClickHouse内核分析-MergeTree的Merge和Mutation机制

5_6_3

注:以下分析基于开源 v19.15.2.2-stable 版本进行

引言

ClickHouse内核分析系列文章,继上一篇文章 MergeTree查询链路 之后,这次我将为大家介绍MergeTree存储引擎的异步Merge和Mutation机制。建议读者先补充上一篇文章的基础知识,这样会比较容易理解。

MergeTree Mutation功能介绍

在上一篇系列文章中,我已经介绍过ClickHouse内核中的MergeTree存储一旦生成一个Data Part,这个Data Part就不可再更改了。所以从MergeTree存储内核层面,ClickHouse就不擅长做数据更新删除操作。但是绝大部分用户场景中,难免会出现需要手动订正、修复数据的场景。所以ClickHouse为用户设计了一套离线异步机制来支持低频的Mutation(改、删)操作。

Mutation命令执行

ALTER TABLE [db.]table DELETE WHERE filter_expr;
ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr;

ClickHouse的方言把Delete和Update操作也加入到了Alter Table的范畴中,它并不支持裸的Delete或者Update操作。当用户执行一个如上的Mutation操作获得返回时,ClickHouse内核其实只做了两件事情:

  1. 检查Mutation操作是否合法;
  2. 保存Mutation命令到存储文件中,唤醒一个异步处理merge和mutation的工作线程;

两者的主体逻辑分别在MutationsInterpreter::validate函数和StorageMergeTree::mutate函数中。

MutationsInterpreter::validate函数dry run一个异步Mutation执行的全过程,其中涉及到检查Mutation是否合法的判断原则是列值更新后记录的分区键和排序键不能有变化。因为分区键和排序键一旦发生变化,就会导致多个Data Part之间之间Merge逻辑的复杂化。剩余的Mutation执行过程可以看做是打开一个Data Part的BlockInputStream,在这个BlockStream的基础上封装删除操作的FilterBlockInputStream,再加上更新操作的ExpressionBlockInputStream,最后把数据通过BlockOutputStream写回到新的Data Part中。这里简单介绍一下ClickHouse的计算层实现,整体上它是一个火山模型的计算引擎,数据的各种filer、投影、join、agg都是通过BlockStrem抽象实现,在BlockStream中数据是按照Block进行传输处理的,而Block中的数据又是按照列模式组织,这使得ClickHouse在单列的计算上可以批量化并使用一些SIMD指令加速。BlockOutputStream承担了MergeTree Data Part列存写入和索引构建的全部工作,我会在后续的文章中会详细展开介绍ClickHouse计算层中各类功能的BlockStream,以及BlockOutputStream中构建索引的实现细节。

在Mutation命令的执行过程中,我们可以看到MergeTree会把整条Alter命令保存到存储文件夹下,然后创建一个MergeTreeMutationEntry对象保存到表的待修改状态中,最后唤醒一个异步处理merge和 mutation的工作线程。这里有一个关键的问题,因为Mutation的实际操作是异步发生的,在用户的Alter命令返回之后仍然会有数据写入,系统如何在异步订正的过程中排除掉Alter命令之后写入的数据呢?下一节中我会介绍MergeTree中Data Part的Version机制,它可以在Data Part级别解决上面的问题。但是因为ClickHouse写入链路的异步性,ClickHouse仍然无法保证Alter命令前Insert的每条纪录都被更新,只能确保Alter命令前已经存在的Data Part都会被订正,推荐用户只用来订正T+1场景的离线数据。

异步Merge&Mutation

Batch Insert和Mutation的数据一致性

struct MergeTreePartInfo
{String partition_id;Int64 min_block = 0;Int64 max_block = 0;UInt32 level = 0;Int64 mutation = 0;   /// If the part has been mutated or contains mutated parts, is equal to mutation version number..../// Get block number that can be used to determine which mutations we still need to apply to this part/// (all mutations with version greater than this block number).Int64 getDataVersion() const { return mutation ? mutation : min_block; }...    bool operator<(const MergeTreePartInfo & rhs) const{return std::forward_as_tuple(partition_id, min_block, max_block, level, mutation)< std::forward_as_tuple(rhs.partition_id, rhs.min_block, rhs.max_block, rhs.level, rhs.mutation);}
}

在具体展开MergeTree的异步merge和mutation机制之前,先需要详细介绍一下MergeTree中对Data Part的管理方式。每个Data Part都有一个MergeTreePartInfo对象来保存它的meta信息,MergeTreePartInfo类的结构如上方代码所示。

  1. partition_id:表示所属的数据分区id。
  2. min_block、max_block:blockNumber是数据写入的一个版本信息,在上一篇系列文章中讲过,用户每次批量写入的数据都会生成一个Data Part。同一批写入的数据会被assign一个唯一的blockNumber,而这个blockNumber是在MergeTree表级别自增的。以及MergeTree在merge多个Data Part的时候会准守一个原则:在同一个数据分区下选择blockNumber区间相邻的若干个Data Parts进行合并,不会出现在同一个数据分区下Data Parts之间的blockNumber区间出现重合。所以Data Part中的min_block和max_block可以表示当前Data Part中数据的版本范围。
  3. level:表示Data Part所在的层级,新写入的Data Part都属于level 0。异步merge多个Data Part的过程中,系统会选择其中最大的level + 1作为新Data Part的level。这个信息可以一定程度反映出当前的Data Part是经历了多少次merge,但是不能准确表示,核心原因是MergeTree允许多个Data Part跨level进行merge的,为了最终一个数据分区内的数据merge成一个Data Part。
  4. mutation:和批量写入数据的版本号机制类似,MergeTree表的mutation命令也会被assign一个唯一的blockNumber作为版本号,这个版本号信息会保存在MergeTreeMutationEntry中,所以通过版本号信息我们可以看出数据写入和mutation命令之间的先后关系。Data Part中的这个mutation表示的则是当前这个Data Part已经完成的mutation操作,对每个Data Part来说它是按照mutation的blockNumber顺序依次完成所有的mutation。

解释了MergeTreePartInfo类中的信息含义,我们就可以理解上一节中遗留的异步Mutation如何选择哪些Data Parts需要订正的问题。系统可以通过MergeTreePartInfo::getDataVersion() { return mutation ? mutation : min_block }函数来判断当前Data Part是否需要进行某个mutation订正,比较两者version即可。

Merge&Mutation工作任务

ClickHouse内核中异步merge、mutation工作由统一的工作线程池来完成,这个线程池的大小用户可以通过参数background_pool_size进行设置。线程池中的线程Task总体逻辑如下,可以看出这个异步Task主要做三块工作:清理残留文件,merge Data Parts 和 mutate Data Part。

BackgroundProcessingPoolTaskResult StorageMergeTree::mergeMutateTask()
{....try{/// Clear old parts. It is unnecessary to do it more than once a second.if (auto lock = time_after_previous_cleanup.compareAndRestartDeferred(1)){{/// TODO: Implement tryLockStructureForShare.auto lock_structure = lockStructureForShare(false, "");clearOldPartsFromFilesystem();clearOldTemporaryDirectories();}clearOldMutations();}///TODO: read deduplicate option from table configif (merge(false /*aggressive*/, {} /*partition_id*/, false /*final*/, false /*deduplicate*/))return BackgroundProcessingPoolTaskResult::SUCCESS;if (tryMutatePart())return BackgroundProcessingPoolTaskResult::SUCCESS;return BackgroundProcessingPoolTaskResult::ERROR;}...
}

需要清理的残留文件分为三部分:过期的Data Part,临时文件夹,过期的Mutation命令文件。如下方代码所示,MergeTree Data Part的生命周期包含多个阶段,创建一个Data Part的时候分两阶段执行Temporary->Precommitted->Commited,淘汰一个Data Part的时候也可能会先经过一个Outdated状态,再到Deleting状态。在Outdated状态下的Data Part仍然是可查的。异步Task在收集Outdated Data Part的时候会根据它的shared_ptr计数来判断当前是否有查询Context引用它,没有的话才进行删除。清理临时文件的逻辑较为简单,在数据文件夹中遍历搜索"tmp_"开头的文件夹,并判断创建时长是否超过temporary_directories_lifetime。临时文件夹主要在ClickHouse的两阶段提交过程可能造成残留。最后是清理数据已经全部订正完成的过期Mutation命令文件。

enum class State{Temporary,       /// the part is generating now, it is not in data_parts listPreCommitted,    /// the part is in data_parts, but not used for SELECTsCommitted,       /// active data part, used by current and upcoming SELECTsOutdated,        /// not active data part, but could be used by only current SELECTs, could be deleted after SELECTs finishesDeleting,        /// not active data part with identity refcounter, it is deleting right now by a cleanerDeleteOnDestroy, /// part was moved to another disk and should be deleted in own destructor};

Merge逻辑

StorageMergeTree::merge函数是MergeTree异步Merge的核心逻辑,Data Part Merge的工作除了通过后台工作线程自动完成,用户还可以通过Optimize命令来手动触发。自动触发的场景中,系统会根据后台空闲线程的数据来启发式地决定本次Merge最大可以处理的数据量大小,max_bytes_to_merge_at_min_space_in_pool和max_bytes_to_merge_at_max_space_in_pool参数分别决定当空闲线程数最大时可处理的数据量上限以及只剩下一个空闲线程时可处理的数据量上限。当用户的写入量非常大的时候,应该适当调整工作线程池的大小和这两个参数。当用户手动触发merge时,系统则是根据disk剩余容量来决定可处理的最大数据量。

接下来介绍merge过程中最核心的逻辑:如何选择Data Parts进行merge?为了方便理解,这里先介绍一下Data Parts在MergeTree表引擎中的管理组织方式。上一节中提到的MergeTreePartInfo类中定义了比较操作符,MergeTree中的Data Parts就是按照这个比较操作符进行排序管理,排序键是(partition_id, min_block, max_block, level, mutation),索引管理结构如下图所示:
image.png
自动Merge的处理逻辑,首先是通过MergeTreeDataMergerMutator::selectPartsToMerge函数筛选出本次merge要合并的Data Parts,这个筛选过程需要准守三个原则:

  1. 跨数据分区的Data Part之间不能合并;
  2. 合并的Data Parts之间必须是相邻(在上图的有序组织关系中相邻),只能在排序链表中按段合并,不能跳跃;
  3. 合并的Data Parts之间的mutation状态必须是一致的,如果Data Part A 后续还需要完成mutation-23而Data Part B后续不需要完成mutation-23(数据全部是在mutation命令之后写入或者已经完成mutation-23),则A和B不能进行合并;

所以我们上面的Data Parts组织关系逻辑示意图中,相同颜色的Data Parts是可以合并的。虽然图中三个不同颜色的Data Parts序列都是可以合并的,但是合并工作线程每次只会挑选其中某个序列的一小段进行合并(如前文所述,系统会限定每次合并的Data Parts的数据量)。对于如何从这些序列中挑选出最佳的一段区间,ClickHouse抽象出了IMergeSelector类来实现不同的逻辑。当前主要有两种不同的merge策略:TTL数据淘汰策略和常规策略。

  • TTL数据淘汰策略:TTL数据淘汰策略启用的条件比较苛刻,只有当某个Data Part中存在数据生命周期超时需要淘汰,并且距离上次使用TTL策略达到一定时间间隔(默认1小时)。TTL策略也非常简单,首先挑选出TTL超时最严重Data Part,把这个Data Part所在的数据分区作为要进行数据合并的分区,最后会把这个TTL超时最严重的Data Part前后连续的所有存在TTL过期的Data Part都纳入到merge的范围中。这个策略简单直接,每次保证优先合并掉最老的存在过期数据的Data Part。
  • 常规策略:这里的选举策略就比较复杂,基本逻辑是枚举每个可能合并的Data Parts区间,通过启发式规则判断是否满足合并条件,再有启发式规则进行算分,选取分数最好的区间。启发式判断是否满足合并条件的算法在SimpleMergeSelector.cpp::allow函数中,其中的主要思想分为以下几点:系统默认对合并的区间有一个Data Parts数量的限制要求(每5个Data Parts才能合并);如果当前数据分区中的Data Parts出现了膨胀,则适量放宽合并数量限制要求(最低可以两两merge);如果参与合并的Data Parts中有很久之前写入的Data Part,也适量放宽合并数量限制要求,放宽的程度还取决于要合并的数据量。第一条规则是为了提升写入性能,避免在高速写入时两两merge这种低效的合并方式。最后一条规则则是为了保证随着数据分区中的Data Part老化,老龄化的数据分区内数据全部合并到一个Data Part。中间的规则更多是一种保护手段,防止因为写入和频繁mutation的极端情况下,Data Parts出现膨胀。启发式算法的策略则是优先选择IO开销最小的Data Parts区间完成合并,尽快合并掉小数据量的Data Parts是对在线查询最有利的方式,数据量很大的Data Parts已经有了很较好的数据压缩和索引效率,合并操作对查询带来的性价比较低。

Mutation逻辑

StorageMergeTree::tryMutatePart函数是MergeTree异步mutation的核心逻辑,主体逻辑如下。系统每次都只会订正一个Data Part,但是会聚合多个mutation任务批量完成,这点实现非常的棒。因为在用户真实业务场景中一次数据订正逻辑中可能会包含多个Mutation命令,把这多个mutation操作聚合到一起订正效率上就非常高。系统每次选择一个排序键最小的并且需要订正Data Part进行操作,本意上就是把数据从前往后进行依次订正。

Mutation功能是MergeTree表引擎最新推出一大功能,从我个人的角度看在实现完备度上还有一下两点需要去优化:

  1. mutation没有实时可见能力。我这里的实时可见并不是指在存储上立即原地更新,而是给用户提供一种途径可以立即看到数据订正后的最终视图确保订正无误。类比在使用CollapsingMergeTree、SummingMergeTree等高级MergeTree引擎时,数据还没有完全merge到一个Data Part之前,存储层并没有一个数据的最终视图。但是用户可以通过Final查询模式,在计算引擎层实时聚合出数据的最终视图。这个原理对mutation实时可见也同样适用,在实时查询中通过FilterBlockInputStream和ExpressionBlockInputStream完成用户的mutation操作,给用户提供一个最终视图。
  2. mutation和merge相互独立执行。看完本文前面的分析,大家应该也注意到了目前Data Part的merge和mutation是相互独立执行的,Data Part在同一时刻只能是在merge或者mutation操作中。对于MergeTree这种存储彻底Immutable的设计,数据频繁merge、mutation会引入巨大的IO负载。实时上merge和mutation操作是可以合并到一起去考虑的,这样可以省去数据一次读写盘的开销。对数据写入压力很大又有频繁mutation的场景,会有很大帮助。
for (const auto & part : getDataPartsVector()){...size_t current_ast_elements = 0;for (auto it = mutations_begin_it; it != mutations_end_it; ++it){MutationsInterpreter interpreter(shared_from_this(), it->second.commands, global_context);size_t commands_size = interpreter.evaluateCommandsSize();if (current_ast_elements + commands_size >= max_ast_elements)break;current_ast_elements += commands_size;commands.insert(commands.end(), it->second.commands.begin(), it->second.commands.end());}auto new_part_info = part->info;new_part_info.mutation = current_mutations_by_version.rbegin()->first;future_part.parts.push_back(part);future_part.part_info = new_part_info;future_part.name = part->getNewName(new_part_info);tagger.emplace(future_part, MergeTreeDataMergerMutator::estimateNeededDiskSpace({part}), *this, true);break;}

最后在经过后台工作线程一轮merge和mutation操作之后,上一节中展示的MergeTree表引擎中的Data Parts可能发生的变化如下图所示,2020-05-10数据分区下的头两个Data Parts被merge到了一起,并且完成了Mutation 37和Mutation 39的数据订正,新产生的Data Part如红色所示:
image.png
Clickhouse产品链接:https://www.aliyun.com/product/clickhouse
223.png

ClickHouse内核分析系列文章:

MergeTree查询链路
希望通过内核分析系列文章,让大家更好地了解这款世界领先的列式存储分析型数据库。
图片.gif

原文链接
本文为云栖社区原创内容,未经允许不得转载。

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

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

相关文章

el-table中奇偶行背景色显示不同的颜色

默认样式 深色主题 border ref"singleTable" highlight-current-row current-change"handleCurrentChange" :row-class-name"tableRowClassName" :header-cell-style"{background:#004d8c,color:#FFFFFF}"事件方法 //奇偶行背景色不…

阿里云专属数据库,重新定义云数据库新形态

阿里云数据库专属集群专属链接 云专属数据库&#xff0c;重新定义云数据库新形态 数据库是一个有着超过40年历史的悠久行业&#xff0c;前期一直被传统的如Oracle等少数几家厂商把持。云计算的先行者AWS在2009年率先推出RDS服务&#xff08;Relational Database Service &…

软考零散知识点

网络命令 多态 强制多态&#xff1a;数字类型运算的自动拆装箱 过载多态&#xff1a;子类重写父类的方法 参数多态&#xff1a;方法的重载 包含多态&#xff1a;父类的引用指向子类的对象 主存和cache映射 RAID RAID RAID0&#xff1a;无冗余备份&#xff0c;带化。每条数据…

ServiceMesh最火项目:Istio架构解析

Istio 是一个开源的服务网格&#xff0c;可为分布式微服务架构提供所需的基础运行和管理要素。随着各组织越来越多地采用云平台&#xff0c;开发者必须使用微服务设计架构以实现可移植性&#xff0c;而运维人员必须管理包含混合云部署和多云部署的大型分布式应用。Istio 采用一…

docker-compose 实战案例

文章目录一、Compose入门案例1. 依赖2. 实体类3. mapper接口4. 启动类5. yml配置6. 测试案例7. 打包二、制作 DockerFile和docker-compose.yml2.1. 制作 DockerFile2.2. docker-compose.yml三、打包部署3.1. 资料上传3.2. 启动docker-compose3.3. 创建表3.4. 接口测试3.5. 数据…

F5打造“感知可控,随需而变的应用”  助力企业实现非凡数字体验

2020年12月16日&#xff0c;F5举办线上发布会&#xff0c;介绍其全新理念—“感知可控&#xff0c;随需而变的应用”(Adaptive Applications)&#xff0c;以及相应的创新性整体解决方案。在当前数字化转型加速的背景下&#xff0c;F5致力于为企业打造感知可控、随需应变的应用&…

软考 - 排序算法

文章目录1.总览1.待操作数组2.直接插入排序&#xff08;O(n2)&#xff09;3.希尔排序4.直接选择排序5.堆排序5.1.堆的分类5.2.原理&#xff1a;5.3. 堆排序方法&#xff1a;6.冒泡排序7.快速排序8.归并排序9.基数排序1.总览 1.待操作数组 private static int[] ori {30, 70, …

“数据湖”:概念、特征、架构与案例

写在前面&#xff1a; 最近&#xff0c;数据湖的概念非常热&#xff0c;许多前线的同学都在讨论数据湖应该怎么建&#xff1f;阿里云有没有成熟的数据湖解决方案&#xff1f;阿里云的数据湖解决方案到底有没有实际落地的案例&#xff1f;怎么理解数据湖&#xff1f;数据湖和大数…

DockerFile 入门到精通

文章目录一、DockerFile快速入门1. DockerFile 解析2. DockerFile编写规范3. DockerFile指令二、构建自己centos镜像2.1. 制作Dockerfile2.2. 构建镜像2.3. 运行容器一、DockerFile快速入门 1. DockerFile 解析 一个镜像文件到底是如何创建&#xff1f; dockerfile 描述出镜…

案例解析|广东自由流收费稽核方案,AI稽核新模式

随着取消省界收费站工程落成&#xff0c;我国逐步迈进全国高速公路“一张网”运行感知新时代。借助交通强国和“撤站”政策&#xff0c;2019年12月&#xff0c;广东联合电服和阿里云共同宣布&#xff0c;全国首个高速不停车收费AI稽核项目正式落地广东&#xff0c;在业内率先使…

赠书 | 读懂 x86 架构 CPU 虚拟化,看这文就够了

作者 | 王柏生、谢广军导读&#xff1a;本文摘自于王柏生、谢广军撰写的《深度探索Linux系统虚拟化&#xff1a;原理与实现》一书&#xff0c;介绍了CPU虚拟化的基本概念&#xff0c;探讨了x86架构在虚拟化时面临的障碍&#xff0c;以及为支持CPU虚拟化&#xff0c;Intel在硬件…

nacos 持久化 mysql(windows/linux环境)

文章目录1. 下载nacos-server2. 修改配置3. 创建数据库4. 初始化数据库脚本5. 启动nacos1. 下载nacos-server nacos-server-1.4.3.zip https://github.91chi.fun//https://github.com//alibaba/nacos/releases/download/1.4.3/nacos-server-1.4.3.zip解压 略 2. 修改配置 c…

HSF服务注册失败,项目启动后,EDAS列表无法发现注册的服务

背景&#xff1a; 本地使用edas轻量配置中心进行开发联调。 异常现象&#xff1a; 1.redis和edas已经启动正常&#xff0c;本地http://localhost:8080可以打开&#xff0c;但是配置列表和服务列表为空。 2.项目可以正常启动&#xff0c;但是postman调用时&#xff0c;发生hs…

解密阿里云高效病原体基因检测工具

1.背景介绍 病原体基因检测&#xff0c;为各种严重感染的诊断提供了基础。病原体检测流程分成五个步骤&#xff1a;&#xff08;1&#xff09;采集病人的样本&#xff0c;比方说静脉血&#xff0c;痰液&#xff0c;肺泡灌洗液&#xff0c;或者脑脊髓液等。&#xff08;2&#…

企业使用云计算低效益怎么办?区块链或成良药

作者 | Ged Alexander翻译 | 火火酱,责编 | 晋兆雨出品 | CSDN云计算头图 | 付费下载于视觉中国在云资源浪费现象激增的大环境中&#xff0c;企业如何才能寻得一线生机&#xff1f;借助云计算服务&#xff0c;企业和开发人员能够通过互联网远程组织资源并运行工作负载。全球范围…

基于X-Engine引擎的实时历史数据库解决方案揭秘

实时历史库需求背景 在当今的数字化时代&#xff0c;随着业务的迅速发展&#xff0c;每天产生的数据量会是一个惊人的数量&#xff0c;数据库存储的成本将会越来越大&#xff0c;通常的做法是对历史数据做归档&#xff0c;即将长期不使用的数据迁移至以文件形式存储的廉价存储…

seata 整合 nacos(windows/linux环境)

文章目录一、下载安装nacos-server二、 seata-server下载配置2.1. 下载seata-server-1.4.2.zip2.2. 修改配置2.3. 创建命名空间2.4. 配置registry.conf2.5. 创建数据库2.6. 初始化sql脚本2.7. 配置config.txt2.8. 创建nacos-config.sh2.9. 初始化脚本数据到nacos2.10. 数据验证…

加密相关(对称加密、非对称加密、信息摘要、数字签名、CA数字证书)

1.对称加密&#xff1a; 说明&#xff1a;加密的密钥和解密的密钥相同&#xff1b;效率快&#xff1b;适合加密大信息量 常见算法&#xff1a;DES、3DES、AES、RC-5 块加密&#xff1a; 流加密&#xff1a;数据量大时效率高 2.非对称加密&#xff1a; 说明&#xff1a;加密的…

ClickHouse内核分析-MergeTree的存储结构和查询加速

注&#xff1a;以下分析基于开源 v19.15.2.2-stable 版本进行 引言 ClickHouse是最近比较火的一款开源列式存储分析型数据库&#xff0c;它最核心的特点就是极致存储压缩率和查询性能&#xff0c;本人最近正在学习ClickHouse这款产品中。从我个人的视角来看存储是决定一款数据…

“智汇光大 E启未来” 中国光大集团ESBU协同核心系统1.0正式发布

12月22日&#xff0c;“中国光大集团ESBU协同核心系统1.0”正式发布&#xff0c;标志着光大集团战略和光大数字化发展取得又一重大进展。光大集团党委书记、董事长李晓鹏现场发布“E-SBU协同核心系统”及“光大云生活”超级APP。集团党委副书记、副董事长、总经理吴利军在发布会…