了解到,分布式DDL功能对Zookeeper的依赖情况还是比较轻量级的,接下来介绍的ReplicatedMergeTree表引擎对Zookeeper的依赖可以说是所有表操作全方面的依赖,真实集群中大量的ReplicatedMergeTree表会对Zookeeper造成非常大的请求压力,需要用户关注Zookeeper的运维。
- ReplicatedMergeTree表引擎实现的主备同步和传统主备同步有很大的差异:1)它不是一个(抢主,主节点执写入更新,备节点同步follow)的模型,ClickHouse的主节点和备节点都可以写,同步是双向的;2)它不是物理同步,ClickHouse没有基于物理文件的WAL;3)它的逻辑同步日志粒度是MergeTree的Data Part级别的(没有单条记录的同步日志),包含Data Part的增、删、改。ReplicatedMergeTree表的Data Part Log主要包含以下几类:
- 1)降低代码逻辑复杂度,MergeTree表引擎有两类后台异步任务(Merge/Mutation),同时又有所有节点可写的设定,这两个逻辑融合到一起的话复杂度会爆炸,ClickHouse的内核实现中是把写入和异步动作的链路完全解耦开的。主节点负责分发各种异步任务到Zookeeper上的任务队列,Shard下的所有节点观察任务队列进行follow执行。当万一某个其他节点上的数据和主节点不一致无法完成某个异步任务时,还有保底方法是让它直接从主节点去下载完成merge / mutation的Data Part。
- 2)MergeTree结构的表引擎有众多的变种merge逻辑(ReplacingMergeTree、CollapsingMergeTree等),再加上异步mutation的机制,多副本之间独立merge / mutation的话,副本间的数据视图同步进度就会完全失控(用户可能需要停写很长时间再加上手动Optimize才能达到副本间一致)。
MergeTree表对Data Part的管理方式,要实现基于Data Part Log的同步,首先要确保节点之间的Data Part有统一的命名体系,而决定Data Part命名的核心因素是每一批数据写入时被赋予的blockNumber(数据的写入版本号),ClickHouse的写入链路利用了Zookeeper来生成全局一致的blockNumber序列。其次为了数据一致性保证,ClickHouse把ReplacingMergeTree表引擎中的所有Data Parts都注册到了Zookeeper上(包括它们的列信息和checksum),最终本地数据都要以Zookeeper上的状态为准。
ReplicatedMergeTree同步写入
ReplicatedMergeTree的写入链路分为三步:
- 1)把数据写入到本地的临时Data Part中
- 2)从Zookeeper上申请自增的blockNumber序列号
- 3)commit临时Data Part,生成一条"GET_PART"的同步日志上传到Zookeeper任务队列中。和分布式DDL执行任务队列不同,每一个ReplicatedMergeTree表引擎在Zookeeper上都有一个独立的Znode,这个Znode的路径可以在建表示配置。
- 4)一次常规的ReplicatedMergeTree批量写入首先会把写入的数据按照数据分区进行拆分,然后依次处理拆分后的每个数据Block。把数据Block写入到存储的临时Data Part后,ClickHouse需要从Zookeeper中获取下一个全局的blockNumber,这部分逻辑主要在StorageReplicatedMergeTree::allocateBlockNumber函数中,核心是调用Zookeeper生成一个Ephemeral Sequential Znode来获取全局唯一的序列(这里的"全局"是单个数据分区级别唯一,跨数据分区是可以重合的)。最后是commit这个Data Part,commit的过程需要完成一系列的"检查动作"最后上传一个"GET_PART"类型的Log到Shard对应的Zookeeper目录下的的log Znode下,其他副本通过观察Zookeeper会异步来拷贝写入的Data Part。前序的"检查动作"目前包括,检查本地表的meta(列信息)版本是否已经落后于Zookeeper上的状态,注册写入Data Part的columns信息和checksums到Data Part的Znode下。从这里可以看出一次Batch写入的过程和Zookeeper交互的次数不下10次,要是Batch数据跨10个数据分区的话那就是100次。老话重提一下:使用ClickHouse时一定要做Batch写入并且按照数据分区提前聚合。
ReplicatedMergeTree的写入链路有关的还有几个开关值得注意:
- use_minimalistic_part_header_in_zookeeper,这是一个降低Zookeeper压力的配置(默认关闭)。开启之后每个新写入的Data Part不再注册自己的columns信息和checksums到Zookeeper上。而是压缩成Hash值写到Data Part的Znode data中。
- insert_quorum,这个开关会强迫写入链路检查数据同步的副本数达到要求才能成功返回(默认是0)。开启之后写入节点在Commit Data Part时还会创建一个Shard级别的quorum/status Znode,其他节点同步完数据之后需要更新到quorum/status,写入节点这边通过Watch机制收到通知再返回客户的写入请求。这个开关不建议开启的,因为写入链路的RT肯定会明显上升,同时因为quorum/status Znode是Shard级别创建,不能再多个副本并行写入。
- insert_deduplicate,简单实用的数据去重功能(默认开启)。ClickHouse会对每次收到的批量写入数据计算一个Hash Value,然后注册到Zookeeper上。后续如果出现完全重复的一批数据,写入链路上会出现Zookeeper创建重复节点异常,用户就会收到重复写入反馈。当然批量写入的Hash Value保存是有窗口大小限制的,有统一的异步后台线程会清理这些Zookeeper上的过期记录,清理的逻辑代码在ReplicatedMergeTreeCleanupThread::run函数中,有兴趣的同学可以仔细看一下这块代码。
ReplicatedMergeTree异步Task
ReplicatedMergeTree表引擎中除了上述的一些异步Task在调度运行之外,还有一些后台线程在一直工作:
- ReplicatedMergeTreeCleanupThread是负责清理Zookeeper上的过期数据,上述所有异步Task在Zookeeper上的过期数据都由该线程统一清理。
ReplicatedMergeTreeAlterThread是负责监听Zookeeper上Shard级别的表列信息变更,并执行实际的Alter操作。在ReplicatedMergeTree表上的Alter操作流程和第一节中将的分布式DDL执行很像,当某个副本节点收到Alter命令时,它就现在本地完成Alter操作,然后把新的表结构版本发布到Zookeeper上,等待其他副本follow执行。
ReplicatedMergeTreePartCheckThread是专门处理在两阶段提交过程中如何和Zookeeper失联,就把对应的Data Part丢到一个异步Check队列里,由这个线程去延迟检查和Zookeeper的状态是否一致并修复数据。 - ReplicatedMergeTreeRestartingThread负责在Zookeeper上的心跳注册管理。
这一章中讲到的所有ReplicatedMergeTree表引擎的异步Task以及在Zookeeper上的任务队列、心跳注册、状态存储都是ReplicatedMergeTree表级别独立的,集群里的ReplicatedMergeTree表数量、副本数量、写入流量都会影响Zookeeper的服务压力(压力山大),这也是ClickHouse在表引擎级别实现副本逻辑的代价。大家在大规模集群环境中需要谨慎运维Zookeeper。最后ClickHouse在引入异步Mutation机制之后,对副本同步链路的复杂度有比较大的影响,mutation和merge的处理逻辑最大的不同是mutation一次涉及的Input Data Parts几乎是全表,它不能像merge任务一样一次把所有的Input转化到Output,mutation任务需要对Input Data Parts拆分进行挨个操作,任务执行的生命周期特别长,并且Input Data Parts可能动态变化。
这一章中讲到的所有ReplicatedMergeTree表引擎的异步Task以及在Zookeeper上的任务队列、心跳注册、状态存储都是ReplicatedMergeTree表级别独立的,集群里的ReplicatedMergeTree表数量、副本数量、写入流量都会影响Zookeeper的服务压力(压力山大),这也是ClickHouse在表引擎级别实现副本逻辑的代价。大家在大规模集群环境中需要谨慎运维Zookeeper。最后ClickHouse在引入异步Mutation机制之后,对副本同步链路的复杂度有比较大的影响,mutation和merge的处理逻辑最大的不同是mutation一次涉及的Input Data Parts几乎是全表,它不能像merge任务一样一次把所有的Input转化到Output,mutation任务需要对Input Data Parts拆分进行挨个操作,任务执行的生命周期特别长,并且Input Data Parts可能动态变化。
参考阿里云分享:https://developer.aliyun.com/article/762917?spm=a2c6h.12873581.0.dArticle762917.5cb1802fs4UmR7&groupCode=clickhouse