ClickHouse - 多卷存储扩大存储容量(生产环境必备)

笔者最近工作有点忙,加上培训较多,近期文章更新慢了一拍。不过,今天为 ClickHouse 的爱好者带来一篇非常不错的文章,部分内容来自 Altinity,以及笔者补充和整理而成。

长期使用 ClickHouse 的用户都知道,每个 ClickHouse 服务都是单个进程,可访问位于单个存储设备上的数据。该设计提供了操作简便性,这是一个很大的优点,但将用户使用的所有数据限制在单一存储类中。缺点是难以选择成本/性能,特别是对于大型集群。

在 2019 年期间,Altinity 和 ClickHouse 社区一直在努力使 ClickHouse 表能够将存储划分为包含多个设备的卷,并在它们之间自动移动数据。从 19.15 版本开始,ClickHouse 开始实现多卷存储的功能。它具有多种用途,其中最重要的用途是将热数据和冷数据存储在不同类型的存储中。这种配置被称为分层存储,正确使用它可以极大地提高 ClickHouse 服务的经济性。

在接下来的文章中,笔者将介绍 ClickHouse 的多卷存储并展示其具体的工作方式。为了提供生产环境实战的参考,笔者将演示如何实现分层存储,并说明其他用途。

多卷存储的目的

假设有一个 ClickHouse 集群来处理保留30天数据的服务日志。用户从当天开始对数据进行 95% 的交互式查询。其余 5% 是用于趋势分析和机器学习的长期运行的批查询。因此,这里的选择是:使用快速的 SSD 存储在 3% 的数据上快速运行常见查询,或者使用便宜的 HDD 来满足其他 97% 的数据。快速的或便宜的,而不是两者兼而有之。

幸运的是,ClickHouse 提供了一个更好的方案:分层存储。

该想法是将新数据存储在 NVMe SSD 等快速存储中,然后再将其移动到速度较慢的存储(例如 HDD)中。这不仅节省了存储空间,而且通常可以减少服务器的总数而不牺牲性能,这是效率的双赢,可以将硬件成本降低多达80%。毫不奇怪,多年来分层存储一直是最重要的功能要求。

在多卷存储功能实现之前,用户借助 Merge 引擎和部分手动操作将一个逻辑表的数据存储在多个物理磁盘上。这种方法允许用户解决多层存储的问题,但这是一个非常不方便的解决方法。

ClickHouse 多卷存储使分层存储成为可能,甚至非常方便。我们可以将表数据存储在多个设备上,分为多个卷,甚至可以自动跨卷移动数据。多卷存储也解决了其他问题,例如,多卷存储还提供了一种简单的机制,可以通过附加新设备来扩展服务器容量。

ClickHouse 多卷存储架构

在进入示例之前,笔者先介绍一下 ClickHouse 中多卷存储的工作原理。一图胜千言,如图所示。 

该方案看似复杂,但实际上非常简单。每个 MergeTree 表都与一个存储策略( storage_policy)相关联。策略只是用于编写 MergeTree 表数据的规则,将磁盘分为一个或多个卷。它们还定义了每个卷中磁盘的写顺序,以及有多个卷时数据在卷之间的移动方式。

存储策略与旧表向后兼容。ClickHouse 始终有一个名为 default 的磁盘,该磁盘指向 config.xml 中的数据目录路径。还有一个相应的策略称为 default。如果 MergeTree 表没有存储策略,则 ClickHouse 将使用默认策略并写入默认磁盘。

接下来,笔者将通过示例来进行说明。

服务器设置

接下来,我们将多卷安装在具有多个块设备的 ClickHouse 服务器上。

首先,需要一台具有多个硬盘驱动器的服务器。对于此特定测试,我在 Ubuntu 18.04 (笔者生产环境为 CentOS)上连接了三个附加存储卷:

  • 2 x SSD 400GB

  • 1 x HDD 1000GB

可以使用方便的 lsblk 命令查看额外的块设备。

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
...
nvme0n1     259:0    0  400G  0 disk
nvme2n1     259:1    0   10G  0 disk
└─nvme2n1p1 259:3    0   10G  0 part /
nvme1n1     259:2    0 1000G  0 disk
nvme3n1     259:4    0  400G  0 disk

笔者生产环境磁盘数超过4块,官方建议使用 RAID-6 或 RAID-50,但是综合考虑实际情况笔者使用 RAID5。

使用 ext4 文件系统格式化磁盘并挂载它们。

sudo mkfs -t ext4 /dev/nvme0n1
sudo mkfs -t ext4 /dev/nvme1n1
sudo mkfs -t ext4 /dev/nvme3n1
sudo mkdir /mnt/ebs_gp2_1
sudo mkdir /mnt/ebs_gp2_2
sudo mkdir /mnt/ebs_sc1_1
sudo mount -o noatime,nobarrier /dev/nvme0n1 /mnt/ebs_gp2_1
sudo mount -o noatime,nobarrier /dev/nvme3n1 /mnt/ebs_gp2_2
sudo mount -o noatime,nobarrier /dev/nvme1n1 /mnt/ebs_sc1_1

为了使本示例简单起见,我们将跳过将卷添加到 fstab 的操作,因此重新启动后将不会自动挂载它们。

现在,我们来安装 ClickHouse。当前,笔者建议您使用最新的 19.17 稳定版本进行测试。(也可以使用更高版本,介于原作者实验,本文中的示例基于版本 19.16.3.6)

sudo apt-get install dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
echo "deb http://repo.yandex.ru/clickhouse/deb/stable/ main/" | sudo tee /etc/apt/sources.list.d/clickhouse.list
sudo apt-get update
sudo apt-get install -y clickhouse-server clickhouse-client
sudo systemctl start clickhouse-server
sudo systemctl status clickhouse-server
clickhouse-client --query='SELECT version()'
19.16.3.6

配置 ClickHouse 存储磁盘

安装完成后,ClickHouse 会使用默认配置,并将数据默认存储在 /var/lib/clickhouse/data 文件夹的根分区中。为了验证默认存储位置,我们可以使用 system.disks 表,该表显示 ClickHouse 使用的磁盘:

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/ │ 7.49 GiB │ 9.63 GiB │ 0.00 B   │
└─────────┴──────────────────────┴──────────┴──────────┴──────────┘

由于我们有可用的额外磁盘,因此现在配置 ClickHouse 并使用它们。我们将以下配置添加到 /etc/clickhouse-server/config.d/storage.xml

注意:

将存储配置放在 config.d 中,这可以防止它在升级时被删除。

<yandex>
  <storage_configuration>
    <disks>
      <!--
          default disk is special, it always
          exists even if not explicitly
          configured here, but you can't change
          it's path here (you should use <path>
          on top level config instead)
      -->
      <default>
         <!--
             You can reserve some amount of free space
             on any disk (including default) by adding
             keep_free_space_bytes tag
         -->
         <keep_free_space_bytes>1024</keep_free_space_bytes>
      </default>
      <ebs_gp2_1>
         <!--
         disk path must end with a slash,
         folder should be writable for clickhouse user
         -->
         <path>/mnt/ebs_gp2_1/</path>
      </ebs_gp2_1>
      <ebs_gp2_2>
          <path>/mnt/ebs_gp2_2/</path>
      </ebs_gp2_2>
      <ebs_sc1_1>
          <path>/mnt/ebs_sc1_1/</path>
      </ebs_sc1_1>
    </disks>
  </storage_configuration>
</yandex>

要应用更改,必须重新启动 ClickHouse。每次我们更改存储配置时都需要这样做。

