从 19.15 版本开始,ClickHouse 开始实现多卷存储的功能。
主要是将数据分层存储,比如冷热数据分层存储,数据分不同卷存储等等。话不多说,开干~
添加三块新磁盘:
/dev/sdb 20G
/dev/sdc 20G
/dev/sdd 30G
echo "- - -" > /sys/class/scsi_host/host1/scan
echo "- - -" > /sys/class/scsi_host/host0/scan
echo "- - -" > /sys/class/scsi_host/host2/scan
fdisk -l
Disk /dev/sdb: 21.5 GB, 21474836480 bytes, 41943040 sectors
Disk /dev/sdc: 21.5 GB, 21474836480 bytes, 41943040 sectors
Disk /dev/sdd: 32.2 GB, 32212254720 bytes, 62914560 sectors
fdisk /dev/sdb n,p,w
fdisk /dev/sdc
fdisk /dev/sdd
mkfs -t ext4 /dev/sdb
mkfs -t ext4 /dev/sdc
mkfs -t ext4 /dev/sdd
mkdir /mnt/fast_ssd/clickhouse/ -p
mkdir /mnt/hdd1/clickhouse/ -p
mkdir /mnt/hdd2/clickhouse/ -p
mount -o noatime,nobarrier /dev/sdd /mnt/fast_ssd/clickhouse/
mount -o noatime,nobarrier /dev/sdb /mnt/hdd1/clickhouse/
mount -o noatime,nobarrier /dev/sdc /mnt/hdd2/clickhouse/
chown -R clickhouse.clickhouse /mnt
df -h
.......
/dev/sdd 30G 45M 28G 1% /mnt/fast_ssd/clickhouse
/dev/sdc 20G 45M 19G 1% /mnt/hdd2/clickhouse
/dev/sdb 20G 45M 19G 1% /mnt/hdd1/clickhouse
clickhouse-client --query="select version()"
20.8.3.18
SELECT name, path,formatReadableSize(free_space) AS free,formatReadableSize(total_space) AS total,formatReadableSize(keep_free_space) AS reserved FROM system.disks;
Row 1:
──────
name: default
path: /var/lib/clickhouse/
free: 53.11 GiB
total: 57.78 GiB
reserved: 0.00 B
vim /etc/clickhouse-server/config.d/storage.xml
<yandex>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>1024keep_free_space_bytes>
default>
<disk_1>
<path>/mnt/fast_ssd/clickhouse/path>
disk_1>
<disk_2>
<path>/mnt/hdd1/clickhouse/path>
<keep_free_space_bytes>10485760keep_free_space_bytes>
disk_2>
<disk_3>
<path>/mnt/hdd2/clickhouse/path>
<keep_free_space_bytes>10485760keep_free_space_bytes>
disk_3>
disks>
storage_configuration>
yandex>
chown -R clickhouse.clickhouse /etc/clickhouse-server/config.d
clickhouse-client
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 │ /var/lib/clickhouse/ │ 53.13 GiB │ 57.78 GiB │ 0.00 B │
│ disk_1 │ /mnt/fast_ssd/clickhouse/ │ 27.85 GiB │ 29.40 GiB │ 0.00 B │
│ disk_2 │ /mnt/hdd1/clickhouse/ │ 18.49 GiB │ 19.55 GiB │ 10.00 MiB │
│ disk_3 │ /mnt/hdd2/clickhouse/ │ 18.49 GiB │ 19.55 GiB │ 10.00 MiB │
└─────────┴───────────────────────────┴───────────┴───────────┴───────────┘
此时的存储依旧在/var/lib/clickhouse/data/目录下:
CREATE TABLE sample1 (id UInt64) Engine=MergeTree ORDER BY id;
INSERT INTO sample1 SELECT * FROM numbers(1000000);
SELECT name, data_paths FROM system.tables WHERE name = 'sample1'\G
Row 1:
──────
name: t_table_ttl
data_paths: ['/var/lib/clickhouse/data/default/t_table_ttl/']
在不同磁盘上存储数据的规则由存储策略设置。在新安装或升级的 CH 服务中,有一个存储策略,称为 default,表示所有数据都应放置在默认磁盘上。此策略可确保对现有表的向后兼容性。我们可以通过从 system.storage_policies 表中查询来查看定义,该表用于帮助管理分层存储。
SELECT policy_name, volume_name, disks FROM system.storage_policies;
┌─policy_name──────────────┬─volume_name─┬─disks───────────────┐
│ default │ default │ ['default'] │
└──────────────────────────┴─────────────┴─────────────────────┘
引入名称为 policy_1_only 的存储策略,该策略将所有数据存储在包含disk_2磁盘的 volume_1 上。
配置放在文件 /etc/clickhouse-server/config.d/storage.xml 中。
vim /etc/clickhouse-server/config.d/storage.xml
<yandex>
<storage_configuration>
........
disk_3>
disks>
<policies>
<policy_1_only>
<volumes>
<volume_1>
<disk>disk_2disk>
<max_data_part_size_bytes>1073741824max_data_part_size_bytes>
volume_1>
volumes>
policy_1_only>
policies>
storage_configuration>
yandex>
SELECT policy_name, volume_name, disks FROM system.storage_policies;
┌─policy_name───┬─volume_name─┬─disks───────┐
│ default │ default │ ['default'] │
│ policy_1_only │ volume_1 │ ['disk_2'] │
└───────────────┴─────────────┴─────────────┘
如何使用?
创表示例:
CREATE TABLE sample2 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'policy_1_only';
INSERT INTO sample2 SELECT * FROM numbers(1000000);
SELECT count(*) FROM sample2;
┌─count()─┐
│ 1000000 │
└─────────┘
查询 system.tables 显示的结果,可以看到两个表具有不同的数据路径和不同的存储策略。表元数据保留在默认磁盘上。
SELECT name, data_paths, metadata_path, storage_policy FROM system.tables WHERE name LIKE 'sample%' \G
Row 1:
──────
name: sample1
data_paths: ['/var/lib/clickhouse/data/default/sample1/']
metadata_path: /var/lib/clickhouse/metadata/default/sample1.sql
storage_policy: default
Row 2:
──────
name: sample2
data_paths: ['/mnt/hdd1/clickhouse/data/default/sample2/']
metadata_path: /var/lib/clickhouse/metadata/default/sample2.sql
storage_policy: policy_1_only
ll /mnt/hdd1/clickhouse/data/default/sample2/
total 12
drwxr-x--- 2 clickhouse clickhouse 4096 Mar 12 09:15 all_1_1_0
drwxr-x--- 2 clickhouse clickhouse 4096 Mar 12 09:15 detached
-rw-r----- 1 clickhouse clickhouse 1 Mar 12 09:15 format_version.txt
– 无论源表和目标表位于何处,都可以使用常规的 INSERT … SELECT 将数据从一个表复制到另一表。
INSERT INTO sample2 SELECT * FROM numbers(1000)
SELECT count(*) FROM sample2
┌─count()─┐
│ 1001000 │
└─────────┘
ll /mnt/hdd1/clickhouse/data/default/sample2/
total 16
drwxr-x--- 2 clickhouse clickhouse 4096 Dec 16 09:03 all_1_1_0
drwxr-x--- 2 clickhouse clickhouse 4096 Dec 16 09:09 all_2_2_0
drwxr-x--- 2 clickhouse clickhouse 4096 Dec 16 09:00 detached
-rw-r----- 1 clickhouse clickhouse 1 Dec 16 09:00 format_version.txt
使用存储策略在一个卷中将两个或多个磁盘分组,数据将以轮循方式在磁盘之间分配
每次 insert(或 merge)都会在卷中的下一个磁盘上创建 part。parts 的一半存储在一个磁盘上,其余部分存储在另一个磁盘上。这个概念通常称为 JBOD,它是 “Just a Bunch of Disks” 的缩写。
JBOD 卷组织提供了以下好处:
1.通过附加磁盘来扩展存储的简便方法,这比迁移到 RAID 简单得多。
2.在某些情况下(例如,当多个线程并行使用不同的磁盘时),读/写速度会提高。
3.由于每个磁盘上的 parts 数较少,因此表加载速度更快。
JBOD 时,一个磁盘的故障将导致数据丢失。添加更多磁盘会增加丢失至少一些数据的机会。当要求系统有容错能力时,强烈建议使用复制方式。
vim /etc/clickhouse-server/config.d/storage.xml
<yandex>
<storage_configuration>
.....
policy_1_only>
<hdd_jbod>
<volumes>
<jbod_volume>
<disk>disk_2disk>
<disk>disk_3disk>
jbod_volume>
volumes>
hdd_jbod>
policies>
storage_configuration>
yandex>
SELECT policy_name, volume_name, disks FROM system.storage_policies;
┌─policy_name───┬─volume_name─┬─disks───────────────┐
│ default │ default │ ['default'] │
│ hdd_jbod │ jbod_volume │ ['disk_2','disk_3'] │
│ policy_1_only │ volume_1 │ ['disk_2'] │
└───────────────┴─────────────┴─────────────────────┘
CREATE TABLE sample3 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'hdd_jbod';
SELECT name,data_paths,metadata_path,storage_policy FROM system.tables WHERE name = 'sample3'
Row 1:
──────
name: sample3
data_paths: ['/mnt/hdd1/clickhouse/data/default/sample3/','/mnt/hdd2/clickhouse/data/default/sample3/']
metadata_path: /var/lib/clickhouse/metadata/default/sample3.sql
storage_policy: hdd_jbod
INSERT INTO sample3 SELECT * FROM numbers(1000000)
INSERT INTO sample3 SELECT * FROM numbers(1000000)
INSERT INTO sample3 SELECT * FROM numbers(1000000)
INSERT INTO sample3 SELECT * FROM numbers(1000000)
SELECT name,disk_name,path FROM system.parts WHERE table = 'sample3' ;
┌─name──────┬─disk_name─┬─path─────────────────────────────────────────────────┐
│ all_1_1_0 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_1_1_0/ │
│ all_2_2_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_2_2_0/ │
│ all_3_3_0 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_3_3_0/ │
│ all_4_4_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_4_4_0/ │
└───────────┴───────────┴──────────────────────────────────────────────────────┘
INSERT INTO sample3 SELECT * FROM numbers(100)
INSERT INTO sample3 SELECT * FROM numbers(10)
SELECT name,disk_name,path FROM system.parts WHERE table = 'sample3' ;
┌─name──────┬─disk_name─┬─path─────────────────────────────────────────────────┐
│ all_1_1_0 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_1_1_0/ │
│ all_1_4_1 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_1_4_1/ │
│ all_2_2_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_2_2_0/ │
│ all_3_3_0 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_3_3_0/ │
│ all_4_4_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_4_4_0/ │
│ all_5_5_0 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_5_5_0/ │
│ all_6_6_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_6_6_0/ │
└───────────┴───────────┴──────────────────────────────────────────────────────┘
后台合并可以从不同磁盘上的 parts 收集数据,并将合并完成的新的较大 part 放在该卷的其中一个磁盘上(根据轮询算法)
运行 OPTIMIZE TABLE 强制合并来查看此示例中的行为
OPTIMIZE TABLE sample3;
SELECT name,disk_name,path FROM system.parts WHERE (table = 'sample3') AND active;
┌─name──────┬─disk_name─┬─path─────────────────────────────────────────────────┐
│ all_1_5_2 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_1_5_2/ │
│ all_6_6_0 │ disk_3 │ /mnt/hdd2/clickhouse/data/default/sample3/all_6_6_0/ │
└───────────┴───────────┴──────────────────────────────────────────────────────┘
OPTIMIZE TABLE sample3;
SELECT name,disk_name,path FROM system.parts WHERE (table = 'sample3') AND active;
┌─name──────┬─disk_name─┬─path─────────────────────────────────────────────────┐
│ all_1_6_3 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample3/all_1_6_3/ │
└───────────┴───────────┴──────────────────────────────────────────────────────┘
后台合并往往会随着时间的流逝创建越来越大的 parts,从而将每个生成的 part 移至其中一个磁盘。因此,存储策略不能保证数据将均匀地分布在磁盘上,它也不能保证 JBOD 上的 I/O 吞吐量要比最慢的磁盘上的 I/O 吞吐量更好。为了获得这样的保证,应该改用 RAID。
使用 JBOD 存储策略的最明显原因是通过添加其他存储而不移动现有数据来增加 ClickHouse 服务的容量。
问题:
1.在初始插入新数据应该存储在哪里?
2.何时它将移动到较慢的存储空间?
ClickHouse 19.15 版本使用基于 part 大小的启发式方法来确定何时在卷之间移动 part。在 ClickHouse 中,part 大小和 part 寿命通常是紧密相关的。MergeTree 引擎一直在进行后台合并,将新插入的数据和小的 part 随时间合并为越来越大的 part。这意味着大的 part 会在几次合并后出现,因此通常情况下,part 越大就越老。
vim /etc/clickhouse-server/config.d/storage.xml
<yandex>
<storage_configuration>
......
hdd_jbod>
<policy_hot_and_cold_data>
<volumes>
<hot_volume>
<disk>disk_1disk>
<max_data_part_size_bytes>104857600max_data_part_size_bytes>
hot_volume>
<cold_volume>
<disk>disk_2disk>
<max_data_part_size_bytes>10737418240max_data_part_size_bytes>
cold_volume>
volumes>
policy_hot_and_cold_data>
policies>
storage_configuration>
yandex>
存储在磁盘上时,ClickHouse首先尝试将其放置在第一个卷中,然后放置在第二个卷中,依此类推。
select policy_name,volume_name,volume_priority,disks,formatReadableSize(max_data_part_size) max_data_part_size ,move_factor from system.storage_policies WHERE policy_name = 'policy_hot_and_cold_data';
Row 1:
──────
policy_name: policy_hot_and_cold_data
volume_name: hot_volume
volume_priority: 1
disks: ['disk_1']
max_data_part_size: 100.00 MiB
move_factor: 0.1
Row 2:
──────
policy_name: policy_hot_and_cold_data
volume_name: cold_volume
volume_priority: 2
disks: ['disk_2']
max_data_part_size: 10.00 GiB
move_factor: 0.1
– move_factor 表示当前卷的空间不足(默认10%)时,移动part到其他卷释放空间
CREATE TABLE sample4 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'policy_hot_and_cold_data';
INSERT INTO sample4 SELECT rand() FROM numbers(10000000);
SELECT disk_name,formatReadableSize(bytes_on_disk) AS size FROM system.parts WHERE (table = 'sample4') AND active;
┌─disk_name─┬─size──────┐
│ disk_1 │ 29.08 MiB │
│ disk_1 │ 5.04 MiB │
│ disk_1 │ 5.04 MiB │
│ disk_1 │ 5.04 MiB │
│ disk_1 │ 2.74 MiB │
└───────────┴───────────┘
-- 目前,所有数据都是“热”数据,并存储在快速磁盘上。
SELECT name,disk_name,path FROM system.parts WHERE (table = 'sample4') AND active;
┌─name──────────┬─disk_name─┬─path─────────────────────────────────────────────────────────┐
│ all_401_406_1 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_401_406_1/ │
│ all_407_407_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_407_407_0/ │
│ all_408_408_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_408_408_0/ │
│ all_409_409_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_409_409_0/ │
│ all_410_410_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_410_410_0/ │
└───────────────┴───────────┴──────────────────────────────────────────────────────────────┘
-- 继续插入数据测试
INSERT INTO sample4 SELECT rand() FROM numbers(10000000);
INSERT INTO sample4 SELECT rand() FROM numbers(10000000);
SELECT disk_name,formatReadableSize(bytes_on_disk) AS size FROM system.parts WHERE (table = 'sample4') AND active ;
┌─disk_name─┬─size───────┐
│ disk_2 │ 121.29 MiB │
│ disk_1 │ 5.04 MiB │
│ disk_1 │ 2.74 MiB │
└───────────┴────────────┘
-- 目前合并的数据超过100M的放置在disk_2的cold_volume上,新插入不足100M的放置在disk_1的hot_volume上
SELECT name,disk_name,path FROM system.parts WHERE (table = 'sample4') AND active;
┌─name──────────┬─disk_name─┬─path─────────────────────────────────────────────────────────┐
│ all_401_428_2 │ disk_2 │ /mnt/hdd1/clickhouse/data/default/sample4/all_401_428_2/ │
│ all_429_429_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_429_429_0/ │
│ all_430_430_0 │ disk_1 │ /mnt/fast_ssd/clickhouse/data/default/sample4/all_430_430_0/ │
└───────────────┴───────────┴──────────────────────────────────────────────────────────────┘
可以看到合并创建了一个很大的部分,该部分被放置在冷藏库disk_2中
该卷hot由一个SSD磁盘(fast_ssd)组成,该卷上可以存储的部件的最大大小为100MB。大小大于100MB的所有部件将直接存储在cold包含HDD磁盘的卷上disk1。
同样,一旦磁盘的fast_ssd容量超过90%,数据将disk1通过后台进程传输到cold包含HDD磁盘的卷上disk1。
根据新 part 大小的估计来确定放置新 part 的位置,可能与实际 part 大小不同。对于 insert 操作,使用未压缩的方式估算 part 大小。对于 merge 操作,ClickHouse 使用合并 parts 的压缩大小之和 + 10%。该估计值是近似值,可能并非在所有情况下都完全准确。我们可能会看到某些 parts 比慢速存储上的限制小一些,或者有些 parts 在快速存储上大一些。