Oracle PGA 内存增高排查

排查 .NET 应用连接 Oracle 数据库时 PGA 内存持续增高的根因与解决方案

Oracle PGA 内存增高排查

问题现象

生产环境运行一段时间后,Oracle 数据库服务器上的 PGA(Program Global Area)内存占用持续增长,最终触发数据库性能告警甚至 OOM。

初步排查特征:

  • 应用端使用 ADO.NET / SqlClient 连接 Oracle 数据库
  • 业务高峰期 PGA 增长明显,低峰期未见明显释放
  • 重启应用后 PGA 回落,随后再次缓慢攀升
  • 数据库层面未发现明显的 SQL 泄漏或大结果集问题

排查过程

第一阶段:定位问题方向

通过 AWR 报告和 ASH 等待事件分析,发现:

指标 观察值
PGA 总使用量 持续上升,峰值远超预期
主要消耗来源 大量空闲会话仍持有 PGA 分配
会话数 正常范围内

关键线索:PGA 并非被活跃 SQL 占用,而是被大量看似空闲的数据库会话所持有。

第二阶段:分析连接池行为

.NET 应用的数据库连接由 SqlClient 连接池 管理。连接池的核心机制是复用物理连接,减少频繁创建/销毁的开销。但这里存在一个关键问题——连接池中的"闲置连接"在 Oracle 服务端是否真正释放了资源?

查阅微软官方文档和 GPT 辅助分析后,聚焦到两个参数:

Connection Lifetime(别名:Load Balance Timeout)

1
2
3
4
5
6
7
参数名称   :Connection Lifetime / LoadBalanceTimeout
含义       :连接在池中保留的最小存活时间(单位:秒)
类型       :整数值
默认值     :0(表示不启用此机制)
验证规则   :必须 ≥ 0,否则抛出异常
作用时机   :当连接从池中被取出使用时,检查其创建时间,
             若已超过 Connection Lifetime,则直接销毁重建

核心逻辑:这个参数控制的是连接池内连接的最大生命周期,而不是空闲超时。当连接在池中停留时间超过该阈值且被取出复用时,才会被标记为过期并重新创建。

第三阶段:查看源码验证

查看 Microsoft.Data.SqlClient(net8.0) 源码:

1
2
3
4
5
6
7
8
9
// SqlClient 内部常量定义
internal const bool IntegratedSecurity = false;
internal const SqlConnectionIPAddressPreference IPAddressPreference = SqlConnectionIPAddressPreference.IPv4First;

internal const int LoadBalanceTimeout = 0;  // ← 默认值为 0!

internal const int MaxPoolSize = 100;        // 默认最大连接数 100
internal const int MinPoolSize = 0;          // 默认最小连接数 0
internal const bool MultipleActiveResultSets = false;

关键发现LoadBalanceTimeout 的默认值是 0,意味着永远不会因为生命周期而主动回收旧连接

进一步查看连接字符串关键字映射(SqlConnectionOptions.cs):

1
2
3
// 第 197 行左右
AddKeywordToMap(DbConnectionStringKeywords.LoadBalanceTimeout,
                DbConnectionStringSynonyms.ConnectionLifetime);  // ← 同义词映射!

这证实了 Connection LifetimeLoadBalanceTimeout 是同一个参数的两个名字

第四阶段:根因确认

结合以上信息,完整的问题链路如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──────────────────────────────────────────────────────────────┐
│                     问题链路图                                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 应用启动,连接池初始化                                     │
│     ↓                                                        │
│  2. 业务请求到来,创建物理连接到 Oracle                          │
│     ↓                                                        │
│  3. 请求结束,连接归还到池中                                   │
│     ↓                                                        │
│  4. Oracle 服务端会话进入 IDLE 状态,但 PGA 内存仍然分配        │
│     ↓                                                        │
│  5. 由于 LoadBalanceTimeout=0(默认),                        │
│     池中连接永不过期                                          │
│     ↓                                                        │
│  6. 即使长时间不用,这些连接一直占着 Oracle 的 PGA              │
│     ↓                                                        │
│  7. 新的业务波动 → 新连接创建 → 更多 PGA 被锁定               │
│     ↓                                                        │
│  8. PGA 只增不减 → 最终告警/OOM                               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

