clickhouse数据复制表replication以及数据转换和恢复
一、概述
分布式存储要保证高可用,就必须有数据冗余——即副本(replica)。ClickHouse依靠ReplicatedMergeTree引擎族与ZooKeeper实现了复制表机制,成为其高可用的基础。
在实际操作中,为了最大化性能与稳定性,分片和副本几乎总是一同使用。
仅 MergeTree 系列中的表支持复制,以下为正交图:
复制工作在单个表的级别,而不是整个服务器。服务器可以同时存储复制表和非复制表。
复制不依赖于分片。每个分片都有自己独立的复制。
INSERT
复制和查询的压缩数据ALTER
。
CREATE
, DROP
, ATTACH
,DETACH
和RENAME
查询在单个服务器上执行并且不会被复制:
- 该
CREATE TABLE
查询在运行查询的服务器上创建一个新的可复制表。如果此表已存在于其他服务器上,则会添加一个新副本。 - 该
DROP TABLE
查询将删除位于运行查询的服务器上的副本。 - 该
RENAME
查询重命名副本之一上的表。换句话说,复制的表在不同的副本上可以有不同的名称。
ClickHouse 使用Apache ZooKeeper存储副本元信息。使用 ZooKeeper 版本 3.4.5 或更高版本。
要使用复制,请在zookeeper服务器配置部分设置参数。
注意:不要忽视安全设置。ClickHouse 支持 ZooKeeper 安全子系统的digest
ACL 方案。
ZooKeeper集群地址设置示例:
<zookeeper> <node> <host>example1</host> <port>2181</port> </node> <node> <host>example2</host> <port>2181</port> </node> <node> <host>example3</host> <port>2181</port> </node> </zookeeper>
ClickHouse 还支持通过提供 ZooKeeper 集群名称和路径作为引擎参数将副本元信息存储在辅助 ZooKeeper 集群中。也就是说,它支持将不同表的元数据存储在不同的 ZooKeeper 集群中。
设置辅助 ZooKeeper 集群地址的示例:
<auxiliary_zookeepers> <zookeeper2> <node> <host>example_2_1</host> <port>2181</port> </node> <node> <host>example_2_2</host> <port>2181</port> </node> <node> <host>example_2_3</host> <port>2181</port> </node> </zookeeper2> <zookeeper3> <node> <host>example_3_1</host> <port>2181</port> </node> </zookeeper3> </auxiliary_zookeepers>
要将表数据元存储在辅助 ZooKeeper 集群而不是默认 ZooKeeper 集群中,我们可以使用 SQL 使用 ReplicatedMergeTree 引擎创建表,如下所示:
CREATE TABLE table_name ( ... ) ENGINE = ReplicatedMergeTree('zookeeper_name_configured_in_auxiliary_zookeepers:path', 'replica_name') ...
可以指定任何现有的 ZooKeeper 集群,系统将使用其上的目录存储自己的数据(该目录在创建可复制表时指定)。
如果配置文件中没有设置 ZooKeeper,则无法创建复制表,并且任何现有的复制表都将是只读的。
ZooKeeper 不用于SELECT
查询,因为复制不会影响性能,SELECT
并且查询的运行速度与非复制表一样快。查询分布式复制表时,ClickHouse 行为由设置max_replica_delay_for_distributed_queries和fallback_to_stale_replicas_for_distributed_queries控制。
对于每个INSERT
查询,通过几个事务将大约十个条目添加到 ZooKeeper。(更准确地说,这是针对每个插入的数据块;INSERT 查询包含一个块或每行一个块。)与非复制表相比,max_insert_block_size = 1048576
这会导致稍长的延迟。INSERT
但是,如果按照建议以不超过INSERT
每秒一个的批量插入数据,则不会产生任何问题。INSERTs
用于协调一个 ZooKeeper 集群的整个 ClickHouse 集群每秒总共有几百个。数据插入的吞吐量(每秒的行数)与非复制数据的吞吐量一样高。
对于非常大的集群,可以为不同的分片使用不同的 ZooKeeper 集群。但是,根据我们的经验,基于大约 300 台服务器的生产集群,这并没有被证明是必要的。
复制是异步的和多主的。INSERT
查询(以及ALTER
)可以发送到任何可用的服务器。数据插入到运行查询的服务器上,然后复制到其他服务器。因为它是异步的,所以最近插入的数据会以一定的延迟出现在其他副本上。如果部分副本不可用,则在它们可用时写入数据。如果副本可用,则延迟是通过网络传输压缩数据块所需的时间。为复制表执行后台任务的线程数可以通过background_schedule_pool_size设置来设置。
ReplicatedMergeTree
引擎使用单独的线程池进行复制提取。池的大小受background_fetches_pool_size设置的限制,可以通过服务器重新启动进行调整。
默认情况下,INSERT 查询等待确认仅从一个副本写入数据。如果数据仅成功写入一个副本,并且具有该副本的服务器不复存在,则存储的数据将丢失。要启用对来自多个副本的数据写入的确认,请使用该insert_quorum
选项。
每个数据块都是原子写入的。INSERT 查询被分成最多max_insert_block_size = 1048576
行的块。换句话说,如果INSERT
查询的行数少于 1048576,则自动生成。
数据块被重复数据删除。对于同一个数据块的多次写入(相同大小的数据块包含相同顺序的相同行),该块只被写入一次。这样做的原因是当客户端应用程序不知道数据是否已写入数据库时发生网络故障,因此INSERT
可以简单地重复查询。将相同数据发送到哪个副本 INSERT 并不重要。INSERTs
是幂等的。重复数据删除参数由merge_tree服务器设置控制。
在复制期间,只有要插入的源数据通过网络传输。进一步的数据转换(合并)以相同的方式在所有副本上进行协调和执行。这最大限度地减少了网络使用,这意味着当副本驻留在不同的数据中心时,复制工作得很好。(请注意,在不同的数据中心复制数据是复制的主要目标。)
可以拥有相同数据的任意数量的副本。根据经验,一个相对可靠和方便的解决方案可以在生产中使用双重复制,每台服务器使用 RAID-5 或 RAID-6(在某些情况下使用 RAID-10)。
系统监控副本上的数据同步性,并能够在发生故障后恢复。故障转移是自动的(对于数据的微小差异)或半自动的(当数据差异太大时,这可能表示配置错误)。
数据插入流程图如下:
二、创建复制表
Replicated
前缀被添加到表引擎名称中。例如:ReplicatedMergeTree
。
Replicated*MergeTree 参数
zoo_path
— ZooKeeper 中表的路径。replica_name
— ZooKeeper 中的副本名称。other_parameters
— 用于创建复制版本的引擎的参数,例如ReplacingMergeTree
.
例子:
CREATE TABLE table_name ( EventDate DateTime, CounterID UInt32, UserID UInt32, ver UInt16 ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{layer}-{shard}/table_name', '{replica}', ver) PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID);
不推荐使用的语法示例
CREATE TABLE table_name ( EventDate DateTime, CounterID UInt32, UserID UInt32 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/table_name', '{replica}', EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID), EventTime), 8192);
如示例所示,这些参数可以包含大括号中的替换。替换值取自配置文件的宏部分。
例子:
<macros> <layer>05</layer> <shard>02</shard> <replica>example05-02-1</replica> </macros>
ZooKeeper 中表的路径对于每个复制的表应该是唯一的。不同分片上的表应该有不同的路径。在这种情况下,路径由以下部分组成:
/clickhouse/tables/
是通用前缀。我们建议完全使用这个。
{layer}-{shard}
是分片标识符。在此示例中,它由两部分组成,因为示例集群使用双层分片。对于大多数任务,可以只保留 {shard} 替换,它将扩展为分片标识符。
table_name
是 ZooKeeper 中表的节点名称。使其与表名相同是个好主意。它是显式定义的,因为与表名相比,它在 RENAME 查询后不会更改。 提示:也可以在前面添加数据库名称table_name
。例如db_name.table_name
这两个内置替换{database}
和{table}
可以使用,它们分别扩展为表名和数据库名(除非这些宏在macros
节中定义)。所以zookeeper路径可以指定为'/clickhouse/tables/{layer}-{shard}/{database}/{table}'
。使用这些内置替换时要小心表重命名。Zookeeper 中的路径无法更改,重命名表时,宏会扩展为不同的路径,表将引用 Zookeeper 中不存在的路径,并进入只读模式。
副本名称标识同一张表的不同副本。可以为此使用服务器名称,如示例中所示。该名称只需要在每个分片中是唯一的。
可以显式定义参数,而不是使用替换。这对于测试和配置小型集群可能很方便。ON CLUSTER
但是,在这种情况下,不能使用分布式 DDL 查询 ( )。
在处理大型集群时,我们建议使用替换,因为它们可以降低出错的可能性。
Replicated
可以在服务器配置文件中为表引擎指定默认参数。例如:
<default_replica_path>/clickhouse/tables/{shard}/{database}/{table}</default_replica_path> <default_replica_name>{replica}</default_replica_name>
在这种情况下,可以在创建表时省略参数:
CREATE TABLE table_name ( x UInt32 ) ENGINE = ReplicatedMergeTree ORDER BY x;
它相当于:
CREATE TABLE table_name ( x UInt32 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/table_name', '{replica}') ORDER BY x;
CREATE TABLE
在每个副本上运行查询。此查询创建一个新的复制表,或向现有表添加一个新副本。
如果在表已经包含其他副本上的一些数据之后添加新副本,则在运行查询后数据将从其他副本复制到新副本。换句话说,新副本将自己与其他副本同步。
要删除副本,请运行DROP TABLE
. 但是,只有一个副本被删除 - 驻留在运行查询的服务器上的副本。
# 复制表的创建,其中创建的几个重要监控线程 StorageReplicatedMergeTree::StorageReplicatedMergeTree queue // 表的队列,保存了需要处理的操作 restarting_thread // 重连zk的线程,保证能够一直连接zk ReplicatedMergeTreeRestartingThread::run queue_updating_task // 队列更新任务,监控zk上的log,将它们加载到queue中 mutations_updating_task // mutation更新任务,监控zk上的mutations节点 merge_selecting_task // 选择part进行merge或者mutate的任务 background_executor // 后台处理队列的线程 # 数据的写入 InterpreterFactory::get InterpreterInsertQuery::execute StorageReplicatedMergeTree::write ReplicatedMergeTreeBlockOutputStream ReplicatedMergeTreeBlockOutputStream::write writeTempPart // 数据写入本地磁盘 commitPart // 向zk提交插入的part信息 renameTempPartAndAdd // 临时目录变为正式目录 makeCreateRequest // 提交log_entry,zk上会创建log-xxx mergeSelectingTask // 主动触发merge任务 # 队列更新线程 queueUpdatingTask,也就是log entry的监控线程 ReplicatedMergeTreeQueue::pullLogsToQueue // 监控zk上是否有新的log产生 makeCreateRequest // 将log的内容放到zk上的/queue节点下 insertUnlocked // 在queue中新增当前entry # 队列任务处理进程 scheduling_task StorageReplicatedMergeTree::getDataProcessingJob selectQueueEntry queue.selectEntryToProcess // 从queue中选择可以执行的log entry processQueueEntry // 处理队列中的任务 processEntry executeLogEntry // 如果当前entry指定的part在本节点上,则return,否则从副本fetch executeFetch // 找到一个有该part的副本就可以 fetchPart fetcher.fetchPart // 通过http获取具体数据 removeProcessedEntry // 删除zk上的队列
三、故障后恢复
如果服务器启动时 ZooKeeper 不可用,复制的表将切换到只读模式。系统会定期尝试连接到 ZooKeeper。
如果 ZooKeeper 在 期间不可用INSERT
,或者与 ZooKeeper 交互时发生错误,则会引发异常。
连接到 ZooKeeper 后,系统会检查本地文件系统中的数据集是否与预期的数据集匹配(ZooKeeper 存储此信息)。如果存在轻微的不一致,系统会通过将数据与副本同步来解决它们。
如果系统检测到损坏的数据部分(文件大小错误)或无法识别的部分(写入文件系统但未记录在 ZooKeeper 中的部分),则会将它们移动到detached
子目录(它们不会被删除)。任何缺失的部分都会从副本中复制。
请注意,ClickHouse 不会执行任何破坏性操作,例如自动删除大量数据。
当服务器启动(或与 ZooKeeper 建立新会话)时,它只检查所有文件的数量和大小。如果文件大小匹配但字节在中间某处发生了更改,则不会立即检测到,但仅在尝试读取SELECT
查询数据时才检测到。该查询引发关于不匹配校验和或压缩块大小的异常。在这种情况下,数据部分被添加到验证队列中,并在必要时从副本中复制。
如果本地数据集与预期的数据差异太大,则会触发安全机制。服务器在日志中输入此内容并拒绝启动。这样做的原因是这种情况可能表明配置错误,例如,如果一个分片上的副本被意外配置为不同分片上的副本。但是,此机制的阈值设置得相当低,并且在正常故障恢复期间可能会发生这种情况。在这种情况下,通过“按下按钮”半自动恢复数据。
要开始恢复,请在 ZooKeeper 中使用任何内容创建节点/path_to_table/replica_name/flags/force_restore_data
,或者运行命令来恢复所有复制的表:
sudo -u clickhouse touch /var/lib/clickhouse/flags/force_restore_data
然后重新启动服务器。启动时,服务器会删除这些标志并开始恢复。
四、数据完全丢失后恢复
如果其中一台服务器上的所有数据和元数据都消失了,请按照以下步骤进行恢复:
- 在服务器上安装 ClickHouse。如果使用它们,请在包含分片标识符和副本的配置文件中正确定义替换。
- 如果有必须在服务器上手动复制的未复制表,请从副本(在目录中
/var/lib/clickhouse/data/db_name/table_name/
)复制它们的数据。 - 从副本复制位于的表定义
/var/lib/clickhouse/metadata/
。如果在表定义中明确定义了分片或副本标识符,请更正它以使其对应于该副本。(或者,启动服务器并进行所有ATTACH TABLE
应该在 .sql 文件中的查询/var/lib/clickhouse/metadata/
。) - 要开始恢复,请创建包含任何内容的 ZooKeeper 节点
/path_to_table/replica_name/flags/force_restore_data
,或运行命令以恢复所有复制的表:sudo -u clickhouse touch /var/lib/clickhouse/flags/force_restore_data
然后启动服务器(重新启动,如果它已经在运行)。数据将从副本下载。
另一种恢复选项是从 ZooKeeper ( ) 中删除有关丢失副本的信息,然后按照“创建复制表/path_to_table/replica_name
”中所述再次创建副本。
恢复期间对网络带宽没有限制。如果要同时恢复多个副本,请记住这一点。
五、从 MergeTree 转换为 ReplicatedMergeTree
我们使用该术语MergeTree
来指代 中的所有表引擎MergeTree family
,与 for 相同ReplicatedMergeTree
。
如果有一个MergeTree
手动复制的表,则可以将其转换为复制表。如果已经在表中收集了大量数据MergeTree
并且现在想要启用复制,则可能需要执行此操作。
如果各个副本上的数据不同,请先同步它,或者删除除一个之外的所有副本上的此数据。
重命名现有的 MergeTree 表,然后ReplicatedMergeTree
使用旧名称创建一个表。将旧表中的数据移动到detached
包含新表数据的目录内的子目录中 ( /var/lib/clickhouse/data/db_name/table_name/
)。然后ALTER TABLE ATTACH PARTITION
在其中一个副本上运行以将这些数据部分添加到工作集中。
六、从 ReplicatedMergeTree 转换为 MergeTree
创建一个具有不同名称的 MergeTree 表。将包含ReplicatedMergeTree
表数据的目录中的所有数据移动到新表的数据目录中。然后删除ReplicatedMergeTree
表并重新启动服务器。
如果想在ReplicatedMergeTree
不启动服务器的情况下删除表:
.sql
删除元数据目录 (/var/lib/clickhouse/metadata/
)中的相应文件。- 删除 ZooKeeper (
/path_to_table/replica_name
) 中的对应路径。
之后,可以启动服务器,创建MergeTree
表,将数据移动到其目录,然后重新启动服务器。
七、Zookeeper 集群中元数据丢失或损坏时的恢复
如果 ZooKeeper 中的数据丢失或损坏,可以通过将数据移动到如上所述的未复制表来保存数据。
八、ReplicatedMergeTree表删除更新数据等操作
1)drop 表相关操作
在我们删除本地表和分布式表后,立即重建是没有问题的。唯一有问题的就是复制表,因为复制表需要在zookeeper上建立一个路径,存放相关数据。clickhouse默认的库引擎是原子数据库引擎,删除Atomic数据库中的表后,它不会立即删除,而是会在480秒后删除。由下面这个参数控制:
config.xml
<database_atomic_delay_before_drop_table_sec>480</database_atomic_delay_before_drop_table_sec>
①使用普通数据库而不是原子数据库。 create database … Engine=Ordinary。
②使用uniq ZK路径。{uuid}/clickhouse/tables/{layer}-{shard}-{uuid}/。
③减少database_atomic_delay_before_drop_table_sec = 0 & drop table … sync
2)alter update delete 等操作
对于非副本表,所有ALTER查询都是同步执行的。对于副本表,查询只是向ZooKeeper添加适当操作的指令,操作本身会尽快执行,可以通过replication_alter_partitions_sync设置控制执行等待,如果为0表示不等待,如果为1表示只等待自己执行(默认,即一个),如果为2表示需要等待所有节点完成。另外还可以通过replication_wait_for_inactive_replica_timeout参数设置等待时间,0表示不等待,负整数表示无限制等待,正整数表示等待秒数。如果 replication_alter_partitions_sync = 2,某些副本ALTER操作超过 replication_wait_for_inactive_replica_timeout 时间,则会抛出 UNFINISHED 异常。
3)truncate table操作
会等待所有的副本处理结束,即使某些副本出现故障了,也还会继续等待,没有设置超时时间。truncate 操作没有回滚机制,在它分别遍历分区和副本时,如果存在多个分区和多个副本,且某个副本存在故障时,那么遍历到故障副本时,流程会一直卡住(无法满足stop_waiting条件,该副本的log一直无法处理)。但是,此时,之前遍历过的副本已经处理完了truncate操作。这就导致两点不一致:
①副本间的数据已经不一致了。
②已经遍历过的副本只删除了部分数据,一般是只有第一个分区的数据被删除了
整体流程如下:
StorageReplicatedMergeTree::truncate // 入口 // 如下遍历所有分区 dropAllPartsInPartition // 停止merge并在zk上创建删除分区的log, 类型为LogEntry::DROP_RANGE waitForAllReplicasToProcessLogEntry // 等待所有副本执行完删除分区操作 // 如下遍历所有副本,包括自己 waitForTableReplicaToProcessLogEntry // 根据log_pointer和log编号判断是否已经处理完当前log-entry,没有处理完或者没有满足stop_waiting的条件,则一直等待 // 等待queue节点下文件消失 // 将log节点下的内容拉取到queue节点下,并将entry放到变量queue中 ReplicatedMergeTreeQueue::pullLogsToQueue ... // 处理队列的后台线程 StorageReplicatedMergeTree::getDataProcessingJob selectQueueEntry processQueueEntry // 处理队列中的任务 processEntry executeLogEntry executeDropRange // 真正删除part removeProcessedEntry /
如执行中未立即得到结果,建议等待,如果等待太久,只能进行节点重启,之后再检查数据是否删除成功。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?