$ sudo systemctl restart clickhouse-server
$ sudo systemctl status clickhouse-server
● clickhouse-server.service - ClickHouse Server (analytic DBMS for big data)
   Loaded: loaded (/etc/systemd/system/clickhouse-server.service; enabled; vendor preset: enabled)
   Active: activating (auto-restart) (Result: exit-code) since Mon 2019-11-04 15:02:06 UTC; 4s ago
  Process: 3146 ExecStart=/usr/bin/clickhouse-server --config=/etc/clickhouse-server/config.xml --pid-file=/run/clickhouse-server/clickhouse-server.pid (code=exited, status=70)
 Main PID: 3146 (code=exited, status=70)

糟糕,ClickHouse 无法启动,不过不要紧张,让我们检查一下日志:

sudo tail -n 10 /var/log/clickhouse-server/clickhouse-server.err.log
...
<Error> Application: DB::Exception: There is no RW access to disk ebs_gp2_1

错误提示很明显,就是忘记了将存储文件夹的权限授予 ClickHouse 用户。修复一下权限问题:

sudo chown clickhouse:clickhouse -R /mnt/ebs_gp2_1 /mnt/ebs_gp2_2 /mnt/ebs_sc1_1
sudo systemctl restart clickhouse-server
sudo systemctl status clickhouse-server

再次重启后,查看 ClickHouse 服务状态。

sudo systemctl status clickhouse-server
● clickhouse-server.service - ClickHouse Server (analytic DBMS for big data)
   Loaded: loaded (/etc/systemd/system/clickhouse-server.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2019-11-04 15:10:55 UTC; 2s ago
 Main PID: 3714 (clickhouse-serv)
    Tasks: 40 (limit: 4633)
   CGroup: /system.slice/clickhouse-server.service
           └─3714 /usr/bin/clickhouse-server --config=/etc/clickhouse-server/config.xml --pid-file=/run/clickhouse-server/clickhouse-server.pid
... systemd[1]: Started ClickHouse Server (analytic DBMS for big data).

好的,现在 ClickHouse 正在运行,我们可以看到所有的存储磁盘:

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/ │ 7.49 GiB   │ 9.63 GiB   │ 1.00 KiB │
│ ebs_gp2_1 │ /mnt/ebs_gp2_1/      │ 366.06 GiB │ 392.72 GiB │ 0.00 B   │
│ ebs_gp2_2 │ /mnt/ebs_gp2_2/      │ 372.64 GiB │ 392.72 GiB │ 0.00 B   │
│ ebs_sc1_1 │ /mnt/ebs_sc1_1/      │ 933.21 GiB │ 983.30 GiB │ 0.00 B   │
└───────────┴──────────────────────┴────────────┴────────────┴──────────┘

此时,我们有4个可用磁盘用于存储数据。

接下来笔者将正式介绍 ClickHouse 存储策略,并从简单配置扩展到复杂配置,这将帮助我们达到配置分层存储的目标。

存储策略

上面,笔者已经讲解了如何配置多个磁盘。但是,为了使用多卷存储还有更多工作要做。此时,表 MergeTree 数据仍存储在默认磁盘上(即 /var/lib/clickhouse/)。可以从 system.tables 查询信息:

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:       sample1
data_paths: ['/var/lib/clickhouse/data/default/sample1/']
SELECT name, disk_name, path FROM system.parts
   WHERE (table = 'sample1') AND active\G
Row 1:
──────
name:      all_1_1_0
disk_name: default
path:      /var/lib/clickhouse/data/default/sample1/all_1_1_0/

在不同磁盘上存储数据的规则由存储策略设置。在新安装或升级的 ClickHouse 服务中,有一个存储策略,称为 default,表示所有数据都应放置在默认磁盘上。此策略可确保对现有表的向后兼容性。我们可以通过从 system.storage_policies 表中查询来查看定义,该表用于帮助管理分层存储。

SELECT policy_name, volume_name, disks
FROM system.storage_policies
┌─policy_name────┬─volume_name─────────┬─disks─────────────────────┐
│ default        │ default             │ ['default']               │
└────────────────┴─────────────────────┴───────────────────────────┘

单磁盘策略

笔者将带大家从一个非常简单的示例开始,我们将引入名称为 ebs_gp2_1_only 的存储策略,该策略将所有数据存储在包含 ebs_gp2_1 磁盘的 ebs_gp2_1_volume 上。

我们已经所有与存储相关的配置放在文件 /etc/clickhouse-server/config.d/storage.xml 中。

以下示例显示了之前创建的磁盘配置:

<yandex>
  <storage_configuration>
    <disks>
      <!--
          default disk is special, it always
          exists even if not explicitly
          configured here, but you can't change
          it's path here (you should use <path>
          on top level config instead)
      -->
      <default>
         <!--
             You can reserve some amount of free space
             on any disk (including default) by adding
             keep_free_space_bytes tag
         -->
         <keep_free_space_bytes>1024</keep_free_space_bytes>
      </default>
      <ebs_gp2_1>
         <!--
         disk path must end with a slash,
         folder should be writable for clickhouse user
         -->
         <path>/mnt/ebs_gp2_1/</path>
      </ebs_gp2_1>
      <ebs_gp2_2>
          <path>/mnt/ebs_gp2_2/</path>
      </ebs_gp2_2>
      <ebs_sc1_1>
          <path>/mnt/ebs_sc1_1/</path>
      </ebs_sc1_1>
    </disks>
    <policies>
      <ebs_gp2_1_only> <!-- name for new storage policy -->
        <volumes>  
          <ebs_gp2_1_volume> <!-- name of volume -->
            <!--
                we have only one disk in that volume
                and we reference here the name of disk
                as configured above in <disks> p
            -->
            <disk>ebs_gp2_1</disk>
          </ebs_gp2_1_volume>
        </volumes>
      </ebs_gp2_1_only>
    </policies>
  </storage_configuration>
