首页 游戏 软件 排行 专题

ClickHouse部署的5个坑,第2个直接把小VPS内存干爆

来源: 本站整理 日期:2026/11/3 20:37:54

去年接了一个小单子:客户有一堆 Nginx 访问日志和业务埋点日志,量不算大,一天大概两千万行上下,原始文件加一起几十个 G。原来的方案是丢进一台 4 核 8G 的云主机,用 MySQL 存明细表、定时跑聚合 SQL 出一张日报。结果上线不到两个月,客户就开始抱怨:查一天的 UV,要等三分多钟还没出来;有一次统计任务直接把数据库连接打满,业务接口跟着卡了。客户问我有没有更省事又扛得住的办法,我给他换成了 ClickHouse不是因为他机器不够大,是因为他用错了存储模型。整件事里我踩了 5 个坑,有两个差点让这台小机器直接被 OOM 打死,今天我把过程完整写下来。

如果你现在也打算自己搭一套日志分析环境,机器怎么选反而是最后一步的事,先看看 datawave(云服务器与统一控制台) 这类控制台比较干净的商家;如果是多地采集再汇总到一台,可以再对比一下 荫云(多地部署的云端主机)。机器到位只是第一步,下面这几个坑才是真正决定它能不能跑起来的。

一、为什么是列存:先搞懂它跟 MySQL 的根本差别

ClickHouse 是列式存储。MySQL、PostgreSQL 是行式存储,一行数据的所有字段是挨着存在一起的,你要查一个字段,磁盘上就得把整行读出来。列式存储反过来,同一列的数据物理上连续存放,查询只关心哪几列,就只读那几列的压缩块。

日志分析这件事天生就是「少字段、超大表、聚合查询」:UV 统计只关心 IP 和时间戳,接口耗时只关心 URL 和 latency,原始日志里有 30 多个字段但你一次查询往往只用两三个。列式存储在这上面的优势是压倒性的同样一台机器,行存要读几百 G 的磁盘页才能把结果算出来,列存读几个 G 就够了,再加上它自带的稀疏主键索引,跳过的数据比读进来的多一个数量级。这也是为什么同样 4 核 8G 的机器,ClickHouse 处理两千万行/天的日志会比 MySQL 轻松太多。

还有一个容易被忽略的点:压缩比。日志里 url、ua、ip 这几列重复度极高,列存配合 LZ4/ZSTD 压缩之后,原始几十 G 的数据落盘可能只剩五分之一都不到。这一点在小硬盘的机器上直接决定了你敢不敢保留 30 天以上的历史数据。

列式存储压缩写入与内存占用示意

运维工程师在终端前查看列式数据库的分区表结构

二、坑1:默认配置根本不管内存,第一次查询就把机器打死

我第一次部署的时候,直接官方文档抄了一份 docker-compose,跑起来、写进去两千万行,一切正常。第二天客户要看一个「按小时聚合的来源分布」,一条 GROUP BY 跑了不到两分钟,这台 4 核 8G 的机器内存直接吃到 94%,紧接着 ClickHouse 自己把这条查询 kill 掉了,报错是 Memory limit (for query) exceeded,差点把整机拖进 OOM。

原因在于默认值的逻辑:ClickHouse 的 max_server_memory_usage 默认是系统总内存的 90%,也就是 8G 机器上它认为「7.2G 都是我能用的」。但它的分配策略是按查询最大可能来算的,一个稍微复杂点的聚合,估算出来的峰值内存可能远超真实需求。再加上后台的 merge 任务也要吃内存,两边撞车就是雪崩。

现在我的做法是把这几个值都显式写死,不要指望默认值:

max_server_memory_usage = 5368709120(留 2G 给系统和 ClickHouse 自身)、max_memory_usage = 1073741824(单条查询上限 1G,超了直接报错而不是把整机吃满)、max_threads = 2(4 核机器不要开太多并发,查询并行度一高,每个线程各要一份内存)、background_pool_size = 4(限制后台 merge 的并发)。

核心思路是宁可让查询报错,也不要让它把机器拖死。运维服务器上一个查询 OOM 掉整台机器,连业务都跟着挂,那是本末倒置。

三、坑2:ORDER BY 写错,几千万行被当成一行行扫

第二个坑是排序键(ORDER BY)写得太随意。我一开始建表写的是 ORDER BY (event_time, url),结果后来查询全是按 url 过滤、按小时聚合,排序键跟查询模式完全不匹配,分区剪枝和主键索引全废掉,一条查询要扫全表两千万行,CPU 直接打满。

列式存储的性能几乎完全由两件事决定:PARTITION BY(分区)和 ORDER BY(排序键)。分区决定「哪些数据文件根本不用读」,排序键决定「读进来的这些文件里怎么跳过不相关的 granule」。

日志场景下的经验值是这样的:分区用天,最简单也最有效,PARTITION BY toDate(event_time),配合 TTL 让老分区自动过期;如果需要按业务线分,就 PARTITION BY (toDate(event_time), biz_line)。排序键要「查询里最常出现的过滤条件在前」,比如最常按时间范围 + 某个维度过滤,就写 ORDER BY (biz_line, event_time),低基数字段放前面,基数高的放后面。

改完排序键之后,同一条查询的扫描量从两千万行降到了不到八十万行,耗时从 2 分多钟降到 3 秒。这一步是收益最大的一步,比加内存、加 CPU 都管用。

