Doris 3.1.4 fe节点内存泄露(kimi分析)

Viewed 23

Doris 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 ErrorNull packet received from networkSocket is closed by peeredit 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.logNull 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 注册/注销机制

// 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 点
// 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.ConnectContextUtilorg.apache.doris.job.extensions.insert.InsertTaskorg.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节点是否发现类似内存泄露反馈?

1 Answers