Skip to content

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

Description

@wangcool

Search before asking

  • I had searched in the issues and found no similar issues.

Version

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 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节点是否发现类似内存泄露反馈?

What's Wrong?

doris 3.1.4 fe节点出现内存泄露,分析是否正确?

What You Expected?

确认doris 3.1.4 fe是否有内存泄露的bug

How to Reproduce?

No response

Anything Else?

doris 3.1.4 fe节点出现内存泄露,分析是否正确?

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions