Clickhouse 内存问题排查
Clickhouse 内存问题排查

1. 业务层排查:定位高消耗源头
1.1. 查看当前正在运行的查询
通过 system.processes 查看哪些 SQL 正在大量消耗内存。
SELECT
hostname(),
query_id,
user,
formatReadableSize(memory_usage),
elapsed,
query
FROM clusterAllReplicas('ck_cluster', system.processes)
WHERE memory_usage > 0
ORDER BY memory_usage DESC;
1.2. 分析历史查询峰值
利用 system.query_log 找出过去一段时间内的“内存杀手”。
SELECT
argMax(hostName(), memory_usage) AS node_host,
argMax(user, memory_usage) AS execution_user,
formatReadableSize(max(memory_usage)) AS max_mem_readable,
max(memory_usage) AS raw_max_mem,
argMax(query_start_time, memory_usage) AS start_time,
argMax(event_time, memory_usage) AS end_time,
round(argMax(query_duration_ms, memory_usage) / 1000, 2) AS duration_seconds,
argMax(query, memory_usage) AS worst_query
FROM clusterAllReplicas('ck_cluster', system.query_log)
WHERE type = 'QueryFinish'
AND event_time >= now() - INTERVAL 1 MONTH -- AND hostname() like '%单独节点名字%'
GROUP BY normalized_query_hash
ORDER BY raw_max_mem DESC
limit 20;
1.3. 检查特殊引擎表(目前可忽略)
Memory、Set、Join 等引擎表的数据常驻内存,需重点检查。(目前我们主要的业务方都没有该类表引擎)
2. 系统层排查:定位内部组件分配
2.1. 查看组件内存分布
使用 system.asynchronous_metrics 或 system.metrics 查看缓存(如 MarkCache、UncompressedCache)的占用。
- system.metrics(实时指标表):这个表里的数据是 ClickHouse C++ 源码中维护的全局实时原子计数器。只要 ClickHouse 内部发生了一次内存分配 (malloc),这个计数器就会 +1。所以你的第二个查询里 MemoryTracking = 2.24 GiB,这就是 ClickHouse 源码自己算出来的:“我当前申请且没释放的内存总量就是这么多”。
- system.asynchronous_metrics(异步指标表):ClickHouse 内部有一个后台线程,每隔一秒钟,它会主动去读取 Linux 系统的 /proc/meminfo(查系统内存)、读取 /sys/fs/cgroup/memory/(查 K8s Pod 内存)、并统计自己各个 Cache 占用的字节数。然后把这些结果写到这张表里。
SELECT hostname(), metric, value, formatReadableSize(value)
FROM clusterAllReplicas('ck_cluster', system.asynchronous_metrics)
WHERE metric LIKE '%Cache%' OR metric LIKE '%Memory%';
SELECT
hostname(),
name,
value,
formatReadableSize(value) as size
FROM clusterAllReplicas('ck_cluster', system.metrics)
WHERE value > 0
ORDER BY value DESC LIMIT 10;
2.1.1. asynchronous_metrics 内存参数详解表
2.1.1.1. 进程真实物理内存(最重要,直接决定是否 OOM)
这部分是 ClickHouse 作为一个 Linux 进程,真正在服务器上占用的情况。
|
指标名称 (metric) |
当前值 (可读) |
实际含义与排查价值 |
|
MemoryResident |
2.03 GiB |
当前实际使用的物理内存 (RSS)。这是 ClickHouse 真正吃掉的物理内存,非常健康。 |
|
MemoryResidentMax |
12.79 GiB |
历史最高物理内存占用峰值。自进程启动以来,最高曾飙到过 12.8G。如果这个值接近 Pod 的 Limit,就有 OOM 风险。 |
|
MemoryShared |
317.36 MiB |
共享内存大小(通常是和其他进程共享的库文件)。 |
|
MemoryCode |
391.94 MiB |
ClickHouse 自身二进制执行代码加载到内存中占用的大小。 |
2.1.1.2. 进程虚拟内存
这部分是 ClickHouse 向操作系统“画的饼”,并不是真正占用的物理内存。
|
指标名称 (metric) |
当前值 (可读) |
实际含义与排查价值 |
|
MemoryVirtual |
194.53 GiB |
虚拟内存总空间 (VIRT)。因为 ClickHouse 大量使用 mmap 映射磁盘文件,所以虚拟内存极大,这是完全正常的,绝对不会导致 OOM。 |
|
MemoryDataAndStack |
193.60 GiB |
虚拟内存中的数据和栈空间,包含在上述 194G 中。 |
2.1.1.3. 操作系统 (OS) 与容器 (CGroup) 内存环境
ClickHouse 会去探查它所在的宿主机/Pod 的整体内存环境,以决定自己该怎么克制。
|
指标名称 (metric) |
当前值 (可读) |
实际含义与排查价值 |
|
OSMemoryTotal |
15.57 GiB |
宿主机(或虚拟机)的总物理内存约为 16G。 |
|
OSMemoryAvailable |
8.71 GiB |
宿主机当前完全可用(空闲)的内存。 |
|
OSMemoryCached |
9.53 GiB |
Linux 系统的 Page Cache(文件缓存)。这部分内存如果进程需要,系统会随时释放给进程用。 |
|
CGroupMemoryUsed |
12.02 GiB |
当前 Pod(容器)已使用的总内存。注意:这里是 12G,而 ClickHouse 进程才 2G,说明 Page Cache 占了大部分。 |
|
CGroupMemoryTotal |
8.00 EiB |
当前 Pod 的内存上限。显示 8 EiB (Exabytes) 意味着你的这个 Pod 没有在 K8s 中设置 limits.memory(无限大)。 |
2.1.1.4. . ClickHouse 内部核心缓存(决定查询速度)
这部分是 ClickHouse 内部账本,记录了各种加速查询的 Cache 大小。
|
指标名称 (metric) |
当前值 (可读) |
实际含义与排查价值 |
|
MarkCacheBytes |
67.02 MiB |
标记缓存大小。这是 ClickHouse 最核心的缓存,用来快速定位磁盘数据。当前只用了 67MB,非常少。 |
|
MarkCacheFiles |
22.8 万个 |
标记缓存中存放的条目数量。 |
|
TotalPrimaryKeyBytes... |
6.68 MiB |
常驻内存的主键大小。ClickHouse 的稀疏主键是永远放在内存里的,当前只占 6.6MB,说明你的表数据量还没达到极其海量的程度。 |
|
CompiledExpression... |
1.27 MiB |
JIT 编译表达式的缓存大小。用于加速复杂函数的计算。 |
|
UncompressedCache... |
0 |
未压缩数据缓存。你没有开启这个功能(默认也是关的,开启极耗内存)。 |
|
QueryCacheBytes |
0 |
查询结果缓存。说明你目前没有使用把查询结果直接缓存下来的功能。 |
2.1.2. system.metrics(实时指标表)参数详解
|
指标名 |
说明 |
类型 |
|
|
服务器当前已分配内存 |
核心 |
|
|
mmap 分配的内存总字节 |
mmap 分配 |
|
|
mmap 分配次数 |
次数 |
|
|
mmap 文件映射大小 |
文件映射 |
|
|
Query Cache 占用内存 |
缓存 |
|
|
未压缩缓存占用 |
缓存 |
|
|
Mark Cache 占用内存 |
读索引缓存 |
|
|
JIT 编译缓存占用 |
表达式缓存 |
|
|
文件系统缓存大小 |
FS Cache |
|
|
Buffer 表占用内存 |
Buffer 引擎 |
2.2. 内存分配采样 (Memory Profiling)
ClickHouse 允许对内部内存分配进行采样。通过 system.trace_log 可以分析哪些 C++ 函数在分配内存。
-- 需要开启内存采样设置(默认是开启的)
-- SET memory_profiler_step = 4194304;
select * from system.settings where name like '%memory_profile%';
-- 最近一个月“总内存分配量 Top 10
SELECT
query_id,
formatReadableSize(sum(size)) AS total_allocated,
sum(size) AS total_bytes
FROM system.trace_log
WHERE trace_type = 'Memory'
AND size > 0
AND event_date >= today() - 30
GROUP BY query_id
ORDER BY total_bytes DESC
LIMIT 10;
2.2.1.1. 对内存分析的含义
trace_type --- Enum8('Real' = 0, 'CPU' = 1, 'Memory' = 2, 'MemorySample' = 3, 'MemoryPeak' = 4, 'ProfileEvent' = 5)
|
trace_type |
含义 |
|
|
超过 memory_profiler_step 时的内存分配 / 释放采样(最常用) |
|
|
随机内存采样(概率采样) |
|
|
内存峰值变化记录 |
|
|
ProfileEvents 事件(不是内存分配本身) |
3. 工具与底层排查
3.1. jemalloc 统计
ClickHouse 默认使用 jemalloc 管理内存。可以查看 jemalloc 的状态来确认是否存在堆外内存泄漏或严重的碎片化。
SELECT
hostname(),
metric,
value,
formatReadableSize(value) AS readable_value,
description
FROM clusterAllReplicas('ck_cluster', system.asynchronous_metrics)
WHERE metric LIKE 'jemalloc%' and hostname() like '%你的节点名%'
ORDER BY value DESC;
3.1.1. 核心内存指标(直接关系到 OOM 和内存使用)
这是排查内存时最需要看的几个指标,它们描绘了内存从“应用层”到“操作系统层”的层次关系。
|
指标名称 (jemalloc.*) |
当前值 (易读) |
实际含义与排查重点 |
|
allocated |
1.88 GiB |
ClickHouse 代码真正在使用的内存。 这是 ClickHouse 发出 malloc 请求并正在使用的净内存大小。 |
|
active |
2.12 GiB |
jemalloc 分配的活跃内存(包含碎片)。它比 allocated 略大,多出来的 0.24 GiB 就是 jemalloc 内部的内存碎片。这个差值越小越健康。 |
|
resident |
2.61 GiB |
实际占用的物理内存 (RSS)。 jemalloc 认为它当前持有的操作系统物理内存。这个值最接近你在 K8s Pod 或 Linux top 命令中看到的实际内存使用量 (RES/Working Set)。 |
|
mapped |
2.76 GiB |
jemalloc 映射的总内存。 通常略大于 resident。 |
|
retained |
181.54 GiB |
保留的虚拟地址空间(不用慌,不是物理内存!) jemalloc 曾经向 OS 申请过这些内存,后来 ClickHouse 释放了。jemalloc 使用 madvise 告诉系统“这些物理页可以回收”,但保留了虚拟地址映射以便下次直接用。这 181G 是虚拟内存(VIRT),不会导致你的 Pod OOM。 |
|
metadata |
481.66 MiB |
jemalloc 自身的管理开销。 jemalloc 为了管理上述内存,自己内部维护数据结构消耗了 481MB 内存。 |
诊断你的情况:
你的 ClickHouse 代码用了 1.88G,加上碎片和元数据,最终在操作系统层面大概占了 2.61G (Resident) 的物理内存。当前状态非常健康,没有出现 jemalloc 严重扣留物理内存导致伪泄漏的情况。
3.1.2. 后台清理与脏页指标(决定内存释放速度)
jemalloc 会通过后台线程,将不再使用的“脏页”(Dirty Pages)清理并还给操作系统。
|
指标名称 (jemalloc.*) |
你的原始值 |
实际含义 (注意:不可用可读格式衡量) |
|
arenas.all.dirty_purged |
63,596,881,780 |
累计清理的脏页总字节数 (约 59 GiB)。 代表 ClickHouse 启动至今,jemalloc 后台已经成功将这么多的废弃物理内存归还给了操作系统。 |
|
background_thread.num_threads |
4 |
后台清理线程数。<br/>当前有 4 个后台线程在持续工作,帮 jemalloc 回收内存给操作系统。 |
|
background_thread.num_runs |
30,019,053 |
后台线程的运行总次数(计数器)。<br/>说明清理线程非常活跃,已经跑了 3000 多万次。 |
|
background_thread.run_intervals |
140,186,662... |
线程运行的时间间隔统计(纳秒级累加值)。 (所以它被错误显示成 127 TiB,其实是个时间累加计数器)。 |
|
arenas.all.pactive |
554,970 |
活跃内存页的数量 (Pages)。 一页通常是 4KB。55万页 * 4KB ≈ 2.12 GiB,刚好等于上面的 active 内存。 |
|
arenas.all.pdirty |
14,959 |
脏页的数量 (Pages)。<br/>当前有约 1.5 万个脏页(约 58 MB)等待后台线程清理并还给系统。这个值很小,说明清理很及时。 |
3.1.3. 其他未激活/空指标
这些指标在你当前系统中为 0,了解即可。
|
指标名称 (jemalloc.*) |
当前值 |
实际含义 |
|
arenas.all.muzzy_purged |
0 |
累计清理的 Muzzy(半空闲/模糊)内存页大小。Muzzy 是 jemalloc 的一种特殊的内存缓冲状态,这里为 0 说明没用到。 |
|
arenas.all.pmuzzy |
0 |
当前处于 Muzzy 状态的内存页数量。 |
|
metadata_thp |
0 |
透明大页 (Transparent Huge Pages) 的元数据占用。为 0 代表没有使用 THP(ClickHouse 官方也强烈建议关闭系统 THP,所以为 0 是正确的)。 |
|
prof.active |
0 |
jemalloc 内存剖析器 (Profiler) 是否开启。0 代表未开启(开启后会严重影响性能,仅用于 Debug 查内存泄漏)。 |
|
epoch |
7,876,161 |
jemalloc 内部状态刷新的版本/世代号(计数器)。每次读取指标时刷新,无排查意义。 |
3.2. 版本已知问题
检查是否由于异步日志(如 text_log)积压导致的内存异常。在某些版本(如 v25.10 之前)中,过高的日志采集频率可能引发类似泄漏的现象。
内存碎片情况
- Active vs Allocated:
active(2.1GB) /allocated(1.88GB) ≈ 1.11。这意味着实际物理内存分配效率尚可,碎片率在 10% 左右,属于正常范围。 - Mapped (2.7GB):指 jemalloc 当前实际占用的总物理内存页。与
resident(2.6GB) 基本匹配,说明没有明显的 RSS(物理内存)泄露。
更多推荐
所有评论(0)