maoxinwu opened a new issue, #1947:
URL: https://github.com/apache/cloudberry/issues/1947
## Summary
Long-lived backends running ORCA queries accumulate memory continuously
(never returned to the OS). In production a single connection grew to **769 MB
after 6,802 commands**, and the accumulated memory is only released at process
exit, causing cascading OOM across the cluster over time (5,094 live PIDs at
the OOM moment).
The same design flaw exists identically in Apache Cloudberry 2.1.0 and
GPDB6; upgrading between them does not fix it.
## Environment
- Apache Cloudberry 2.1.0-incubating (root cause also reproduces on GPDB6)
- `optimizer = on` (ORCA), `optimizer_metadata_caching = on` (default),
`optimizer_use_gpdb_allocators = on` (default)
## Reproduction
Setup two tables (see `orca_leak_setup.sql`, 10k + 50k rows), then
repeatedly run identical simple queries on a **single psql connection** while
watching the backend RSS:
```bash
./orca_leak_setup.sql # psql -d <db> -f orca_leak_setup.sql
./orca_leak_reproduce.sh <db> 5000 default
```
### Measured results (single connection, GPDB6, RSS of backend process)
| Iteration | RSS (MB) | Notes |
|-----------|----------|-------|
| 0 | 54.1 | baseline |
| 200 | 57.6 | ~0.018 MB/cmd |
| 1600 | 76.1 | ~0.016 MB/cmd |
| 2800 | 97.6 | ~0.018 MB/cmd |
| 4000 | 117.1 | ~0.016 MB/cmd |
| 5000 | 135.6 | ~0.019 MB/cmd |
- Total growth: **81.5 MB over 5,000 queries (~0.016 MB/cmd)**, and the
growth is **accelerating** over time (fragmentation).
- Production estimate: **~0.113 MB/cmd** on real tables (more tables /
larger/JSON metadata → more ORCA temp allocations).
### Controlled comparison
| Test | Config | Initial RSS | Final RSS | Growth |
|------|--------|-------------|-----------|--------|
| #1 | ORCA default (metadata caching on) | 56.5 MB | 98.5 MB | **+42.0 MB**
|
| #2 | ORCA, `optimizer_metadata_caching=off` | 56.9 MB | 56.9 MB | **+0
MB** |
| #3 | ORCA off (`optimizer=off`) | 40.5 MB | 40.5 MB | **+0 MB** |
| #4 | Short-lived connections (new conn per query) | — | — | no growth
(released at exit) |
Findings:
1. ORCA default long-lived connections **do** accumulate memory.
2. PostgreSQL's own planner (`optimizer=off`) does **not** grow.
3. Short connections don't grow (memory is only released at process exit).
4. Turning off metadata caching removes the growth — **cache entries are
never recycled on the steady-state query path**, while temp query memory is
never physically returned to the OS.
## Root cause analysis (source-level)
The leak is a **design issue**, not a one-off code bug.
1. **`OptimizerMemoryContext` has a process-level lifetime.** It is created
as a child of `TopMemoryContext` (Cloudberry: lazy-created on first ORCA query
in `src/backend/optimizer/plan/planner.c:388-417`; GPDB6: at startup in
`postinit.c:657`). `TopMemoryContext` is only cleaned up at process exit, so
every ORCA allocation under it lives for the entire backend lifetime.
2. **Per-query pools are only *logically* freed.** Each query's
`CAutoMemoryPool` creates a child `CMemoryPoolPalloc` context
(`src/backend/gpopt/utils/CMemoryPoolPalloc.cpp`) whose `TearDown()` calls
`MemoryContextDelete` — but glibc `free()` marks the `malloc` block free
without `munmap`, so **RSS never drops**. AllocSet's `maxBlockSize` is 8 MB;
large blocks can't be returned even when fully free.
3. **`CMDCache` entries are reused across queries and never cleared on the
normal path.** `COptTasks::OptimizeTask()` does not call `CMDCache::Reset()`;
cache pools live under the same process-level context. This is why
`optimizer_metadata_caching=off` eliminates the growth.
4. **`CCacheFactory` uses its own `CMemoryPoolManager` pool** — also
process-level.
5. **`optimizer_use_gpdb_allocators` cannot fix it.** Whether true (Palloc →
MemoryContext, process-level) or false (glibc malloc/free, which also doesn't
return memory to the OS), memory is never physically returned after a query.
So Cloudberry 2.1.0 and GPDB6 share exactly the same defect; the only
Cloudberry differences (lazy context creation,
`MemoryContextDeclareAccountingRoot`) are cosmetic and don't change
allocation/release behavior.
## Suggested fixes
- **Short term (ops):** cap long-lived connection lifetime
(`pg_terminate_backend` on sessions older than ~4h), use application
connection-pool `maxLifetime`, and/or set `optimizer_metadata_caching=off` for
high-command-count ETL/COPY sessions (eliminates growth at a QPS cost —
observed ~90→35 in test).
- **Long term (code):**
- (A, recommended) Make `OptimizerMemoryContext` per-query — create at
query start, delete at end — and relocate `CMDCache` / `CCacheFactory` pools so
cross-query metadata caching is managed independently.
- (B) `MemoryContextReset(OptimizerMemoryContext)` at end of
`OptimizeTask()` after moving `CMDCache`'s pool out from under it.
- (C) Use `mmap`/`munmap` for large allocations so freed blocks are
returned to the OS.
## Repro scripts
`orca_leak_setup.sql`, `orca_leak_reproduce.sh`/`.py` (default/nocache/noop
modes), `orca_leak_shortconn.sh`, `orca_leak_run_all.sh`.
## References / source locations (Cloudberry)
- `src/backend/optimizer/plan/planner.c:388-417` (lazy
`OptimizerMemoryContext` create)
- `src/backend/utils/init/postinit.c` (cleanup at exit)
- `src/backend/gpopt/utils/CMemoryPoolPalloc.cpp` (Palloc bridge,
logical-only free)
- `src/backend/gpopt/gpdbwrappers.cpp:2505-2528`
(`GPDBAllocSetContextCreate` + accounting root)
- `src/backend/gpora/libgpopt/src/mdcache/CMDCache.cpp` (cross-query
metadata cache)
- `src/backend/gpora/libgpos/src/memory/CCacheFactory.cpp`
- `src/backend/gpora/libgpos/src/memory/CMemoryPoolManager.cpp`
---
## Reproduction scripts
Full self-contained reproduction scripts (tested on GPDB6; portable to
Cloudberry).
### 1. Setup tables (`orca_leak_setup.sql`)
```sql
-- orca_leak_setup.sql - 创建 GPORCA 内存泄漏复现测试表
-- 用法: psql -d <dbname> -f orca_leak_setup.sql
-- 确认 ORCA 生效
SHOW optimizer;
-- 创建有足够复杂度的表,确保 ORCA 优化器被调用
DROP TABLE IF EXISTS leak_test_t2;
DROP TABLE IF EXISTS leak_test_t1;
CREATE TABLE leak_test_t1 (
id serial PRIMARY KEY,
name text,
value numeric,
created_at timestamp default now()
) DISTRIBUTED BY (id);
CREATE TABLE leak_test_t2 (
id serial PRIMARY KEY,
t1_id int REFERENCES leak_test_t1(id),
status text,
amount numeric
) DISTRIBUTED BY (id);
-- 插入基础数据
INSERT INTO leak_test_t1 (name, value)
SELECT 'user_' || i, random() * 10000
FROM generate_series(1, 10000) i;
INSERT INTO leak_test_t2 (t1_id, status, amount)
SELECT (random() * 9999 + 1)::int,
(ARRAY['active','inactive','pending'])[ceil(random()*3)::int],
random() * 5000
FROM generate_series(1, 50000) i;
-- 验证数据
SELECT 'leak_test_t1' AS tbl, count(*) FROM leak_test_t1
UNION ALL
SELECT 'leak_test_t2', count(*) FROM leak_test_t2;
\echo '测试表创建完成'
```
### 2. Long-lived connection leak repro (`orca_leak_reproduce.sh`)
usage: `./orca_leak_reproduce.sh <db> <iterations> <mode>` — modes:
`default` | `nocache` | `noop`
```bash
#!/bin/bash
# orca_leak_reproduce.sh - 复现 GPORCA 长连接内存累积
#
# 核心思路: 生成 SQL 文件,psql -f 保持单连接执行,
# 同时后台监控进程 RSS
#
# 用法:
# ./orca_leak_reproduce.sh [数据库名] [迭代次数] [模式]
# 模式: default (ORCA默认) | nocache (ORCA无缓存) | noop (关闭ORCA)
set -euo pipefail
DBNAME=${1:-postgres}
ITERATIONS=${2:-1000}
MODE=${3:-default}
mode_desc() {
case "$1" in
default) echo "ORCA默认 (optimizer=on, metadata_caching=on)" ;;
nocache) echo "ORCA无缓存 (optimizer=on, metadata_caching=off)" ;;
noop) echo "无ORCA (optimizer=off)" ;;
esac
}
echo "============================================================"
echo "GPORCA 长连接内存累积复现测试"
echo "============================================================"
echo "数据库: $DBNAME"
echo "迭代次数: $ITERATIONS"
echo "测试模式: $(mode_desc "$MODE")"
echo ""
# 生成 SQL 文件
SQL_FILE=$(mktemp /tmp/orca_leak_XXXXXX.sql)
MONITOR_FILE=$(mktemp /tmp/orca_leak_XXXXXX.out)
trap 'rm -f "$SQL_FILE" "$MONITOR_FILE"' EXIT
# 输出 PID 标记
echo "\echo LEAK_TEST_PID_START" >> "$SQL_FILE"
echo "SELECT pg_backend_pid();" >> "$SQL_FILE"
echo "\echo LEAK_TEST_PID_END" >> "$SQL_FILE"
# 应用配置
case "$MODE" in
nocache) echo "SET optimizer_metadata_caching=off;" >> "$SQL_FILE" ;;
noop) echo "SET optimizer=off;" >> "$SQL_FILE" ;;
esac
# 确认配置
echo "SHOW optimizer;" >> "$SQL_FILE"
echo "SHOW optimizer_metadata_caching;" >> "$SQL_FILE"
# 预热
echo "SELECT count(*) FROM leak_test_t1 WHERE value > 5000;" >> "$SQL_FILE"
if [[ "$MODE" != "noop" ]]; then
echo "SELECT t1.name FROM leak_test_t1 t1 JOIN leak_test_t2 t2 ON
t1.id=t2.t1_id LIMIT 1;" >> "$SQL_FILE"
echo "SELECT * FROM leak_test_t1 WHERE id IN (SELECT t1_id FROM
leak_test_t2 WHERE amount > 4000) LIMIT 1;" >> "$SQL_FILE"
fi
echo "\echo LEAK_TEST_WARMUP_DONE" >> "$SQL_FILE"
# 生成循环查询
for ((i=1; i<=$ITERATIONS; i++)); do
case $(( i % 3 )) in
1) echo "SELECT count(*) FROM leak_test_t1 WHERE value > 5000;" >>
"$SQL_FILE" ;;
2) if [[ "$MODE" != "noop" ]]; then
echo "SELECT t1.name FROM leak_test_t1 t1 JOIN leak_test_t2
t2 ON t1.id=t2.t1_id LIMIT 1;" >> "$SQL_FILE"
else
echo "SELECT count(*) FROM leak_test_t1 WHERE name LIKE
'user_1%';" >> "$SQL_FILE"
fi ;;
0) if [[ "$MODE" != "noop" ]]; then
echo "SELECT * FROM leak_test_t1 WHERE id IN (SELECT t1_id
FROM leak_test_t2 WHERE amount > 4000) LIMIT 1;" >> "$SQL_FILE"
else
echo "SELECT max(value) FROM leak_test_t1;" >> "$SQL_FILE"
fi ;;
esac
# 每200次插入进度标记
if (( i % 200 == 0 )); then
echo "\echo LEAK_TEST_PROGRESS:$i" >> "$SQL_FILE"
fi
done
echo "\echo LEAK_TEST_DONE" >> "$SQL_FILE"
echo "SQL 文件已生成: $SQL_FILE ($(wc -l < "$SQL_FILE") 行)"
echo "正在执行查询..."
# 后台启动 psql
psql -X -d "$DBNAME" -f "$SQL_FILE" > "$MONITOR_FILE" 2>&1 &
PSQL_PID=$!
# 读取 RSS (KB)
get_rss() {
awk '/VmRSS/ {print $2}' /proc/$1/status 2>/dev/null || echo 0
}
# 等待 PID 出现
BACKEND_PID=""
for attempt in $(seq 1 30); do
if [[ -f "$MONITOR_FILE" ]]; then
BACKEND_PID=$(sed -n
'/LEAK_TEST_PID_START/,/LEAK_TEST_PID_END/{/LEAK_TEST/d;p;}' "$MONITOR_FILE"
2>/dev/null | grep -oP '\d+' | head -1 || true)
if [[ -n "$BACKEND_PID" ]] && [[ "$BACKEND_PID" -gt 0 ]]
2>/dev/null; then
break
fi
fi
sleep 1
done
if [[ -z "$BACKEND_PID" ]] || ! [[ "$BACKEND_PID" -gt 0 ]] 2>/dev/null; then
echo "ERROR: 无法获取 Backend PID"
echo "psql 输出:"
head -20 "$MONITOR_FILE"
kill "$PSQL_PID" 2>/dev/null || true
exit 1
fi
echo "Backend PID: $BACKEND_PID"
# 等待预热完成
for attempt in $(seq 1 30); do
if grep -q "LEAK_TEST_WARMUP_DONE" "$MONITOR_FILE" 2>/dev/null; then
break
fi
sleep 1
done
# 显示当前配置
OPT_VAL=$(sed -n '/LEAK_TEST_PID_END/,/LEAK_TEST_WARMUP_DONE/{/on\|off/p;}'
"$MONITOR_FILE" 2>/dev/null | head -1 | tr -d '[:space:]' || echo "?")
echo "当前配置: optimizer=$OPT_VAL"
INIT_RSS=$(get_rss "$BACKEND_PID")
PREV_RSS=$INIT_RSS
echo ""
printf "%-8s %-12s %-12s %-12s\n" "ROUND" "RSS_MB" "DELTA_KB" "AVG_KB"
echo "--------------------------------------------------"
START_TIME=$(date +%s)
LAST_PROGRESS=0
# 监控循环
while kill -0 "$PSQL_PID" 2>/dev/null; do
RSS=$(get_rss "$BACKEND_PID")
if [[ "$RSS" -eq 0 ]]; then
break
fi
# 从日志检测进度
CURRENT_PROGRESS=$(grep -c 'LEAK_TEST_PROGRESS' "$MONITOR_FILE"
2>/dev/null || echo 0)
CURRENT_PROGRESS=${CURRENT_PROGRESS//[^0-9]/}
CURRENT_PROGRESS=${CURRENT_PROGRESS:-0}
if [[ "$CURRENT_PROGRESS" -ne "$LAST_PROGRESS" ]]; then
ROUNDS=$((CURRENT_PROGRESS * 200))
DELTA=$((RSS - PREV_RSS))
AVG_KB=$(echo "scale=1; ($RSS - $INIT_RSS) / $ROUNDS" | bc
2>/dev/null || echo "0")
printf "%-8d %-12.1f %-12d %-12s\n" "$ROUNDS" "$(echo "scale=1;
$RSS/1024" | bc)" "$DELTA" "$AVG_KB"
PREV_RSS=$RSS
LAST_PROGRESS=$CURRENT_PROGRESS
fi
sleep 3
done
# 等待 psql 完成
wait "$PSQL_PID" 2>/dev/null || true
# 最终采样
FINAL_RSS=$(get_rss "$BACKEND_PID")
if [[ "$FINAL_RSS" -eq 0 ]]; then
FINAL_RSS=$PREV_RSS
fi
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
GROWTH_KB=$((FINAL_RSS - INIT_RSS))
GROWTH_MB=$(echo "scale=1; $GROWTH_KB/1024" | bc)
AVG_PER_CMD=$(echo "scale=3; $GROWTH_KB/$ITERATIONS/1024" | bc)
QPS=$(echo "scale=1; $ITERATIONS/$ELAPSED" | bc 2>/dev/null || echo "N/A")
echo ""
echo "============================================================"
echo "测试结果"
echo "============================================================"
echo "初始 RSS: $(echo "scale=1; $INIT_RSS/1024" | bc) MB"
echo "最终 RSS: $(echo "scale=1; $FINAL_RSS/1024" | bc) MB"
echo "总增长: $GROWTH_MB MB"
echo "每查询平均增长: $AVG_PER_CMD MB"
echo "线上估算速率: 0.113 MB/cmd"
echo "总耗时: ${ELAPSED}s ($QPS qps)"
echo ""
case "$MODE" in
default)
if (( $(echo "$AVG_PER_CMD > 0.05" | bc -l 2>/dev/null || echo 0)
)); then
echo ">>> 确认: ORCA 默认配置下长连接存在内存累积 <<<"
elif (( $(echo "$AVG_PER_CMD < 0.01" | bc -l 2>/dev/null || echo 0)
)); then
echo ">>> 未检测到明显累积,可能需要更多迭代或更复杂查询 <<<"
else
echo ">>> 检测到轻微累积,与线上估算相比偏低,可能需要更长测试 <<<"
fi
;;
nocache)
echo ">>> 对比: 关闭缓存后每查询增长 $AVG_PER_CMD MB <<<"
;;
noop)
echo ">>> 对比: 关闭 ORCA 后每查询增长 $AVG_PER_CMD MB <<<"
;;
esac
```
### 3. Short-connection comparison (`orca_leak_shortconn.sh`)
usage: `./orca_leak_shortconn.sh <db> <iterations>`
```bash
#!/bin/bash
# orca_leak_shortconn.sh - 短连接对比测试
# 每次查询新建连接,验证短连接模式下内存不会持续增长
#
# 用法: ./orca_leak_shortconn.sh [数据库名] [迭代次数]
set -euo pipefail
DBNAME=${1:-postgres}
ITERATIONS=${2:-200}
echo "============================================================"
echo "短连接对比测试 - 每次查询新建连接"
echo "============================================================"
echo "数据库: $DBNAME"
echo "迭代次数: $ITERATIONS"
echo ""
QUERY="SELECT count(*) FROM leak_test_t1 WHERE value > 5000"
# 采集所有 postgres 后端进程的总 RSS
get_total_postgres_rss() {
ps -eo rss,args 2>/dev/null | grep 'postgres:' | grep -v grep | awk
'{sum+=$1} END {print sum+0}'
}
INIT_TOTAL=$(get_total_postgres_rss)
PREV_TOTAL=$INIT_TOTAL
echo "初始 postgres 总 RSS: $(echo "scale=1; $INIT_TOTAL/1024" | bc) MB"
echo ""
printf "%-8s %-14s %-12s\n" "ROUND" "TOTAL_RSS_MB" "DELTA_MB"
echo "----------------------------------"
for ((i=1; i<=$ITERATIONS; i++)); do
# 每次新建连接
psql -X -t -A -d "$DBNAME" -c "$QUERY" > /dev/null 2>&1
if (( i <= 10 || i % 50 == 0 || i == ITERATIONS )); then
TOTAL=$(get_total_postgres_rss)
DELTA=$((TOTAL - PREV_TOTAL))
printf "%-8d %-14.1f %-12.1f\n" "$i" "$(echo "scale=1; $TOTAL/1024"
| bc)" "$(echo "scale=1; $DELTA/1024" | bc)"
PREV_TOTAL=$TOTAL
fi
done
FINAL_TOTAL=$(get_total_postgres_rss)
GROWTH_MB=$(echo "scale=1; ($FINAL_TOTAL - $INIT_TOTAL)/1024" | bc)
echo ""
echo "============================================================"
echo "短连接测试结果"
echo "============================================================"
echo "初始 postgres 总 RSS: $(echo "scale=1; $INIT_TOTAL/1024" | bc) MB"
echo "最终 postgres 总 RSS: $(echo "scale=1; $FINAL_TOTAL/1024" | bc) MB"
echo "总增长: $GROWTH_MB MB"
echo ""
if (( $(echo "scale=0; $GROWTH_MB / 1" | bc 2>/dev/null || echo 0) < 50 ));
then
echo ">>> 短连接模式下无持续内存增长,连接退出后内存释放 <<<"
else
echo ">>> 注意: 存在可观增长 ($GROWTH_MB MB),可能有其他进程干扰 <<<"
fi
```
### 4. Run everything
```bash
# 1. setup (first run only)
psql -d <db> -f orca_leak_setup.sql
# 2. single tests
./orca_leak_reproduce.sh <db> 3000 default # ORCA default
./orca_leak_reproduce.sh <db> 3000 nocache # ORCA, metadata_caching=off
./orca_leak_reproduce.sh <db> 3000 noop # ORCA off
# 3. short-connection comparison
./orca_leak_shortconn.sh <db> 200
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]