日志系统每天产生几十GB的写入,查询却变得越来越慢,单机 Elasticsearch 集群动不动就 CPU 打满、GC 卡顿,这是很多团队在实时日志分析场景里都遇到过的问题。我在线上环境折腾过一段时间 ClickHouse 分布式集群,从最开始的三台物理机,到后来把实时日志链路完整跑起来,整个过程里踩了不少坑,也沉淀了一些很实用的调优方法。这篇就围绕 CentOS 8 平台上如何配置与优化 ClickHouse 分布式集群、提升实时日志分析性能这件事,把从选型到落地的完整经验捋一遍,尤其适合那些日志量大、对查询响应有明确要求的运维和后端同学参考。
1. 为什么用 ClickHouse 扛日志分析:选型背后的真实考量
1.1 从一次日志需求说起
团队当时的痛点很典型:业务模块多,日志分散在十几台服务器上,排查问题时需要同时翻好几台机器的文件,或者依赖老的 ELK 方案,但 Elasticsearch 的索引膨胀太快,磁盘和内存都扛不住,聚合查询也经常超时。后来我们要求做到日志写入后秒级可查,并且支持按时间范围、业务模块、用户 ID 等维度做快速筛选和统计。
评估了一圈,最终锁定 ClickHouse。核心原因是它针对分析场景做了大量极致优化:列式存储让聚合查询只需读取涉及的列,向量化执行引擎能最大化利用 CPU,数据压缩比高,日志这类文本密集型数据往往能压缩到原始大小的十分之一甚至更低。相比 Elasticsearch 的倒排索引,ClickHouse 的 MergeTree 家族引擎在批量写入和范围扫描上的性能表现,对日志分析这种"写入多、聚合多、点查少"的场景更匹配。
1.2 对比 ES 和 Doris,为什么选了 ClickHouse
关于 Doris 和 ClickHouse 的选型,也是这几年社区里经常讨论的话题。我当时也简单评估过 Apache Doris。两者的定位确实有重叠,都是 OLAP 引擎,都支持大规模并行处理。但如果团队的核心诉求是快速搭建一套成熟的日志分析平台,ClickHouse 的生态更加通用,文档和案例丰富,基于 MergeTree 的表引擎基本能覆盖各种日志场景;Doris 在联合查询和联邦分析上更强,适合有复杂多表关联建模需求的数仓场景。就我们的日志场景来说,ClickHouse 上手更直接,运维成本也相对低。
另外,Flink 同步 MySQL 数据到 ClickHouse 是非常常见的组合,日志场景虽然不是直接用 Flink 同步 MySQL,但这个思路可以借鉴——用 Kafka 作为日志缓冲层,再通过轻量级消费者写入 ClickHouse,整个链路也是经典的"生产端 → 消息队列 → ClickHouse"模式,后面我会详细说。
1.3 分布式集群要解决的核心问题
所谓分布式,不能只是把多台机器装个 ClickHouse 就算完事。真实的分布式集群必须解决三个问题:数据分片、数据冗余、统一查询入口。
数据分片解决的是单机容量和写入性能瓶颈,把数据分散到多台机器上;数据冗余是通过副本机制保证某台机器挂了数据不丢、查询不中断;统一查询入口则是让上层应用感觉在查一个库,而不是去维护一堆节点地址。ClickHouse 的分布式表(Distributed 表引擎)和 ReplicatedMergeTree 表引擎,就是分别解决这三个问题的核心工具。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与部署:三台机器打底
2.1 硬件配置与容量估算
先说硬件。日志分析场景下,ClickHouse 是典型的 CPU 密集型加 IO 密集型负载,CPU 核数直接决定向量化执行和并行查询的能力,磁盘吞吐决定写入和合并的速度。我当时的规划是三个节点,每台配置 16 核 CPU、64G 内存、两块 1TB NVMe SSD(一块放系统,一块放数据),万兆网卡互联。这个配置在当时的日志量(日均 50GB 左右,保留 7 天)下非常宽裕。
关于容量和性能估算,可以这样算:假设单条日志平均 1KB,日均 50GB 大约是 5000 万条。ClickHouse 对文本日志的压缩比通常在 8 到 12 倍,换算下来原始 50GB 数据落盘大约只有 5GB 左右。保留 7 天就是 35GB,再加上 MergeTree 合并过程中的临时空间和 parts 冗余,单机 1TB 磁盘完全够用。内存方面,64G 通常足够支撑 16 核 CPU 的并发查询,但要记住 ClickHouse 的内存消耗大头在查询时的聚合计算和排序,日志场景下不要盲目堆内存,关键是控制并发度和查询模式。
2.2 CentOS 8 系统层面的准备步骤
CentOS 8 虽然在 2021 年底停止了维护,但很多存量生产环境仍在运行。如果你的公司对系统生命周期有严格要求,可以基于这个思路平移到兼容的发行版,比如 Rocky Linux 或 AlmaLinux。系统层面的准备工作其实大同小异,我按 CentOS 8 的实际操作记录一下步骤。
首先关闭 SELinux,否则 ClickHouse 在访问某些路径或进行网络绑定时会遇到权限问题:
bash复制sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
setenforce 0
然后调整文件描述符限制。ClickHouse 在高并发写入、多线程合并时,文件描述符消耗非常快,默认的 1024 根本不够,建议直接调到 655360:
bash复制echo "* soft nofile 655360" >> /etc/security/limits.conf
echo "* hard nofile 655360" >> /etc/security/limits.conf
echo "* soft nproc 131072" >> /etc/security/limits.conf
echo "* hard nproc 131072" >> /etc/security/limits.conf
这里有个容易踩的坑:只是重启 ClickHouse 进程还不够,最好把整个会话退出重新登录,或者重启一次系统,让 limits 配置真正生效。否则你会看到 ClickHouse 日志里不停报"Too many open files",但明明已经改了配置。
还要注意透明大页(THP)的问题。ClickHouse 官方文档建议关闭 THP,因为它会导致内存分配不稳定,在某些场景下造成查询延迟抖动。临时关闭命令是:
bash复制echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
如果要永久生效,把它写入 /etc/rc.d/rc.local 并加上执行权限。
2.3 安装步骤与基础目录规划
ClickHouse 在 CentOS 8 上的官方安装方式很成熟,直接使用官方 RPM 仓库即可。当时我选用的是 21.8.15.7 这个版本,这个版本处于一个比较稳定的迭代周期,和后续的 22.x、23.x 相比,语法兼容性很好,社区讨论也多,遇到问题容易搜到答案。安装命令如下:
bash复制yum install -y wget
wget https://packages.clickhouse.com/rpm/stable/clickhouse-common-static-21.8.15.7.tgz
wget https://packages.clickhouse.com/rpm/stable/clickhouse-client-21.8.15.7.tgz
wget https://packages.clickhouse.com/rpm/stable/clickhouse-server-21.8.15.7.tgz
tar -xzf clickhouse-common-static-21.8.15.7.tgz
tar -xzf clickhouse-client-21.8.15.7.tgz
tar -xzf clickhouse-server-21.8.15.7.tgz
cd clickhouse-common-static-21.8.15.7 && ./install/doinst.sh
cd ../clickhouse-server-21.8.15.7 && ./install/doinst.sh
cd ../clickhouse-client-21.8.15.7 && ./install/doinst.sh
安装完成后,服务默认会被注册为 systemd 服务。数据目录默认在 /var/lib/clickhouse,日志目录在 /var/log/clickhouse-server,配置文件在 /etc/clickhouse-server。有条件的话,建议把数据目录单独挂载到 SSD 数据盘上,避免和系统盘争抢 IO:
bash复制mkdir -p /data/clickhouse
chown -R clickhouse:clickhouse /data/clickhouse
然后在 /etc/clickhouse-server/config.xml 中修改 path 和 tmp_path 指向 /data/clickhouse 下的对应目录。
3. 集群搭建:分片与副本一次配好
3.1 配置文件体系:config.xml 与宏配置
ClickHouse 集群的配置核心是两个文件:config.xml 是主配置文件,定义服务级参数;另一个是通常由用户自定义的 metrika.xml,用来定义集群拓扑、分片和副本的映射关系,然后在 config.xml 中引入。这个设计非常实用,因为集群拓扑变更时,只改 metrika.xml 就行,不需要动主配置。
在 config.xml 中找到 <remote_servers> 标签所在的位置,默认是空的,通过 include 标签引入外部配置:
xml复制<remote_servers>
<include_from>/etc/clickhouse-server/metrika.xml</include_from>
</remote_servers>
3.2 metrika.xml 集群拓扑定义详解
我在 metrika.xml 中定义了一个名为 log_cluster 的集群,包含 2 个分片,每个分片 1 个副本。为什么 2 分片?当时三台机器里,数据量按 2 分片规划,第三台作为其他用途?坦白说,如果按我的实际使用,更推荐 2 分片每分片 2 副本,但当时受机器数量限制,用的是 2 分片加 1 副本,配合另一台机器做备份和查询节点。日志场景如果允许短暂的数据不可用,2 分片 1 副本是可以接受的,但如果数据重要,一定要开副本。
下面是一个标准的 2 分片 2 副本定义(假设你有 4 台机器),如果你只有 3 台,可以把其中一个分片的一副本指向第三台,做成不均匀分布,或者直接用 3 分片 1 副本:
xml复制<clickhouse>
<remote_servers>
<log_cluster>
<shard>
<replica>
<host>ch-node01</host>
<port>9000</port>
</replica>
<replica>
<host>ch-node03</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>ch-node02</host>
<port>9000</port>
</replica>
<replica>
<host>ch-node03</host>
<port>9000</port>
</replica>
</shard>
</log_cluster>
</remote_servers>
<macros>
<shard>01</shard>
<replica>ch-node01</replica>
</macros>
</clickhouse>
注意每个节点上的
3.3 创建本地表与分布式表
集群拓扑定义好之后,还需要在每台机器上分别启动 ClickHouse,并验证节点间能互相通信:
bash复制systemctl start clickhouse-server
systemctl status clickhouse-server
clickhouse-client --host ch-node01 --query "SELECT * FROM system.clusters"
如果 system.clusters 能查到 log_cluster 的信息,说明配置加载成功。此时各节点间 TCP 端口 9000 必须互通,防火墙或安全组策略要提前放行。
接下来是建表。日志场景强烈建议使用 ReplicatedMergeTree 引擎族的表,配合 Distributed 表作为查询入口。假设我们存储的是 Nginx 访问日志,本地表定义如下(每个节点都要执行):
sql复制CREATE TABLE nginx_log_local ON CLUSTER log_cluster (
timestamp DateTime('Asia/Shanghai'),
client_ip IPv4,
request_method String,
request_uri String,
status UInt16,
body_bytes UInt32,
user_agent String,
agent_id UInt32
) ENGINE = ReplicatedMergeTree(
'/clickhouse/log_cluster/tables/nginx_log', -- 该表在 ZooKeeper 中的路径
'{replica}' -- 副本名,取自 macros
)
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (timestamp, client_ip)
TTL timestamp + INTERVAL 7 DAY;
注意 ON CLUSTER 语法让建表语句自动在集群所有节点上执行,不用手动连每台机器敲一遍。ReplicatedMergeTree 的第一个参数是 ZooKeeper 路径,第二个参数是副本名。没有 ZooKeeper 的情况下,Replicated 表无法工作,所以集群还必须要部署一套 ZooKeeper 或者 ClickHouse Keeper。这里不展开 ZooKeeper 部署,但建议至少用 3 个节点组成高可用。若有需要,也可以使用 ClickHouse Keeper,它是 ClickHouse 内置的替代品,配置更简单。
然后创建分布式表,作为统一的查询和写入入口:
sql复制CREATE TABLE nginx_log ON CLUSTER log_cluster (
timestamp DateTime('Asia/Shanghai'),
client_ip IPv4,
request_method String,
request_uri String,
status UInt16,
body_bytes UInt32,
user_agent String,
agent_id UInt32
) ENGINE = Distributed(log_cluster, default, nginx_log_local, rand());
Distributed 引擎的四个参数分别是集群名、库名、本地表名、分片键。这里我用 rand() 做分片键,让数据随机均匀分布到各分片;如果日志需要按某个维度聚合查询,比如按 client_ip 聚合,可以用 client_ip 做分片键,保证相同 IP 的日志落在同一分片,查询时能减少跨节点数据合并。
3.4 ZooKeeper 的接入配置
ReplicatedMergeTree 依赖 ZooKeeper 做元数据协调和副本日志同步。在 metrika.xml 里加上 ZooKeeper 配置:
xml复制<zookeeper>
<node>
<host>zk-node01</host>
<port>2181</port>
</node>
<node>
<host>zk-node02</host>
<port>2181</port>
</node>
<node>
<host>zk-node03</host>
<port>2181</port>
</node>
</zookeeper>
配置好之后重启 ClickHouse。如果 ZooKeeper 连接不上,启动会报错,system.clusters 也查不到副本状态。此时可以检查 ClickHouse 日志中的 ZooKeeper 异常信息,通常能看到连接超时或 session expired 之类的字样。
4. 性能优化:参数级调优实录
4.1 内存参数与线程模型
ClickHouse 默认配置偏保守,很多参数需要根据机器规格调整。我对 16 核 64G 内存的节点做了如下调整。
在 config.xml 中,<max_server_memory_usage> 默认是物理内存的一半,即 32G。日志场景下,如果机器只跑 ClickHouse,可以适度提高到 48G,给 OS 页缓存留出空间。但注意不要直接设为 64G,因为合并任务和查询都可能触发 OOM,留出余量更安全:
xml复制<max_server_memory_usage>53687091200</max_server_memory_usage>
<max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
线程方面,<max_thread_pool_size> 默认是 10000,基本不用动。真正影响查询性能的是每个查询内部的并行度。MergeTree 表查询时,可以通过 max_threads 设置来控制单查询使用多少线程。如果机器 16 核且没有太多并发查询,可以设置 max_threads 为 8 到 12;如果并发查询较多,过高的线程数反而造成上下文切换,得不偿失。
4.2 MergeTree 写入与合并参数
日志场景下写入是首先要保障的。MergeTree 表写入时,数据先形成一个个小的 part 文件,后台线程再把这些 part 合并成大文件。如果 part 数量增长过快,会导致查询时扫描大量文件,性能骤降。
关键参数是 background_pool_size,它控制后台合并和下载副本任务的线程数。默认是 16,日志集群如果写入量大,可以适当调高到 24:
xml复制<background_pool_size>24</background_pool_size>
另一个重要参数是 merge_tree 的 max_part_loading_threads,它影响启动时加载分区元数据的速度,如果分区数很多,调高它可显著加快启动过程。
在表级别,有两种控制合并的方式。通过 settings 调整 parts 的触发阈值:
sql复制ALTER TABLE nginx_log_local MODIFY SETTING parts_to_throw_insert = 600;
当单次插入生成的 part 数量超过阈值时,ClickHouse 会直接拒绝写入,这个值太低会导致高峰期写入报错,太高则会让 parts 堆积,一般建议 300 到 600 之间。同时要关注 parts_to_delay_insert,建议设置为 200 左右,让 ClickHouse 在 parts 过多时先自动降速等待合并,而不是直接报错。
我还习惯给写入任务设置 max_insert_block_size,默认是 1048448 行,日志批量写入时建议调到 100000 到 300000 行。过大的 block 会消耗更多内存,太小的 block 则增加调度开销。
4.3 查询并发与限流策略
日志分析平台通常会有多个业务方查询,必须对资源进行合理分配。ClickHouse 提供了很多 Profile 级别的限制,我一般这样设置:
xml复制<profiles>
<default>
<max_memory_usage>20000000000</max_memory_usage>
<max_execution_time>60</max_execution_time>
<max_result_rows>5000000</max_result_rows>
<max_result_bytes>1000000000</max_result_bytes>
<max_concurrent_queries_for_user>10</max_concurrent_queries_for_user>
</default>
<readonly_profile>
<max_memory_usage>10000000000</max_memory_usage>
<max_execution_time>30</max_execution_time>
<max_result_rows>1000000</max_result_rows>
<readonly>1</readonly>
</readonly_profile>
</profiles>
max_memory_usage 限制单查询最大内存,防止某个大聚合查询把内存吃满;max_execution_time 限制查询最长执行时间;readonly_profile 是只读用户专用的限制,比默认更严格,避免业务方误操作或者跑出一个极端消耗资源的查询。注意,这里 20000000000 字节约等于 18.6G,对于 64G 内存机器是合理的单查询上限。
4.4 写入链路优化:从 Kafka 到 ClickHouse
日志场景最常用的写入链路是 Kafka → ClickHouse。我用的是官方 kafka 表引擎配合物化视图的方式。先在 ClickHouse 中建 Kafka 引擎表:
sql复制CREATE TABLE kafka_log_raw ON CLUSTER log_cluster (
message String
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka-node01:9092,kafka-node02:9092',
kafka_topic_list = 'nginx_access_log',
kafka_group_name = 'clickhouse_log_group',
kafka_format = 'JSONEachRow';
然后建物化视图,把数据从 Kafka 表实时写入本地表:
sql复制CREATE MATERIALIZED VIEW mv_nginx_log TO nginx_log_local AS
SELECT
parseDateTimeBestEffort(JSONExtractString(message, 'timestamp')) AS timestamp,
IPv4StringToNum(JSONExtractString(message, 'client_ip')) AS client_ip,
JSONExtractString(message, 'request_method') AS request_method,
JSONExtractString(message, 'request_uri') AS request_uri,
toUInt16OrZero(JSONExtractString(message, 'status')) AS status,
toUInt32OrZero(JSONExtractString(message, 'body_bytes')) AS body_bytes,
JSONExtractString(message, 'user_agent') AS user_agent,
toUInt32OrZero(JSONExtractString(message, 'agent_id')) AS agent_id
FROM kafka_log_raw;
这个链路的好处是写入 Kafka 的原始日志可以做到秒级进入 ClickHouse 可查询状态,而且 Kafka 表本身不落盘,只是作为数据管道。如果 Kafka 消费积压,可以通过 Kafka 侧的 lag 监控来发现,ClickHouse 侧的 kafka 表会一直消费。
5. Schema 设计与查询优化:日志场景的关键细节
5.1 字段类型选择:能用低基数绝不用字符串
日志数据往往包含大量重复值,比如 HTTP method,无非 GET、POST、PUT、DELETE 等有限几种;status code 也就那几十个。这些字段如果都用 String,存储和查询开销都会放大。正确做法是选择合适的数据类型:
- HTTP method 可以用 LowCardinality(String),ClickHouse 会对这种类型做字典编码,压缩比高、查询速度快。
- 状态码用 UInt16。
- 客户端 IP 用 IPv4 类型,占用 4 字节,比 String 的 15 字节省太多。
- 时间字段用 DateTime,如果不精确到秒可以用 DateTime64。
尤其是 agent_id 这种业务标识字段,如果基数相对可控,用 LowCardinality(UInt32) 会非常高效。不过要注意,低基数数据类型适合基数在几千到几万的维度,如果字段基数非常高(比如用户 ID 上千万),不要用 LowCardinality,否则字典维护本身也是开销。
5.2 分区键、排序键与主键的设计原则
很多刚接触 ClickHouse 的人容易把主键和排序键混为一谈。在 MergeTree 家族中,ORDER BY 才是决定数据在磁盘物理排序的键,主键只是在排序键基础上生成稀疏索引。日志场景下,时间字段是天然的排序维度,所以最常用的设计是:
sql复制ORDER BY (timestamp, client_ip)
这样同一秒内的数据按 IP 有序排列,查询某段时间范围内某个 IP 的日志时,可以借助稀疏索引快速跳过大量无关数据。分区键用 toYYYYMMDD(timestamp) 按天分区,这样查询某天日志时,可以直接跳过其他分区的 parts,甚至在后台数据保留时直接按分区删除,减少 TTL 合并开销。
这里有个细节:如果查询经常按 agent_id 过滤,而 agent_id 没有出现在 ORDER BY 中,可以通过跳数索引来加速,不需要把它加进排序键。否则可能导致排序键过长,插入时排序开销增大。我常用的跳数索引是:
sql复制ALTER TABLE nginx_log_local ADD INDEX idx_agent_id (agent_id) TYPE set(0) GRANULARITY 2;
set(0) 表示每个 granule 记录不重复的 agent_id 集合,GRANULARITY 2 表示每 2 个 granule 建一个索引块。这种索引在基数不太高时效果很明显。
5.3 用物化视图加速常用聚合场景
日志分析最常见的聚合场景是统计 PV、UV、响应码分布、TOP IP 等。对实时性要求高的指标,直接在原始表上聚合,每次查询都要扫描大量数据,虽然 ClickHouse 速度够快,但高峰期多个看板同时刷新时还是会占资源。更好的做法是使用物化视图,在数据写入时同步做预聚合。
例如按分钟统计 PV 和 UV:
sql复制CREATE MATERIALIZED VIEW mv_pv_uv_minute
ENGINE = SummingMergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (ts, client_ip)
AS SELECT
toStartOfMinute(timestamp) AS ts,
client_ip,
count() AS pv,
uniqExact(client_ip) AS uv
FROM nginx_log_local
GROUP BY ts, client_ip;
注意物化视图的数据不会主动删除,要设置 TTL 清理历史数据,避免视图表无限膨胀。这里 SummingMergeTree 会在后台把相同 ORDER BY 的行进行求和合并,查询时再配合 sum 聚合即可得到准确值。
6. 踩坑实录与问题排查
6.1 节点间通信与端口问题
第一个大坑是集群配置完成后,某些副本状态显示不正常,system.replicas 里 is_leader 或 zookeeper_exception 有异常。排查下来发现是安全组只放行了 8123(HTTP 端口)和 9000(原生 TCP 端口),但忽略了 ClickHouse 节点间同步还会用到 9009 端口。9009 是集群节点间数据复制和分布式查询的专属端口,忘了放行导致 ReplicatedMergeTree 的副本同步一直失败。
所以务必检查 8123、9000、9009 三个端口在节点间的连通性:
bash复制nc -vz ch-node01 9009
nc -vz ch-node02 9000
6.2 副本数据不同步
副本数据不同步通常不是端口问题,而是 ZooKeeper 会话超时。默认的 session_timeout_ms 是 30000 毫秒,当 ZooKeeper 节点负载高或网络抖动时,ClickHouse 与 ZooKeeper 的会话可能中断,导致副本停止拉取数据。
解决办法是提高超时阈值并增加重连次数:
xml复制<zookeeper>
<session_timeout_ms>60000</session_timeout_ms>
<operation_timeout_ms>10000</operation_timeout_ms>
</zookeeper>
同时可以在 ClickHouse 客户端执行:
sql复制SYSTEM SYNC REPLICA nginx_log_local;
手动触发副本同步,观察是否恢复正常。
6.3 磁盘 IO 与文件句柄导致的写入变慢
高峰期写入变慢,排查发现磁盘 IO 已经接近饱和。日志写入会产生大量小 parts,合并线程也在同时做 IO,两者互相争抢。优化方案有两个方向:
第一,调整表配置,降低 parts 触发合并的阈值,让合并更积极,减少小 part 对 IO 的消耗。第二,把系统临时目录 tmp_path 从系统盘迁移到数据盘,并确保 tmp 目录所在磁盘有足够空间。实际经验中,还要注意 Kafka 消费端的 batch size,如果每次消费只写入几百行就 flush,会产生大量极小 part,加剧合并压力。建议把 min_insert_block_size_rows 设置为 50000,让写入攒够一批再落盘。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| system.replicas 报 zookeeper_exception | ZooKeeper 会话超时或地址不通 | 检查 2181 端口连通性;调大 session_timeout_ms |
| 查询报 Memory limit exceeded | 单查询内存超限 | 调大 profile 中的 max_memory_usage,或优化 SQL 减少 sort/group 中间结果 |
| 写入报 Too many parts | parts 合并速度跟不上写入速度 | 调大 parts_to_throw_insert;增加 background_pool_size;降低写入并发 |
| 副本一直落后 | 网络带宽、9009 端口被限制 | 检查节点间网络;用 SYSTEM SYNC REPLICA 手动触发 |
| 启动很慢 | 分区数多、part 加载慢 | 调大 max_part_loading_threads;检查是否积压了大量未合并 parts |
| ClickHouse 进程被 OOM Kill | 总内存超限 | 调低 max_server_memory_usage;限制查询内存和并发数 |
关于 OOM Kill,我再补充一个经验。ClickHouse 的 memory tracker 能够感知内存压力,但如果服务器同时运行了 ZooKeeper、监控采集等进程,非常容易触发系统级 OOM。建议对 ClickHouse 服务设置 systemd 的 MemoryLimit,宁可牺牲一点性能也要保证进程稳定。
结尾还是用实际的感受收一下。这套配置跑下来,最大的体会是 ClickHouse 分布式集群的性能上限,很大程度不取决于单机多强,而取决于你对写入路径、合并策略和查询资源隔离这三件事的控制粒度。日志场景下,数据模型越规整,类型用得越贴合,后期要做的优化就越少。比如我把 agent_id 和 status 改成低基数类型后,同样的查询直接快了一倍,这种收益比任何参数调优都来得明显。如果你也正在折腾日志平台,不要急着堆机器,先把手上的节点按这篇文章的思路把分片副本、索引设计、资源限制理清楚,大概率你会发现瓶颈并不在 ClickHouse 本身,而是之前根本没把参数调到适合业务的那一档。