</yandex>

重新启动 ClickHouse 以应用配置更改并检查配置的新策略是否可见:

SELECT policy_name, volume_name, disks
FROM system.storage_policies
┌─policy_name────┬─volume_name─────────┬─disks─────────────────────┐
│ default        │ default             │ ['default']               │
│ ebs_gp2_1_only │ ebs_gp2_1_volume    │ ['ebs_gp2_1']             │
└────────────────┴─────────────────────┴───────────────────────────┘

太酷了,那么我们现在如何使用它们呢?

其实针对用户来说,使用起来很简单,只需在新表上添加即可:

SETTINGS storage_policy = 'ebs_gp2_1_only'

创建表的完整示例如下:

CREATE TABLE sample2 (id UInt64) Engine=MergeTree 
ORDER BY id SETTINGS storage_policy = 'ebs_gp2_1_only';
INSERT INTO sample2 SELECT * FROM numbers(1000000);

现在我们可以看到该表具有存储策略 ebs_gp2_1_only,并且所有 parts 都存储在 /mnt/ebs_gp2_1 上:

SELECT name, data_paths, metadata_path, storage_policy
FROM system.tables
WHERE name LIKE 'sample%'
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/ebs_gp2_1/data/default/sample2/']
metadata_path:     /var/lib/clickhouse/metadata/default/sample2.sql
Storage_policy:    ebs_gp2_1_only

根据查询 system.tables 显示的结果,可以看到两个表具有不同的数据路径和不同的存储策略。

请注意,表元数据保留在默认磁盘上。

我们还可以检查每个 part 的存储位置:

SELECT table, disk_name, path
FROM system.parts
WHERE table LIKE 'sample%'
Row 1:
──────
table:     sample1
disk_name: default
path:      /var/lib/clickhouse/data/default/sample1/all_1_1_0/
Row 2:
──────
table:     sample2
disk_name: ebs_gp2_1
path:      /mnt/ebs_gp2_1/data/default/sample2/all_1_1_0/

到此,我们知道了如何在另一个磁盘上存储表。

当然,无论源表和目标表位于何处,都可以使用常规的 INSERT ... SELECT 将数据从一个表复制到另一表。

JBOD:具有多个磁盘的单层卷

在前面的示例中,我们将数据存储在一个磁盘上。那么我们如何将数据存储在多个磁盘上?

我们可以使用存储策略在一个卷中将两个或多个磁盘分组。当我们这样做时,数据将以轮循(round-robin)方式在磁盘之间分配:

每次 insert(或 merge)都会在卷中的下一个磁盘上创建 part。parts 的一半存储在一个磁盘上,其余部分存储在另一个磁盘上。这个概念通常称为 JBOD,它是 “Just a Bunch of Disks” 的缩写。

JBOD 卷组织提供了以下好处:

1. 通过附加磁盘来扩展存储的简便方法,这比迁移到 RAID 简单得多。

2. 在某些情况下(例如,当多个线程并行使用不同的磁盘时),读/写速度会提高。

3. 由于每个磁盘上的 parts 数较少,因此表加载速度更快。

请注意,当使用 JBOD 时,一个磁盘的故障将导致数据丢失。添加更多磁盘会增加丢失至少一些数据的机会。当要求系统有容错能力时,请始终使用复制方式。

现在,让我们尝试将两个 SSD 磁盘连接到一个 JBOD 卷中。我们将以下存储策略添加到 storage.xml 文件中:

<ebs_gp2_jbod> > <!-- name for new storage policy -->
  <volumes>
    <ebs_gp2_jbod_volume> <!-- name of volume -->
       <!--
          the order of listing disks inside
          volume defines round-robin sequence
          -->
          <disk>ebs_gp2_1</disk>
          <disk>ebs_gp2_2</disk>
    </ebs_gp2_jbod_volume>
  </volumes>
</ebs_gp2_jbod>

我们重新启动 ClickHouse 并检查 system.storage_policies 中的存储策略。

SELECT policy_name, volume_name, disks
FROM system.storage_policies
┌─policy_name────┬─volume_name─────────┬─disks─────────────────────┐
│ default        │ default             │ ['default']               │
│ ebs_gp2_1_only │ ebs_gp2_1_volume    │ ['ebs_gp2_1']             │
│ ebs_gp2_jbod   │ ebs_gp2_jbod_volume │ ['ebs_gp2_1','ebs_gp2_2'] │
└────────────────┴─────────────────────┴───────────────────────────┘

让我们使用新的策略创建另外一张表:

CREATE TABLE sample3 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'ebs_gp2_jbod';
SELECT
    name,
    data_paths,
    metadata_path,
    storage_policy
FROM system.tables
WHERE name = 'sample3'
Row 1:
──────
name:           sample3
data_paths:     ['/mnt/ebs_gp2_1/data/default/sample3/','/mnt/ebs_gp2_2/data/default/sample3/']
metadata_path:  /var/lib/clickhouse/metadata/default/sample3.sql
storage_policy: ebs_gp2_jbod

接着,我们可以添加数据并检查 parts 存储的位置:

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 │ ebs_gp2_1 │ /mnt/ebs_gp2_1/data/default/sample3/all_1_1_0/ │
│ all_2_2_0 │ ebs_gp2_2 │ /mnt/ebs_gp2_2/data/default/sample3/all_2_2_0/ │
│ all_3_3_0 │ ebs_gp2_1 │ /mnt/ebs_gp2_1/data/default/sample3/all_3_3_0/ │
│ all_4_4_0 │ ebs_gp2_2 │ /mnt/ebs_gp2_2/data/default/sample3/all_4_4_0/ │
└───────────┴───────────┴────────────────────────────────────────────────┘

后台合并可以从不同磁盘上的 parts 收集数据,并将合并完成的新的较大 part 放在该卷的其中一个磁盘上(根据轮询算法)。我们可以通过运行 OPTIMIZE TABLE 强制合并来查看此示例中的行为。

OPTIMIZE TABLE sample3
Ok.
0 rows in set. Elapsed: 0.240 sec.
SELECT
    name,
    disk_name,
    path
