您好:
    具体问题,请查看附件





 王晓华
[email protected]




# Doris3.1.4 FE OOM Heap Dump 分析报告

| 项目 | 内容 |
|---|---|
| 集群节点 | FE 172.4.8.10(MASTER) |
| Doris 版本 | doris-3.1.4-rc02 |
| JDK | 17.0.12(崩溃时)/ 17.0.17(现网) |
| 事故时间 | 2026-08-17 10:55:29 (+08:00) OOM,进程被 `OnOutOfMemoryError=kill -9` 杀死 |
| 堆配置 | 崩溃时 `-Xmx32768m`,重启后调整为 `-Xmx49152m` |
| Dump 文件 | `/data01/doris/fe/log/java_pid72774.hprof`(30,463,119,875 
字节,545,178,838 个对象,写入耗时 98.7s) |
| 分析工具 | Eclipse MAT 1.16.1(服务器 /tmp/mat116,解析堆 80GB) |

---

## 一、结论速览

**OOM 根因是 `Env.aliveSessionSet`(存活会话 ID 集合)泄漏**:该集合堆积了 **~1.8 亿条会话 UUID 
字符串**,占满 32GB 堆的 **~98.5%**。

- **触发线程**:`all-fe-session-mgr-pool-0`
- **触发点**:`Env.getAllAliveSessionIds()` → `new ArrayList<>(aliveSessionSet)` → 
`Arrays.copyOf` 申请 `Object[180,221,098]`(1.44 GB 连续数组)失败 → 
`java.lang.OutOfMemoryError: Java heap space`
- **泄漏主体**:**FE 内部高频创建 ConnectContext 的组件**(统计任务、Routine Load、Group Commit、HTTP 
接口、PLSQL、MV 等 40+ 处),以及 `FrontendServiceImpl.forward()` 三参构造导致的设计性泄漏
- **与业务客户端基本无关**:三个高流量用户(prod_cd_option_w / test_cd_option_w / 
prod_cd_zquity_track_w)即使所有连接全部泄漏,14 天也仅 ~11.6 万条,占泄漏总量 **0.06%**
- **增大堆至 48GB 只能延缓,不能根治**;清理逻辑自身存在"整体拷贝"缺陷,集合增大后清理必然自爆,须代码级修复或升级

---

## 二、崩溃时间线(fe.out / fe.log / fe.gc.log 佐证)

| 时间 | 事件 |
|---|---|
| 2026-08-03 16:05:50 | FE 以 `-Xmx32768m` 启动(PID 72774) |
| 2026-08-17 02:10 | 日志尚正常(report-thread 常规 WARN) |
| 2026-08-17 ~09:18 | 开始频繁 Full GC;**Full GC 后堆底 ~25GB**(活对象不可回收,泄漏已达 ~25GB) |
| 2026-08-17 09:18–10:54 | Full GC 每 ~2 分钟一次,堆反复顶到 32GB;业务侧开始报错 |
| 2026-08-17 10:50–10:53 | thrift/mysql 大量异常:`TThreadPoolServer Thrift 
Error`、`Null packet received from network`、`Socket is closed by peer`;`edit log 
insert` 写 bdb 耗 19.3s、锁持有 18.4s(GC 停顿所致) |
| 2026-08-17 10:55:29 | `java.lang.OutOfMemoryError: Java heap space`,写 dump 
98.7s 后 `kill -9` |
| 2026-08-17 11:04:35 | FE 重启,`-Xmx49152m` |

辅助信号:`fe.audit.log` 中 `Null packet received` 由 8/16 全天 84 次激增至 8/17 的 1153 
次(客户端 172.24.16.108 异常断连增多,系内存吃紧的并发症而非根因)。

---

## 三、MAT 分析结果

总使用堆 **28GB / 5.45 亿对象**。两个 Problem Suspect 指向**同一个对象(`Env.aliveSessionSet`)**:

### Problem Suspect 1 — 占堆 57.64%
- **180,268,901 个 `java.lang.String`** = **17,305,670,752 字节(17.3 GB)**
- 内容清一色为 UUID 格式会话 ID(如 `4041343b-f038-427d-a967-76570abb5b2e`)
- 其下挂 180,268,875 个 `byte[]`(11.5 GB)——字符串底层数组
- 被一个 `java.lang.Object[180,221,098] @ 0x14e4f8000000`(1.44 GB)引用 —— **正是 OOM 
时正在分配的那个数组**

