79.【数据库】ClickHouse从入门到放弃-MergeTree 多路径存储策略

911 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情 |

文档参考:《ClickHouse原理解析与应用实践(数据库技术丛书)(朱凯)》

70.【数据库】ClickHouse从入门到放弃-MergeTree的创建方式 - 掘金 (juejin.cn)

71.【数据库】ClickHouse从入门到放弃-MergeTree的存储结构 - 掘金 (juejin.cn)

72.【数据库】ClickHouse从入门到放弃-数据分区 - 掘金 (juejin.cn)

73.【数据库】ClickHouse从入门到放弃-一级索引 - 掘金 (juejin.cn)

74.【数据库】ClickHouse从入门到放弃-二级索引 - 掘金 (juejin.cn)

75.【数据库】ClickHouse从入门到放弃-数据存储 - 掘金 (juejin.cn)

76.【数据库】ClickHouse从入门到放弃-数据标记 - 掘金 (juejin.cn)

77.【数据库】ClickHouse从入门到放弃-对于分区、索引、标记和压缩数据的协同总结 - 掘金 (juejin.cn)

78.【数据库】ClickHouse从入门到放弃-MergeTree 数据TTL - 掘金 (juejin.cn)

MergeTree作为家族系列最基础的表引擎,提供了数据分区、一级索引和二级索引等功能。对于它们的运行机理,前面已经进行了详细介绍。今天进一步学习MergeTree家族独有的另外两项能力——数据TTL与存储策略。

2 多路径存储策略

在ClickHouse 19.15版本之前,MergeTree只支持单路径存储,所有的数据都会被写入config.xml配置中path指定的路径下,即使服务器挂载了多块磁盘,也无法有效利用这些存储空间。为了解决这个痛点,从19.15版本开始,MergeTree实现了自定义存储策略的功能,支持以数据分区为最小移动单元,将分区目录写入多块磁盘目录。

根据配置策略的不同,目前大致有三类存储策略。

·默认策略:MergeTree原本的存储策略,无须任何配置,所有分区会自动保存到config.xml配置中path指定的路径下。

·JBOD策略:这种策略适合服务器挂载了多块磁盘,但没有做RAID的场景。JBOD的全称是Just a Bunch of Disks,它是一种轮询策略,每执行一次INSERT或者MERGE,所产生的新分区会轮询写入各个磁盘。这种策略的效果类似RAID 0,可以降低单块磁盘的负载,在一定条件下能够增加数据并行读写的性能。如果单块磁盘发生故障,则会丢掉应用JBOD策略写入的这部分数据。(数据的可靠性需要利用副本机制保障,这部分内容将会在后面介绍副本与分片时介绍。)

·HOT/COLD策略:这种策略适合服务器挂载了不同类型磁盘的场景。将存储磁盘分为HOT与COLD两类区域。HOT区域使用SSD这类高性能存储媒介,注重存取性能;COLD区域则使用HDD这类高容量存储媒介,注重存取经济性。数据在写入MergeTree之初,首先会在HOT区域创建分区目录用于保存数据,当分区数据大小累积到阈值时,数据会自行移动到COLD区域。而在每个区域的内部,也支持定义多个磁盘,所以在单个区域的写入过程中,也能应用JBOD策略。

存储配置需要预先定义在config.xml配置文件中,由storage_configuration标签表示。在storage_configuration之下又分为disks和policies两组标签,分别表示磁盘与存储策略。

disks的配置示例如下所示,支持定义多块磁盘:

<storage_configuration>        
    <disks>
        <disk_name_a> <!--自定义磁盘名称 -->
            <path>/chbase/data</path><!—磁盘路径 -->
            <keep_free_space_bytes>1073741824</keep_free_space_bytes>
        </disk_name_a>
 
        <disk_name_b>
            <path></path>
        </disk_name_b>
    </disks>

其中:

