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. 检查特殊引擎表(目前可忽略)

MemorySetJoin 等引擎表的数据常驻内存,需重点检查。(目前我们主要的业务方都没有该类表引擎)

2. 系统层排查:定位内部组件分配

2.1. 查看组件内存分布

使用 system.asynchronous_metrics 或 system.metrics 查看缓存(如 MarkCacheUncompressedCache)的占用。

  • 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(实时指标表)参数详解

指标名

说明

类型

MemoryTracking

服务器当前已分配内存

核心

MMappedAllocBytes

mmap 分配的内存总字节

mmap 分配

MMappedAllocs

mmap 分配次数

次数

MMappedFileBytes

mmap 文件映射大小

文件映射

QueryCacheBytes

Query Cache 占用内存

缓存

UncompressedCacheBytes

未压缩缓存占用

缓存

MarkCacheBytes

Mark Cache 占用内存

读索引缓存

CompiledExpressionCacheBytes

JIT 编译缓存占用

表达式缓存

FilesystemCacheSize

文件系统缓存大小

FS Cache

StorageBufferBytes

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

超过 memory_profiler_step 时的内存分配 / 释放采样(最常用)

MemorySample

随机内存采样(概率采样)

MemoryPeak

内存峰值变化记录

ProfileEvent

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 Allocatedactive (2.1GB) / allocated (1.88GB) ≈ 1.11。这意味着实际物理内存分配效率尚可,碎片率在 10% 左右,属于正常范围。
  • Mapped (2.7GB):指 jemalloc 当前实际占用的总物理内存页。与 resident (2.6GB) 基本匹配,说明没有明显的 RSS(物理内存)泄露。


 

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