### Problem Suspect 2 — 占堆 35.97%
- 一个 `java.util.concurrent.ConcurrentHashMap$Node[268,435,456] @ 
0x14e428000000` = **10,800,711,176 字节(10.8 GB)**
- 即 `aliveSessionSet` 的底层哈希表,内含 **180,239,068 个 `ConcurrentHashMap$Node`**(8.65 
GB)
- 经 `all-fe-session-mgr-pool-0` 线程栈上的 `ConcurrentHashMap$KeyIterator.toArray()` 
关联

### OOM 调用栈(MAT 还原)
```
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Object[], int)                       
(Arrays.java:3481)
    at java.util.concurrent.ConcurrentHashMap$CollectionView.toArray 
(ConcurrentHashMap.java:4471)
    at java.util.ArrayList.<init>(Collection)                       
(ArrayList.java:181)
    at org.apache.doris.catalog.Env.getAllAliveSessionIds()          
(Env.java:7031)
    at org.apache.doris.catalog.FESessionMgr$FEAliveSessionHandler.run() 
(FESessionMgr.java:94)
    ...
```

### 内存构成(泄漏占堆 ~98.5%)

| 组件 | 大小 | 占比 |
|---|---|---|
| 会话 UUID 字符串(String + byte[]) | 17.3 GB | 57.6% |
| aliveSessionSet 底层 CHM Node[] + Node | 10.8 GB | 36.0% |
| OOM 时分配的 Object[180M] 拷贝数组 | 1.44 GB | 4.8% |
| FE 正常工作对象 | ~0.4 GB | ~1.5% |

---

## 四、根因定位(反编译 doris-fe.jar 核实)

### 4.1 注册/注销机制
```java
// Env.java:154 附近 – Guava 并发 Set(底层 ConcurrentHashMap)
private Set<String> aliveSessionSet = Sets.newConcurrentHashSet();

public void registerSessionInfo(String id)   { aliveSessionSet.add(id); }      
// 唯一注册点
public void unregisterSessionInfo(String id) { aliveSessionSet.remove(id); }   
// 唯一注销点
public List<String> getAllAliveSessionIds()  { return new 
ArrayList<>(aliveSessionSet); } // ★OOM 点
```

```java
// ConnectContext.java
public ConnectContext(StreamConnection, boolean) {
    ...
    init();                     // 324: invokevirtual init() —— 构造函数即注册!
}
public void init() {
    ...
    this.sessionId = UUID.randomUUID().toString();
    if (!runningUnitTest) Env.getCurrentEnv().registerSessionInfo(sessionId);  
// 注册
}
public void cleanup()       { ... unregisterSessionInfo(sessionId); }
protected void killConnection() { ... unregisterSessionInfo(sessionId); }
```

**关键结论:`new ConnectContext(...)` 一出生就会在 `aliveSessionSet` 注册一条随机 UUID;只有走到 
`cleanup()/killConnection()` 才注销。**

### 4.2 注销路径核对(JDBC 连接的清理路径是完整的)

| 路径 | 是否调用 cleanup/注销 | 依据 |
|---|---|---|
| MySQL 客户端正常退出(COM_QUIT) | ✅ | `ReadListener` 
正常分支:isKilled→stopAcceptQuery→cleanup→ConnectContext.remove |
| 客户端异常断连(peer 关 socket / null packet) | ✅ | `ReadListener` 异常分支:记录 `Exception 
happened in one session(...)` 后 setKilled→cleanup(8/17 的 1157 次异断均被清理) |
| 会话超时被 TimeoutChecker 杀 | ✅ | `ConnectScheduler$TimeoutChecker`→killConnection 
|
| KILL connection / 查询取消 | ✅ | ConnectProcessor→killConnection |
| **三参构造 `ConnectContext(stream, bool, String)`** | ❌ **构造即泄漏** | `init()` 
已注册随机 UUID,随后 sessionId **被覆盖**;cleanup 只移除"覆盖后的 id",随机 UUID 成为**永远清不掉的孤儿** |
| FE 内部任务 `new ConnectContext()` 不做 cleanup | ❌ 视调用方而定 | 多个内部组件(见 4.3)未保证 
cleanup |

### 4.3 三参构造漏洞(设计性泄漏)

唯一调用方:`org.apache.doris.service.FrontendServiceImpl.forward()`(FE→FE 
转发,follower 把 master-only 操作转发给 MASTER):

```
2043: invokespecial 
ConnectContext."<init>":(Lorg/xnio/StreamConnection;ZLjava/lang/String;)V
        // init() 注册 UUID_A
        // 构造器尾部 putfield sessionId = 传入id
        // cleanup() 时移除的是“传入id”,UUID_A 永久泄漏
```

