ClickHouse 内存使用率超过90%告警排查:一条 DataWorks ETL insert 吃满 60G 主机(Code 241 Memory limit exceeded)
一句话结论:这条「内存使用率超过 90%」的告警,是 DataWorks 的一个 ETL 任务反复跑同一条
insert,单条要吃 67 GiB(占满 60G 主机),把内存反复顶到 ~93% 触发的——其他所有来源加起来不到 1 GiB,可忽略。下面是把它从「主机内存高」一层层追到「具体哪条 SQL、从哪台机、哪个调度任务、谁负责、跑了几次、每次把内存顶到多高」的全过程。每个数字都来自线上
system.query_log/system.asynchronous_metric_log/ DataWorks API 实查,可复现。
注:文中 IP(192.168.x.x)/ 库表名(biz_db.*)/ 人名(开发A)均为脱敏占位,替换成你自己的即可。

0. 告警
【持续一分钟】内存使用率超过90%
告警时间:2026-06-23 16:43:26
告警实例:ch-prod-1 (单机 ClickHouse,60 GiB 内存)
触发时值:91.84%
到「内存大户是 ClickHouse → 建议调低 ratio / 扩容」就收手,是症状层。真问题是:哪条查询、谁发起的、为什么反复发生。
1. 哪条查询吃的?——system.query_log
SELECT event_time, type, formatReadableSize(memory_usage) AS mem,
initial_address, left(query, 45) AS query -- 把 query 也 SELECT 出来,结果里才看得到是哪条 SQL
FROM system.query_log
WHERE event_time BETWEEN '2026-06-23 16:30:00' AND '2026-06-23 17:30:00'
AND query LIKE '%dm_revenue_attr%' AND lower(query) LIKE 'insert%'
AND memory_usage > 30000000000
ORDER BY event_time;
| event_time | query(前 45 字) | mem | type | 来源 IP |
|---|---|---|---|---|
| 16:43:27 | insert into biz_db.dm_revenue_attr with ... |
67.72 GiB | ExceptionWhileProcessing | 192.168.1.124 |
| 16:50:49 | insert into biz_db.dm_revenue_attr with ... |
69.07 GiB | ExceptionWhileProcessing | 192.168.1.124 |
| 17:17:52 | insert into biz_db.dm_revenue_attr with ... |
59.92 GiB | ExceptionWhileProcessing | 192.168.1.124 |
注:原 SQL 的
SELECT里漏了query列,跑出来只有时间/内存/IP、看不到是哪条语句——这里补上left(query,45),结果就能直接看到insert into ...,不必只靠WHERE过滤去信。
三条 insert into biz_db.dm_revenue_attr,单条 67~69 GiB,全部 ExceptionWhileProcessing,异常正文:
Code: 241. Memory limit (total) exceeded: would use 67.72 GiB, maximum: 54.38 GiB.
OvercommitTracker decision: Query was selected to stop
撑到 ClickHouse 的 max_server_memory_usage(54.38 GiB ≈ 主机 90%)被 OvercommitTracker 杀掉——ClickHouse 的内存保护在工作,所以才停在 ~90% 没真把机器 OOM。这条查询本身是 9 层 CTE、19 个 join、扫 31 天数据的复杂 ETL 写入。
2. 「对比着看」:是不是它一家独大?——各来源内存占用对比
只说「它是元凶」没说服力。把告警时段各来源的峰值内存拉出来排队:
▸ 结论:DataWorks insert 最大,67.72GiB,占合计 99%(绝对大头,其余可忽略);其余 2 项合计 0.9GiB。
告警时刻各来源峰值内存:
DataWorks insert(192.168.1.124) ██████████████████████████████████████████████████ 67.72GiB ← 元凶
Java client(192.168.1.123) █ 0.5GiB
QuickBI 报表(192.168.1.80) █ 0.4GiB
一眼看清:DataWorks 这一条占了 99%,其它全是零头。不用猜,就是它。
3. 192.168.1.124 是什么?——内网 IP 反查
default 账号、JDBC(ClickHouse Java Client)——不是人在终端敲的。那这台机是什么?
- 不是 ECS:
DescribeInstances --PrivateIpAddresses '["192.168.1.124"]'→ 0 条。 - 查 ENI:
DescribeNetworkInterfaces --VpcId xxx --PrivateIpAddress.1 192.168.1.124→
Type: Member Description: created by dataworks InstanceId: (空)
Tags: serverless/eni-creator: asi-cni-service
192.168.1.124 = 阿里云 DataWorks 的 serverless 调度资源组。这条 insert 是 DataWorks 调度任务跑的。
4. 哪个任务、谁负责、跑了几次?——DataWorks API
ListNodes(name=dm_revenue_attr) → 节点 id=<NodeId>
GetNode → TaskId=<TaskId>, owner=<用户ID>
ListProjectMembers → <用户ID> = 开发A
归属:DataWorks 任务 dm_revenue_attr,负责人 开发A。
但「归属 ≠ 这次是他执行的」——owner 只是「这表归谁」。要证执行,看任务操作日志 + 实例运行日志:
ListTaskOperationLogs(TaskId=<TaskId>):
15:26 / 15:37 / 15:53 / 16:27 / 16:35 开发A 创建补数据工作流
15:33 开发A Update 节点(改 SQL)
17:21 开发A 手动重跑实例 #<实例ID>
GetTaskInstanceLog(#<实例ID>, 逐次 RunNumber):
SKYNET_ONDUTY=<用户ID> ← 执行环境盖的「值班人」= 开发A
start run sql: insert into biz_db.dm_revenue_attr ...
java.sql.SQLException: Code: 241. Memory limit exceeded ...
Exit code 1
一条运行日志里同时有「执行人戳 + 跑的就是这条 insert + Code241 失败」——这才是执行级实证,不是靠 owner 旁推。
数据要准 / 归属≠执行:这是我反复栽过、最后立成纪律的两条。owner 是「这表归谁」,只有「实例日志的
SKYNET_ONDUTY+ 跑了该查询 + 失败时刻」三者齐了,才敢点名「某人某时执行触发」;拿不到就老老实实写「执行人待核」,绝不拿 owner 反推谁干的。
5. 跑了几次、每次把内存顶到多高?——内存时序
光说「一条查询 67 GiB」不够,关键是反复。查 ClickHouse 自己记的主机可用内存(system.asynchronous_metric_log 的 OSMemoryAvailable,60 GiB 机):
主机可用内存(GiB,柱长∝数值):
16:30 ████████████████████████████████████████ 36.9
16:43 ████ 4.0 ← insert#1 67.72GiB,使用率 ~93%,触发告警
16:44 █████████████████████████████ 28.2 ← 查询被杀,内存释放
16:50 ███ 3.3 ← insert#2 69.07GiB,~94%
16:51 ██████████████████████████████████████████████████ 48.3 ← 释放
16:58 ███████████ 10.4 ← 又一次
17:02 ███████ 6.8
17:17 █████████ 9.1 ← insert#3 59.92GiB
17:18 ███████████████████████████████████████████ 42.1 ← 释放
17:19~ ~48 平稳,不再有大查询
一条典型的「反复 OOM 锯齿」:每次 开发A 触发补数据/重跑 → ClickHouse 内存冲到 ~54 GiB 上限(主机可用砸到 3~10 GiB、利用率 83~94%)→ 被杀 → 内存弹回 → 下一次再来。16:43 那次顶到 91.84%、越过 90% 阈值,告警就响了。
口径说明:
query_log.memory_usage(67 GiB)是查询追踪的峰值分配,会超过物理内存;OSMemoryAvailable是 OS 真实可用。两者一起看——查询想要 67 GiB,撑到 54.38 GiB 的 server 上限被杀,对应 OS 可用砸到 3~4 GiB,互相印证。
6. 完整因果链(每环都有实查证据)
内存告警 91.84% @16:43
└─ 元凶:insert into dm_revenue_attr,67.72 GiB OOM [query_log]
└─ 占告警时段总内存压力 99%,其余来源可忽略 [query_log 按来源聚合]
└─ 来源 IP 192.168.1.124 = DataWorks serverless 资源组 [ENI created by dataworks]
└─ 产出节点 dm_revenue_attr,负责人 开发A [DataWorks API]
└─ 开发A 15:26~17:21 反复补数据/重跑 [操作日志]
└─ 实例日志 SKYNET_ONDUTY=开发A + 跑该 insert + Code241 [实例日志]
└─ 每次执行主机内存砸到 ~90%+,反复 OOM 锯齿 [metric_log 时序]

根因:开发A 下午改了 dm_revenue_attr 的 SQL 后,反复用 DataWorks「补数据 / 手动重跑」触发这条 9-CTE/31-天的 ETL 大写入;单条需 67 GiB 超过主机 60 GiB,每次都把 ClickHouse 顶到内存上限、OOM 失败,其中一次越过 90% 触发告警。
处置(对症、非动宿主机):
- 给这条 insert 加
max_memory_usage(如 40 GiB)/ 拆批写入 / 降并行; default用户级max_memory_usage限流,防单查询打满;- 业务高峰别反复手动补数据;
- 找任务负责人 开发A 落实——而不是去调主机内存 ratio。
7. 方法论:这套「反查链」可复用
| 步 | 内存告警 | CPU/负载告警 | 工具 |
|---|---|---|---|
| 找元凶 | query_log ORDER BY memory_usage |
ORDER BY ProfileEvents['OSCPUVirtualTimeMicroseconds'] / read_bytes |
ClickHouse |
| 对比各来源 | query_log 按 initial_address 聚合 |
同 | ClickHouse |
| IP→服务 | DescribeNetworkInterfaces 看 ENI Description |
同 | 阿里云 ECS API |
| →归属/执行人 | ListNodes→GetNode→Members、实例日志 SKYNET_ONDUTY |
同 | DataWorks API |
| 对齐严重度 | asynchronous_metric_log 内存时序 |
CPU/IO 时序 | ClickHouse |
两条纪律(踩坑换来的):
- 数据要准:每个数字亲手实查、全链路对账(query_log 时刻 ↔ 实例日志失败时刻 ↔ 内存时序谷底要对得上);
ok=True≠ 数据有效。 - 先一句话给结论:别让人去读柱状图——开头直接「DataWorks 占 99%、其余可忽略」点破,图作佐证。
这套链路最后做进了 SRE 告警机器人:
query_log 定元凶 → 各来源占比对比 → resolve_source_ip(IP→服务) → trace_dataworks_task(→任务+负责人) → 内存时序图,让排查卡片自动从「default 从某 IP」升级到「DataWorks 任务 dm_revenue_attr、负责人 开发A、占 99%、反复 OOM」。

浙公网安备 33010602011771号