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)均为脱敏占位,替换成你自己的即可。

ClickHouse 内存告警 → 反查归因链

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_logOSMemoryAvailable,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% 触发告警。

处置(对症、非动宿主机)

  1. 给这条 insert 加 max_memory_usage(如 40 GiB)/ 拆批写入 / 降并行;
  2. default 用户级 max_memory_usage 限流,防单查询打满;
  3. 业务高峰别反复手动补数据;
  4. 找任务负责人 开发A 落实——而不是去调主机内存 ratio。

7. 方法论:这套「反查链」可复用

内存告警 CPU/负载告警 工具
找元凶 query_log ORDER BY memory_usage ORDER BY ProfileEvents['OSCPUVirtualTimeMicroseconds'] / read_bytes ClickHouse
对比各来源 query_loginitial_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」。

posted @ 2026-06-24 15:06  Hello_worlds  阅读(46)  评论(0)    收藏  举报