### 4.4 内部 ConnectContext 创建点(jar 全量扫描,40+ 处)

调用频次较高/可疑的代表:

| 类 | 处数 | 说明 |
|---|---|---|
| `org.apache.doris.statistics.util.StatisticsUtil` | 3 | 自动统计收集(ANALYZE 任务) |
| `org.apache.doris.service.FrontendServiceImpl` | 3 | thrift 接口(含 forward 
漏洞路径) |
| `org.apache.doris.load.routineload.RoutineLoadJob` | 3 | Routine Load 调度 |
| `org.apache.doris.httpv2.rest.LoadAction` | 2 | **FE 侧 HTTP stream load 
入口,类内无 cleanup** |
| `org.apache.doris.httpv2.controller.BaseController` | 2 | REST API 基类 |
| `org.apache.doris.load.StreamLoadHandler` / `GroupCommitManager` / 
`GroupCommitPlanner` | 1/1/1 | Stream load / 分组提交 |
| `org.apache.doris.plsql.executor.*` | 4 | PL/SQL 执行器 |
| `org.apache.doris.mtmv.MTMVPlanUtil` | 1 | 物化视图 |
| `org.apache.doris.nereids.minidump.MinidumpUtils` | 2 | Minidump |
| 
`org.apache.doris.qe.ConnectContextUtil`、`org.apache.doris.job.extensions.insert.InsertTask`、`org.apache.doris.load.ExportTaskExecutor`
 等 | 1–3 | 内部任务 |

### 4.5 清理逻辑的致命缺陷(放大器)

`FESessionMgr.FEAliveSessionHandler`(周期 
`alive_session_update_interval_second`)在清理本节点会话前,必须先执行 
`getAllAliveSessionIds()` —— **把整个 Set 整体拷贝成 `ArrayList`(O(n) 连续大数组)**。

集合涨到上亿后:**拷贝动作本身先 OOM → 清理线程永远无法执行 → 集合只增不减 → 必然崩溃**。形成不可恢复的正反馈。

相关配置(`fe.conf`):
```
alive_session_update_interval_second   # 同步/清理周期
fe_session_mgr_threads_num
fe_session_mgr_blocking_queue_size
```

### 4.6 压缩指针关闭(次要放大因素)

Dump 元信息 `Compressed object pointers = false`(32GB 堆边界触发 HotSpot 关闭压缩指针)→ 
每个对象引用按 8 字节计,`Object[180M]` 拷贝体积翻倍(1.44GB vs 720MB),OOM 提前到来。

---

## 五、泄漏主体排查(为什么是内部组件而不是业务客户端)

### 5.1 数量级对不上(决定性)

8/3 16:05 重启 → 8/17 10:55 OOM(13.6 天),aliceSessionSet 累积 **~1.8 亿条 ≈ 153 条/秒**:

| 指标(14 天合计) | 数量 | 折算速率 | 与泄漏量比 |
|---|---|---|---|
| `aliveSessionSet` 累积 | **~180,000,000** | **~153/s** | 100% |
| 审计 SQL(全部用户/全部语句) | ~3,400,000 | 2.8/s | <2% |
| 新建 JDBC 连接(握手查询 `SELECT @@session...`) | ~116,000 | 0.096/s | 0.06% |
| Stream load 事务(BE 端 begin/commit 指标) | ~60,000/BE | 0.05/s | 0.03% |

**三个高流量用户(prod_cd_option_w / test_cd_option_w / prod_cd_zquity_track_w)占业务流量 
~95%,但即便其所有连接 100% 泄漏,也只占泄漏总量的 ~0.06%。**

### 5.2 JDBC 连接的注销路径完整(见 4.2)

正常断开、异常断开、超时杀掉、KILL 均会 `unregisterSessionInfo`。8/17 激增的异常断连(`Null packet` 
↑84→1153)有日志佐证均走了 cleanup。

### 5.3 Stream load 不产生 FE MySQL 会话

Stream load 走 HTTP 直达 BE(8040,FE 仅 307 重定向),不创建 9030 MySQL 协议会话,与本泄漏无关。BE 指标 14 
天仅 ~6 万次。

### 5.4 内部组件是唯一可达 153/s 量级的来源

内部任务(统计、Routine Load、MV、PLSQL、REST/HTTP 等)创建 `ConnectContext` 
**不产生审计行**,且部分路径(构造器即注册的默认行为 + 缺 cleanup + forward 覆盖 bug)可长期静默累积。

---

我的问题是,目前Doris3.1.4 fe节点是否发现类似内存泄露反馈?

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to