ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Apache Doris内存管理实战:从OOM排查到调优策略

Apache Doris内存管理实战:从OOM排查到调优策略 1. 先聊清楚Doris 的内存到底花在哪了做 Doris 运维这几年我最大的体会是Doris 本身不复杂复杂的是它的内存。很多团队把 Doris 部署起来跑个 demo数据量几百 GB查询也简单觉得一切都很美好。一旦进入生产环境并发上来、数据倾斜出现、大查询和导入任务叠在一起内存就成了最先暴雷的地方。BE 进程直接 OOM、查询报 memory limit exceeded、导入任务卡住不动这些情况我全都在生产环境里踩过一遍。在说优化策略之前得先把 Doris 的内存结构搞清楚。Doris 是典型的 MPP大规模并行处理架构FE 只负责元数据管理和查询解析调度真正干活、真正吃内存的是 BE。所以下面聊的内存基本都是围绕 BE 进程展开的。1.1 BE 进程的六块主要内存开支BE 是 C 实现的进程它的内存大致可以分成六块查询执行内存这是最容易被大家感知到的一部分。Join 时的 HashTable、聚合计算的中间状态、排序、窗口函数、Scan 阶段的交换缓冲全都在这块里。查询越复杂、扫描的数据量越大、倾斜越严重这块内存就涨得越猛。导入写入内存导入任务Stream Load、Broker Load、Routine Load会把数据先写进 MemTableMemTable 攒到一定阈值再刷成底层的 Segment 文件。多个导入任务并发时会有多路 MemTable 同时在内存里构建。各类缓存DataPageCache、IndexPageCache、SegmentCache以及 FE 过来的 Schema 相关缓存。这些缓存设计出来是为了提速但如果不加控制它们也会悄悄吃掉大量内存。存储引擎与元数据内存Tablet 的元信息、版本号、Compaction 执行过程中的临时数据。单看一个小 tablet 没多少内存但一个集群如果攒了数以万计的 tablet这块内存会变得非常可观。系统级开销内存分配器Doris 底层会用 TCMalloc 这类分配器的预留和碎片、线程栈、网络连接缓冲。这部分经常被忽略但在高并发下并不算小。其他杂项权限信息、用户信息、各种统计信息等。我用一个生活化的类比来帮助理解BE 进程像一个大仓库查询是随时进来的订单要在仓库里拣货、打包、发货导入是进货进货的时候必须腾出空间临时堆放货物缓存是把常用货物放在最靠外的货架上方便快速取用存储引擎是仓库自己的货架维护系统隔一段时间要把货架重新归整一下Compaction。仓库的总面积就是这台机器的内存任何一个环节失控都会让仓库爆仓。1.2 为什么内存问题比磁盘问题更让人头疼磁盘问题好解决满了就加盘、清理孤儿文件、调整保留策略逻辑很直白。内存问题则不一样它有三个让运维头疼的特性。第一复现性差。同一个查询昨天跑得好好的今天数据量稍微变一点、并发稍微高一点就直接 OOM。你很难稳定地在一个测试环境里复现问题所以很多团队只能等它再爆一次才能去抓现场。第二排查链路长。Doris 一个查询要经过 FE 解析、生成执行计划、分发到多个 BE、在每个 BE 上做 Scan、Join、聚合、排序再通过网络汇合结果。内存是在哪个节点、哪个算子、哪个阶段爆掉的需要一层一层去剥。第三处理手段有限。磁盘满了可以清理内存爆了你只能杀查询、重启 BE或者调整参数。而参数之间还有互相影响比如你把查询内存调小确实防住了 OOM但大查询就会频繁报内存不足业务方会来找你。所以我的态度一直是内存问题要在架构设计、参数配置、运维监控三个层面同时下手而不是等出了事故再去救火。1.3 官方内存统计口径MemTrackerDoris 内部有一套挺完整的内存追踪机制核心组件叫 MemTracker。简单理解每个查询、每个 BE 内部的关键模块都会挂一个自己的计数器记下自己用了多少内存、峰值到过多少。连接到 Doris 之后可以直接执行SHOW PROC /mem_tracker;你会看到类似这样的输出结构里面有进程级别的内存分类汇总。重点看两个字段Current是当前值Peak是历史峰值。我强烈建议看峰值而不是只看当前值因为很多内存问题其实就是一瞬间冲上去的当前值回落之后你很难判断到底是不是它干的。如果集群里正在跑一堆查询还可以用SHOW PROC /mem_tracker/query;看一眼每个查询自己占了多少内存这是定位大查询最快的方式。注意MemTracker 统计的是 BE 内能追踪到的内存像 TCMalloc 内部的碎片、线程栈这类系统级开销未必能完整体现在某个具体的 tracker 里。所以如果发现统计出来的总量比实际进程 RSS 小很多别奇怪这是正常的系统级内存是那个“隐形黑洞”。2. 定位内存问题三板斧和两个关键习惯很多人一遇到 BE OOM 就直接重启然后该干嘛干嘛结果没过几天又挂一次。我这里先说一套标准化的排查流程把这套流程跑熟了绝大多数内存问题都能在五分钟内看到方向。2.1 第一板斧看 BE 的 Web 页面每个 BE 节点默认会开一个 Web 端口一般是 8040打开浏览器访问http://be_host:8040/页面里通常有内存相关的标签页能看到这个 BE 当前的内存使用总量、各类缓存占用、MemTable 数量等信息。很多新版部署在页面上就能看到 MemTracker 的分类数据比命令行更方便。2.2 第二板斧翻日志Doris 的 BE 日志在log/目录下关键文件是be.INFO。OOM 或者内存超限的时候日志里通常会有类似Memory limit exceeded、Process mem limit is reached这样的关键字而且会带上当时是哪个模块触发的比如query、load、compaction。我处理问题的习惯是grep -i memory be.INFO | tail -200先看最近几百行内存相关日志基本就能知道是查询内存超了还是导入内存超了还是系统内存整体达到了上限。2.3 第三板斧看实时查询和慢查询如果问题还在持续需要快速找出是哪个查询在吃内存。可以用 ManagerDoris 的图形化管理工具查看正在运行的查询按内存排序或者用命令行查SHOW PROC /current_queries;这个命令能看到当前活跃查询以及各自的内存使用量。确认了是哪个查询之后如果业务方不急可以等它跑完如果已经危及到集群稳定性那就别犹豫直接把查询杀掉。Doris 提供 CANCEL QUERY 语句通过 query_id 就能取消。CANCEL QUERY WHERE queryid xxx;这里有个容易踩的坑杀掉查询之后内存不一定立刻回落。BE 进程不会马上把内存归还给操作系统因为 C 的内存分配器可能把内存留在自己的缓存池里。所以杀完查询后需要观察几分钟看内存指标是否逐步下降。如果一直不降再考虑下一步处理。2.4 两个关键习惯记录基线和看趋势我强烈建议每个 Doris 集群从部署第一天就做好两项基础工作。第一记录内存基线。集群刚部署完、还没有业务流量的时候记录每个 BE 的基础内存使用量。这个值包含了各种缓存、元数据、系统开销是后续所有判断的起点。如果过了两周你发现基础内存涨了一大截那说明多半是元数据膨胀或者缓存配置有问题而不是业务查询导致的。第二看趋势而不是只看瞬时值。内存监控一定要有粒度到分钟级的趋势图。瞬时值只能告诉你“现在爆了”趋势图才能告诉你“它是慢慢涨上来的还是在某个查询进来后突然冲上去的”。慢慢涨一般是缓存、元数据、MemTable 堆积突然冲一般是有大查询或者批量导入。3. 优化策略从参数到查询再到集群规划内存优化不是把某个参数调大调小那么简单它是一套组合拳。下面我按场景一个一个讲。3.1 BE 内存上限mem_limit 到底设多少先说最基础的参数BE 的mem_limit。这个参数在be.conf里配置默认是物理内存的 90%。含义是BE 进程认为自己最多能用到这一比例的物理内存超过之后就会触发一系列的内存保护动作比如拒绝新查询、强制刷 MemTable、清理缓存等。实际生产环境我一般不会用默认的 90%而是显式设置到 80% 到 85% 左右。原因很简单一台机器除了 BE 还要跑操作系统、监控 agent、日志采集另外 Doris 的 FE 如果部署在同一台机器上也要吃内存。如果 BE 自己定到 90%一旦内存冲到高位操作系统可能连响应监控命令的余地都没有那时候你连上去重启都是挣扎。设置方式很直接编辑be.confmem_limit 85%然后重启 BE 生效。注意mem_limit设得太低也不好。Doris 在内存接近上限时会触发自我保护机制比如提前 Flush MemTable、清理缓存这些操作本身会带来性能损耗。如果设成 60% 以下你会发现自己明明机器内存很大但查询稍微复杂一点就报内存不足。实践下来80% 到 85% 是一个比较均衡的区间。3.2 查询侧优化让每条 SQL 少吃点内存查询内存是压垮 BE 最常见的原因尤其是大 Join 和大聚合。第一个建议控制单查询的内存上限。Doris 支持在会话级别设置exec_mem_limit新版也兼容query_mem_limit这个命名。比如SET exec_mem_limit 8589934592; -- 8GB这个值代表单个查询在单个 BE 上可以使用的内存上限。设置它最大的好处是即便业务方跑了个写得稀烂的 SQL也只是那个查询自己报错不会拖垮整个 BE。第二个建议优化 Join 的写法。Doris 在做 Join 时通常会把右表或者优化器选择的一侧加载到内存构建 HashTable。如果这一侧的数据量特别大内存就会爆炸。实际优化时可以考虑三个方向尽量让大表和小表 Join小表作为构建侧。比如网约车大数据项目里订单明细表关联城市维表城市维表很小就应该让它做构建侧。利用 Doris 的 Colocate Join / Bucket Shuffle Join让 Join 在本地节点完成减少数据在网络上的传输也减少中间结果的缓存。如果某张表数据严重倾斜单独一个大 Key 把内存吃满可以考虑对倾斜值做打散处理。第三个建议少碰大窗口函数和 SELECT *。窗口函数里的ORDER BY、RANGE窗口会在内存里维护大量排序缓冲。如果业务确实需要先确认数据量级再评估要不要改成分层聚合。另一个我反复强调的习惯是不要无脑SELECT *只查需要的列。列数少了Scan 阶段的数据缓冲、后续的交换缓冲都会降下来。第四个建议合理设置并行度。Doris 的并行度会影响内存消耗。parallel_fragment_exec_instance_num控制每个 BE 上执行的实例数实例数越多内存一般也越多。很多集群默认值偏高并发上来之后每个实例分到的内存被摊薄反而会频繁触发内存保护。要根据查询特征和 BE 机器规格慢慢调不要一次调太多。3.3 导入侧优化MemTable 是那个容易被忽视的大头查询内存大家都会关注导入内存反而经常被忽略。实际上在生产环境里凌晨跑批导入把 BE 内存打爆的例子我见得太多了。Doris 的导入流程是数据进入 BE 后先写进内存里的 MemTableMemTable 攒够一定大小再刷成磁盘上的 Segment 文件。如果同时有多个导入任务在跑或者单个导入任务特别大、涉及的分区特别多内存里就会有多个 MemTable 同时在构建。针对导入侧我一般做这么几件事第一控制导入并发。在应用层限制同时发起的 Stream Load 数量避免高峰期一股脑全压进来。Routine Load比如消费 Kafka 的网约车订单数据要严格控制max_batch_interval和单批次大小确保每批数据都能快速刷盘。第二给导入任务单独设置内存限制。BE 配置里有load_mem_limit这个参数可以控制导入任务的内存上限。在有多个 BE 的集群里这个参数是单个 BE 上导入任务总共能用的内存上限设得太小会导致导入变慢设得太大又可能挤占查询内存。我的做法是先按“总内存的 30% 左右”给一个保守值然后根据监控调整。第三减少不必要的高基数合并。如果导入的数据存在大量重复 KeyMemTable 在内存里做合并去重的时候消耗会明显增加。这种情况建议在源头先把数据清洗一遍或者调整导入方式别把脏活都丢给 Doris 的内存。3.4 缓存与存储引擎侧PageCache、分桶数量、CompactionDoris 的缓存设计很完善但缓存大了也会变成负担。这里重点说三个点。第一个点DataPageCache 和 IndexPageCache。这两类缓存分别缓存列存数据页和索引页目的是减少磁盘 IO。默认情况下 Doris 会动态管理但在内存紧张的集群里可以通过配置限制它们的上限。如果你发现 BE 的 RSS 里“缓存”占了很大一块而业务查询并没有什么明显加速效果那就值得把缓存上限降下来。第二个点分桶数量Tablet 数量对元数据内存的影响。这个我必须专门说。很多刚接触 Doris 的人一听说要分桶就给每张表都分几百个桶结果单表 tablet 数量几千上万个。每个 tablet 都有对应的元数据信息、版本信息存在 BE 内存里。集群里表多了之后这部分内存是会累加的。热搜词里有个问题问得很典型“如果只有几 MB 数据是不是不需要分桶”答案是确实不需要分那么多桶甚至一个桶就够了。分桶的核心目的是并行扫数据和分散数据几 MB 的数据一个桶和一个桶几十个查询速度区别不大但后者会白白浪费元数据内存。我的建议是小表就一两个桶中表按数据量评估单桶数据控制在几 GB 到几十 GB 之间比较合理。第三个点Compaction 的内存消耗。Compaction 是 Doris 后台做数据合并的机制它读取多个 Segment 文件合并后写成一个更大的 Segment。这个过程中会有一批内存用于读写缓冲。如果集群业务有明显的波峰波谷可以考虑把 Compaction 线程数调小让它在业务低峰期慢慢做避免和查询高峰期抢内存。3.5 新版本的新武器查询落盘Doris 较新的版本开始支持查询落盘能力。简单理解当 Join、聚合、排序这些操作所需的内存超过阈值时可以把中间结果临时写到本地磁盘而不是死磕内存。这个能力对大查询非常友好从根本上改变了“内存不够就报错”的局面。不过要提醒的是落盘不是银弹。落盘之后查询延迟会明显增加因为多了一轮磁盘 IO如果临时目录选到了性能差的盘整个查询会慢到让你怀疑人生。我在生产环境里的原则是正常查询尽量不带落盘只有那些确实需要跑、但又经常撞内存的“重查询”才针对性打开。3.6 集群规划层面的内存分配最后要说的是集群规划。Doris 集群部署策略本身就决定了内存优化的上限。最容易犯的错是 FE 和 BE 混部。FE 是 Java 进程JVM 堆动辄几个 GBBE 是 C 进程把内存当饭吃。两者混在一台机器上很难平衡。能用独立机器部署 BE 的就尽量独立实在不行至少保证 FE 的 JVM 堆和 BE 的mem_limit加起来不超过物理内存的 90%。另外要留出操作系统内存。Linux 本身需要页缓存来加速文件读写监控 agent、日志写盘也需要内存。一台 64GB 的机器如果 BE 的mem_limit给到 90%再叠加 FE 和其他进程OS 很容易陷入内存紧张的状态。我通常会把 BE 的可控内存控制在物理内存的 75% 到 80% 之间剩下的留给系统和其他组件。4. 生产案例复盘三次典型内存事故理论说了那么多我挑三个自己处理过的真实案例复盘一下。这三个案例分别对应查询内存、导入内存和元数据内存很有代表性。4.1 案例一一处大 Join 直接把 BE 打挂背景是一个网约车大数据的分析场景业务方写了一条 SQL把某一天的订单明细表和司机信息表做 Join然后再和城市维表做关联。订单明细表有上亿行司机信息表也有几百万行。现象是BE 进程在凌晨批量查询时直接 OOM 崩溃重启后过一会又崩。从be.INFO日志看报错指向查询模块的 HashTable 内存超限。排查后发现两个问题。第一司机信息表虽然只有几百万行但它在 Doris 里没有做分区分桶优化扫描出来的中间结果很大第二这条 SQL 里关联条件写得很宽Doris 优化器最终选择把司机表加载到内存构建 HashTable一次性吃掉了 30 多 GB。解决方案分两步。第一步给这类大查询设置了exec_mem_limit把它限制在 16GB 以内保证单个查询不会拖死整个 BE。第二步和业务方一起改 SQL把 Join 顺序调整让较小的城市维表作为构建侧同时给司机信息表按照常用过滤字段做了分桶优化。改完之后同一条查询的内存峰值降了 60% 以上BE 再也没因为这条 SQL 崩过。4.2 案例二凌晨例行导入任务引发内存雪崩另一个场景是数仓团队每天凌晨从 Kafka 拉取前一天的日志数据做导入用的 Routine Load同时开了一堆导入任务。现象是每天凌晨 2 点准时开始内存告警BE 的 MemTracker 显示导入模块内存快速攀升偶尔会导致查询报错。这个问题的根源是导入并发太高多路 MemTable 同时在内存构建而 BE 的load_mem_limit没有显式配置按默认值其实相当于没限制。处理方式是把导入并发从 10 路降到 4 路同时调大单批次刷盘阈值让 MemTable 到一定大小就尽快刷盘再给load_mem_limit设了一个明确的上限。调整后凌晨的内存使用率从 90% 以上降到了 70% 左右导入任务虽然变慢了一点但总能在业务查询高峰前跑完整体反而更稳定。这里我有一个判断经验如果导入任务本身不急白天也可以跑就别在夜里把所有导入挤在一起。Doris 不是不能扛并发而是内存有限错峰导入永远是性价比最高的优化手段。4.3 案例三几万个 tablet 让元数据内存悄悄膨胀第三个案例比较隐蔽。一个校园大数据的项目建了很多张表每一张表都习惯性分了 48 个桶半年下来集群里的 tablet 数量到了五六万个。现象是 BE 的基础内存使用率一天比一天高没有大查询、没有导入任务但内存就是缓慢往上爬。用SHOW PROC /mem_tracker看发现存储引擎相关的 Meta 内存明显偏大。排查下来就是 tablet 数量太多每个 tablet 的元数据、版本信息常驻内存积少成多。更麻烦的是这个问题不像查询 OOM 那样会在日志里留下明显报错它更像慢性病。处理思路是对已有的大表重新评估分桶数能合并的就通过重建表来合并对新建表制定规范小表一律一两个桶只有数据量确实大的表才适当增加分桶。经过几轮清理BE 的基础内存下降了 30% 左右。这件事之后我对团队的要求是建表时必须写明分桶依据禁止拍脑袋分桶。4.4 一个判断“泄漏”的实用技巧很多人一看到内存慢慢涨就怀疑“是不是内存泄漏”。其实 Doris 的缓存机制本身就决定了内存会有一定幅度的波动不一定是泄漏。我的判断方法是把 BE 重启后记录启动 24 小时、48 小时、72 小时的“空闲内存”曲线。如果内存涨到一定程度就稳定下来那大概率是缓存和元数据达到了新的平衡不是泄漏如果无上限地一直涨直到撑满那才是真的泄漏。大多数情况下所谓的内存取舍其实只是缓存配置和 tablet 数量问题。5. 内存优化程度可以到什么水平很多朋友会问内存优化到底能优化到什么程度有没有一个标准我拿一个真实集群举个例。一个 3 台 BE、每台 64GB 内存、总数据量约 5TB 的 Doris 集群优化前 BE 内存使用率经常冲到 90% 以上一有大查询就报警。优化后稳定在 65% 到 75% 之间高峰期也很少超过 80%。具体做了这几件事给 BE 设置mem_limit 80%留下系统余地给大查询设置exec_mem_limit 8GB防止单查询吃满把 Routine Load 并发降低给load_mem_limit设置 20GB 的上限清理了历史遗留的过度分桶表关闭了几个不常用大表的 PageCache 上限控制把省出来的内存留给查询调优不是一次性的跑一段时间后还要根据监控数据再微调。但只要监控到位、参数合理Doris 的内存是完全可控的。5.1 常见问题速查表我把平时被问到最多的问题整理成一张表方便大家直接对照。现象大概率原因第一步处理建议查询报 memory limit exceeded单查询内存超过限制看SHOW PROC /mem_tracker/query定位查询来源再决定调大exec_mem_limit还是优化 SQLBE 进程直接 OOM 崩溃进程整体内存超限查看be.INFO里是 query、load 还是 compaction 触发降低对应模块的内存上限内存缓慢持续上涨tablet 元数据或缓存增长检查 tablet 总数和 MemTracker 里的 Meta 项评估是否需要合并 tablet导入高峰时内存暴涨MemTable 并发过高降低导入并发设置load_mem_limit并发查询一多就报错查询并行度或mem_limit设置不匹配适当降低parallel_fragment_exec_instance_num大查询不死但很慢查询落盘导致 IO 瓶颈检查是否开启了查询落盘确认临时目录在 SSD 上杀完查询后内存不回落分配器缓存未归还系统观察 10 到 30 分钟如果持续不降再考虑重启 BE5.2 关于“小数据量”场景的一个忠告热搜词里有个问题我一直想回应Doris 如果只有几 MB 数据是不是不需要分桶类似的问题还有“几 MB 数据要不要建 Doris”。我的看法是如果只是几 MB 数据首先要想的不是分不分桶而是这个场景到底需不需要 Doris。几 MB 的数据用 MySQL、甚至用 Excel 都能处理得很好引入 Doris 是为了后续数据量增长和复杂分析做的长远规划。如果确定要用 Doris那几 MB 的表就别造作分桶了一个桶足够。后面数据量真到了几十 GB、几百 GB再通过动态分区和合理的分桶策略去扩展。架构设计要往前看但不要为了“显得专业”而给三张表分三百个桶。6. 写在最后的一些个人经验全过程聊下来最后分享几个我踩过坑之后沉淀下来的习惯。第一内存监控一定要从集群部署第一天就做。不要等出了问题再想监控方案。Doris 自带的 MemTracker、Web 页面、Manager 工具至少把其中一套用熟。连基础监控都没有的 Doris 集群出问题时就跟瞎子摸象一样。第二调整参数要小步快跑。内存相关的参数往往互相影响一次只改一个参数观察一段时间再动下一个。我见过有人一口气改了十个参数结果查询全报错都不知道是哪个参数改坏的。第三永远给操作系统留口粮。不管 Doris 官方的默认值是多少我的实践原则是 BE 的可控内存不要超过物理内存的 85%最好控制在 75% 到 80%。留出来的内存看似浪费实际是给操作系统换口气的余地关键时刻比什么都值钱。第四学会看查询 Profile。除了内存指标Doris 的查询 Profile 能告诉你每个算子花了多少时间、用了多少内存。数据倾斜、Join 顺序不合理、扫描量过大这些问题在 Profile 里都看得清清楚楚。先把 Profile 看明白再谈优化。内存管理这件事说穿了就是四个字心里有数。知道内存花在哪、花了多少、为什么花剩下的就是调整参数和耐心观察。希望这篇内容能帮你少踩几个坑。
返回列表