·<disk_name_*>,必填项,必须全局唯一,表示磁盘的自定义名称;

·,必填项,用于指定磁盘路径;

·<keep_free_space_bytes>,选填项,以字节为单位,用于定义磁盘的预留空间。

在policies的配置中,需要引用先前定义的disks磁盘。policies同样支持定义多个策略,它的示例如下:

<policies>
    <policie_name_a> <!--自定义策略名称 -->
        <volumes>
            <volume_name_a> <!--自定义卷名称 -->
                <disk>disk_name_a</disk>
                <disk>disk_name_b</disk>
                <max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
            </volume_name_b>
        </volumes>
        <move_factor>0.2</move_factor>
    </policie_name_a>
 
    <policie_name_b>
    </policie_name_b>
</policies>    

其中:

·<policie_name_*>,必填项,必须全局唯一,表示策略的自定义名称;

·<volume_name_*>,必填项,必须全局唯一,表示卷的自定义名称;

·,必填项,用于关联配置内的磁盘,可以声明多个disk,MergeTree会按定义的顺序选择disk;

·<max_data_part_size_bytes>,选填项,以字节为单位,表示在这个卷的单个disk磁盘中,一个数据分区的最大存储阈值,如果当前分区的数据大小超过阈值,则之后的分区会写入下一个disk磁盘;

·<move_factor>,选填项,默认为0.1;如果当前卷的可用空间小于factor因子,并且定义了多个卷,则数据会自动向下一个卷移动。

在知道了配置格式之后,现在用一组示例说明它们的使用方法。

2.1 JBOD策略

首先,在config.xml配置文件中增加storage_configuration元素,并配置3块磁盘:

<storage_configuration>
    <!--自定义磁盘配置 -->
    <disks>
        <disk_hot1> <!--自定义磁盘名称 -->
            <path>/chbase/data</path>
        </disk_hot1>
        <disk_hot2>
            <path>/chbase/hotdata1</path>
        </disk_hot2>
        <disk_cold>
            <path>/chbase/cloddata</path>
            <keep_free_space_bytes>1073741824</keep_free_space_bytes>
        </disk_cold>
    </disks>
   …省略

接着,配置一个存储策略,在volumes卷下引用两块磁盘,组成一个磁盘组:

<!-- 实现JDOB效果 -->
   <policies>
       <default_jbod> <!--自定义策略名称 -->
           <volumes>
               <jbod> <!—自定义名称 磁盘组 -->
                   <disk>disk_hot1</disk>
                   <disk>disk_hot2</disk>
               </jbod>
           </volumes>
       </default_jbod>

   </policies>
   </storage_configuration>   

至此,一个支持JBOD策略的存储策略就配置好了。在正式应用之前,还需要做一些准备工作。首先,需要给磁盘路径授权,使ClickHouse用户拥有路径的读写权限:

sudo chown clickhouse:clickhouse -R /chbase/cloddata /chbase/hotdata1

由于存储配置不支持动态更新,为了使配置生效,还需要重启clickhouse-server服务:

sudo service clickhouse-server restart

服务重启好之后,可以查询系统表以验证配置是否已经生效:

SELECT 
name,
path,formatReadableSize(free_space) AS free,
formatReadableSize(total_space) AS total,
formatReadableSize(keep_free_space) AS reserved 
FROM system.disks
┌─name─────┬─path────────┬─free────┬─total────┬─reserved─┐
│ default     │ /chbase/data/     │ 38.26 GiB  │ 49.09 GiB   │ 0.00 B    │
│ disk_cold   │ /chbase/cloddata/ │ 37.26 GiB  │ 48.09 GiB   │ 1.00 GiB  │
│ disk_hot1   │ /chbase/data/     │ 38.26 GiB  │ 49.09 GiB   │ 0.00 B    │
│ disk_hot2   │ /chbase/hotdata1/ │ 38.26 GiB  │ 49.09 GiB   │ 0.00 B    │
└────────┴────────────┴────────┴────────┴───────┘

通过system.disks系统表可以看到刚才声明的3块磁盘配置已经生效。接着验证策略配置:

SELECT policy_name,
volume_name,
volume_priority,
disks,
formatReadableSize(max_data_part_size) max_data_part_size ,
move_factor FROM 
system.storage_policies
┌─policy_name─┬─volume_name─┬─disks──────────┬─max_data_part_size─┬─move_factor─┐
│ default      │ default    │ ['default']0.00 B0           │
│ default_jbod │ jbod       │ ['disk_hot1','disk_hot2']0.00 B0.1         │
└────────┴────────┴─────────────┴──────────┴─────────┘

通过system.storage_policies系统表可以看到刚才配置的存储策略也已经生效了。

现在创建一张MergeTree表,用于测试default_jbod存储策略的效果:

CREATE TABLE jbod_table(
  id UInt64
)ENGINE = MergeTree()
ORDER BY id
SETTINGS storage_policy = 'default_jbod'

在定义MergeTree时,使用storage_policy配置项指定刚才定义的default_jbod存储策略。存储策略一旦设置,就不能修改了。现在开始测试它的效果。首先写入第一批数据,创建一个分区目录:

  INSERT INTO TABLE jbod_table SELECT rand() FROM numbers(10)  

查询分区系统表,可以看到第一个分区写入了第一块磁盘disk_hot1:

SELECT name, disk_name FROM system.parts WHERE table = 'jbod_table'
┌─name─────┬─disk_name   

接着写入第二批数据,创建一个新的分区目录:

INSERT INTO TABLE jbod_table SELECT rand() FROM numbers(10) 

再次查询分区系统表,可以看到第二个分区写入了第二块磁盘disk_hot2:

SELECT name, disk_name FROM system.parts WHERE table = 'jbod_table'
┌─name─────┬─disk_name─┐
│ all_1_1_0   │ disk_hot1 │
│ all_2_2_0   │ disk_hot2 │
└────────┴───────┘    

最后触发一次分区合并动作,生成一个合并后的新分区目录:

 optimize TABLE jbod_table   

还是查询分区系统表,可以看到合并后生成的all_1_2_1分区,再一次写入了第一块磁盘disk_hot1:

┌─name─────┬─disk_name─┐
│ all_1_1_0   │ disk_hot1  │
│ all_1_2_1   │ disk_hot1  │
│ all_2_2_0   │ disk_hot2  │
└────────┴───────┘

至此,大家应该已经明白JBOD策略的工作方式了。在这个策略中,由多个磁盘组成了一个磁盘组,即volume卷。每当生成一个新数据分区的时候,分区目录会依照volume卷中磁盘定义的顺序,依次轮询并写入各个磁盘。

2.2 HOT/COLD策略

现在介绍HOT/COLD策略的使用方法。首先在上一小节介绍的配置文件中添加一个新的策略:

 <policies>
    …省略
    <moving_from_hot_to_cold><!--自定义策略名称 -->
        <volumes>
            <hot><!--自定义名称 ,hot区域磁盘 -->
                <disk>disk_hot1</disk>
                <max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
             </hot>
              <cold><!--自定义名称 ,cold区域磁盘 -->
                  <disk>disk_cold</disk>
               </cold>
           </volumes>
            <move_factor>0.2</move_factor>
    </moving_from_hot_to_cold>
</policies>   

存储配置不支持动态更新,所以为了使配置生效,需要重启clickhouse-server服务:

sudo service clickhouse-server restart

通过system.storage_policies系统表可以看到,刚才配置的存储策略已经生效了。

┌─policy_name────────┬─volume_name┬─disks────┬max_data_part_size─┬─move_factor─┐
│ moving_from_hot_to_cold │ hot       │ ['disk_hot1']1.00 MiB          │  0.2         │
│ moving_from_hot_to_cold │ cold      │ ['disk_cold']0.00 B0.2        │
└───────────────┴───────┴────────┴──────────┴────────┘