监控大屏上显示查询耗时与内存占用曲线

四、坑3:小文件太多,merge 追不上,parts 数暴涨

第三个坑是写入姿势。我一开始按每分钟一个批次写入,每批一两万行,机器上还配了个定时任务每 5 分钟跑一次 TTL 清理。跑了三天,system.parts 里的 active parts 数量从几百一路涨到 12000 多,ClickHouse 开始在日志里刷 Too many parts 警告,磁盘 IO 被后台 merge 吃满,查询又开始变慢。

这事的本质是:每次 INSERT 都会生成一批新的数据 part,ClickHouse 后台再把它们合并成大 part。如果写入太碎太频繁,后台 merge 的速度追不上生产速度,parts 就会越堆越多,查询时每个 part 都要单独处理一遍元数据和索引,查询自然变慢。

解决思路有两条:一是改批量写入,用官方的 clickhouse-client 批量 INSERT,或者用 Filebeat/Kafka 这类支持攒批的采集端,单批至少几千行,别用几百行一批的碎写;二是控制 TTL 清理的粒度,TTL 触发删除其实也是生成 mutation,同样会产生合并压力,别写个每分钟删一次的小脚本去折腾。

监控上我加了三个指标:active parts 数量(超过 1000 就要警觉)、每个 part 的平均行数(太小说明写入太碎)、以及 system.query_log 里 merge 相关查询的耗时。把这三个盯住,parts 基本不会再失控。

五、坑4:TTL 没配,磁盘被历史数据吃光

第四个坑最朴素但最容易中招。日志数据是持续增长的,我最初觉得「留着总没坏处」,结果一个月后数据盘 100G 满了,ClickHouse 因为磁盘写不进去直接拒绝写入,新日志全丢了,客户是发现数据断档才找过来的。

日志分析场景必须配 TTL。可以在表级别配 TTL event_time + INTERVAL 30 DAY,也可以在列级别单独配,比如原始的 ua、referer 这种大字段留 7 天,几个聚合用的字段留 90 天。配合 TTL ... DELETE 让老数据自动清理,再加一条 TTL event_time + INTERVAL 90 DAY TO VOLUME 'cold' 把更老的数据挪到冷盘(如果配了多卷的话)。

另外三个建议:一是给 ClickHouse 的数据目录单独划一个分区或者单独一块盘,跟系统盘隔开;二是把 max_server_disk_usage_to_free_space_ratio 设成 0.9,超过阈值自动删最老的分区,宁可少几天数据也别让服务直接挂;三是磁盘水位一定要监控,80% 就告警,别等 100%。

六、坑5:单机没有副本,数据丢一次就是永久丢失

第五个坑是我自己最后悔的。ClickHouse 单机部署没有副本,数据文件损坏或者机器直接挂掉,数据就没了。客户那份埋点数据是业务侧算 KPI 的依据,丢一天就得出一次错误的报表,还找不出原因。

ClickHouse 自带 BACKUP 命令可以增量备份到 S3 兼容的对象存储,我现在的做法是每天一次全量备份加每小时增量,配合对象存储的版本保护。恢复的时候用 RESTORE 拉回来,实测从对象存储恢复 300G 数据大约 40 分钟,比从别的机器 scp 过来快得多。

另外强烈建议上 ReplicatedMergeTree,哪怕只有一台机器,也先在本地起一个 Keeper,做成两副本。机器直接挂掉的时候,另一个副本还在,数据可以从另一台机器同步过来。这件事在出事那天和没出事那天的差别,是几个小时和几天的量级。

磁盘上按日期归档的数据分块文件

七、把排查变成日常

跑起来之后,我把这套东西固化成了几张监控图和几条告警规则:

一是 system.query_log 里的慢查询,按耗时排序,超过 10 秒的查询直接记下来,第二天人工看一遍,这类查询通常意味着排序键或者分区设置有问题;二是 system.parts 的 parts 数量和大小;三是 system.events 里的 OOM 事件数;四是磁盘水位和写入拒绝次数。

还有一点实操经验:每条慢查询都要用 EXPLAIN indexes = 1 跑一遍,看看主键索引到底跳过了多少 granule。如果扫的行数和你预期差一个数量级,那八成是排序键的问题,改表结构比重写 SQL 划算得多。

客户端我用的是 DBeaver,连 ClickHouse 直接写 SQL 就能查,看表结构、看 part、看执行计划都方便,省得每次都开终端敲 clickhouse-client。

写在最后

回头看这 5 个坑,其实有一个共同点:都是「默认值很美好,但不适合真实环境」。内存默认按 90% 用、排序键默认按你想不到的方式写、写入默认按你随手写的频率来、TTL 默认没有、副本默认没有。每一个都需要你明确地把它改掉。

如果这台机器一开始就只有 2 核 4G,我建议第一件事不是升级配置,而是先按上面的方式把内存上限、排序键、分区和 TTL 四个地方改对。我那台 4 核 8G 的机器,配置改完之后跑两千万行/天的日志,日均查询量几千条,CPU 长期在 30% 上下,内存稳定在 4G 出头,磁盘占用是原始数据的五分之一,整整半年没再因为数据量出过任何问题。

机器的规格是下限,数据的组织方式才是上限。这句话在列式数据库上尤其成立。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载