关于数据库:Shopee-ClickHouse-冷热数据分离存储架构与实践

2次阅读

共计 11305 个字符,预计需要花费 29 分钟才能阅读完成。

本文首发于微信公众号“Shopee 技术团队”。

摘要

Shopee ClickHouse 是一款基于开源数据库 ClickHouse 做二次开发、架构演进的高可用分布式剖析型数据库。本文将次要介绍 Shopee ClickHouse 的冷热拆散存储架构和反对公司业务的实际。

Shopee ClickHouse 的冷热拆散存储架构应用 JuiceFS 客户端 mount 远端对象存储到本地机器门路,通过编写 ClickHouse 的存储策略,如同应用多卷存储一样应用远端对象存储。因为咱们用同一个 ClickHouse DB 集群反对多个团队的业务,不同团队甚至雷同团队的不同业务之间对数据的冷热划分基准可能都不同,所以在做冷热拆散时策略须要做到 ClickHouse 的表级别。

为了做到表级别的冷热拆散,咱们按照提前编辑好的存储策略,针对存量须要做冷热隔离的业务表,批改表的存储策略。对于新的须要做冷热拆散的业务表,建表时指明应用反对数据落在远端存储的存储策略,再通过细化 TTL 表达式判断数据应该落在本地还是远端。

冷热拆散存储架构上线后,咱们遇到了一些问题和挑战,比方:juicefs object request error、Redis 内存增长异样、suspicious broken parts 等。本文会针对其中一些问题,联合场景上下文,并通过源码剖析来给出解决方案。

总的来说 Shopee ClickHouse 冷热存储架构的整体设计思维是:本地 SSD 存储查问热数据,远端存储查问绝对不那么频繁的数据,从而节约存储老本,反对更多的数据存储需要。

1. Shopee ClickHouse 集群总架构

ClickHouse 是一款开源的列存 OLAP(在线剖析查问)型数据库,实现了向量化执行引擎,具备优良的 AP 查问性能。Shopee ClickHouse 则是基于 ClickHouse 继续做二次迭代开发和产品架构演进的剖析型数据库。

下图展现了 Shopee ClickHouse DB 集群的架构:

从上到下顺次是用户申请染指 SLB、Proxy 层、ClickHouse DB 集群层,最下方是远端对象存储,这里咱们用的是 Shopee STO 团队提供的 S3。

其中,SLB 提供用户申请路由 Proxy 层提供了查问路由,申请会依据用户连贯串中的集群名,路由到对应的集群中,也提供了局部写入 balance 和查问路由的能力;ClickHouse DB 集群层是由 Shopee ClickHouse 数据库组成的分布式集群,目前有以 SSD 磁盘作为热数据存储介质的计算型分布式集群,和计算型单节点集群,还有以 SATA Disk 作为存储介质的存储型分布式集群; 最下方的远端存储则用作冷数据存储介质

2. 冷热拆散存储架构计划

用户心愿数据能够存储得更多更久,查问速度更快。然而通常数据存储得越多,在雷同查问条件下,返回延时就会越高。

从资源利用率上来说,咱们心愿存储在 Shopee ClickHouse 上的数据能够被更多地拜访和利用,为业务提供更宽泛的反对。所以,起初咱们要求业务方存储到 Shopee ClickHouse 数据库中的数据是用户的业务热数据。

然而这样也带来了一些问题,比方:用户有时候须要查问工夫绝对久一点的数据做剖析,这样就得把那局部不在 ClickHouse 的数据导入后再做剖析,剖析完结后还要删除这部分数据。再比方:一些通过日志服务做聚合剖析和检索剖析的业务,也须要绝对久一点的日志服务数据来帮忙监管和剖析日常业务。

基于此类需要,咱们一方面心愿资源的最大化利用,一方面心愿反对更多的数据存储量,同时不影响用户热数据的查问速度,所以应用 冷热数据拆散的存储架构 就是一个很好的抉择。

通常,冷热拆散计划的设计须要思考以下几个问题:

  • 如何存储冷数据?
  • 如何高效稳固简略地应用冷存介质?
  • 热数据如何下沉到冷存介质?
  • 架构的演进如何不影响现有的用户业务?

而冷数据存储介质的抉择个别通过以下几个要点做比照剖析:

  • 老本
  • 稳定性
  • 功能齐全(数据在下沉过程中仍然能够被正确查问,数据库的数据也能够被正确写入)
  • 性能
  • 扩展性

2.1 冷存介质的抉择和 JuiceFS

能够用作冷存储的介质个别有 S3、Ozone、HDFS、SATA Disk。其中,SATA Disk 受限于机器硬件,不易扩大,能够先淘汰。而 HDFS、Ozone 和 S3 都是比拟好的冷存介质。

同时,为了高效简略地应用冷存介质,咱们把眼光锁定在了 JuiceFS 上。JuiceFS 是一种基于 Redis 和云对象存储构建的开源 POSIX 文件系统,能够使咱们更加便捷和高效地拜访远端对象存储。

JuiceFS 应用私有云中已有的对象存储,如 S3、GCS、OSS 等。用 JuiceFS 做存储,数据实际上存储在远端,而 JuiceFS 重点关注这些存储在远端的数据文件的元数据管理。JuiceFS 抉择 Redis 作为存储元数据的引擎,这是因为 Redis 存储都在内存中,能够满足元数据读写的低延时和高 IOPS,反对乐观事务,满足文件系统对元数据操作的原子性[1]。

JuiceFS 提供了一种高效便捷的远端存储拜访形式,只须要通过 JuiceFS 的客户端,应用 formatmount 命令,就能够将远端存储 mount 到本地门路。咱们 ClickHouse 数据库拜访远端存储就能够如同拜访本地门路一样拜访。

抉择了 JuiceFS 后,咱们再把眼光转回冷数据存储介质的筛选。因为 JuiceFS 次要反对的后盾存储层为对象存储类别,余下的选项变成了 S3 和 Ozone。咱们设计了一个如下的 benchmark , 应用 ClickHouse TPCH Star Schema Benchmark 1000s(benchmark 详细信息能够参照 ClickHouse 社区文档[2])作为测试数据,别离测试 S3 和 Ozone 的 Insert 性能,并应用 Star Schema Benchmark 的 select 语句做查问性能比照。

查问的数据处于以下三种存储状态:

  • 一部分在 Ozone/S3,一部分在本机 SSD 磁盘;
  • 全副在 Ozone/S3;
  • 全副在 SSD 上。

以下是咱们的测试抽样后果:

(1)Insert 性能抽样后果

Insert Lineorder 表数据到 Ozone:

Insert Lineorder 表数据到 S3:

能够看出,S3 的 Insert 性能略微强势一点

(2)查问性能抽样后果

依据 ClickHouse Star Schema Benchmark,在导入结束 Customer、Lineorder、Part、Supplier 表后,须要依据四张表的数据创立一个打平的宽表。

CREATE TABLE lineorder_flat  
ENGINE = MergeTree  
PARTITION BY toYear(LO_ORDERDATE)  
ORDER BY (LO_ORDERDATE, LO_ORDERKEY)  
AS  
SELECT  
l.LO_ORDERKEY AS LO_ORDERKEY,  
l.LO_LINENUMBER AS LO_LINENUMBER,  
l.LO_CUSTKEY AS LO_CUSTKEY,  
l.LO_PARTKEY AS LO_PARTKEY,  
l.LO_SUPPKEY AS LO_SUPPKEY,  
l.LO_ORDERDATE AS LO_ORDERDATE,  
l.LO_ORDERPRIORITY AS LO_ORDERPRIORITY,  
l.LO_SHIPPRIORITY AS LO_SHIPPRIORITY,  
l.LO_QUANTITY AS LO_QUANTITY,  
l.LO_EXTENDEDPRICE AS LO_EXTENDEDPRICE,  
l.LO_ORDTOTALPRICE AS LO_ORDTOTALPRICE,  
l.LO_DISCOUNT AS LO_DISCOUNT,  
l.LO_REVENUE AS LO_REVENUE,  
l.LO_SUPPLYCOST AS LO_SUPPLYCOST,  
l.LO_TAX AS LO_TAX,  
l.LO_COMMITDATE AS LO_COMMITDATE,  
l.LO_SHIPMODE AS LO_SHIPMODE,  
c.C_NAME AS C_NAME,  
c.C_ADDRESS AS C_ADDRESS,  
c.C_CITY AS C_CITY,  
c.C_NATION AS C_NATION,  
c.C_REGION AS C_REGION,  
c.C_PHONE AS C_PHONE,  
c.C_MKTSEGMENT AS C_MKTSEGMENT,  
s.S_NAME AS S_NAME,  
s.S_ADDRESS AS S_ADDRESS,  
s.S_CITY AS S_CITY,  
s.S_NATION AS S_NATION,  
s.S_REGION AS S_REGION,  
s.S_PHONE AS S_PHONE,  
p.P_NAME AS P_NAME,  
p.P_MFGR AS P_MFGR,  
p.P_CATEGORY AS P_CATEGORY,  
p.P_BRAND AS P_BRAND,  
p.P_COLOR AS P_COLOR,  
p.P_TYPE AS P_TYPE,  
p.P_SIZE AS P_SIZE,  
p.P_CONTAINER AS P_CONTAINER  
FROM lineorder AS l  
INNER JOIN customer AS c ON c.C_CUSTKEY = l.LO_CUSTKEY  
INNER JOIN supplier AS s ON s.S_SUPPKEY = l.LO_SUPPKEY  
INNER JOIN part AS p ON p.P_PARTKEY = l.LO_PARTKEY

再执行这条 SQL 语句,当数据全副在 Ozone 上时,产生了如下 Error:

Code: 246. DB::Exception: Received from localhost:9000. DB::Exception: Bad size of marks file '/mnt/jfs/data/tpch1000s_juice/customer/all_19_24_1/C_CUSTKEY.mrk2': 0, must be: 18480

Select 数据一部分在 Ozone,并且此过程中产生了数据从 SSD 磁盘下沉到 Ozone 的状况。

后果:Hang 住,无奈查问。

做这个测试时,咱们应用的 Ozone 是社区版本 1.1.0-SNAPSHOT,此次测试后果仅阐明 Ozone 1.1.0-SNAPSHOT 不是很适宜咱们的应用场景。

因为 Ozone 1.1.0-SNAPSHOT 在咱们的应用场景中有功能性的毛病,所以后续的 Star Schema Benchmark 的性能测试报告重点放在 SSD 和 S3 的性能比照上(具体 Query SQL 语句能够从 ClickHouse 社区文档获取)。

Query No. Query Latency Data on JuiceFS Query Latency Data on ⅓ JuiceFs + ⅔ SSD Query Latency Data on SSD
Q1.1 8.884 s 8.966 s 1.417 s
Q1.2 0.921 s 0.998 s 0.313 s
Q1.3 0.551 s 0.611 s 0.125 s
Q2.1 68.148 s 36.273 s 5.450 s
Q2.2 54.360 s 20.846 s 4.557 s
Q2.3 55.329 s 22.152 s 4.297 s
Q3.1 60.796 s 27.585 s 7.999 s
Q3.2 67.559 s 29.123 s 5.928 s
Q3.3 45.917 s 20.682 s 5.606 s
Q3.4 0.675 s 0.202 s 0.188 s
Q4.1 100.644 s 41.498 s 7.019 s
Q4.2 32.294 s 2.952 s 2.464 s
Q4.3 33.667 s 2.813 s 2.357 s

最终,在各个方面的比照下,咱们抉择 S3 作为冷存介质

因而,冷热存储拆散的计划采纳 JuiceFS+S3 实现,下文将简述实现过程。

2.2 冷热数据存储拆散的实现

首先,咱们通过应用 JuiceFS 客户端,mount S3 bucket 到本地存储门路 /mnt/jfs,而后编辑 ClickHouse 存储策略配置 ../config.d/storage.xml 文件。编写存储策略配置文件时要留神,不要影响到历史用户存储(即保留之前的存储策略)。在这里,default 就是咱们的历史存储策略,hcs_ck 是冷热拆散的存储策略。

详细信息能够参照下图:

有须要冷热拆散存储的业务,只须要在建表 Statement 外面写明存储策略为 hcs_ck,而后通过 TTL 的表达式来管制冷数据下沉策略。

上面通过一个例子阐明应用形式和数据拆散过程。表 hcs_table_name 是一个须要冷热存储拆散的业务日志数据表,以下是建表语句:

CREATE TABLE db_name.hcs_table_name  
(  
    .....  
    `log_time` DateTime64(3),  
    `log_level` String,  
    .....  
    `create_time` DateTime DEFAULT now())  
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/db_name.hcs_table_name', '{replica}')  
PARTITION BY toYYYYMMDD(log_time)  
ORDER BY (ugi, ip)  
TTL toDateTime(log_time) TO VOLUME 'v_ssd',  
        toDateTime(log_time) + toIntervalDay(7) TO VOLUME 'v_cold',  
        toDateTime(log_time) + toIntervalDay(14)  
SETTINGS index_granularity = 16384,  
                   storage_policy = 'hcs_ck',   
                    parts_to_throw_insert = 1600

通过 TTL 表达式能够看到,hcs_table_name 这个表指明最近 7 天的数据存储在本地 SSD 磁盘,第 8 到 14 天的数据存储在远端 S3,超过 14 天的数据过期删除。

大体流程如下图所示:

hcs_table_name 的 data parts(ClickHouse 的数据存储以 data part 为根本解决单位)会被后台任务调度,后台任务由线程 BgMoveProcPool 执行,这个线程来自 back_ground_move_pool(留神和 back_ground_pool 不是同一个)。

std::optional<BackgroundProcessingPool> background_move_pool; /// The thread pool for the background moves performed by the tables.

后台任务调度会判断 data parts 是否须要 move(数据是否须要下沉挪动到远端存储上)和是否能够 move。

如果须要执行 move,后盾 move_pool 会创立一个 move 的 task。这个 task 的外围逻辑是:首先抉择须要 move 的 data parts,而后再 move 这些 data parts 到目标存储。

在接口:

MergeTreePartsMover::selectPartsForMove

中依据 TTL Expression 表达式获取 ttl_entry,而后依据 data parts 中的 ttl_move 信息,选出须要 move 的 data parts,存储 data parts 的 move_entry(蕴含 IMergeTreeDataPart 指针和须要预留的存储空间大小)到 vector 中。之后会调用接口:

MergeTreeData::moveParts

实现 move 操作,move 的过程简略来说就是 clone SSD 磁盘上的 data parts 到远端存储 S3 上 hcs_table_name 表的 detach 目录下,而后再从 detach 目录下把 data parts 移出来,最初这些在 SSD 磁盘上的 data parts 会在 IMergeTreeDataPart 的析构函数中被革除。

所以整个 move 过程中,表始终是可查的,因为是 clone 操作,同一时刻下 move 的 data parts 要么在 SSD 磁盘上为 active,要么在远端存储上为 active。

对于表 data parts 的 move 信息,也能够查问零碎表 system.parts 的以下三个字段:

move_ttl_info.expression;move_ttl_info.min; 
move_ttl_info.max; 

3. 实际分享

在 Shopee ClickHouse 冷热数据拆散存储架构上线后,咱们总结了一些实际中遇到的问题。

3.1 Redis 内存增长异样

S3 上的数据存储量并没有减少太多,Redis 内存却继续高速增长。

JuiceFS 应用 Redis 存储 S3 上的数据文件的元数据,所以失常状况下,S3 上的数据文件越多,Redis 存储使用量也就越多。个别这种异常情况是因为指标表有很多小文件没有 merge 而间接下沉,很容易打满 Redis。

这也会引入另一个问题:一旦 Redis 内存打满,JuiceFS 就不能再胜利写数据到 S3 上,如果 unmount 掉 JuiceFS 客户端,也无奈再次胜利 mount 下来,再次 mount 的时候会抛 Error:

Meta: create session: OOM command not allowed when used memory > 'maxmemory'.

要防止这种问题产生,首先应该做好 ClickHouse merge 状态的监控。clickhouse-exporter 会采集一个 merge 指标 clickhouse_merge,这个指标会采集到以后正在触发的 merge 个数(通过查问 system.metrics 表 metric=‘merge’),每触发一次 merge 会有一个表的多个 data parts 做合并操作。依照咱们的教训来看,若每三个小时 merge 的均匀次数小于 0.5,那么很有可能是这台机器的 merge 呈现了问题。