根本原因:SqlClient 连接池默认不会主动淘汰旧连接,导致 Oracle 端大量空闲会话长期持有 PGA 内存不被释放。


解决方案

在连接字符串中添加 Connection Lifetime

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- 修改前 -->
<add name="OracleConn"
     connectionString="Data Source=ORCL;User Id=myUser;Password=myPwd;"
     providerName="Oracle.ManagedDataAccess.Client" />

<!-- 修改后 -->
<add name="OracleConn"
     connectionString="Data Source=ORCL;User Id=myUser;Password=myPwd;
                    Connection Lifetime=300;"
     providerName="Oracle.ManagedDataAccess.Client" />

或通过代码方式设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var connectionString = "Server=myServer;Database=myDB;" +
                      "User Id=myUser;Password=myPwd;" +
                      "Connection Lifetime=300;";   // 单位:秒

// 或使用 SqlConnectionStringBuilder
var builder = new SqlConnectionStringBuilder
{
    DataSource = "myServer",
    InitialCatalog = "myDB",
    UserID = "myUser",
    Password = "myPwd",
    ConnectTimeout = 30,
    // 关键参数:连接在池中最长存活 5 分钟
    LoadBalanceTimeout = 300      // 等价于 Connection Lifetime
};
string connStr = builder.ConnectionString;

参数调优建议

场景 推荐值 说明
一般业务系统 180 ~ 300(3~5 分钟) 兼顾连接复用率和资源释放
高并发短连接场景 60 ~ 120(1~2 分钟) 加快旧连接淘汰速度
低频长事务场景 600 ~ 900(10~15 分钟) 减少不必要的连接重建
不确定 300 开始,根据监控调整 观察 PGA 变化趋势后再微调

验证修复效果

1. 应用层验证

修改连接串部署后,观察以下指标:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 查看 Oracle 当前会话及 PGA 使用情况
SELECT s.sid,
       s.serial#,
       s.username,
       s.status,
       s.logon_time,
       ROUND(p.pga_used_mem / 1024 / 1024, 2) AS pga_used_mb,
       ROUND(p.pga_alloc_mem / 1024 / 1024, 2) AS pga_alloc_mb,
       s.program,
       s.machine
FROM v$session s
JOIN v$process p ON s.paddr = p.addr
WHERE s.username IS NOT NULL
ORDER BY p.pga_alloc_mem DESC;

2. 监控对比项

指标 修复前 修复后(预期)
PGA 峰值 持续攀升 在一定范围内波动
空闲会话 PGA 长期占用 定期释放
连接重建频率 极低 轻微增加(可接受)

相关参数速查

连接字符串参数 别名 默认值 说明
Connection Lifetime Load Balance Timeout 0(永不过期) 连接池中连接的最大存活秒数
Max Pool Size 100 连接池最大连接数
Min Pool Size 0 连接池最小保持连接数
Connect Timeout Connection Timeout 15 连接超时(秒)

总结

本次问题的根因是 SqlClient 连接池的 LoadBalanceTimeout(即 Connection Lifetime)默认值为 0,导致连接池中的物理连接永不主动回收,进而造成 Oracle 端大量空闲会话持续占用 PGA 内存。解决方案非常简单——在连接串中加入 Connection Lifetime=xxx 即可让连接池定期淘汰旧连接,从而间接释放 Oracle 端的 PGA 资源。

经验教训:在使用连接池访问 Oracle 时,不要忽略连接生命周期参数的配置,尤其是在长时间运行的生产环境中。

comments powered by Disqus
使用 Hugo 构建😊 主题 StackJimmy 设计