跳转至

Vertica 内存压力诊断与调优

作者:JiangChong | 发布时间:2026-04-07

适用场景:集群频繁出现 RESOURCE_REJECTED、查询排队严重、query_budget 不足导致 Spill,或内存使用率持续 > 90%。

关联Vertica 资源池最佳实践 | Vertica 监控最佳实践 | Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 | Vertica CPU 持续高负载诊断与优化


理解全文脉络

本文按照「理解内存模型 → 发现内存问题 → 定位内存消耗者 → 解决问题」的路径组织。如果你刚收到 RESOURCE_REJECTED 告警,可以从第 2 节开始排查;如果你想理解 Vertica 内存分配机制以便做容量规划,从第 1 节读起。


1. 理解 Vertica 的内存模型

1.1 Vertica 如何使用内存

Vertica 的内存消耗与传统数据库有本质区别。理解这几点是诊断内存问题的基础:

  • 没有 shared buffer pool:PostgreSQL 有 shared_buffers,MySQL 有 InnoDB buffer pool,而 Vertica 不使用全局缓存。每次查询都直接从磁盘读取数据到查询专属的内存空间中处理。
  • 内存按查询分配:每条查询在开始执行时,Resource Manager 会根据资源池的 query_budget 为它预留一块内存。如果查询实际需要更多内存,可以在执行中申请额外内存(AcquireAdditional 请求)。
  • 内存换磁盘 I/O:Vertica 的算子(如 GroupByHashJoinHash)倾向将数据尽量保留在内存中处理。如果内存不够,数据会溢出(Spill)到磁盘临时文件——Spill 不仅增加 I/O,还会因额外的排序操作增加 CPU 消耗。
  • 内存是硬限制:当资源池的 MAXMEMORYSIZE 被打满,新的查询请求会被拒绝(RESOURCE_REJECTED)或排队等待(RESOURCE_QUEUES)。

关键结论:Vertica 的内存消耗模式是「爆发式」的——平时可能很空闲,但一旦有复杂查询,瞬间申请几十 GB。这与传统 OLTP 数据库的「稳定水位」完全不同。

1.2 内存压力的三种表现

当系统出现内存压力时,通常表现为以下三种模式之一或组合:

表现 症状 对应的系统表
内存资源拒绝(RESOURCE_REJECTED) 查询提交后被直接拒绝,报错 RESOURCE_REJECTED v_monitor.resource_rejections
查询排队(Queuing) 查询长时间处于 Queue 状态,position_in_queue 不断增长 v_monitor.resource_queues
查询溢出磁盘(Spill) 查询执行正常但极慢,执行计划中出现 GROUP_BY_SPILLEDJOIN_SPILLED v_monitor.query_eventsexecution_engine_profiles

1.3 内存来源分类

在 Vertica 内部,内存消耗可以分解为以下几个维度:

来源 说明 典型特征
查询执行内存 每个算子的内存申请(哈希表、排序缓冲区、网络缓冲区) 占比最大,集中在 GroupByHashJoinHashSort 算子
Tuple Mover 内存 mergeout/moveout 过程中的排序和压缩 TM 资源池独立配置,默认 MAXCONCURRENCY=7PLANNEDCONCURRENCY=4query_budget 很小
Recovery 内存 节点恢复时的数据同步和校验 恢复完成即释放
Catalog / Metadata 内存 表定义、投影定义、统计信息等元数据加载 Catalog 过大(每节点 > 20GB)时消耗显著
操作系统开销 文件系统缓存(page cache)、网络缓冲区、进程开销 Linux free -hbuff/cache 占用

详见 Vertica 监控最佳实践


2. 系统级内存监控:先看清全局

2.1 使用 system_resource_usage 查看历史内存

v_monitor.system_resource_usage 表以固定间隔(默认每 10 秒)采集每个节点的内存使用百分比。

SELECT node_name,
       end_time,
       average_memory_usage_percent,
       average_cpu_usage_percent
FROM v_monitor.system_resource_usage
WHERE end_time > sysdate - INTERVAL '1 hour'
ORDER BY end_time DESC;

如何解读结果

  • average_memory_usage_percent > 90% 持续超过 10 分钟:内存已成为瓶颈,新查询大概率被拒绝或排队。
  • 各节点内存使用差异 > 30%:说明某些节点承担了不成比例的数据或查询负载(数据倾斜或查询路由不均)。
  • 内存高但 CPU 低:典型的内存溢出(Spill)场景——查询在等磁盘 I/O,CPU 在等数据。优化方向是增大 query_budget 或优化 SQL。
  • 内存和 CPU 同时高:正常的大计算场景,系统在全力工作。如果查询响应时间可以接受,无需干预。

2.2 使用 Linux 命令辅助定位

# 查看系统内存使用(重点关注 available 列)
free -h

# 查看 Vertica 进程的内存占用(RSS)
ps aux | grep vertica | awk '{sum+=$6} END {print "Total RSS: " sum/1024/1024 " GB"}'

# 查看内存使用 Top 10 的进程
ps aux --sort=-%mem | head -11

如何解读

  • free -havailable < 10% 总内存:操作系统层面内存紧张,可能触发 OOM Killer。
  • buff/cache 占用过高:Linux 文件系统缓存挤占了可用内存。可通过 echo 3 > /proc/sys/vm/drop_caches 释放(需谨慎,会影响 I/O 性能)。
  • 单个 Vertica 进程 RSS 超过节点总内存的 80%:可能有内存泄漏查询或资源池配置不当。

3. 定位内存消耗来源

3.1 第一步:查看资源拒绝事件

RESOURCE_REJECTED 是内存不足的最直接信号。每当 Resource Manager 拒绝了某条查询的内存申请,就会在 v_monitor.resource_rejections 中记录一条。

SELECT node_name,
       pool_name,
       reason,
       resource_type,
       rejection_count,
       first_rejected_timestamp,
       last_rejected_timestamp,
       last_rejected_value
FROM v_monitor.resource_rejections
WHERE last_rejected_timestamp > sysdate - INTERVAL '1 hour'
ORDER BY last_rejected_timestamp DESC
LIMIT 20;

如何解读

  • reason 字段直接告诉你是谁被拒绝了:
  • Memory(KB) Exceeded:池的 MAXMEMORYSIZE 已被打满,无法接受新请求
  • Request exceeds limits:单条查询申请的额外内存超过了池上限
  • rejection_count:该资源在该时间段内被拒绝的次数。如果该值很大,说明大量查询重复申请失败。
  • last_rejected_value:最后一次被拒绝时请求的值(与 reason 配合定位具体缺口大小)。
  • 集中在某个 pool_name:说明该资源池的并发或内存配置需要调整,其他池可能正常。
  • resource_type:被拒绝的资源类型(如 Memory(KB)FileHandles),帮助区分是内存不足还是文件句柄不足。

3.2 第二步:查看资源队列

当资源池的 MAXCONCURRENCY 被打满时,新查询进入队列等待。队列越深,说明资源越紧张。

SELECT node_name,
       pool_name,
       transaction_id,
       statement_id,
       position_in_queue,
       queue_entry_timestamp
FROM v_monitor.resource_queues
WHERE pool_name NOT IN ('sysquery', 'sysdata')
ORDER BY queue_entry_timestamp;

如何解读

  • position_in_queue > 5:严重排队,说明资源池并发限制太低或查询执行太慢。
  • 同一 pool_name 下有大量排队记录:该池的 MAXCONCURRENCYMAXMEMORYSIZE 需要调整。
  • queue_entry_timestamp 很久远:查询在队列中等待了很长时间,可能永远拿不到资源。考虑设置 QUEUETIMEOUT 让超时查询自动取消。

3.3 第三步:查看资源池内存状态

resource_pool_status 是监控内存最核心的表。它显示每个资源池在每个节点上的实时内存和并发状态。

SELECT pool_name,
       memory_inuse_kb,
       memory_size_actual_kb,
       general_memory_borrowed_kb,
       max_memory_size_kb,
       running_query_count,
       max_concurrency,
       query_budget_kb
FROM v_monitor.resource_pool_status
WHERE node_name = (SELECT local_node_name())
  AND pool_name NOT IN ('sysquery', 'sysdata')
ORDER BY running_query_count DESC;

如何解读

  • memory_inuse_kb 接近 max_memory_size_kb:该池内存即将打满。新查询要么排队,要么被拒绝。
  • general_memory_borrowed_kb > 0:该池从 general 池借了内存。少量借入正常,但如果借入量持续 > 池自身的 max_memory_size_kb 的 20%,说明该池配置的内存不够用。
  • query_budget_kb 只有几百 MB:每查询平均只能拿到几百 MB 内存,几乎必然发生 Spill。公式:query_budget = MAXMEMORYSIZE / PLANNEDCONCURRENCY
  • memory_size_actual_kb 远小于 max_memory_size_kb:该池配置了较大的内存上限但实际未使用。可能是被其他池借走,或者该池的查询确实不消耗内存。

3.4 第四步:通过 execution_engine_profiles 定位内存大户

这是定位单条查询内部内存消耗的最精确方法。使用 PROFILE 关键字后,查询每个算子的内存指标。

-- 找出内存消耗最大的算子和路径
SELECT operator_name,
       path_id,
       SUM(DECODE(counter_name, 'memory reserved (bytes)', counter_value, NULL)) AS mem_reserved_bytes,
       SUM(DECODE(counter_name, 'execution time (us)', counter_value, NULL)) AS exec_time_us,
       SUM(DECODE(counter_name, 'cumulative size of raw temp data (bytes)', counter_value, NULL)) AS spill_bytes
FROM v_monitor.execution_engine_profiles
WHERE transaction_id = :t_id
  AND statement_id = :s_id
  AND counter_name IN ('memory reserved (bytes)', 'execution time (us)', 'cumulative size of raw temp data (bytes)')
GROUP BY operator_name, path_id
ORDER BY mem_reserved_bytes DESC
LIMIT 20;

关键计数器解读

  • memory reserved (bytes):优化器在查询规划时为该算子预留的内存。这个值由 query_budget 和统计信息共同决定。如果 reserved 很大但查询依然 Spill,说明统计信息严重低估了数据量
  • peak memory requested (bytes):算子在执行中实际申请的内存峰值。如果 peak 远大于 memory reserved,说明优化器低估了内存需求——通常是统计信息不准确导致的。注意此计数器在 v_monitor.execution_engine_profiles 中也可能表现为 current memory requested (bytes)
  • cumulative size of raw temp data (bytes):如果该值 > 0,说明算子发生了 Spill,数据溢出到了磁盘。这是内存不足的最直接证据。
  • rows produced:算子实际处理的数据行数。与 memory reserved 对比,如果行数很大但预留内存很小,优化器明显低估了数据规模。

3.5 第五步:检查 Spill 事件

Spill 是内存不足的直接后果。当 GroupByHashJoinHash 算子无法在内存中完成计算时,数据被写入磁盘临时文件,然后通过外部排序算法处理。

SELECT event_type,
       operator_name,
       path_id,
       event_description,
       suggested_action
FROM v_monitor.query_events
WHERE transaction_id = :t_id
  AND statement_id = :s_id
  AND event_type IN ('GROUP_BY_SPILLED', 'JOIN_SPILLED');

Spill 的连锁反应

  1. 算子需要的内存不够 → 溢出到磁盘
  2. 磁盘 I/O 增加 → 算子执行时间变长
  3. 算子变慢 → 下游算子等待更久(input queue wait 增加)
  4. 查询总体执行时间增加 → 其他查询排队更久
  5. 排队查询堆积 → 并发内存占用增加 → 更频繁的 RESOURCE_REJECTED

3.6 第六步:识别内存泄漏查询

「内存泄漏查询」指执行过程中不断申请新内存却从不释放的异常查询。特征是在执行时间增长的同时,memory_inuse_kb 持续线性增长。

-- 通过 resource_acquisitions 找出频繁申请额外内存的查询
SELECT transaction_id,
       statement_id,
       pool_name,
       COUNT(*) AS acquisition_count,
       SUM(memory_inuse_kb) AS total_memory_kb,
       MAX(duration_ms) AS max_duration_ms
FROM v_monitor.resource_acquisitions
WHERE request_type = 'AcquireAdditional'
  AND acquisition_timestamp > sysdate - INTERVAL '1 hour'
GROUP BY transaction_id, statement_id, pool_name
HAVING COUNT(*) > 5  -- 申请了超过 5 次额外内存
ORDER BY total_memory_kb DESC
LIMIT 10;

如何解读

  • acquisition_count > 10:该查询频繁申请额外内存,SQL 复杂度可能超出优化器预估,或者存在统计信息严重缺失。
  • total_memory_kb 巨大(几十 GB):典型的「贪吃查询」,应检查该 SQL 的 WHERE 条件和 JOIN 逻辑。
  • 结合 dc_query_executionsExecutePlan 阶段耗时:如果 ExecutePlan 阶段耗时线性增长,说明查询在执行中不断等待资源,而非纯粹的 CPU 计算。