FROM system.parts
WHERE (table = 'sample3') AND active
┌─name──────┬─disk_name─┬─path───────────────────────────────────────────┐
│ all_1_4_1 │ ebs_gp2_1 │ /mnt/ebs_gp2_1/data/default/sample3/all_1_4_1/ │
└───────────┴───────────┴────────────────────────────────────────────────┘

后台合并往往会随着时间的流逝创建越来越大的 parts,从而将每个生成的 part 移至其中一个磁盘。因此,我们的存储策略不能保证数据将均匀地分布在磁盘上,它也不能保证 JBOD 上的 I/O 吞吐量要比最慢的磁盘上的 I/O 吞吐量更好。为了获得这样的保证,应该改用 RAID。

使用 JBOD 存储策略的最明显原因是通过添加其他存储而不移动现有数据来增加 ClickHouse 服务的容量。

多层存储:具有不同优先级的卷

现在,我们准备研究多卷功能最有趣的用例,即分层存储的配置。

让我们回到本篇文章开始讨论的问题。我们有新插入的数据,这些数据被认为是热数据,并且经常访问,这需要快速但昂贵的存储。接下来,我们有冷数据。在批处理查询之外很少访问它,并且查询性能不是一个很大的考虑因素。冷数据可以存储在速度更慢、价格更低的存储上。

要在存储策略中实施此操作,我们需要定义以下内容:

  • 在初始插入时新数据应该存储在哪里

  • 何时将它移到较慢的存储空间

ClickHouse 19.15 版本使用基于 part 大小的启发式方法来确定何时在卷之间移动 part。在 ClickHouse 中,part 大小和 part 寿命通常是紧密相关的。MergeTree 引擎一直在进行后台合并,将新插入的数据和小的 part 随时间合并为越来越大的 part。这意味着大的 part 会在几次合并后出现,因此通常情况下,part 越大就越老。

注意:

我们还在研究另一种 age-based 的方法来跨层移动数据。它扩展了 TTL 机制,该机制目前用于删除 MergeTree 中的旧表行,例如30天之后。当该功能完成时,我们将能够使用 TTL 表达式在卷之间移动数据。

现在,使用以下存储策略配置分层存储。与之前操作一样,我们需要重新启动才能生效。

<ebs_hot_and_cold>
  <volumes>
    <hot_volume>
       <disk>ebs_gp2_2</disk>
        <!--
           that volume allowed to store only parts which
           size is less or equal 200Mb
        -->
        <max_data_part_size_bytes>200000000</max_data_part_size_bytes>
    </hot_volume>
    <cold_volume>
        <disk>ebs_sc1_1</disk>
        <!--
          that volume will be used only when the first
          has no space of if part size doesn't satisfy
          the max_data_part_size_bytes requirement of the
          first volume, i.e. if part size is greater
          than 200Mb
        -->
    </cold_volume>
  </volumes>
</ebs_hot_and_cold>

上述策略中卷的顺序非常重要。将新的 part 存储在磁盘上时,ClickHouse 首先尝试将其放置在第一个卷中,然后放置在第二个卷中,依此类推。

SELECT *
FROM system.storage_policies
WHERE policy_name = 'ebs_hot_and_cold'
Row 1:
──────
policy_name:        ebs_hot_and_cold
volume_name:        hot_volume
volume_priority:    1
disks:              ['ebs_gp2_2']
max_data_part_size:     200000000
move_factor:        0.1
Row 2:
──────
policy_name:        ebs_hot_and_cold
volume_name:        cold_volume
volume_priority:    2
disks:              ['ebs_sc1_1']
Max_data_part_size:    0
move_factor:        0.1

为了验证效果,创建一个使用新的分层存储配置的表。

CREATE TABLE sample4 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'ebs_hot_and_cold';
INSERT INTO sample4 SELECT rand() FROM numbers(10000000); 
-- repeat 10 times
SELECT
    disk_name,
    formatReadableSize(bytes_on_disk) AS size
