ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Doris内存管理与OOM排查:从原理到优化的完整实战指南

2026/9/28 22:55:25 拓冰建站 浏览量
Doris内存管理与OOM排查:从原理到优化的完整实战指南 聊到 Doris喜欢它的人大多会说它查询快、能存能算MPP 架构下的性能表现在大数据圈子里确实算得上能打。但真正让人头疼的往往是另一件事集群跑着跑着某个 BE 进程的内存蹭蹭往上飙再往后就是 OOM、进程被杀、查询全断。我做大数据这行十来年Doris 从 0.x 版本就开始接触中间踩过不少内存的坑。这篇就好好聊聊 Doris 的内存管理和优化策略包括内存到底花在哪、OOM 怎么一步步定位、以及从配置、SQL、数据模型三个层面把它压住的方法。如果你正在做 Doris 集群维护或者准备把离线报表迁到 Doris 上这篇文章应该能帮你在正式上线前把雷拆掉。1. 先搞明白 Doris 的内存都去哪了1.1 内存管理的基本盘FE 和 BE 谁才是大头很多人一遇到 Doris OOM第一反应就是“加内存”但实际上加内存有时候只能把问题往后拖真正该做的是先搞清楚内存属于谁、消耗在哪个环节。Doris 的架构分成 FE 和 BE 两部分。FE 是 Java 写的负责元数据、查询规划、事务管理等它的内存基本由 JVM 参数决定常见的问题是表数量和分区数量太多导致 FE 堆内存涨上去。BE 是 C 写的负责真正的计算和存储绝大多数 OOM 事故都发生在 BE 这里。所以排查内存问题第一步就是分清是 FE 撑不住还是 BE 撑不住。FE 出问题通常表现为查询较慢、元数据操作卡顿或者 FE 日志频繁报 Full GCBE 出问题往往就是进程突然消失或者 SQL 执行到一半直接报 memory limit exceeded。这里有个容易被忽略的点BE 进程的内存使用并不是一次分配就能算清楚的它既有堆上的小块内存也有大块的 mmap、以及各种算子临时分配的缓冲。Doris 会通过内部的内存跟踪机制去统计这些内存而不是简单看top里的 RES。所以光靠free -g看物理内存剩余多少很难定位到具体是哪条 SQL 出了问题。1.2 BE 内存里的四个“钉子户”BE 的内存大体可以分成四块我习惯叫它“四个钉子户”。第一个是数据导入时的内存缓冲也就是 MemTable。每次导入任务在真正写入磁盘文件之前数据会先攒在内存里的 MemTable 上攒到一定阈值再刷盘。一个导入任务的内存不算大但如果几十个导入任务同时发过来MemTable 加起来就是一笔不小的开销。第二个是查询执行期的中间结果。MPP 查询会把任务切到多个 BE 并行执行每个执行节点都可能创建 HashTable 做聚合、创建 Join Build 表、开排序缓冲、保存 Shuffle 中间结果。这部分内存最不可控也是大多数 OOM 的真凶。尤其是 SQL 里出现大表 Join、超多维度 Group By或者排序字段非常多时内存会像坐火箭一样涨。第三个是数据缓存。Doris 会把读过的数据页、索引块、Segment 元数据放进缓存减少重复扫描的 IO。缓存本身是好事但缓存占用也是内存的一部分而且如果没有合理设置上限它会在查询高峰期和计算抢内存。第四个是后台任务比如 Compaction、版本合并、副本修复。Compaction 在合并小文件时需要加载行集信息tablet 数量太多或者小文件堆积严重时后台内存同样会上涨。很多人只看查询内存忽略了 Compaction 啃内存的问题。1.3 为什么 MPP 对内存失控更敏感传统数据库单机跑如果 OOM 了也只是单机上的业务受影响Doris 这种 MPP 架构不一样一个查询要拆到多个 BE 节点上协同执行只要其中一个 BE 节点内存不够抛出异常整个查询就会失败。而且 Query 在多个节点之间还有互相等待的环节某个节点内存爆了其他节点的 Shuffle 数据也可能卡在那里等超时最后表现就是一堆查询同时失败。另外MPP 架构天然有一个“木桶效应”内存资源不是看集群总和而是看每个 BE 节点自己的水位。比如一条查询在所有节点上平均只需要 4GB但某个节点的数据分布不均、或者是热点 tablet 集中导致它单独需要 10GB那么这条查询在这个节点就可能会挂。这也是为什么内存优化不能只看单条 SQL 的时间复杂度还要看数据分桶是否均匀、是否把热点数据打散。2. 定位 OOM把自己变成内存侦探2.1 两种 OOM 的表现要分清Doris 的 OOM 严格说有两种手心手背完全不同的表现处理方式也完全不同。第一种是 Doris 自身的内存跟踪器发现进程内统计的总内存超过了 BE 配置的mem_limit然后在 BE 日志里报memory limit exceeded查询被拒绝但 BE 进程还活着。这种情况其实是“软限流”说明已经越过阈值但不至于被内核杀掉。第二种是内存尖峰太快Doris 来不及触发自身限制或者物理机本身内存耗尽Linux 内核直接启动 OOM Killer把 BE 进程或者 FE 进程杀掉。表现就是进程消失、dmesg里出现Out of memory: Kill process的记录。很多新手一看进程没了就急着重启但重启之后问题还会复现。我处理这类问题的顺序是先看dmesg确认是不是内核杀再翻 BE 日志里的最后一条报错最后再用 Doris 自己的内存视图定位具体是哪个模块。这个顺序别搞反否则方向很容易跑偏。2.2 用 Memory Tracker 看“谁在吃内存”Doris 提供了比较完善的内存跟踪机制我习惯直接通过 FE 的 MySQL 协议执行show proc /mem_tracker去看内存汇总。这里面会列出每个内存分类的当前值、峰值、上限以及对应的标签。比如看到标签带Query的大头那就去查具体 query标签带Load的大头就去查导入任务标签带TabletManager或者Compaction的就去看后台任务。不同版本的展示字段会有一点差异但核心思路一直没变不要看整体内存占用就蒙要顺着 Memory Tracker 一层层往下钻。如果发现某个 Query 占了好几个 GB下一步就要找到这条 SQL。操作上可以打开审计日志或者根据报错信息里的query id去 BE log 里反查。查到 SQL 之后再看它的表扫描量、Join 方式、聚合维度基本就能判断是哪个算子造成的。2.3 一张“事故现场”速查表诊断阶段的信息比较碎我列了一个常用来对照的表遇到类似现象可以直接套。现象初步指向常用处理动作查询执行到一半失败报 memory limit exceeded单条 Query 内存超过限制先优化 SQL再考虑调大 exec_mem_limit多个 BE 进程同时被杀dmesg 有 OOM 记录集群总内存不足或 mem_limit 设置过高降低并发、合理设置 mem_limit必要时扩容查询不活跃但 BE 内存持续偏高导入并发高或 Compaction 堆积控制导入并发、检查 tablet 数量和小文件FE 内存越来越高GC 频繁元数据太多或查询规划压力大检查表/分区数量、调大 FE 堆内存单条 SQL 平时没事凌晨定时任务一跑就挂大查询集中调度内存峰值叠加给定时任务错峰、限制任务并行度这张表不敢说覆盖所有情况但至少能帮你快速分辨“到底是查询、导入还是后台任务惹的祸”。实际排查中很多问题并不是单一因素导致的往往是导入和查询高峰期重叠再加上 Compaction 也在抢内存最后一起引爆。3. 从三个方向做优化配置、SQL、数据模型3.1 先把进程级和查询级参数分开Doris 内存优化最容易犯的错是把所有问题都归结为“mem_limit 不够”然后一个劲调大 BE 的进程内存比例。真这么干的集群最终大概率是被内核 OOM Killer 一波带走。原因是mem_limit只是让 Doris 在统计内存超标时主动报错它不能让物理机变出更多内存。一旦机器本身没内存了不管 Doris 内部怎么限制内核都会出手。我一般建议把mem_limit设置在物理内存的 80% 左右不要超过 90%剩下给操作系统 page cache 和其他服务留点余地。想要特别严格的环境甚至可以压到 70%。这不是不信任 Doris 的内存统计而是线上环境什么情况都可能发生给内核留点缓冲不是坏事。查询级参数里最常见的是exec_mem_limit这个参数控制一个查询允许使用的最大内存。默认值我印象里不太大但生产环境往往需要按业务调整。可以在 session 级别设置-- 当前会话设置查询内存上限为 8GB SET exec_mem_limit 8589934592;调这个参数要克制。如果一个查询需要 30GB 才能跑过正确做法是先看有没有多余的扫描列、能不能加过滤条件、能不能改 Join 方式而不是简单把它调到 64GB。把exec_mem_limit无脑调大只会让本来集群能扛住 10 个并发查询变成只能扛 2 个。3.2 SQL 算子层面让 Hash Table 别再成为胖子查询内存高发区域基本都在三个算子聚合、Join、排序。聚合最怕 Group By 维度爆炸。比如按用户 ID 加几十个维度标签做 Group By结果集非常大聚合算子需要维护一个巨大的 HashTable。碰到这种 SQL可以先做“预聚合”把明细层先收敛成中间层再让 Doris 跑最外层的聚合。这一步通常能从十几 GB 的中间结果降到几百 MB。Join 的内存大头在 Hash Build 表。用EXPLAIN看执行计划时重点看哪一张表被物化成了 Hash Table。如果 Build 端是几十亿行的大表而 Probe 端反而很小那内存不吃紧才怪。对于这种情况可以先过滤再 Join尽量把参与 Join 的数据量降下来或者让业务重写 SQL把条件过滤后的“小表”放在 Build 侧。排序和窗口函数这里很多人会忽略。ORDER BY字段多、ROW_NUMBER()或者其他窗口函数同时开好几个都会生成大量排序中间结果。能减少排序字段就减少能用过滤条件缩小排序范围就加过滤。有时候一条 SQL 看起来只是几十行代码但背后扫了整张几十亿行的表这才是内存失控的根源。3.3 数据模型侧分桶不是越多越好几 MB 数据别硬造很多人在 Doris 建表时有个误解觉得分桶数越多并行度越高跑得就越快。实际上分桶数量影响的是 tablet 的分布和粒度分桶太多但数据量很小只会凭空制造大量小文件让 Compaction 频繁、元数据膨胀反而白白浪费内存和 IO。我经常给团队举一个例子如果一张表只有几 MB 数据那根本不需要分几十个桶。像这种小表老老实实建一个单桶就挺好CREATE TABLE small_table ( pk INT, name VARCHAR(64) ) DISTRIBUTED BY HASH(pk) BUCKETS 1;Doris 对数据量小的表完全有能力用一个 tablet 处理单桶还能减少 Compaction 的合并压力内存开销最小。只有数据量增大到单 tablet 的读写成为瓶颈时才需要考虑增加分桶数。一般的经验值单个 tablet 数据量控制在 1GB 到 3GB 之间比较舒服具体还要看机器规格和查询模型。另一个容易忽视的点是排序键的写入顺序。Doris 的存储模型按 Key 排序如果业务写入的数据分布和排序列完全不匹配后台 Compaction 就要频繁做重排合并内存消耗成倍上涨。所以建表时要尽量把常用过滤字段、高基数字段放在 Key 前面让导入的数据天然有序减少 Compaction 开销。3.4 兜底手段缓存、后台任务与导入节奏到了这一步配置也调了、SQL 也优化了、分桶也定了但集群还是有可能在高并发下内存紧张。这时候就要考虑几个兜底手段。第一个是缓存的调整。BE 里的 PageCache 和索引缓存如果开得过大会影响查询执行内存开得过小又会让频繁查询的数据不断去读磁盘。最好根据业务特点来决定如果是报表看板这种“同一批热点数据反复查”的场景缓存可以给足如果是“每次查不同数据或者数据不断更新”的场景缓存价值不大可以调低上限把内存留给真正的计算。第二个是 Compaction 的治理。Doris 会定期把多个版本的小文件合并成大文件但如果 tablet 数特别多、小文件堆积严重Compaction 任务本身会成为内存大户。这一类问题要从源头解决比如控制分桶数量、控制导入频率、避免过多小文件产生。第三个是导入并发控制。Stream Load 虽然方便但不要几十个任务同一时间打到同一张表。每个导入任务都会创建对应的 MemTable并发一多内存峰值很容易重叠。如果业务确实有很多小批量写入可以考虑用 Group Commit 或把多个小导入合并成一个批次让内存峰值变得平滑。4. 实战复盘一次凌晨 OOM 的完整排查4.1 事故现场去年有一次生产集群在凌晨三点报警监控显示 BE 进程内存持续走高然后其中一台 BE 进程直接消失紧接着另一台 BE 也出现同样趋势整个集群陷入“一边被杀一边重启”的恶性循环。业务方反馈是凌晨定时同步任务在打宽表宽表关联了四五张明细表跑了大概半小时后开始报错。当时第一反应不是重启而是先看系统日志。dmesg里果然看到了Out of memory: Kill process的一行记录被杀的进程是doris_be。再看 BE 日志发现进程被杀前已经反复出现内存超限的警告但 Doris 的软限制没来得及拦住那波瞬时尖峰内核只能接管。说白了就是进程级阈值给得太高导致物理机内存先扛不住了。4.2 用 Memory Tracker 一步步反向定位拿到现场现象后我先用show proc /mem_tracker把 BE 上的内存大块找出来。排序后排在前面的是两个 QueryTracker各占十几个 GB再往下是 StandardCache 和 SegmentCache后台 Compaction 也有一些消耗。这时候已经很明确问题是查询内存不是缓存、不是导入。顺着 QueryTracker 里的 query id我在 BE 日志里找到了对应的 SQL。看了之后发现这条 SQL 做了两张大表 JoinJoin 之后又 Group By 了四十多个字段然后把结果再套一层窗口函数。更糟糕的是SQL 最外层没有分区过滤虽然业务只需要最近一天的数据但 SQL 实际扫描的是全表近一个月的分区。这种写法对 Doris 来说就是妥妥的内存炸弹。Stage 中间的 HashTable 要被撑爆后面还要做内存排序exec_mem_limit设成默认的 2GB 根本不可能够用。但如果只是把exec_mem_limit调到 20GB高峰期再来几个类似查询集群照样被打垮。4.3 优化动作SQL 为主参数为辅这次修复我做了四步。第一步是改 SQL最外层加上了近一天的分区过滤让扫描和 Join 的数据量直接降了一个量级。然后调整了 Join 顺序把过滤后数据量最小的表放到了 Build 侧这样 HashTable 的体量小了很多。最后把窗口函数改成先聚合、再在聚合结果上做排序避免明细数据进入排序环节。第二步是给这个业务单独设置 session 级别的exec_mem_limit从默认值调到 8GB。这里不是鼓励所有查询都调大而是明确知道这条 SQL 解析后大概需要多少内存给它留下合理余量同时避免其他慢查询跟着沾光。第三步是调整 BE 的mem_limit从原来的 0.9 降到了 0.8。很多人会问不是应该调高吗其实降低进程阈值反而更安全。因为 Doris 会在内存接近阈值时主动拒绝新请求、释放缓存而不是默默等到物理内存耗尽被内核杀死。主动保护总比被动杀进程好。第四步是重新梳理了宽表的数据模型。原来这张宽表有 48 个分桶但单分区实际数据量也就在几个 GB分桶明显偏多。后来重建表时把分桶从 48 降到 16后台 Compaction 的资源和内存占用明显下降。这四步做完之后同一套定时任务没有再出现过 OOM凌晨的内存水位稳定了很多。4.4 复盘沉淀下来的检查清单每次处理完线上问题我都会把它沉淀成一张检查清单方便团队其他同学复用。这里也分享给你。检查点自查方式常用动作物理机内存是否足够free -g对比 mem_limit动态节点不要超卖给系统预留空间BE 内存大头在哪show proc /mem_tracker区分 Query、Load、Cache、Compaction单条 SQL 是否扫描过多EXPLAIN看 scan 行数/字节数加过滤条件、只取需要的列Join 的 Build 端是否偏大EXPLAIN看 hash 表构建调整 Join 顺序、使用预聚合分桶是否严重过多show tablets看 tablet 数量和大小小表建 1 桶大表单 tablet 控制在 1~3GB定时任务是否扎堆查看调度时间错峰跑、限制任务并发缓存是否挤占计算内存Memory Tracker 看 cache 占比按业务热点调整缓存上限这次复盘之后我现在的体会是Doris 内存优化不是让你把mem_limit调到 1.0而是先保证数据结构合理再限制单查询最后才碰进程参数。分桶、SQL 过滤、Join 顺序这些细节才是省内存的真正性价比。如果你以后也遇到 Doris 内存问题建议从这几个点逐一排查大概率能少熬几个夜。