4. 使用资源池控制内存

4.1 资源池内存参数详解

参数 作用 默认值 内存影响
MEMORYSIZE 给该池预留的独占内存,其他池不能借用 0%(不预留) 谨慎使用——预留后即使空闲也不会释放给其他池
MAXMEMORYSIZE 该池中所有查询总共能用的内存上限 95%(general 池) 最常调整的参数。太小→频繁拒绝,太大→一个池挤占全局
PLANNEDCONCURRENCY 预期该池同时运行的查询数,决定 query_budget AUTO(=CPU 核心数) 与实际并发越接近,优化器估算越准
MAXCONCURRENCY 该池最多同时运行多少条查询 无硬限制(general 池) 硬刹车——超过后查询排队,但不会消耗内存
QUERY_BUDGET_KB 每条查询的初始内存预算(只读计算列) = MAXMEMORYSIZE / PLANNEDCONCURRENCY 太小→频繁 Spill,太大→并发降低
EXECUTIONPARALLELISM 单查询最大线程数 AUTO(=CPU 核心数) 线程越多,内存中同时活跃的算子越多,总内存占用越高

完整参数列表详见 Vertica 资源池最佳实践 中的参数表。

4.2 query_budget:内存调优的核心杠杆

query_budget 是 Vertica 内存管理的核心概念。它决定了优化器为每条查询分配多少「初始内存」,直接影响查询是否会发生 Spill。

计算公式

query_budget = [MAX]MEMORYSIZE / PLANNEDCONCURRENCY

例如在 256GB 节点上:

  • general 池 MAXMEMORYSIZE = 95%PLANNEDCONCURRENCY = 48(AUTO = 48 核)
  • query_budget = 256 × 95% / 48 ≈ 5.1 GB

但这只是初始预算。如果查询实际需要更多内存,可以发起 AcquireAdditional 请求。优化器根据表的统计信息估算内存需求:

  • 统计信息准确 → 优化器估算准确 → 不需要或少需要额外申请
  • 统计信息缺失或过时 → 优化器低估 → 频繁 AcquireAdditional → 可能触发 RESOURCE_REJECTED

4.3 策略一:增大 query_budget,减少 Spill

当查询普遍出现 GROUP_BY_SPILLEDJOIN_SPILLED 时,说明 query_budget 太小。

调整方法(二选一):

  • 降低 PLANNEDCONCURRENCY:减少预期并发数,每条查询分到更多内存。适合「查询少但每条查询复杂」的场景。
  • 增大 MAXMEMORYSIZE:扩大池的总内存。适合「节点内存有余量但池配置偏保守」的场景。
-- 方案 A:降低 PLANNEDCONCURRENCY(节点 256GB,目标预算 8GB)
-- query_budget = 256 × 95% / 30 ≈ 8.1 GB
ALTER RESOURCE POOL general PLANNEDCONCURRENCY 30;

-- 方案 B:创建专用报表池(大预算、低并发)
CREATE RESOURCE POOL report_pool
    MAXMEMORYSIZE '40%'
    PLANNEDCONCURRENCY 6
    MAXCONCURRENCY 4;
-- query_budget = 256 × 40% / 6 ≈ 17 GB

取值建议

  • 短查询(< 5 秒):1-3 GB 预算通常够用
  • 中等分析查询(10-60 秒):4-8 GB
  • 大报表/ETL 查询(> 2 分钟):8-20 GB
  • 注意:query_budget 只是初始预留,不是硬上限。查询可以通过 AcquireAdditional 申请更多。

4.4 策略二:设置 MAXMEMORYSIZE 防止单池挤占全局

默认的 general 池拥有 MAXMEMORYSIZE = 95%,意味着它可以用掉几乎所有节点内存。如果用户自定义池也没有设置限制,多个池可能超分内存。

-- 检查各池的最大内存分配
SELECT pool_name, max_memory_size_kb,
       pool_name, max_memory_size_kb / (1024*1024) AS max_gb
FROM v_monitor.resource_pool_status
WHERE node_name ILIKE '%0001%'
  AND pool_name NOT IN ('sysquery', 'sysdata');