FROM system.parts
WHERE (table = 'sample4') AND active
┌─disk_name─┬─size───────┐
│ ebs_gp2_2 │ 121.30 MiB │
│ ebs_gp2_2 │ 123.53 MiB │
│ ebs_gp2_2 │ 131.84 MiB │
│ ebs_gp2_2 │ 5.04 MiB   │
│ ebs_gp2_2 │ 2.74 MiB   │
└───────────┴────────────┘

目前,所有数据都是热数据,并存储在 SSD 上。

-- 重复执行该 INSERT 操作至少 8 次
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───────┐
│ ebs_sc1_1 │ 570.56 MiB │
│ ebs_gp2_2 │ 26.90 MiB  │
│ ebs_gp2_2 │ 29.08 MiB  │
│ ebs_gp2_2 │ 26.90 MiB  │
│ ebs_gp2_2 │ 5.04 MiB   │
│ ebs_gp2_2 │ 5.04 MiB   │
│ ebs_gp2_2 │ 5.04 MiB   │
│ ebs_gp2_2 │ 5.04 MiB   │
│ ebs_gp2_2 │ 2.74 MiB   │
└───────────┴────────────┘

可以看到合并时创建了一个很大的 part,该 part 被放置在冷的存储中。

根据新 part 大小的估计来确定放置新 part 的位置,可能与实际 part 大小不同。对于 insert 操作,使用未压缩的方式估算 part 大小。对于 merge 操作,ClickHouse 使用合并 parts 的压缩大小之和 + 10%。该估计值是近似值,可能并非在所有情况下都完全准确。我们可能会看到某些 parts 比慢速存储上的限制小一些,或者有些 parts 在快速存储上大一些。

手动移动 parts

除了后台合并过程中发生的自动移动之外,还可以使用新的 ALTER 命令语法手动移动 parts 和 partitions。

ALTER TABLE sample4 MOVE PART 'all_570_570_0' TO VOLUME 'cold_volume'
ALTER TABLE sample4 MOVE PART 'all_570_570_0' TO DISK 'ebs_gp2_1'
ALTER TABLE sample4 MOVE PARTITION tuple() TO VOLUME 'cold_volume'

同样,当一个卷的可用空间不足 10% 时,ClickHouse 在后台尝试通过将不足空间中的 part 移动到下一个卷来释放空间。我们可以通过更改 move_factor 来调整它,默认情况下为 0.1 = 10%。我们还可以通过使用 move_factor 为 0 来完全禁用自动后台移动。

后台移动以与后台合并相同的方式执行。当前,我们看不到正在运行某个移动,但是可以在 system.part_log 中看到移动的日志。

让我们看看它是如何工作的。

我们将启用 part_log 并设置一个很高的 move_factor 来查看会发生什么。

切记:

请勿在生产中这样做!

<ebs_hot_and_cold_movefactor99>
  <volumes>
    <hot_volume>
        <disk>ebs_gp2_2</disk>
        <max_data_part_size_bytes>200000000</max_data_part_size_bytes>
    </hot_volume>
    <cold_volume>
        <disk>ebs_sc1_1</disk>
    </cold_volume>
  </volumes>
  <move_factor>0.9999</move_factor>
</ebs_hot_and_cold_movefactor99>

如果想知道如何启用 part_log 表,请将以下配置添加到 /etc/clickhouse-server/config.d/part_log.xml 中:

<yandex>
  <part_log>
    <database>system</database>
    <table>part_log</table>
    <flush_interval_milliseconds>7500</flush_interval_milliseconds>
  </part_log>
</yandex>

现在创建一个表并加载一些数据。

CREATE TABLE sample5 (id UInt64) Engine=MergeTree ORDER BY id SETTINGS storage_policy = 'ebs_hot_and_cold_movefactor99';
INSERT INTO sample5 SELECT rand() FROM numbers(100);
SELECT event_type, path_on_disk
FROM system.part_log
┌─event_type─┬─path_on_disk───────────────────────────────────┐
│ NewPart    │ /mnt/ebs_gp2_2/data/default/sample5/all_1_1_0/ │
│ MovePart   │ /mnt/ebs_sc1_1/data/default/sample5/all_1_1_0/ │
└────────────┴────────────────────────────────────────────────┘