而 merge 异样的起因可能有很多(例如 HTTPHandler 线程、ZooKeeperRecv 线程继续占据了大量 CPU 资源等),这个不是本文的介绍重点,在此不再开展。所以能够设置告警规定,如果三小时内 merge 次数小于 0.5 次,告警给 ClickHouse 的开发运维团队同学,防止大量小文件产生。

如果曾经有大量小文件下沉到 S3 应该怎么办

首先要阻止数据持续下沉,能够通过两种形式找到有大量小文件下沉的用户业务表。

第一种形式:查看 ClickHouse 的 Error Log,找到抛 too many parts 的表,再进一步判断抛 Error 的表是否有冷热存储。

第二种形式:通过查问 system.parts 表,找出 active parts 显著过多,并且 disk_name 等于冷存的别名的。定位到产生大量小文件的表后,通过 ClickHouse 系统命令 SQL:

SYSTEM STOP MOVES [[db.]merge_tree_family_table_name] 

进行数据持续下沉,防止 Redis 内存打满。

如果表比拟小,比方压缩后小于 1TB(这里的 1TB 是一个经验值,咱们已经应用 insert into ... select * from … 形式导表数据,如果大于 1TB,导入工夫会很久,还有肯定的可能性在导入中途失败),在确认 merge 性能恢复正常后,能够抉择创立 temp table > insert into this temp table > select * from org table,而后 drop org table > rename temp table to org table。

如果表比拟大,确认 merge 性能恢复正常后,尝试通过系统命令 SQL:

SYSTEM START MERGES [[db.]merge_tree_family_table_name]

唤醒 merge 线程。如果 merge 进行迟缓,能够查问 system.parts 表,找到曾经落在 S3 上的 data parts,而后手动执行 Query:

ALTER TABLE table_source MOVE PART/PARTITION partition_expr TO volume 'ssd_volume'

将落在 S3 上的小文件移回到 SSD 上。因为 SSD 的 IOPS 比 S3 要高很多(即便是通过 JuiceFS 拜访减速后),这样一方面放慢 merge 过程,一方面因为文件移出 S3,会开释 Redis 内存。

3.2 JuiceFS 读写 S3 失败

数据下沉失败,通过 JuiceFS 拜访 S3,无奈对 S3 进行读写操作,这个时候用户查问如果笼罩到数据在 S3 上的,那么查问会抛 S3 mount 的本地门路上的数据文件无法访问的谬误。遇到这个问题能够查问 JuiceFS 的日志。

JuiceFS 的日志在 Linux CentOS 中存储在 ​​syslog 上,查问日志能够用办法 cat/var/log/messages|grep 'juicefs',不同操作系统对应的日志目录能够参照 JuiceFS 社区文档[3]。

咱们遇到的问题是 send request to S3 host name certificate expired。起初通过分割 S3 的开发运维团队,解决了拜访问题。

那么如何监控这类 JuiceFS 读写 S3 失败的状况呢?能够通过 JuiceFS 提供的指标 juicefs_object_request_errors 监控,如果呈现 Error 就告警团队成员,及时查问日志定位问题。

3.3 clickhouse-server 启动失败

对历史表须要做冷热数据存储拆散的复制表(表引擎含有 Replicated 前缀)批改 TTL 时,clickhouse-server 本地 .sql 文件元数据中的 TTL 表达式和 ZooKeeper 上存储的 TTL 表达式不统一。这个是咱们在测试过程中遇到的问题,如果没有解决这个问题而重启 clickhouse-server 的话,会因为表构造没有对齐而使 clickhouse-server 启动失败。

这是因为对复制表的 TTL 的批改是先批改 ZooKeeper 内的 TTL,而后才会批改同一个节点下的机器上表的 TTL。所以如果在批改 TTL 后,本地机器 TTL 还没有批改胜利,而重启了 clickhouse-server,就会产生上述问题。

3.4 suspicious_broken_parts

重启 clickhouse-server 失败,抛出 Error:

DB::Exception: Suspiciously many broken parts to remove

这是因为 ClickHouse 在重启服务的时候,会从新加载 MergeTree 表引擎数据,次要代码接口为:

MergeTreeData::loadDataParts(bool skip_sanity_checks) 

在这个接口中会获取到每一个表的 data parts,判断 data part 文件夹下是否有 #DELETE_ON_DESTROY_MARKER_PATH 也就是 delete-on-destroy.txt 文件存在。如果有,将该 part 退出到 broken_parts_to_detach,并将 suspicious_broken_parts 统计个数加 1。

那么在冷热数据存储拆散的场景下,data parts 通过 TTL 做下沉的时候,在外围接口 move 操作的函数中会有如下的代码调用关系:

MergeTreeData::moveParts->MergeTreePartsMover::swapClonedPart->MergeTreeData::swapActivePart

在最初一个函数中替换 active parts 的门路指向,也就是上文说的,data parts 在 move 过程中,数据是可查的,要么在 SSD 为 active,要么在 S3 为 active。

void MergeTreeData::swapActivePart(MergeTreeData::DataPartPtr part_copy)  
{auto lock = lockParts();  
    for (auto original_active_part : getDataPartsStateRange(DataPartState::Committed)) // NOLINT (copy is intended)  
    {if (part_copy->name == original_active_part->name)  
        {  
            .....  
            String marker_path = original_active_part->getFullRelativePath() + DELETE_ON_DESTROY_MARKER_PATH;  
            try  
            {disk->createFile(marker_path);  
            }  
            catch (Poco::Exception & e)  
            ...  
}  

在这个接口中,旧的 active parts(也就是 replacing parts)内会创立 #DELETE_ON_DESTROY_MARKER_PATH 文件来把 state 批改为 DeleteOnDestory,用于前期 IMergeTreeDataPart 析构时删除该 state 的 data parts。

这也就是在咱们的应用场景下会呈现 suspicious_broken_parts 的起因,这个值超过默认阈值 10 的时候就会影响 ClickHouse 服务启动。

解决方案有两种:第一种,删除这个机器上抛出该谬误的表的元数据 .sql 文件、存储数据、ZooKeeper 上的元数据,重启机器后从新建表,数据会从备份机器上同步过去。第二种,在 ClickHouse /flags 门路下用 clickhouse-server 过程的运行用户创立 force_restore_data flag,而后重启即可。

从上述问题中能够看到,应用 JuiceFS+S3 实现了冷热数据拆散存储架构后,引入了新的组件(JuiceFS+Redis+S3),数据库的应用场景更加灵便,相应地,各个方面的监控信息也要做好。这里分享几个比拟重要的监控指标:

  • JuiceFS:juicefs_object_request_errors:JuiceFS 对 S3 读写的衰弱状态监控。
  • Redis:Memory Usage:监控 Redis 的内存应用状况。
  • ClickHouse:clickhouse_merge:监控集群中机器的 merge 状态是否失常。

4. 冷热存储架构收益总述

冷热数据存储拆散后,咱们更好地反对了用户的数据业务,进步了整体集群的数据存储能力,缓解了各个机器的本地存储压力,对业务数据的治理也更加灵便。

冷热数据拆散架构上线前,咱们的集群机器均匀磁盘使用率靠近 85%。上线后,通过批改业务用户表 TTL,这一数据降落到了 75%。并且整体集群在原有的业务量根底上,又反对了两个新的数据业务。如果没有上线冷热隔离,咱们的集群在扩容前就会因为磁盘用量有余而无奈承接新的我的项目。以后咱们下沉到远端 S3 的数据量大于 90TB(压缩后)。

将来 Shopee ClickHouse 会继续开发更多有用的 feature,也会继续演进产品架构。目前 JuiceFS 在咱们生产环境中的应用十分稳固,咱们后续会进一步应用 JuiceFS 拜访 HDFS,进而实现 Shopee ClickHouse 存储计算拆散架构。

本文提到的各个产品组件版本信息如下:

  • Shopee ClickHouse:以后基于社区版 ClickHouse 20.8.12.2-LTS version
  • JuiceFS:v0.14.2
  • Redis:v6.2.2,sentinel model,开启 AOF(策略为 Every Secs),开启 RDB(策略为一天一备份)
  • S3:由 Shopee STO 团队提供
  • Ozone:1.1.0-SNAPSHOT

相干链接

  1. JuiceFS: https://github.com/juicedata/…
  2. ClickHouse 社区文档: https://clickhouse.tech/docs/…
  3. JuiceFS 社区文档: https://github.com/juicedata/…

本文作者

Teng,毕业于新加坡国立大学,来自 Shopee Data Infra 团队。

正文完
 0