分配原则

  • 所有用户池的 MAXMEMORYSIZE 之和 ≤ 80%(留 20% 给系统开销和峰值缓冲)
  • General 池降到 50-60%,留出空间给专用池
  • 独立池(MEMORYSIZE = MAXMEMORYSIZE)不参与共享内存池,需严格控制占比

4.5 策略三:TM 资源池内存调优

Tuple Mover 有自己的内置资源池 tm,默认配置非常保守。当 TM 的 mergeout 操作处理大表时,内存不足会导致 mergeout 极慢,间接让 ROS container 数量失控,最终影响查询性能。

-- 查看 TM 池当前配置
SELECT pool_name, max_memory_size_kb, planned_concurrency,
       max_concurrency, query_budget_kb
FROM v_monitor.resource_pool_status
WHERE pool_name = 'tm'
  AND node_name ILIKE '%0001%';

调优建议(来自 ROS Pushback 故障排查):

  • 将 TM 池的 MEMORYSIZE 增加到 2GB × MAXCONCURRENCY
  • PLANNEDCONCURRENCY 设为与 MAXCONCURRENCY 相等
ALTER RESOURCE POOL tm MEMORYSIZE '16G' PLANNEDCONCURRENCY 7;

5. 深入案例

5.1 虚构案例一:query_budget 过小导致普遍 Spill

📝 虚构案例

场景:3 节点集群(每节点 256GB / 48 核),general 池默认配置。用户反馈 BI 报表查询近期越来越慢,但数据量并未明显增长。

诊断过程

  • Step 1: 查看 query_events

    • → 几乎所有超过 30 秒的查询都出现了 GROUP_BY_SPILLED
  • Step 2: 查看 query_budget

    • → general 池 query_budget ≈ 5 GB(默认值)
  • Step 3: 查看 execution_engine_profiles 内存计数器

    • → GroupByHash 算子:memory_allocated = 8.2 GB,但 memory_reserved = 5 GB
    • → 每次查询都有 3+ GB 的 AcquireAdditional 请求
    • → cumulative size of raw temp data = 12 GB(大量 Spill)
  • Step 4: 查看 resource_acquisitions

    • → 频繁的 AcquireAdditional 请求,成功但耗时

根因分析:默认 query_budget = 5GB 不足以容纳 GroupByHash 的哈希表,导致每次查询都需要额外申请内存 + Spill。不是单条查询的问题,而是所有查询都在「吃不饱」的状态。

修复:降低 PLANNEDCONCURRENCY 从 48 到 24 → query_budget 从 5GB 翻倍到 10GB。同样查询的 Spill 事件从 80% 降到 5%,平均执行时间从 3 分钟降到 45 秒。

5.2 虚构案例二:独占池挤占全局内存

📝 虚构案例

场景:某集群设置了 management_pool 作为独立池(MEMORYSIZE = MAXMEMORYSIZE = 32GB),供高管报表使用。平时没有问题,但某天高管跑了 3 条并发大查询后,general 池的所有查询都开始报 RESOURCE_REJECTED

诊断过程

  • Step 1: 查看 resource_rejections

    • → general 池大量 Memory(KB) Exceeded
  • Step 2: 查看 resource_pool_status

    • → management_pool: memory_inuse_kb = 32GB (100%)
    • → general_pool: general_memory_borrowed_kb = 0 (借不到内存!)
  • Step 3: 确认原因

    • → management_pool 是独立池,32GB 被独占
    • → 即使这 3 条查询只用了一半,剩余 16GB 也不会释放给 general 池

根因分析:独立池(MEMORYSIZE = MAXMEMORYSIZE)的内存是独占的。即使空闲,其他池也无法使用。32GB 看起来合理,但在 256GB 节点上占用了 12.5%,且不能再分配给其他池。

修复

  • management_pool 从独立池改为共享池(移除 MEMORYSIZE,保留 MAXMEMORYSIZE
  • 设置 MAXCONCURRENCY = 2 防止高管同时跑太多查询
  • 或保留独立池但降低 MEMORYSIZE 到 16GB

5.3 真实案例一:query_budget 过小导致大量排队

📋 真实案例

背景:某运营商经分系统,138 节点集群,Vertica v11.1.1。每天早上 9-12 点业务高峰期,app_poolhz_pool 出现大量排队,数据库性能恶化超过 7 倍(平均响应时间从 13 秒升到 94 秒)。

诊断过程

  • 检查 query_budget 与实际内存消耗的差距
  • 发现 app_pool 的 query_budget = 1GB
  • 但 40% 的查询任务实际消耗 > 1GB

关键发现

资源池 任务消耗 > 1GB 比例 query_budget 后果
app_pool 40% 1 GB 40% 任务需要在执行中重新申请内存
hz_pool 10% - 相对较轻

在业务高峰期,执行过程中重新申请内存(AcquireAdditional)可能因内存资源紧张长时间申请不到,导致执行时间延长,连锁引发更多排队。同时发现某大查询因统计信息过期,错误选择了驱动表(将 14.5 亿行大表作为内表),消耗了 28GB 内存。

修复措施

  1. 收集过期表的统计信息:SELECT ANALYZE_STATISTICS('user_dtal');
  2. 增大 app_poolquery_budget(降低 PLANNEDCONCURRENCY

效果:统计信息更新后,大查询执行计划恢复正常;query_budget 调大后,AcquireAdditional 频率大幅下降,高峰期排队缓解。

关键教训:query_budget 小 + 统计信息过期 = 双重放大。优化器低估内存需求(因为统计信息不准),而实际分配的内存又太少(因为 query_budget 小),两者叠加使得查询几乎必然 Spill 或排队。


6. 完整诊断流程实战

📝 虚构场景 · 完整演练

场景

  • 5 节点集群(每节点 512GB / 64 核),Vertica v24.x
  • 用户反馈:每天下午 3 点后新提交的查询频繁报错 RESOURCE_REJECTED
  • 检查发现:内存使用率 98%,general_memory_borrowed_kb 异常高

诊断过程

Step 1:确认内存压力来源

SELECT pool_name, memory_inuse_kb, max_memory_size_kb,
       general_memory_borrowed_kb, running_query_count
FROM v_monitor.resource_pool_status
WHERE node_name ILIKE '%0001%'
  AND pool_name NOT IN ('sysquery', 'sysdata');

→ general 池 memory_inuse_kb 接近上限。3 个用户自定义池(etl_pool、report_pool、adhoc_pool)全部从 general 借了大量内存。

Step 2:找出谁在借内存

SELECT pool_name, memory_inuse_kb, general_memory_borrowed_kb
FROM v_monitor.resource_pool_status
WHERE node_name ILIKE '%0001%'
  AND general_memory_borrowed_kb > 0
ORDER BY general_memory_borrowed_kb DESC;

etl_pool 借了 85GB!远超其自身 MAXMEMORYSIZE = 40GB

Step 3:定位 etl_pool 中的大查询

SELECT transaction_id, statement_id, request_duration_ms, memory_acquired_mb
FROM v_monitor.query_requests
WHERE is_executing = 't'
  AND request LIKE '%etl%'
ORDER BY memory_acquired_mb DESC
LIMIT 5;

→ 发现一条 ETL 查询已运行 45 分钟,内存占用 72GB。这条查询在做 8 张大表的全量 JOIN。

Step 4:PROFILE 分析大查询

PROFILE <etl_query>;
-- 结果:JOIN_SPILLED × 12,GROUP_BY_SPILLED × 4
-- cumulative size of raw temp data = 180GB

→ 不是一条查询的问题,是这条查询触发了连锁反应:它大量 spill 到磁盘 → 执行极慢 → 长时间占用 etl_pool 内存 → etl_pool 借 general → general 池枯竭 → 其他所有查询被拒绝。

修复措施

立即措施:终止失控 ETL 查询,释放内存。

SELECT close_session('etl_session_id');

短期优化

  1. 为 etl_pool 设置 RUNTIMECAP = '00:30:00',防止单条查询无限执行
  2. 设置 MAXCONCURRENCY = 2,限制 etl_pool 同时运行的查询数
  3. 将 etl_pool 的 MAXMEMORYSIZE 从 40% 提升到 60%,减少向 general 池借内存的频率
ALTER RESOURCE POOL etl_pool
    MAXMEMORYSIZE '60%'
    MAXCONCURRENCY 2
    RUNTIMECAP '00:30:00';
  1. 为大 JOIN 表创建预连接投影(pre-join projection),避免运行时全量 JOIN

效果:修复后 RESOURCE_REJECTED 降为零。Etl_pool 不再向 general 借内存。下午高峰期恢复正常。


7. 快速诊断 SQL 工具箱

诊断目标 SQL
系统内存历史 SELECT * FROM system_resource_usage ORDER BY end_time DESC;
资源拒绝事件 SELECT node_name, pool_name, reason, rejection_count, last_rejected_timestamp, last_rejected_value FROM resource_rejections WHERE last_rejected_timestamp > sysdate - INTERVAL '1 hour' ORDER BY last_rejected_timestamp DESC;
资源队列深度 SELECT pool_name, position_in_queue, queue_entry_timestamp FROM resource_queues WHERE pool_name NOT IN ('sysquery', 'sysdata') ORDER BY queue_entry_timestamp;
资源池实时内存 SELECT pool_name, memory_inuse_kb, max_memory_size_kb, general_memory_borrowed_kb, running_query_count, query_budget_kb FROM resource_pool_status WHERE pool_name NOT IN ('sysquery', 'sysdata');
算子内存消耗 SELECT operator_name, path_id, SUM(DECODE(counter_name, 'memory allocated (bytes)', counter_value, NULL)) FROM execution_engine_profiles WHERE transaction_id=:t_id AND statement_id=:s_id GROUP BY 1,2 ORDER BY 3 DESC;
Spill 事件 SELECT event_type, operator_name, suggested_action FROM query_events WHERE transaction_id=:t_id AND statement_id=:s_id AND event_type IN ('GROUP_BY_SPILLED', 'JOIN_SPILLED');
查询内存大户 SELECT transaction_id, statement_id, memory_acquired_mb, request_duration_ms FROM query_requests WHERE is_executing='t' ORDER BY memory_acquired_mb DESC LIMIT 10;
TM 池状态 SELECT pool_name, max_memory_size_kb, planned_concurrency, query_budget_kb FROM resource_pool_status WHERE pool_name = 'tm' AND node_name ILIKE '%0001%';
额外内存申请 SELECT transaction_id, statement_id, COUNT(*) AS cnt, SUM(memory_inuse_kb) FROM resource_acquisitions WHERE request_type = 'AcquireAdditional' GROUP BY 1,2 HAVING COUNT(*) > 5 ORDER BY 3 DESC;

8. 最佳实践清单

  1. 定期监控 RESOURCE_REJECTED:这是内存压力最早期的信号。设置告警在拒绝次数 > 10/小时时触发。
  2. query_budget 宁大勿小:原则是让 80% 以上的查询用初始预算就能完成,不需要 AcquireAdditional。额外申请虽然有兜底机制,但每次申请都增加延迟和不确定性。
  3. 避免使用独立池(MEMORYSIZE ≠ 0%):独立池的独占内存在空闲时也不会释放给其他池,整体内存利用率低。除非确有 SLA 要求(如高管报表必须秒级响应),否则用 MAXMEMORYSIZE 软限制即可。
  4. 设置 RUNTIMECAP 防止内存泄漏:失控查询不仅浪费 CPU,还会持续占用内存不释放。设置合理的时间上限(报表 30 分钟、ETL 2 小时),到期自动终止。
  5. 保持统计信息准确:统计信息过期 → 优化器低估内存需求 → 初始 query_budget 不够 → 额外申请 → 可能被拒绝。每次大批量加载数据后运行 ANALYZE_STATISTICS
  6. TM 池也需要关注:默认 TM 池的 query_budget 通常只有几十 MB,大表 mergeout 会极其缓慢。适当增大 TM 池的内存配置。
  7. 用 Spill 作为内存充足的度量指标:如果 90% 以上的查询没有 Spill 事件,说明内存配置基本合理;如果 Spill 普遍存在,优先调大 query_budget
  8. general 池不要给 95%:降低到 50-60%,将余量分配给专用池。所有查询挤在一个池里,一条大查询就能拖垮全局。
  9. 大表 JOIN 考虑预连接投影:运行时 JOIN 需要将内表全部加载到内存构建哈希表,如果内表有几亿行,内存消耗巨大。预连接投影将 JOIN 提前在数据写入时完成,查询时直接读取结果。
  10. 善用 close_session 应急:当发现某条查询正在吞噬内存并拖慢整个集群时,果断用 SELECT close_session('session_id') 终止它。这比让集群慢慢恢复有效得多。

扩展阅读