根据上面输出结果可以可以看到,insert 内容将新 part 放置在第1个磁盘上,但是后台进程看到,根据移动因子将 part 移动并将其移动到第2个磁盘上。

结合 JBOD 和分层存储

到目前为止,我们已经看到了如何使用一组磁盘(JBOD)创建卷以及如何在单个磁盘之间创建分层存储。在最后一个示例中,我们可以将这些概念组合起来以创建多个分层卷,每个分层卷都包含多个磁盘。

<three_tier>
  <volumes>
    <hot_volume>
      <disk>superfast_ssd1</disk>
      <disk>superfast_ssd2</disk>
      <max_data_part_size_bytes>200000000</max_data_part_size_bytes>
    </hot_volume>
    <jbod_volume>
      <disk>normal_hdd1</disk>
      <disk>normal_hdd2</disk>
      <disk>normal_hdd3</disk>
      <max_data_part_size_bytes>80000000000</max_data_part_size_bytes>
    </jbod_volume>
    <archive_volume>
      <disk>slow_and_huge</disk>
    </archive_volume>
  </volumes>
</three_tier>

总结

本文介绍了如何构建存储策略并应用它们以不同方式分发 MergeTree 表数据。笔者建议最有用的配置是启用分层存储。另外,正如我们所看到的,也可以使用存储策略来解决其他问题。

后续笔者将讨论多卷操作管理以及一些在多卷配置中可能遇到的特殊情况和问题。

参考

  • https://clickhouse.yandex/docs/en/single/#table_engine-mergetree-multiple-volumes

  • https://www.altinity.com/blog/2019/11/27/amplifying-clickhouse-capacity-with-multi-volume-storage-part-1?rq=multi%20user

  • https://www.altinity.com/blog/2019/11/29/amplifying-clickhouse-capacity-with-multi-volume-storage-part-2

  • 11
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
你可以使用 clickhouse-driver 库来将 DataFrame 数据传输到 ClickHouse 库中。具体步骤如下: 1. 首先,安装 clickhouse-driver 库。在终端输入以下命令: ``` pip install clickhouse-driver ``` 2. 在 Python 中导入 clickhouse-driver 库: ``` import clickhouse_driver ``` 3. 创建 ClickHouse 客户端对象,连接到 ClickHouse 服务器: ``` client = clickhouse_driver.Client('localhost') ``` 这里的 localhost 是 ClickHouse 服务器的地址,如果不在本机上,需要填写相应的 IP 地址。 4. 创建数据表。可以使用普通的 SQL 语句来创建数据表,例如: ``` client.execute('CREATE TABLE test (id Int32, name String) ENGINE = Memory') ``` 5. 将 DataFrame 转换为 ClickHouse 中的数据格式。clickhouse-driver 库提供了一个将 DataFrame 转换为 ClickHouse 格式的函数,例如: ``` data = [(1, 'Alice'), (2, 'Bob'), (3, 'Charlie')] columns = ['id', 'name'] df = pd.DataFrame(data, columns=columns) prepared_data = client.prepare_insert('test', df.columns) prepared_data.executemany(df.values) ``` 这里的 df 是一个 Pandas 的 DataFrame,data 是该 DataFrame 中的数据。使用 client.prepare_insert 函数,将 DataFrame 的列名传递给 ClickHouse。然后,使用 prepared_data.executemany 函数,将 DataFrame 中的数据插入到 ClickHouse 表中。 6. 查询数据。可以使用普通的 SQL 语句来查询数据,例如: ``` data = client.execute('SELECT * FROM test') ``` 这里的 data 是一个包含查询结果的列表。 这样,就可以在 Python 中将 DataFrame 数据传输到 ClickHouse 库中了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值