moving_from_hot_to_cold存储策略拥有hot和cold两个磁盘卷,在每个卷下各拥有1块磁盘。注意,hot磁盘卷的max_data_part_size列显示的值是1M,这个值的含义表示,在这个磁盘卷下,如果一个分区的大小超过1MB,则它需要被移动到紧邻的下一个磁盘卷。

与先前一样,现在创建一张MergeTree表,用于测试moving_from_hot_to_cold存储策略的效果:

CREATE TABLE hot_cold_table(
    id UInt64
)ENGINE = MergeTree()
ORDER BY id
SETTINGS storage_policy = 'moving_from_hot_to_cold'

在定义MergeTree时,使用storage_policy配置项指定刚才定义的moving_from_hot_to_cold存储策略。存储策略一旦设置就不能再修改。

现在开始测试它的效果,首先写入第一批数据,创建一个分区目录,数据大小500KB:

--写入500K大小,分区会写入hot
INSERT INTO TABLE hot_cold_table SELECT rand()FROM numbers(100000)

查询分区系统表,可以看到第一个分区写入了hot卷:

SELECT name, disk_name FROM system.parts WHERE table = 'hot_cold_table'
┌─name─────┬─disk_name─┐
│ all_1_1_0   │ disk_hot1 │
└────────┴───────┘

接着写入第二批数据,创建一个新的分区目录,数据大小还是500KB:

INSERT INTO TABLE hot_cold_table SELECT rand()FROM numbers(100000)

再次查询分区系统表,可以看到第二个分区,仍然写入了hot卷:

SELECT name, disk_name FROM system.parts WHERE table = 'hot_cold_table'
┌─name─────┬─disk_name─┐
│ all_1_1_0   │ disk_hot1 │
│ all_2_2_0   │ disk_hot1 │
└────────┴───────┘

这是由于hot磁盘卷的max_data_part_size是1MB,而前两次数据写入所创建的分区,单个分区大小是500KB,自然分区目录都被保存到了hot磁盘卷下的disk_hot1磁盘。现在触发一次分区合并动作,生成一个新的分区目录:

optimize TABLE hot_cold_table

查询分区系统表,可以看到合并后生成的all_1_2_1分区写入了cold卷:

┌─name─────┬─disk_name─┐
│ all_1_1_0   │ disk_hot1 │
│ all_1_2_1   │ disk_cold │
│ all_2_2_0   │ disk_hot1 │
└────────┴───────┘

这是因为两个分区合并之后,所创建的新分区的大小超过了1MB,所以它被写入了cold卷,相关查询代码如下:

SELECT
disk_name,
formatReadableSize(bytes_on_disk) AS size
FROM system.parts
WHERE (table = 'hot_cold_table') AND active = 1
┌─disk_name─┬─size────┐
│ disk_cold  │ 1.01 MiB  │
└────────┴───────┘

注意,如果一次性写入大于1MB的数据,分区也会被写入cold卷。

至此,大家应该明白HOT/COLD策略的工作方式了。在这个策略中,由多个磁盘卷(volume卷)组成了一个volume组。每当生成一个新数据分区的时候,按照阈值大小(max_data_part_size),分区目录会依照volume组中磁盘卷定义的顺序,依次轮询并写入各个卷下的磁盘。

虽然MergeTree的存储策略目前不能修改,但是分区目录却支持移动。例如,将某个分区移动至当前存储策略中当前volume卷下的其他disk磁盘:

ALTER TABLE hot_cold_table MOVE PART 'all_1_2_1' TO DISK 'disk_hot1'

或是将某个分区移动至当前存储策略中其他的volume卷:

ALTER TABLE hot_cold_table MOVE PART 'all_1_2_1' TO VOLUME 'cold'