ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ClickHouse容量统计全解析:从system.parts到分区治理实战

ClickHouse容量统计全解析:从system.parts到分区治理实战 接手ClickHouse集群之后最先被我翻烂的表就是system.parts。无论是排查磁盘告警、评估数据增长趋势还是确认分区合并是否健康所有容量相关的答案最终都落在这张表上。不少刚接触ClickHouse的朋友习惯去information_schema.tables查大小查完回来一脸问号——表明明有几十个G那里面显示的却是0。这个问题的根子不在SQL写错了而是ClickHouse的容量统计压根不走标准元数据那一套真正干活的家伙是system.parts。这篇文章就围绕system.parts展开把我日常用来查数据库容量、表大小、分区大小的方法全部梳理一遍。你会看到底层存储模型为什么决定了这套查询方式也会拿到可以直接抄走的SQL脚本以及我在生产环境里踩过的坑。适合刚接手ClickHouse集群的运维同学也适合正在做容量评估和成本治理的开发朋友。1. 为什么查容量首选system.parts——先理解ClickHouse的存储模型刚用ClickHouse时我也犯过嘀咕明明是一张表为什么目录里碎成一片片的小文件夹每个文件夹名字还长得像乱码。等搞清楚这套机制之后再看容量查询就顺理成章了。1.1 一次数据写入到底发生了什么ClickHouse最常用的表引擎是MergeTree家族它的存储单位叫part分区内的数据片段。每当你执行一次INSERT数据不会直接写进某个大文件而是先在内存里攒成一个part再落盘成一个独立目录。比如一次插入几千行就可能生成一个包含bin文件、mrk标记文件、checksums.txt校验文件的子目录。这个目录就是part。part之间有排序键的约束同一分区内可能存在多个part它们的范围是有序但不重叠的。后台线程会定期把多个小part合并成一个大part这个过程叫merge。合并完成之后旧的part会被标记为 inactive最终由清理线程物理删除。这个机制直接决定了容量统计的方式一张表的磁盘占用等于它所有分区下所有part目录大小之和。正因为数据是分散在多个part里的你没法简单“ls”一个目录拿到整张表的大小必须调用系统表来聚合。1.2 system.parts表结构里最关键的几个字段system.parts是ClickHouse内置的系统表每一行对应一个part。注意不是一张表一行而是一个part一行。如果一张表有100个分区、每个分区3个part那这张表在system.parts里就有300行记录。核心字段我按用途分类整理如下定位字段database库名、table表名、partition_id分区ID、part_typepart类型如Wide、Compact状态字段active1表示当前使用的part0表示已经废弃等合并的旧part、statepart状态比如Committed、Outdated容量字段rowspart内的行数、bytes_on_diskpart在磁盘上的实际字节数、data_compressed_bytes压缩后数据字节、data_uncompressed_bytes未压缩数据字节区段信息min_block_number、max_block_numberpart内数据块编号区间这块在解读part目录名时会提到时间字段modification_time、min_time、max_time对应分区键的取值区间提示bytes_on_disk是整个part目录的物理大小包含了校验文件、标记文件、默认压缩算法下的数据文件是最接近真实磁盘占用的指标。我们做容量统计时默认用它。1.3 和information_schema.tables对比差别在哪information_schema.tables是标准SQL里定义的元数据视图ClickHouse也有提供但total_bytes字段很多情况下并不返回真实值。这跟ClickHouse的分布式架构有关一张分布式表的数据散落在多个分片、多个part目录标准元数据视图不维护逐part的物理统计所以经常是0或者空。反观system.parts它是直接从存储层读取的物理信息每个part目录的大小都在这里如实记录。所以我的经验是所有跟容量相关的查询一律走system.parts不要绕远路。2. 数据库容量和表大小的核心查询语法这一节直接上干货。我日常用的查询基本就是下面这几种场景从库维度、表维度到分区维度都能覆盖读写分离执行即可。2.1 查看整个数据库的容量统计一个库所有表的总大小最简单的写法SELECT database, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows FROM system.parts WHERE (database your_database) AND active 1 GROUP BY database;这里有几个细节要说明。第一active 1这个过滤条件非常重要。如果不加会把那些已经合并完、等待清理的旧part也算进来导致统计结果明显偏大甚至翻倍。我的习惯是任何容量查询默认都带active 1除非你就是为了排查“磁盘为什么迟迟不释放”而专门去看废弃part。第二formatReadableSize是ClickHouse的格式化函数输出结果类似12.34 GiB比直接看字节数直观得多。如果要做后续告警判断建议保留原始字节数或者用formatReadableSize和自算MB两个字段同时输出。第三如果集群是多副本的system.parts在每台节点上只记录本节点的part所以单节点查询得到的是该节点存储引擎层面的容量不是整个集群的。想查全集群总容量得在每个节点执行后再汇总或者用clusterAllReplicas类函数具体后面实操部分再展开。2.2 查看单张表的大小和行数统计某张表的总容量基本查询是SELECT table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows, round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS compression_ratio FROM system.parts WHERE (database your_database) AND (table your_table) AND active 1 GROUP BY table;这里额外加了一列压缩比。压缩比是个很值得关注的指标它在数值上等于“未压缩字节数 / 压缩后字节数”反映的是数据在ClickHouse里的压缩效果。通常情况下日志类文本数据压缩比在5~10之间都很正常指标类数值数据压缩比在2~4之间比较常见如果压缩比低于2你要么存了本来就很难压缩的数据比如已加密、已压缩的图片字段要么列设计上还有优化空间这列数据在做成本评估时很有价值。比如你发现某张表的压缩比很低说明存储成本偏高可以考虑调整压缩算法、更换编码方式或者对低基数字段用字典编码。2.3 按分区统计大小定位“膨胀”的分区分区是MergeTree里最重要的逻辑隔离单位容量排查时经常要看分区维度SELECT partition_id, count() AS part_count, formatReadableSize(sum(bytes_on_disk)) AS partition_size, sum(rows) AS rows FROM system.parts WHERE (database your_database) AND (table your_table) AND active 1 GROUP BY partition_id ORDER BY sum(bytes_on_disk) DESC;这个查询最大的用处是找“分区不均匀”的问题。举个真实例子我有一次接到线上反馈说某张日志表磁盘增长异常我按分区一查发现最近3天生成的分区每个都超过200GB而更早的历史分区每个只有10GB左右。这时候就意识到新增数据里可能混入了大量异常字段或者某个业务方改了写入逻辑导致日志内容膨胀。如果不做分区维度的统计光看全表大小你只能看到总量在涨却很难定位到具体是哪一天、哪个分区出了问题。2.4 用system.tables做快速元数据统计除了system.partssystem.tables里也维护了一份表级别的元数据其中total_bytes在部分场景下可用。它的底层实现其实也是汇总parts信息但胜在查询更轻量适合快速逛一遍集群里所有表的大小SELECT database, table, formatReadableSize(total_bytes) AS disk_size, total_rows FROM system.tables WHERE database your_database ORDER BY total_bytes DESC;需要提醒的是system.tables的total_bytes是否准确和system.parts的统计口径不完全一致。在我见过的版本里它多数是可靠的但遇到表刚被DROP或TRUNCATE后元数据刷新不及时的情况数值可能滞后。所以我偏向于把它当“快速预览”正式做容量报告时还是以system.parts为准。3. part命名规则与实际运维读懂part目录名维护ClickHouse时间长了你会越来越在意part目录名。为什么要单独讲命名因为从part名字里能解读出这个part属于哪个分区、覆盖哪个数据块区间、是第几次合并产生的这对排查容量问题和合并异常非常有帮助。3.1 part目录名的标准格式一个MergeTree表的part目录名长这样202406_0_168_3用下划线分割成四段含义依次是第一段分区ID。比如按日期分区这里通常是202406这样的年月字符串如果是元组分区则可能变成(202406,1)之类的编码形式第二段min_block_number当前part内数据块编号的最小值第三段max_block_number当前part内数据块编号的最大值第四段合并代数表示这个part经历了多少次合并。初始插入的part这一位通常是0每次合并后会递增举个例子202406_0_168_3表示的是202406分区下的一个part它覆盖了数据块编号0到168已经合并过3次。3.2 从system.parts逆向解读part信息对应关系在system.parts中都能找到。查询part基本信息SELECT partition_id, name AS part_name, min_block_number, max_block_number, level, rows, bytes_on_disk, active FROM system.parts WHERE (database your_database) AND (table your_table) ORDER BY min_block_number LIMIT 20;这个查询在平时排查时很常用。你可以观察到哪些part长期处于level0没有合并哪些part的min_block_number和max_block_number之间存在巨大空洞。这些信息比单纯看大小更能反映写入与合并的健康状态。3.3 part命名在容量治理中的两个实践点第一判断合并是否卡住。正常情况下一个分区内part数量应该维持在一个较低水平后台会不断把小part合并成大part。如果你发现某分区的part数量一直上涨而且新part的level一直是0旧part长期处于outdated状态但又不被清理那很可能是合并线程被限制或者磁盘空间不足导致合并失败。这时候容量统计反而要特别小心因为bytes_on_disk会把新旧part都算进去磁盘占用虚高。第二手动触发合并后的反馈。执行OPTIMIZE TABLE ... FINAL之后观察part目录名的变化是最直观的反馈方式。合并成功后max_block_number会变大level会递增多个旧part会变成 inactive。如果执行完OPTIMIZE后part名没变化说明没有可合并的数据或者表已经处于最紧凑状态。4. 实操脚本与监控落地一键统计集群容量SQL单独写不算本事真正好用的是把SQL封装成脚本在故障排查和日常巡检时能够一键执行。这一节分享我实际在用的几个脚本和落地思路。4.1 一次性统计全库所有表的大小要在ClickHouse客户端里查一个库下所有表的大小可以把前面单表查询的SQL去掉table过滤条件并加上按表分组的聚合SELECT table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows, count() AS part_count FROM system.parts WHERE (database your_database) AND active 1 GROUP BY table ORDER BY sum(bytes_on_disk) DESC;如果库里表特别多这个查询也能跑得很快因为system.parts本来就是内存表扫描速度足够。想要跨库全集群汇总可以在clickhouse-client里直接跑SELECT database, formatReadableSize(sum(bytes_on_disk)) AS disk_size FROM system.parts WHERE active 1 GROUP BY database ORDER BY sum(bytes_on_disk) DESC;在分布式集群环境下每台节点各自执行上述查询得到的就是该节点本地的统计。如果只想看某个分片的情况直接连对应节点查询即可。4.2 定时巡检与告警阈值我通常会让脚本每小时跑一次把结果写入一张带日期的统计表然后配置告警。比如某个database的单日新增容量超过过去30天日均的5倍触发扩容提醒某张核心表total_rows增长异常触发排查某分区的part_count超过50触发合并健康检查简单实现方式是在ClickHouse里建一张统计结果表然后用INSERT INTO ... SELECT把上面的查询结果定期写入CREATE TABLE default.capacity_statistics ( stat_date Date, database String, table String, disk_size_bytes UInt64, total_rows UInt64, part_count UInt64 ) ENGINE MergeTree() ORDER BY (stat_date, database, table);然后在外部用cron或调度平台定时执行INSERT INTO default.capacity_statistics SELECT today(), database, table, sum(bytes_on_disk), sum(rows), count() FROM system.parts WHERE active 1 GROUP BY database, table;之后查增长趋势就很方便。比如看某张表最近7天的容量变化SELECT stat_date, formatReadableSize(disk_size_bytes) AS disk_size FROM default.capacity_statistics WHERE (table your_table) AND (stat_date today() - 7) ORDER BY stat_date;配合Grafana或者自建监控面板一张表近30天的容量曲线就出来了容量规划不再靠拍脑袋。4.3 容量异常后的清理策略容量排查的最后一步是治理。如果发现某分区数据特别大且确认不需要了优先用分区裁剪删除ALTER TABLE your_table DROP PARTITION 202405;这条命令会删除整个202405分区的所有数据并且会真正释放磁盘。注意DROP PARTITION的耗时取决于分区大小执行期间对查询有一定影响建议在低峰期操作。如果是历史数据自动过期直接配置TTLALTER TABLE your_table MODIFY TTL event_date INTERVAL 90 DAY;ClickHouse会在后台自动清理过期分区。我自己更喜欢TTL因为不用人为干预但要注意TTL的清理是“懒删除”的它只删除part目录中的数据是否立即释放磁盘取决于后台任务调度不过最终会释放。另外要提一个很容易记得的误解TRUNCATE TABLE清空表后磁盘空间不会立刻下降这在第5部分会展开。5. 常见问题与排查技巧实录sysytem.parts使用避坑这个部分是我最想写的。这些坑不是从文档里看来的全是生产环境里一个个踩出来的。知道了它们你至少能少熬几个大夜。5.1 active0导致磁盘统计虚高有一次我排查磁盘告警发现某节点磁盘使用率一直卡在98%但是按业务表逐个查大小怎么看都只占用了60%。后来查了system.parts里active0的记录才发现有大量废part没被物理删除。这是因为我在业务低峰期执行了多次ALTER TABLE ... DELETE每次删除都会旧part标记为 inactive但不会立即删除物理文件。后台清理线程要等合并和下线流程跑完才动手。所以磁盘被一堆旧part占着而你常规统计时往往只查active的part。解决这个问题没有银弹只能等后台清理或者手动触发SYSTEM DROP MARKED PARTS;我用这个命令的频率不高但在测试环境做数据清理演练时很有效。生产环境执行前建议先在活动量小的节点上试跑一次确认对查询无影响再全面执行。5.2 查询结果为零多半是权限或引擎问题system.parts查询返回0行记录常见原因有三类。第一当前用户没有读取系统表的权限。很多公司会给不同业务线分配独立的账号默认权限并不包含访问所有系统表。我用system.parts之前习惯先确认账号角色或者直接用管理员账号跑巡检。第二表是分布式表而不是本地表。system.parts不统计分布式表的逻辑容量它统计的是本地MergeTree表。你要查的是distributed_logs这种引擎实际容量应该去查它的本地表logs。这是特别常见的坑。第三表使用了非MergeTree引擎。system.parts只面向MergeTree家族表如果是Log、Memory这类引擎parts里根本没有对应记录查出来当然为空。5.3 TRUNCATE后容量不释放别急着怀疑统计口径TRUNCATE TABLE之后我去查system.parts经常能看到active0的part还躺在那里。有同事会误以为表没清干净或者统计口径有问题其实这是ClickHouse的设计。TRUNCATE操作是标记part不可见但物理删除是异步的。想要释放空间比较稳妥的做法是TRUNCATE TABLE your_table; SYSTEM DROP MARKED PARTS;或者干脆用DROP TABLE这个会立即释放目录空间。5.4 system.parts查询太慢怎么办正常执行一个带WHERE (database xx)的统计查询毫秒级就能出来。如果你发现查询特别慢先看是不是把条件写成了WHERE database default OR table xxx这种全表扫描的写法。在超大集群里system.parts里的记录数量可能是百万级甚至更多。强烈建议查询时始终带上database和table的精确过滤条件这能极大缩小扫描范围。另外如果只是日常巡检可以只在凌晨低峰期跑避免和业务查询抢CPU。5.5 关于part的生命周期与增量统计还有一个经验如果做增量容量统计不要只依赖sum(bytes_on_disk) - sum(bytes_on_disk)来算单日新增因为合并会不断重写文件导致part大小短期波动。更可靠的方式是每天固定时间快照bytes_on_disk做环比差值同时把sum(rows)的差值作为参考。当两个差值方向不一致时优先以行数变化为准判断业务增长以磁盘变化为辅判断存储效率。6. 一些关于 system.parts 的补充技巧与扩展思路system.parts除了算容量之外其实还可以做不少事情。我个人常用来做三件额外的事确认分区键设置是否合理、核对备份恢复后的表结构是否健康、以及诊断数据倾斜。6.1 用parts验证分区键设计是否合理分区键选得好不好从part数量就能看出来。如果分区粒度过细比如按小时分区但每小时数据量很少会产生大量小part每个part里rows只有几千bytes_on_disk也小得可怜但是part数量很多合并压力大查询性能反而下降。判断小part是否过多的SQLSELECT partition_id, count() AS part_cnt, min(rows) AS min_rows, max(rows) AS max_rows, round(avg(rows)) AS avg_rows FROM system.parts WHERE (database your_database) AND (table your_table) AND active 1 GROUP BY partition_id ORDER BY part_cnt DESC;如果某个分区的part_cnt特别大且avg_rows很小说明分区键粒度可能过细。这时候要么调整分区键比如从小时改成天要么考虑用OPTIMIZE TABLE ... FINAL强制合并。6.2 从parts信息中发现数据倾斜多副本集群里如果每个分片节点上的part总大小差异巨大说明数据路由可能倾斜。依次在每台节点上执行SELECT hostName() AS host, formatReadableSize(sum(bytes_on_disk)) AS disk_size FROM system.parts WHERE (database your_database) AND (table your_table) AND active 1 GROUP BY host;对比多节点的输出如果某个节点明显大于其他节点大概率是分片键选择不佳写入热点全打到了一个节点上。这个检查我每次在做容量评估时都会顺手跑一遍对后续扩容和分片键调整非常有参考价值。6.3 集群扩容前的容量预判在做磁盘扩容时我习惯先用下面这个SQL预估未来30天每个节点需要的空间SELECT database, table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(bytes_on_disk) / sum(rows) AS bytes_per_row FROM system.parts WHERE active 1 GROUP BY database, table ORDER BY bytes_per_row DESC;bytes_per_row表示每行数据平均占用的磁盘字节数。结合业务方给出的预期日增行数就能算出未来一个月的存储需求比单纯看当前总量靠谱得多。7. 最后分享一下我对system.parts的整体使用心得用system.parts查容量这件事看起来只是几条SQL的事但背后的价值远不止“看到大小”这么简单。它是观察ClickHouse存储层健康状况的一扇窗口part是否正常合并、分区是否合理、数据是否倾斜、压缩是否有效都能从这张表里读出来。我在实际运维中踩过几次大坑之后总结出几条铁律所有容量统计默认带active 1查废弃part时再单独放开统计口径以bytes_on_disk为主行数和压缩比作为辅助判断跨节点容量汇总时按节点分别执行不要试图在单节点上拿到全集群准确值磁盘告警不只看总量还要看part数量和合并状态否则会被“虚高”或“延迟释放”误导如果你刚开始接触ClickHouse的容量管理不用急着写一堆复杂的监控脚本先把system.parts里的字段含义弄清楚把前两节的SQL跑明白你就能解决日常80%的容量问题。剩余的20%靠的是对part生命周期和合并机制的深入理解——这部分只能靠实战慢慢积累。
返回列表