ClickHouse容量统计实战:用system.parts精准定位库表大小
先问一个扎心的问题你的 ClickHouse 集群磁盘告警的时候你是怎么确认到底是哪个库、哪张表把容量吃掉的我最开始的做法和大家差不多直接去看 system.tables 里的 total_bytes结果发现数字和 du -sh 对不上差距还不是一点半点。后来我把统计口径切到 system.parts一切才变得可控。今天这篇就把这套方法完整拆给你。围绕 ClickHouse 查询数据库容量和表大小的几种标准姿势重点讲透 system.parts 这张系统表里那些关键字段怎么读、哪些坑容易踩、除了看容量之外它还能帮你做什么。如果你是刚接触 ClickHouse 的运维或者数据分析同学照着文里的 SQL 抄一遍基本就能掌握自己集群的容量命脉如果你已经写过几条容量统计 SQL那更要看看后面的 part 命名规则和 active 过滤这些细节十有八九会踩中一个。1. 容量统计为什么绕不开 system.parts1.1 ClickHouse 数据存储的最小单元PartClickHouse 和大多数关系型数据库不一样它的 MergeTree 表在磁盘上不是一张表对应一个大文件而是拆成了很多个独立的数据部分官方叫 Data Part。每一次 INSERT 写入哪怕只有几行数据在 MergeTree 里都会生成一个新的 part 目录后台的 merge 线程再把小 part 合并成大 part。这就导致一个结果一张表的真实磁盘占用是这张表所有 part 目录的累计和。你去数表目录里的文件夹有多少个目录基本上就有多少个 part。既然容量存储的物理单位是 part那我们统计容量最精确、最底层的方式自然也就是直接围绕 part 做聚合而 ClickHouse 早就把每个 part 的元数据暴露在 system.parts 里了。刚接触 ClickHouse 的同学可能会疑惑为什么不直接看 system.tables 里的 total_bytes原因后面会详细说核心就一句话system.parts 能下钻到分区和单 part 粒度而且每一列的统计口径都是透明的你能清楚知道它算的是什么。1.2 system.parts 关键字段容量统计到底读哪些列打开 system.parts里面字段很多但做容量统计真正核心的就那么几个。字段含义统计容量时怎么用database数据库名group by 维度table表名group by 维度partition分区值格式化后的分区级统计维度part_typeWide / Compact / InMemory判断是大文件还是小文件rowspart 内行数统计行数规模bytes内存中未压缩数据的近似大小评估内存占用场景bytes_on_diskpart 在磁盘上占用的总字节数统计磁盘容量的核心字段data_compressed_bytes列数据压缩后的字节数算压缩率用data_uncompressed_bytes列数据未压缩的字节数算压缩率用active1 表示当前生效0 表示废 part统计时必须过滤 active 1namepart 目录名下钻排查单个 part最容易被搞混的是 bytes、bytes_on_disk 和 data_compressed_bytes 这三个。简单说bytes 是数据在内存里解压后的大概占用偏逻辑量data_compressed_bytes 只是列数据压缩后的大小而 bytes_on_disk 才是一个 part 目录占用的真实磁盘空间它除了压缩后的列数据之外还包含索引文件、标记文件 mark、校验和 checksums、默认压缩 codec 的元数据等一堆附加文件。所以统计数据库容量表大小的时候永远优先用 bytes_on_disk。1.3 active 字段不搞清楚统计结果会翻倍这是我在生产环境踩过的第一个坑。MergeTree 的合并过程并不是把旧 part 删掉再写新 part而是先基于旧数据生成一个新的合并后 part再把旧 part 标记为失效最后在后台慢慢物理删除。在 ClickHouse 的眼里被标记为失效的旧 part 仍然存在只不过 system.parts 里 active 变成了 0。如果你统计容量时没有过滤 active 1恰好赶上某个大分区正在做合并那新旧 part 会同时被算进去容量直接翻倍。对于高频写入的表这种重复统计几乎是常态。所以后面所有 SQL 我都会先带上这个条件。2. 数据库与表级容量统计三条 SQL 覆盖 90% 需求2.1 数据库级汇总先定位容量大头在哪个库当你接到磁盘告警第一步永远是看整体分布哪个库占得最多。这条 SQL 直接汇总所有库的磁盘占用SELECT database, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active 1 GROUP BY database ORDER BY sum(bytes_on_disk) DESC;这里有个细节我特意说明一下格式化函数 formatReadableSize 只是给人看的它返回的是字符串千万不要在它上面直接 ORDER BY否则会按照字符串顺序排10GB 会被排到 2GB 前面。正确做法是先按 sum(bytes_on_disk) 排序然后再格式化展示。如果集群里有大量系统库的日志表在刷盘你可能会发现排第一的是 system 库这种情况后面专门说怎么排除。2.2 表级大小按表统计并排序库定位好了接下来要看到库内每张表的大小。把查询条件加上 database 过滤即可SELECT table, count() AS parts, sum(rows) AS rows, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active 1 AND database default GROUP BY table ORDER BY sum(bytes_on_disk) DESC;这段 SQL 会返回每张表的 part 数量、总行数和总占用。part 数量这个指标别小看它后面在做健康诊断的时候非常有价值。如果某张表行数不多但 part 数量上千那基本可以断定这个表的 merge 已经跟不上了。2.3 分区级明细定位数据到底涨在哪个分区表很大通常不是均匀分布的而是集中在最近几天或者某些固定分区。这时候要做分区级下钻SELECT partition, count() AS parts, sum(rows) AS rows, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active 1 AND database default AND table events GROUP BY partition ORDER BY sum(bytes_on_disk) DESC LIMIT 20;注意 system.parts 里的 partition 字段是格式化后的分区值比如按 toYYYYMMDD(ts) 做分区键这里显示的就是 20240601 这样的可读值如果你用了 tuple 分区键它会显示成 (2024-06-01, shard1) 这种结构。如果发现涨容量的分区集中那说明保留策略TTL可能没生效或者某个离线任务在补历史数据排查方向就很明确了。2.4 为什么不能用 system.tables.total_bytes我在开头说过系统表 system.tables 里直接有 total_bytes 字段看起来好像一条 select 就能搞定用它的人不在少数但它有几个实际使用中的短板。第一total_bytes 这个数字在不同版本里实现口径有差异有些版本统计的是未压缩的逻辑大小和 du -sh 看到的物理磁盘占用对不上第二你拿它只能看到表这一层没法下钻到分区和单个 part出问题的时候还是要回到 system.parts 来排查第三它不如 system.parts 灵活你没法在它上面做各种条件过滤和自定义分组。所以我个人的习惯是把 system.tables 当目录看把 system.parts 当真正的数据源用。3. 从 part 命名读出写入与合并的历史3.1 part_name 的四个组成部分这是 system.parts 里最有意思的一列也是很多 DBA 容易忽略的信息。一个典型的 part 名称长这样202406_1_10_3用下划线拆开四段含义分别是段位含义示例partition_id分区 ID202406min_block_number该 part 覆盖的最小数据块编号1max_block_number该 part 覆盖的最大数据块编号10levelpart 的合并代数3如果你在配置里开启了 unique part names部分版本默认开启名称末尾还会追加一个 UUID形如 202406_1_10_3_6f5d0e8e-0f9f-4e2c-aaaa-888888888888。这个 uuid 是为了避免分布式环境下 part 重名冲突加的不影响前面四段语义。3.2 level 字段part 被合并了多少次看 level 一眼就能判断这个 part 经历过几次合并。part 名称里的四段不一定完整不需要担心你直接在 system.parts 里查 name 字段就是完整名称。举个例子如果某张表的 part 名称都是 202406_10_10_0说明每个 part 都是一个独立写入的原始块level 全是 0完全没有合并过这种表如果有几十个 part 都是小文件查询性能多半已经受影响了。反过来如果 part 名称是 202406_1_1000_8说明这 1000 个原始数据块已经被 merge 了 8 轮落成了一个相对较大且整齐的 part。level 越高part 越大但数量越少查询时扫描的文件数就越少。3.3 用命名规则快速判断表是否健康我平时巡检会查一张表的 part 明细看 level 分布SELECT partition, count() AS parts, min(level) AS min_level, max(level) AS max_level FROM system.parts WHERE active 1 AND database default AND table events GROUP BY partition ORDER BY parts DESC;如果某个分区的 part 数量很多但 level 普遍很低说明写入压力大但 merge 一直没跟上如果 part 数量不多但 level 很高说明这个分区的数据经历过大量轮次合并整体比较健康。通过这个方式你不需要去看日志直接从 part 命名就能读懂一张表的历史写入节奏。4. 容易踩的坑active、detached 与多副本统计4.1 active0 的幽灵 part 会重复计算前面说了 active0 的旧 part 在 merge 之后不会立刻消失它会按照后台线程的节奏慢慢被物理删除。如果你做容量统计不写 WHERE active 1那么统计出来的数字一定会大于实际有效数据而且这个偏差在 merge 频繁的时段会特别明显。另外一个相关的隐藏场景是 mutation 操作也就是 ALTER TABLE UPDATE/DELETE。ClickHouse 的 mutation 不是原地改数据而是通过重写 part 来实现旧的 part 同样会变成 active 0 在后台清理。如果你刚做了一大批数据订正接着去查容量又忘了过滤 active恭喜你统计结果会非常虚胖。4.2 detached_parts不在 system.parts 里的磁盘占用比 active0 更隐蔽的是 detached parts。当你执行 ALTER TABLE ... DETACH PARTITION 把分区摘下来或者某个 part 因为损坏被自动隔离时数据会移动到表目录下的 detached/ 子目录里。这些 part 不会出现在 system.parts 中也就无法被前面的 SQL 统计到但它们的物理文件真的实实在在占着磁盘。查这些游荡在统计体系之外的 part 要用另一张系统表 system.detached_parts它记录了被分离 part 的库表名、part 名称和分离原因。我遇到过一次磁盘空间不足查 system.parts 发现明明没问题最后排查到表目录下堆了大量 detached part全是之前手动拆分归档分区时留下的清理完之后磁盘立刻松快了。4.3 分布式与副本场景容量要按节点逐个统计system.parts 是 ClickHouse 节点本地的系统表它只能看到当前节点磁盘上的 part。这个特性决定了两个重要结论。第一如果你用的是副本表 ReplicatedMergeTree同一个表的数据在每个副本节点上都有一份完整的 part。查任何一个副本节点看到的容量都是单副本数据量但集群真实的磁盘占用是所有副本节点之和。做成本核算时一定要在所有节点上分别执行统计再求和。做数据规模评估时则按单副本看就足够。第二如果你的集群前面挂了分布式表 Distributed分布式表本身只是一个逻辑路由层它不直接存储数据真正落盘的是各个分片节点上的本地表。所以查询容量时应该到各个分片节点上查询本地表而不是在分布式表上执行。当然更快的办法是借助 clusterAllReplicas 这类函数批量对集群所有节点执行同一段 SQL但这个功能涉及集群配置不同环境差异比较大这里就不展开了。4.4 排除系统库与其他引擎表默认配置下ClickHouse 自己会产生大量的内部日志表比如 query_log、part_log、metric_log 这些它们位于 system 库下。这些表的数据量随着你的查询量线性增长几十 GB 甚至上百 GB 都不稀奇。我们做业务容量盘点时通常要过滤掉 system 库以及 information_schema 等系统库否则结果会混入大量无关数据。还有一个容易被忽略的点不是所有表都在 system.parts 里有记录。MergeTree 家族的表MergeTree、ReplicatedMergeTree、SummingMergeTree、AggregatingMergeTree 等才有 part 概念而 Log、TinyLog、Memory 这些引擎的表无论是物理存储方式还是元数据统计方式都走完全不同的路径。它们的容量统计还是得借助 system.tables 的 total_bytes 字段。所以一个稳妥的做法是容量统计不仅过滤 active 1还要明确过滤掉 system 库同时心里清楚哪些表是用短引擎存储的单独去 system.tables 里补统计。5. 进阶玩法压缩率、Part 数量与容量监控实战5.1 算压缩率判断表是否需要换编码system.parts 里既然有压缩前后字节数那顺手算一下每张表的压缩率再容易不过SELECT database, table, sum(data_uncompressed_bytes) AS uncompressed, sum(data_compressed_bytes) AS compressed, if(compressed 0, 0, round(uncompressed / compressed, 2)) AS ratio FROM system.parts WHERE active 1 AND database default GROUP BY database, table ORDER BY ratio DESC;这里 ratio 的含义是每一份压缩数据能还原出多少份原始数据。ratio 越大压缩效果越好。普通的日志、文本类数据用 LZ4 压缩通常能有 4 到 6 倍的压缩率如果某个表 ratio 不到 2说明数据本身的随机性高、重复度低要么是内容已经压缩过要么就是列编码选得不太合适。这时候可以在建表时考虑换 ZSTD 或者更针对性的 CODEC。当然压缩率高了 CPU 开销也会上来这是一个典型的空间换时间的取舍不能盲目追求高压缩。5.2 Part 数量健康度一张表几百个 part 的代价Part 数量和查询性能的关系很多人要等到线上出故障才有体感。ClickHouse 查询时要读取的表数据分布在哪些 part 里就需要打开这些 part 对应的列文件和标记文件part 数量越多需要打开的文件就越多调度开销和随机 IO 都跟着涨。一个 SELECT 本来读几秒钟如果落在上千个小 part 上可能就变成几十秒。系统里对 part 数量是有保护机制的默认写入侧有 parts_to_delay_insert 和 parts_to_throw_insert 两个阈值大致在 150 和 300 附近不同版本和配置会有差异超过之后写入会变慢甚至直接报错。所以巡检容量的时候顺手看一眼 part 数量分布能把很多潜在性能问题提前暴露出来。5.3 把容量监控落地一条综合巡检 SQL我日常巡检会直接用下面这条 SQL把容量、行数、part 数量都带出来SELECT database, table, count() AS parts, sum(rows) AS total_rows, formatReadableSize(sum(bytes_on_disk)) AS total_size, formatReadableSize(avg(bytes_on_disk)) AS avg_part_size FROM system.parts WHERE active 1 AND database ! system GROUP BY database, table ORDER BY sum(bytes_on_disk) DESC LIMIT 50;avg_part_size 这个字段是我后来加上的。如果一张表的 avg_part_size 只有几十 KB说明这个表一直在写入极小的 partmerge 压力会非常大这种表往往就是未来出现 Too many parts 报错的种子。5.4 一个真实案例小 part 堆积导致查询变慢之前我维护过一张按天分区的日志表每天凌晨会有定时任务批量写入前一天的数据写入频率很高但每次数据量很小。某天业务反馈这张表的查询明显变慢我去 system.parts 里一查发现这张表的 part 数量已经超过两千而且大部分 part 的 level 都是 0也就是说基本没怎么合并过。再看分区分布所有小 part 全堆在最近两三个分区里最老的历史分区反而因为数据量大已经 merge 得比较干净。原因很清楚定时任务每个批次只写几万行写入间隔又短part 生成速度远大于后台合并速度再加上高峰期 CPU 被查询任务占满merge 线程长期抢不到资源。处理方法是两管齐下低峰期手动执行了一次 OPTIMIZE TABLE xxx PARTITION 20240601 FINAL 强制合并同时和任务方约定把写入批次攒大、降低写入频率之后 part 数回落到几十个查询耗时恢复到了秒级。排查线和修复路径都不神秘核心就是靠 system.parts 把哪个表 part 多、哪些 part 小、level 是否异常这三个信息抓出来。6. 几个值得长期记住的操作习惯最后把我个人在实际使用中的几个体会集中说一下。统计 ClickHouse 容量这件事技术上难度不高难的是统计口径一致性和对隐性状态的感知。我的习惯是给所有容量统计 SQL 默认加上 active 1 和 database ! system 两个条件这样就不会被系统库和合并过程中的临时 part 干扰统计频率不用太高容量类的指标一天跑一次足够但 part 数量这类健康度指标最好能纳入到周期性巡检里它往往比容量增长更早暴露问题。另外一点看到容量异常先别急着删数据按库总览 - 表明细 - 分区下钻 - 单 part 定位的顺序一层层查下来通常十分钟内就能定位到问题源头。整个排查链路里没有一条复杂 SQL吃透 system.parts 那几个关键字段的含义比背一堆花哨的查询模板有用得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →