企业数据越存越多,存储容量与查询性能、以及存储成本之间的矛盾对于技术团队来说是个普遍难题。这个难题在 Elasticsearch 与 ClickHouse 这两个场景中尤为突出,为了应对不同热度数据对查询性能的要求,这两个组件在架构设计上就有一些将数据进行分层的策略。
同时,在存储介质方面,随着云计算的发展,对象存储以低廉的价格和弹性伸缩的空间获得了企业的青睐。越来越多的企业将温、冷数据迁移至对象存储。但如果将索引、分析组件直接对接至对象存储时会发生查询性能、兼容性等问题。
这篇文章将为大家介绍这两个场景中冷热数据分层的基本原理,以及如何通过使用 JuiceFS 来应对在对象存储上存在的问题。
01- Elasticsearch 数据分层结构详解
在介绍 ES 如何实现冷热数据分层策略之前先来了解三个相关的概念:Data Stream,Index Lifecycle Management 和 Node Role。
Data Stream
Data Stream(数据流)是 ES 中一个重要概念,它有如下特征:
- 流式写入:它是一个流式写入的数据集,而不是一个固定大小的集合;
- 仅追加写:它是用追加写的方式将数据更新进去,且不需要修改历史数据;
- 时间戳:每一条新增的数据都会有一个时间戳记录是什么时候产生的;
- 多个索引:在 ES 里有一个索引的概念,每一条数据最终会落到它对应的一个索引中,但是数据流是一个更上层、更大的概念,一个数据流背后可能会有很多索引,这些索引是根据不同的规则来生成的。一个数据流虽然由很多的索引来构成,但是只有最新的索引才是可写的,历史索引是只读的,一旦固化好之后就不能再修改。
日志数据就是符合数据流特征的一类数据,它是只追加写,同时也得有时间戳,用户会根据不同的维度,比如按天或者按其他的维度来生成新的索引。
下图是一个数据流建立索引的简单示例,在用数据流的过程中,ES 会直接写到最新的索引,而不是历史索引,历史索引不会被修改。随着后续更多新的数据生成,这个索引也会沉淀成为一个老的索引。
下图,当用户往 ES 里面去写数据时,大致分为两个阶段:
- 阶段 1:数据会先写到内存的 In-memory buffer 缓冲区;
- 阶段 2:缓冲区根据一定的规则和时间,再落到本地磁盘上,就是下图绿色的持久化的数据,在 ES 中叫做 Segment。
这个过程中可能会有一些时间差,在持久化的过程中,如果去触发查询, 新创建的Segment 不能被搜索到。一旦这个 Segment 持久化完成之后,就可以立即被上层的查询引擎搜索。
Index Lifecycle Management
Index Lifecycle Management,简称 ILM,就是索引的生命周期管理。ILM 将索引的生命周期定义为 5 个阶段:
- 热数据(Hot):需要频繁更新或者查询的数据;
- 温数据(Warm):不再更新,但仍会被频繁查询的数据;
- 冷数据(Cold):不再更新,且查询频率较低的数据;
- 极冷数据(Frozen):不再更新,且几乎不会被查询的数据。可以比较放心地把这类数据放在一个相对最低速最便宜的存储介质中;
- 删除数据(Delete) : 不再需要用到,可以放心删除的数据。
一个索引里的数据,不管是 index 还是 segment,都会经历这些阶段,这个分类的规则很好地帮助用户去管理 ES 里的数据,用户可以自己定义不同阶段的规则。
Node Role
在 ES 中,每一个部署节点都会有一个 Node Role,也就是节点角色。每一个 ES 节点会分配不同的角色,比如 master、data、ingest 等。用户可以结合节点角色,以及上文提到的不同生命周期的阶段来组合进行数据管理。
数据节点会有不同的阶段,可能是一个存储热数据的节点,也可能是一个存储温数据、冷数据,甚至极冷数据的节点。需要根据节点的功能去给他分配不同的角色,同时会给不同的角色的节点配置不同的硬件。
比如,对于热数据节点需要配置高性能的 CPU 或者磁盘,对于温冷数据的节点,基本上认为这些数据被查询的频率较低,这个时候其实对于某些计算资源的硬件要求就没有那么高了。
节点角色是根据生命周期的不同阶段来定义的,需要注意的一点是,每一个 ES 节点,可以有多种角色,这些角色并不是一一对应的关系。下面有个示例,在 ES 的 YAML 文件里面配置的时候,node.roles 就是节点角色的配置,可以针对这个节点应该有的角色给它配置多种角色。
代码语言:javascript复制node.roles: ["data_hot", "data_content"]
生命周期策略
在了解完 Data Stream 、Index Lifecycle Management、Node Role 这些概念以后,就可以为数据创建一些不同的生命周期策略(Lifecycle Policy)。
根据生命周期策略中定义的不同维度的索引特征,如索引的大小、索引里的文档的数量、索引创建的时间,ES 可以自动地帮用户把某个生命周期阶段的数据滚动到另一个阶段,在 ES 中的术语是 rollover。
比如,用户可以制定基于索引大小维度的特征,把热数据滚动到温数据,或者根据一些其它规则,再把温数据滚动到冷数据。这样,索引在不同生命周期的阶段之间去滚动的时候,相应的它索引的数据也会去做迁移和滚动。ES 可以自动完成这些工作,但是生命周期策略则需要用户自己来定义。
下面的截图,是 Kibana 的管理界面,用户可以通过图形化的方式去配置生命周期策略。可以看到有三个阶段,从上到下分别是热数据、温数据以及冷数据。
展开其中热数据阶段的高级设置,可以看到更详细,上文提到的基于不同维度特征的策略配置,如在下图右边看到的这三个选项。
- 索引的大小,示意图上的例子是 50GB,当索引的大小超过 50GB 的时候,就会把它从热数据阶段滚动到温数据阶段。
- 最大的文档数,ES 里索引的单元是文档,用户数据是以文档的形式写入 ES 中的,所以文档数也是一个可以衡量的指标。
- 最大索引创建时间,这里的示例是 30 天,假设某个索引已经创建了 30 天了,这个时候就会触发刚刚提到的从热数据阶段到温数据的滚动。
02- ClickHouse 数据分层架构详解
下图是一组从大到小的俄罗斯套娃,它非常形象地展现了 ClickHouse 的数据管理模式, MergeTree 引擎。
- Table: 在图片的最右边是一个最大的概念,用户最开始要创建或者能够直接接触到的就是 Table;
- Partition:是一个更小的维度或者更小的粒度。在 ClickHouse 里,数据分成 Partition 来存储,每个 Partition 会有一个标识;
- Part:在每个 Partition 中,又会再进一步地细分为多个 Part。如果查看 ClickHouse 磁盘上存储的数据格式,可以认为每一个子目录就是一个 Part;
- Column:在 Part 里会看到一些更小粒度的数据,即 Column。ClickHouse 的引擎使用的是列式存储,所有的数据都是按照列存的方式来组织。在 Part 目录里会看到很多列,比如 Table 可能有100 列,就会有 100 个 Column 文件;
- Block:每个 Column 文件里是按照 Block 的粒度来组织。
下面这个示例中,在 table 目录下可以看到有 4 个子目录,每个子目录就是上文提到的 Part。
代码语言:javascript复制$ ls -l /var/lib/clickhouse/data/<database>/<table>
drwxr-xr-x 2 test test 64B Aug 8 13:46 202208_1_3_0
drwxr-xr-x 2 test test 64B Aug 8 13:46 202208_4_6_1
drwxr-xr-x 2 test test 64B Sep 8 13:46 202209_1_1_0
drwxr-xr-x 2 test test 64B Sep 8 13:46 202209_4_4_
图示的最右边这一列,每个子目录的名字前面可能是一个时间,比如 202208 类似这样的前缀,202208 其实就是 Partition 名。Partition 名字是用户自己来定义的,但是按照约定俗成或者一些实践习惯,通常会使用时间来命名。
比如, 202208 这个 Partition,它会有两个子目录,子目录就是 Part,一个 Partition 通常会由多个 Part 来构成。用户在往 ClickHoue 写入数据时,会先写到内存里,再根据内存里的数据结构,持久化到磁盘上。同一个Partition 里面的数据如果比较大的话,在磁盘上就会变成很多 part。ClickHouse 官方建议不要在一个 Table 下创建太多 Part,它会定期或者不定期地对 Part 进行合并,减少总的 Part 数量。Merge 的概念就是合并 Part,这也是 MergeTree 这个引擎的名字来源之一。
再通过一个例子来了解 Part。Part 里会有很多小文件,有一些是元信息,比如索引信息,帮助用户快速查找数据。
代码语言:javascript复制$ ls -l /var/lib/clickhouse/data/<database>/<table>/202208_1_3_0
-rw-r--r-- 1 test test ?? Aug 8 14:06 ColumnA.bin
-rw-r--r-- 1 test test ?? Aug 8 14:06 ColumnA.mrk
-rw-r--r-- 1 test test ?? Aug 8 14:06 ColumnB.bin
-rw-r--r-- 1 test test ?? Aug 8 14:06 ColumnB.mrk
-rw-r--r-- 1 test test ?? Aug 8 14:06 checksums.txt
-rw-r--r-- 1 test test ?? Aug 8 14:06 columns.txt
-rw-r--r-- 1 test test ?? Aug 8 14:06 count.txt
-rw-r--r-- 1 test test ?? Aug 8 14:06 minmax_ColumnC.idx
-rw-r--r-- 1 test test ?? Aug 8 14:06 partition.dat
-rw-r--r-- 1 test test ?? Aug 8 14:06 primary.id
在示例的右侧,以 Column 作为前缀的这些文件是实际的数据文件,相比元信息通常会比较大。这个示例中只有 A、B 两列,实际的表里可能有很多列。所有这些文件,包括元信息、索引信息,都会共同帮助用户快速地在不同文件之间去做跳转或者查找。
ClickHouse 存储策略
如果要在 ClickHouse 里做冷热数据分层,会用到类似于 ES 中提到的生命周期策略,在 ClickHouse 里称为存储策略(Storage Policy)。
与 ES 稍有不同,ClickHouse 官方并没有将数据划分不同的阶段,比如热数据、温数据、冷数据这些不同的阶段,ClickHouse 提供了一些规则和配置方法,需要用户自己来制定分层策略。
每个 ClickHouse 节点支持同时配置多块磁盘,存储介质可以是多种多样的。比如,一般用户为了性能会给 ClickHouse 节点配置 SSD 盘;对于一些温冷数据,用户可以把数据存储在成本更低的介质,如机械盘。ClickHouse 的用户对底层存储介质是无感知的。
与 ES 相似,ClickHouse 用户需要根据数据不同的维度特征去制定存储策略,比如每个 part 子目录的大小、整个磁盘的剩余空间比例等,当满足某个维度特征设定的条件时就会触发存储策略的执行。这个策略会将某一个 part 从一块盘迁移到另外一块盘。在 ClickHouse 中,一个节点配置的多块盘是有优先级的,默认情况下数据会优先落在最高优先级的盘上。这样实现了 Part 从一个存储介质转移到另外一个存储介质上。
通过 ClickHouse 的一些 SQL 命令,如 MOVE PARTITION/PART 命令可以手动触发数据迁移,用户也可以通过这些命令做一些功能性的验证。其次有某些情况下,可能也希望能够通过手动的方式,而不是自动转移的方式来显式把 part 从当前的存储介质上转移到另外一个存储介质上。
ClickHouse 还支持基于时间的迁移策略,这是一个独立于存储策略的概念。数据写入后,ClickHouse 会按照每个表的 TTL 属性设置的时间来触发磁盘上数据的迁移。比如设置 TTL 为 7 天,ClickHouse 就会把表中超过 7 天的数据从当前的磁盘(如默认的 SSD)再写到另外一个更低优先级的磁盘上(如 JuiceFS)。
03- 温冷数据存储:为什么使用对象存储 JuiceFS ?
企业把温、冷数据存放到云上后,存储成本相较于传统的 SSD 架构大为下降。企业还享受到了云上的弹性伸缩空间;不用为数据存储去做任何运维操作,比如扩缩容,或者一些数据清理类的工作。温冷数据所需的存储容量比热数据大很多,尤其是随着时间推移,会产生大量需要长期保存的数据,如果这些数据都存储在本地,相应的运维工作将不堪重负。
但如果在对象存储上使用 Elasticsearch、ClickHouse 这类数据应用组件,会存在写入性能差、兼容性等问题。希望兼顾查询性能的企业,开始在云上寻找解决方案。在这样的背景之下,JuiceFS 被越来越多地应用于数据分层的架构之中。
通过下面 ClickHouse 写入性能测试可以直观了解到写入SSD、JuiceFS 以及对象存储的性能差异。
JuiceFS 的写入吞吐量远大于直接对接对象存储,接近 SSD。当用户把热数据转移到温暖数据这一层时,对于写入性能也有一定要求。在迁移的过程中,如果底层存储介质的写入性能差,整个迁移的流程也会拖得很长,对于整个 pipeline 或数据管理也会带来一些挑战。
下图的 ClickHouse 查询性能测试使用真实业务中的数据,并选取几个典型的查询场景进行测试。其中 q1-q4 是扫描全表的查询,q5-q7 是命中主键索引的查询。测试结果如下图:
JuiceFS 与 SSD 盘的查询性能基本相当,平均差异在 6% 左右,但是对象存储相比 SSD 盘有 1.4 至 30 倍的性能下降。得益于 JuiceFS 高性能的元数据操作以及本地缓存特性,可以自动将查询请求需要的热数据缓存在 ClickHouse 节点本地,大幅提升了 ClickHouse 的查询性能。需要注意的是以上测试中对象存储是通过 ClickHouse 的 S3 磁盘类型进行访问,这种方式只有数据是存储在对象存储上,元数据还是在本地磁盘。如果通过类似 S3FS 的方式把对象存储挂载到本地,性能会有进一步的下降。
另外值得一提的是 JuiceFS 是一个完全兼容 POSIX 的文件系统,它能够与上层应用(如 Elasticsearch、ClickHouse)有很好的兼容。用户对底层存储是分布式文件系统或者是本地磁盘是没有感知的。如果直接使用对象存储,不能很好地实现与上层应用的兼容。
04- 实操:ES JuiceFS
Step 1:准备多种类型节点,分配不同角色。每一个 ES 节点可以分配不同的角色,比如存热数据、温数据、冷数据等,用户需要准备不同机型的节点来匹配不同角色的需求。
Step 2:挂载 JuiceFS 文件系统。一般用户将 JuiceFS 用于温、冷数据的存储,用户需要在 ES 温数据节点或冷数据的节点上把 JuiceFS 文件系统挂载到本地。用户可以通过符号链接或其它方式把挂载点配置到 ES 中去,让 ES 认为它的数据存储在本地目录里,但这个目录背后其实是一个 JuiceFS 文件系统。
Step 3:创建生命周期策略。这个需要每个用户自己去定制,用户既可以通过 ES API 去创建,也可以通过 Kibana 去创建,Kibana 提供了一些相对便捷的方式去创建和管理生命周期策略。
Step 4:为索引设置生命周期策略。创建完生命周期策略之后,用户需要把这个策略应用到索引上,也就是要为索引去设置刚刚创建好的策略。用户可以通过索引模板的方式,可以在 Kibana 里创建索引模板,也可以通过 index.lifycycle.name,显式通过 API 配置。
这里有几个小提示:
Tip 1:Warm 或 Cold 节点的副本数(replica)可以设置为 1。所有数据本质上都是放在 JuiceFS 上,它的底层是对象存储,因而数据的可靠性已经足够高了,所以在 ES 这边可以适当降低副本数,节省存储空间。
Tip 2:开启 Force merge 可能会导致节点 CPU 持续占用,酌情关闭。从热数据转移到温数据这个阶段时,ES 会将所有热数据索引对应的底层 segment 做合并。如果开启 Force merge 这个功能,ES 会先合并完这些 segment 以后,再把它存储到温数据的底层系统。然而合并 segment 是一个非常消耗 CPU 的过程,如果温数据的数据节点同时也需要承载一些查询请求,可以酌情关闭这个功能能,也就是原封不动地把数据保留下来,直接写到底层存储中。
Tip 3:Warm 或 Cold 阶段的索引可以设置为只读。在给温数据和冷数据阶段建立索引时,我们基本上可以认为这些数据是只读的,这些阶段的索引不会被修改。设置为只读可以适当降低温冷数据节点上的资源,比如内存可以释放一些,从而节省一些在温节点或者冷节点上的硬件资源。
05- 实操:ClickHouse JuiceFS
Step 1:在所有 ClickHouse 节点上挂载 JuiceFS 文件系统。这个路径可以是任意路径,因为 ClickHouse 会有一个配置文件去指向挂载点。
Step 2:修改 ClickHouse 配置,新增 JuiceFS 盘。在 ClickHouse 中把刚刚挂载好的 JuiceFS 文件系统挂载点添加进来,让 ClickHouse 可以识别这个新磁盘。
Step 3:新增存储策略,设定下沉数据规则。这个存储策略会根据用户的规则去不定期的、自动地将数据从默认磁盘上下沉到指定的,比如 JuiceFS 中。
Step 4:为特定表设置存储策略及 TTL。存储策略制定好之后,需要把这个策略应用到某一个表上。前期测试阶段和验证阶段,可以把用相对大一点的表去做测试和验证,如果用户希望基于时间维度来实现数据下沉,就同时也需要在表上设置 TTL。整个下沉过程是一个自动的机制,可以通过 ClickHouse 的 system 表查看当前正在进行数据迁移的 part 以及迁移进度。
Step 5:手动移动 part 进行验证。可以通过手动执行 MOVE PARTITION
命令的方式去验证当前的配置或存储策略是否生效。
下图是一个具体示例,在 ClickHouse 中有一个叫做 storage_configuration
的配置项,其中包含 disks 配置,这里会把 JuiceFS 作为一个盘加进来,我们将它命名为“jfs”,但其实可以用任意名字,挂载点是 /jfs
目录。
<storage_configuration> <disks> <jfs> <path>/jfs</path> </jfs> </disks> <policies> <hot_and_cold> <volumes> <hot> <disk>default</disk> <max_data_part_size_bytes>1073741824</max_data_part_size_bytes> </hot> <cold> <disk>jfs</disk> </cold> </volumes> <move_factor>0.1</move_factor> </hot_and_cold> </policies></storage_configuration>
再往下是 policies 配置项,这里定义了一个叫做 hot_and_cold
的存储策略,用户需要定义一些具体的规则,如 volumes 中按照先热后冷的优先级排列,数据首先会落到 volumes 里的第一个 hot 盘上,及默认的 ClickHouse 磁盘,一般是本地的 SSD。
volumes 中的 max_data_part_size_bytes
配置表示当某一个 part 的大小超过设定的大小之后,就会触发存储策略的执行,对应的 part 会下沉到下一个 volume,也就是 cold volume。在上面的示例中,cold volume 就是 JuiceFS。
最下面的 move_factor
配置代表 ClickHouse 会根据当前磁盘的剩余空间比例来触发存储策略的执行。
CREATE TABLE test ( d DateTime, ...) ENGINE = MergeTree...TTL d INTERVAL 1 DAY TO DISK 'jfs'SETTINGS storage_policy = 'hot_and_cold';
如上面的代码所示,有了存储策略之后,在创建表或者修改这个表的 schema 时,可以在 SETTINGS 中设置 storage_policy 为前面定义的 hot_and_cold 存储策略。上述代码中倒数第二行的 TTL 即为上文提过的基于时间的分层规则。在这个示例中,我们指定的表中某一个叫做 d 的列,它的类型是 DateTime,结合 INTERVAL 1 DAY 就表示当新的数据写进来超过一天之后,这些数据就会转移到 JuiceFS 上。
06- 展望
第一,副本共享。无论是 ES 还是 ClickHouse,他们都是由多副本来保证数据的可用性和可靠性。JuiceFS 本质上是一个共享文件系统,任何一份数据写入到 JuiceFS 之后,不再需要维护多个副本。比如,用户有两个 ClickHouse 节点,都有某一个表的或者某一个 part 的副本,这两个节点都下沉到了 JuiceFS,它可能会写两次一样的数据。未来,我们是否可以做到让上层引擎能够感知到下层使用的是一个共享存储,当数据下沉的时候去降低副本数,这样在不同节点之间是可以做副本共享的。从应用层来说,用户查看这个表, part 数还是多副本,但实际在底层的存储上只保了一个副本,因为本质上数据是可以共享的。
第二点,故障恢复。当数据已经下沉到一个远端的共享存储之后,如果 ES 或 ClickHousle 节点宕机故障之后,怎么快速地做故障恢复?除了热数据以外的大部分数据其实都已经转移到了一个远端的共享存储上,这个时候如果要去恢复或创建一个新的节点时,成本会比传统的基于本地盘的故障恢复方式轻量很多,这在 ES 或者 ClickHouse 场景上是值得探索的。
第三点,存算分离。不管 ES 也好,还是 ClickHouse,整个社区也都在尝试或者探索在云原生的大环境下,怎么去让传统的这些基于本地盘的存储系统变成一个真正的存算分离系统。但存算分离不是仅仅简单地把数据和计算分离就好了,同时要满足上层各种复杂的需求,比如对于查询性能的需求、对于写入性能的需求、对各种维度调优的需求,在存量分离整个大的方向上还是有许多值得探索的技术难点。
第四点,其他上层应用组件数据分层探索。除了ES 和 ClickHouse 这两个场景,我们最近也有在做一些尝试,把 Apache Pulsar 中的温冷数据下沉到 JuiceFS 中,用到的一些策略和方案与本文中提到的是类似的,只不过在 Apache Pulsar 中,它需要下沉的数据类型或者数据格式不太一样。有了进一步成功实践后,会分享出来。
相关阅读:
- JuiceFS 在携程海量冷数据场景下的实践
- Shopee x JuiceFS: ClickHouse 冷热数据分离存储架构与实践