Say Goodbye to OOM Crashes

文章讲述了ApacheDoris中内存管理的关键要素,包括内存分配、释放、回收、内存数据结构(如Arena、HashTable和PODArray)以及内存跟踪和限制策略。通过优化内存管理,系统稳定性得到增强,减少了OOM错误和查询性能瓶颈。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内存管理
---

在计算机编程中,内存管理是一项关键任务,用于在程序运行时正确分配和释放内存。一个有效的内存管理系统可以帮助程序提高性能,减少内存泄露和访问错误等问题。

内存管理涉及以下几个方面:

1. 内存分配:为程序中的变量、对象和数据结构分配所需的内存空间。
2. 内存释放:当变量、对象或数据结构不再使用时,将其占用的内存空间释放回操作系统。
3. 内存回收:识别和释放程序中没有被引用的内存块,以便重新分配给其他需要的部分。
4. 内存泄漏:当程序在使用完内存后没有正确释放它,导致内存资源的浪费。

内存管理的方法可以有很多种,包括手动管理和自动管理。手动管理需要程序员显式地跟踪和释放内存。而自动管理则由编程语言或运行时环境提供的垃圾回收机制负责释放不再使用的内存。

在选择适当的内存管理策略时,需要考虑程序的性能要求、内存使用模式和资源限制等因素。

Say Goodbye to OOM Crashes  英文看起来脑壳痛,翻译了一下

还有遇到了实际的问题

---------------------------------------------------------------------

是什么保证了大数据查询任务中的系统稳定性?保证大型数据查询任务的系统稳定性的是有效的内存分配和监控机制。这涉及加速计算、避免内存热点、及时响应内存不足并尽量减少OOM错误。

从数据库用户的角度来看,他们会因为糟糕的内存管理而遭受以下问题:

  1. OOM错误导致后端进程崩溃。引用我们社区成员的一句话:“嗨,Apache Doris,当你内存不足时慢点或失败一些任务都没关系,但是完全宕机就不酷了。”
  2. 后端进程占用过多内存空间,但无法找到具体的任务来定位问题或限制单个查询的内存使用。
  3. 很难为每个查询设置适当的内存大小,所以即使有足够的内存空间,查询也可能会被取消。
  4. 高并发查询速度明显偏慢,而且很难定位内存热点。
  5. 在创建HashTable过程中,无法将中间数据刷新到磁盘,因此在两个大型表之间进行的连接查询经常因为OOM而失败。

幸运的是,那些黑暗的日子已经过去了,因为我们从底层改进了内存管理机制。现在准备好,事情将会变得更加高效。

Memory Allocation

内存分配方面,在Apache Doris中,我们有一个独特的内存分配接口:Allocator。它会根据需要进行调整,以保持内存使用的高效和可控。此外,MemTrackers会跟踪已分配或释放的内存大小,并且有三种不同的数据结构负责操作执行中的大内存分配(我们将立即介绍它们)。

内存中的数据结构

由于不同的查询在执行中具有不同的内存热点模式,ApacheDoris提供了三种不同的内存内数据结构:Arena、HashTable和PODArray。他们都在分配者的统治之下。

Apache Doris 是一个 MPP(Massive Parallel Processing,大规模并行处理)架构的快速、稳定、可靠的开源分析型数据库。它的设计旨在解决大数据环境下的分析查询问题。对于你的问题,关于Apache Doris中的内存数据结构,以下是你的问题的一些基本背景信息。

  1. Arena: Arena 是一种内存分配器,它负责在内存中分配和释放内存块。在 Doris 中,它主要用于存储 HashTable 和 PODArray 的数据。在 Arena 中,内存的管理是非常重要的,因为它需要有效地分配和释放内存,以避免内存泄漏或碎片化。
  2. HashTable: HashTable 是一种数据结构,它允许我们通过键(key)快速查找值(value)。在 Doris 中,HashTable 主要用于存储和检索查询结果。HashTable 的设计需要考虑如何有效地使用内存,同时还需要考虑到查询性能和准确性。
  3. PODArray: PODArray(Plain Old Data Array)是一个简单的、连续的、固定长度的数组结构。在 Doris 中,PODArray 主要用于存储查询结果,特别是在处理复杂查询时。PODArray 的设计旨在提供高效的内存使用和快速的查询性能。

所有这些数据结构都由 Allocator 管理,Allocator 负责分配和管理内存资源。在 Doris 中,Allocator 的设计需要考虑到如何有效地满足各种查询的需求,同时还需要管理内存的使用,避免内存泄漏和碎片化。

这些数据结构和内存管理策略的设计都是为了在处理大规模数据时提供高效和稳定的性能。

  1. Arena

Arena是一个内存池,维护一个块列表,这些块在从Allocator请求时进行分配。这些块支持内存对齐,它们在整个Arena的寿命期间存在,并在销毁时(通常在查询完成时)被释放。这些块主要在Shuffle期间用于存储序列化或反序列化的数据,或在HashTables中用于存储序列化的Keys。

初始的块大小为4096字节。如果当前块小于请求的内存大小,将会添加一个新的块到列表中。如果当前块小于128M,新块的大小将会翻倍;如果它大于128M,新块的大小最多只会比所需的大128M。较小的旧块将不会被分配给新的请求。有一个游标标记已分配块和未分配块的分界线。

  1. HashTable

HashTables适用于Hash连接、聚合、集合操作和窗口函数。PartitionedHashTable结构支持最多16个子HashTables,还支持HashTables的并行合并,每个子HashJoin可以独立扩展。这样可以减少整体内存使用,并降低扩展引起的延迟。

如果当前的HashTable小于8M,将以4的因子进行扩展;

如果它大于8M,将以2的因子进行扩展;

如果它小于2G,当它满50%时将会扩展;

如果它大于2G,当它满75%时将会扩展。

新创建的HashTables将根据它将要拥有的数据量进行预扩展。我们还为不同的情况提供不同类型的HashTables。例如,对于聚合,可以应用PHmap。

  1. PODArray

PODArray,就如其名,是一个POD(Plain Old Data,简单旧式数据)类型的动态数组。它与std::vector的区别在于,PODArray不会初始化元素。它支持内存对齐,并拥有一些与std::vector类似的接口。它是通过2的因子来进行扩展的。在销毁时,它不是对每个元素都调用析构函数,而是释放整个PODArray的内存。PODArray主要用于保存列中的字符串,并且适用于许多函数计算和表达式过滤。

内存接口

作为协调Arena、PODArray和HashTable的唯一接口,Allocator执行大于64M的请求的内存映射(MMAP)分配。小于4K的请求将直接通过malloc/free从系统分配;介于两者之间的请求将由通用缓存的ChunkAllocator加速,这根据我们的基准测试结果带来了10%的性能提升。ChunkAllocator将尝试以无锁的方式从当前核心的FreeList获取一块指定大小的内存块;如果不存在这样的块,它将尝试以基于锁的方式从其他核心获取;如果仍然失败,它将从系统请求指定大小的内存并将其封装到一个块中。

我们在经验丰富之后选择了Jemalloc而不是TCMalloc。在我们的高并发测试中,我们尝试使用TCMalloc,并且注意到CentralFreeList中的Spin Lock占用了总查询时间的40%。禁用“aggressive memory decommit”使事情变得更好,但这带来了更多的内存使用,因此我们不得不使用单独的线程定期回收缓存。另一方面,Jemalloc在高度并发的查询中表现更好、更稳定。在针对其他场景进行微调后,它提供了与TCMalloc相同性能但消耗更少的内存。

内存重用

Apache Doris在执行层广泛执行内存重用。例如,在整个查询执行期间,数据块将被重用。在Shuffle期间,发送端将有两个块并且它们交替工作,一个接收数据而另一个进行RPC传输。在读取一个表时,Doris将重用谓词列,实现循环读取、过滤,将过滤后的数据复制到上层的块中然后进行清除。当将数据摄取到一个聚合键表中时,一旦缓存数据的MemTable达到一定的大小,它将被预先聚合然后更多的数据将被写入。

内存重用也在数据扫描中执行。在扫描开始之前,将为扫描任务分配一定数量的空闲块(取决于扫描器数量和线程数 )。在每个扫描器调度期间,其中一个空闲块将被传递到存储层进行数据读取。在数据读取之后,该块将被放入生产者队列供后续计算中的上层操作符使用。一旦一个上层的操作符从该块复制了计算数据,该块将返回到空闲块中供下一个扫描器调度使用。预分配空闲块的线程也将在数据扫描后负责释放它们,因此不会有多余的开销。空闲块的数量在某种程度上决定了数据扫描的并发性。

内存跟踪

Apache Doris使用MemTrackers来跟踪内存的分配和释放情况,同时分析内存热点。MemTrackers记录每个数据查询、数据摄取、数据压缩任务以及每个全局对象的内存大小,例如Cache和TabletMeta。它同时支持手动计数和MemHook自动跟踪。用户可以在Doris后端的一个Web页面上查看实时的内存使用情况。

MemTrackers的结构

在Apache Doris 1.2.0之前的MemTracker系统是一个层次树结构,包括process_mem_tracker、query_pool_mem_tracker、query_mem_tracker、instance_mem_tracker、ExecNode_mem_tracker等。相邻两层的MemTrackers之间是父子关系,因此子级MemTracker中的任何计算错误都会一路积累并导致更大范围的不可信。

在Apache Doris 1.2.0及更高版本中,我们简化了MemTrackers的结构。MemTrackers仅根据其角色分为两种类型:MemTracker Limiter和其他类型的MemTrackers。MemTracker Limiter监控内存使用情况,每个查询/摄取/压缩任务和全局对象中都是唯一的;其他类型的MemTrackers跟踪查询执行中的内存热点,例如Join/Aggregation/Sort/Window函数中的HashTables和序列化中的中间数据,以了解不同操作符如何使用内存或为数据刷新中的内存控制提供参考。

MemTracker Limiter和其他类型的MemTrackers之间的父子关系仅在快照打印中有所体现。你可以将这种关系视为符号链接。它们不会同时被消耗,并且一个的生命周期不会影响另一个的生命周期。这使得开发人员更容易理解和使用它们。

将MemTrackers(包括MemTracker Limiter和其他类型的MemTrackers)放入一组Maps中。这允许用户打印整体MemTracker类型的快照、查询/加载/压缩任务快照,并找出使用内存最多或内存过度使用的查询/加载。

MemTracker的工作原理

为了计算某个执行过程的内存使用情况,将一个MemTracker添加到当前线程的线程本地堆栈中。通过重新加载Jemalloc或TCMalloc中的malloc/free/realloc,MemHook获取分配或释放的内存的实际大小,并将其记录在当前线程的线程本地堆栈中。当执行完成后,将相关的MemTracker从堆栈中移除。堆栈底部的MemTracker记录整个查询/加载执行过程中的内存使用情况。

接下来让我用简化的查询执行过程来解释一下。

Doris后端节点启动后,所有线程的内存使用情况都记录在进程MemTracker中。

当提交查询时,将在片段执行线程的线程本地存储(TLS)堆栈中添加一个Query MemTracker。

一旦调度ScanNode,将在片段执行线程的TLS堆栈中添加一个ScanNode MemTracker。此后,该线程中分配或释放的任何内存都将记录到Query MemTracker和ScanNode MemTracker中。

调度Scanner后,Scanner线程的TLS堆栈中将添加一个Query MemTracker和一个Scanner MemTracker。

扫描完成后,Scanner线程TLS堆栈中的所有MemTrackers将被移除。当ScanNode调度完成后,从片段执行线程TLS堆栈中移除ScanNode MemTracker。同样地,当调度聚合节点时,将在片段执行线程TLS堆栈中添加一个AggregationNode MemTracker,并在调度完成后移除它。

如果查询已完成,从片段执行线程TLS堆栈中移除Query MemTracker。此时,此堆栈应该为空。然后,您可以从查询配置文件中查看整个查询执行过程中以及各个阶段(扫描、聚合等)的峰值内存使用情况。

使用MemTracker的方法

Doris后端网页展示了实时内存使用情况,这些使用情况被划分为四种类型:查询/加载/压缩/全局。展示内容为当前内存消耗情况和峰值消耗情况。

全局类型包括缓存和TabletMeta的MemTrackers。

从查询类型中,您可以观察当前查询及所关联操作(通过标签可以看出关联关系)的当前内存消耗和峰值内存消耗情况。要查看历史查询的内存统计信息,您可以通过DorisFrontend的审计日志或Backend的INFO日志进行检查。

内存限制

在Doris后端广泛实施内存跟踪之后,我们离消除OOM这一导致后端停机和大规模查询失败的原因又近了一步。下一步是优化查询和进程的内存限制,以控制内存使用情况。

查询的内存限制

用户可以为每个查询设置内存限制。如果执行过程中该限制被超过,查询将被取消。但从1.2版本开始,我们已经允许内存超配(Memory Overcommit),这是一种更灵活的内存限制控制。如果有足够的内存资源,查询可以消耗比限制更多的内存而不会被取消,因此用户不必过多关注内存使用情况;如果没有,查询将等待新的内存空间被分配;只有当新释放的内存不足以供查询使用时,查询才会被取消。

而在Apache Doris 2.0中,我们已经实现了查询的异常安全性。这意味着任何内存分配不足将立即导致查询被取消,从而省去了后续步骤中检查“取消”状态的麻烦。

进程的内存限制

Doris后端会定期从系统中检索进程的物理内存和当前可用内存大小。同时,它收集所有查询/加载/压缩任务的MemTracker快照。如果后端进程超过了其内存限制或内存不足,Doris将通过清除缓存和取消一些查询或数据摄取任务来释放一些内存空间。这些将由单独的GC线程定期执行。

如果进程消耗的内存超过了软内存限制(默认情况下为系统内存的81%),或者可用系统内存下降到警告水位线(小于3.2GB),将触发小垃圾回收(Minor GC)。此时,查询执行将在内存分配步骤暂停,数据摄取任务中的缓存数据将被强制清空,部分数据页缓存和过期的段缓存将被释放。如果新释放的内存不足以覆盖进程内存的10%,并且已启用内存超配,Doris将开始取消最大的“超配者”查询,直到达到10%的目标或所有查询都被取消。然后,Doris将缩短系统内存检查间隔和GC间隔。在有更多可用内存后,查询将继续。

如果进程消耗的内存超过了硬内存限制(默认情况下为系统内存的90%),或者可用系统内存下降到低水位线(小于1.6GB),将触发全面垃圾回收(Full GC)。此时,数据摄取任务将停止,所有数据页缓存和大多数其他缓存将被释放。如果经过所有这些步骤后,新释放的内存仍不足以覆盖进程内存的20%,Doris将查看所有MemTrackers并找到最耗费内存的查询和摄取任务,并逐个取消它们。只有当达到20%的目标后,系统内存检查间隔和GC间隔才会延长,查询和摄取任务才会继续。(一次垃圾回收操作通常需要数百到数十毫秒。)

影响与成果
在进行了内存分配、内存跟踪和内存限制的优化之后,我们显著提高了Apache Doris作为实时分析型数据仓库平台的稳定性和高并发性能。现在,后端出现OOM(内存不足)崩溃的情况已经很少见了。即使出现OOM,用户也可以根据日志定位问题的根源并进行修复。此外,由于查询和数据摄取的内存限制更加灵活,所以在内存空间充足的情况下,用户不必花费额外的精力来处理内存问题。

在下一个阶段,我们计划确保在内存超配情况下查询的完成,这意味着更少的查询将因为内存不足而被取消。我们将这个目标细化为具体的工作方向,包括异常安全性、资源组之间的内存隔离和中途数据的刷新机制。如果你想与我们的开发人员进行交流,可以在这些方面寻找我们的身影。

<think>我们正在分析Linux内核日志以理解系统重启原因。用户提供的引用中有一个示例内核模块,该模块在加载时调用`panic()`函数导致内核崩溃。日志中出现的"SayGoodbyeto theworld ..."可能对应示例代码中的`cleanup_module`函数打印的"Goodbye world.",但请注意,示例中`init_module`函数会先打印"Hello world. Nowwe crash.",然后调用`panic`导致系统崩溃。而在实际重启日志中,我们可能会看到类似的信息。不过,用户的问题是关于分析重启日志,特别是针对包含“SayGoodbyeto theworld ...”的日志信息。我们需要考虑的是,系统重启可能是由多种原因引起的,包括内核崩溃(panic)、硬件故障、软件错误等。内核日志(通常通过`dmesg`命令查看)是分析重启原因的重要依据。 步骤: 1.获取完整的重启前后内核日志。通常,系统重启后,我们可以通过`journalctl`(对于使用systemd的系统)或查看`/var/log/messages`、`/var/log/kern.log`等文件来获取日志。 2.在日志中搜索关键信息,如“panic”、“Oops”、“BUG”等,以及用户提到的“Say Goodbye tothe world...”。注意,示例中的“Goodbye world.”是在模块卸载时打印的,而重启通常发生在panic之后,因此可能不会执行到`cleanup_module`。所以出现“Goodbye world.”的可能性不大,除非是正常卸载模块后重启。但用户提到的是重启日志,所以重点应放在导致重启的错误上。 用户提供的引用[^1]中的代码:-`init_module`:模块加载时调用,打印"Hello world. Nowwe crash.",然后调用`panic("Downwe go, paniccalled!")`导致系统崩溃。- `cleanup_module`:模块卸载时调用,打印"Goodbyeworld."。因此,如果系统因为加载这个内核模块而崩溃,我们会在日志中看到: -"Hello world. Nowwe crash."-然后panic信息:"Downwe go, paniccalled!"-最后系统会记录由于panic导致的崩溃,并可能触发重启(如果设置了`kernel.panic`参数,例如`kernel.panic=10`表示10秒后重启)。而“Goodbye world.”通常不会在崩溃重启时出现,因为模块没有被正常卸载。所以,用户提到的“SayGoodbyeto theworld ...”可能是指类似“Goodbye world.”的信息,但在实际日志中,更可能是由于其他原因导致的关机或重启信息。常见重启原因分析:1.内核崩溃(Kernel Panic):通常由内核遇到无法处理的错误引起,日志中会有“Kernel panic-not syncing:...”。2.硬件错误:如CPU、内存、硬盘等故障,日志中可能有相关硬件错误报告(如MCE(Machine CheckException))。 3.软件触发重启:例如用户执行`reboot`命令,或系统服务(如watchdog)触发重启。4.电源故障或过热保护。 针对用户的问题,我们应指导如何分析日志:###分析步骤:1.**定位重启时间点**:查看日志中的时间戳,找到系统重启前后的记录。2.**查找关键错误信息**:在重启前的最后几条日志中,寻找“panic”、“Oops”、“BUG”、“hard reset”、“soft reset”等字样。3.**检查是否有硬件错误**:查找“MCE”、“EDAC”、“PCIe”等硬件相关错误。 4. **检查系统服务日志**:结合系统日志(如`/var/log/syslog`)查看是否有异常服务退出或触发重启。5.**检查用户命令**:如果重启是由用户命令触发,日志中可能会有“reboot: Restartingsystem”或类似信息。如果用户提供的日志中包含“SayGoodbyeto theworld ...”,这可能是某个自定义内核模块在卸载时打印的信息,表明该模块被卸载,但通常不会直接导致重启。因此,我们需要在卸载这条信息之前查找导致重启的真正原因。 ###示例日志分析:假设我们有以下日志片段(模拟):``` ...[正常系统运行日志]... [1234.567890] Helloworld.Now wecrash.[1234.567891] Kernelpanic -not syncing:Down wego,panic called! [1234.567892] CPU:0 PID:0 Comm: swapper/0 Nottainted5.4.0-90-generic#101-Ubuntu...[堆栈跟踪] ...[1234.567900] Rebooting in10seconds.....[系统重启]... [0.000000] Linuxversion ...``` 这表明系统因内核崩溃而重启。###如何获取日志?-使用`dmesg`命令查看当前启动的日志。-使用`journalctl--list-boots`列出所有启动记录,然后用`journalctl -b -1`查看上一次启动的日志(-1表示上一次,-2表示上两次,以此类推)。-对于非systemd系统,查看`/var/log/kern.log`等文件。###用户问题解答: 根据用户的问题,我们重点分析日志中出现的“SayGoodbyeto theworld ...”。如果这条信息出现在重启之前,那么可能是某个模块卸载的信息,但重启原因可能另有其他。我们需要在日志中继续查找更关键的错误信息。 如果用户能够提供具体的日志片段,我们可以更准确地分析。但根据现有信息,我们只能给出一般性指导。 ###相关问题: §§相关问题§§1.如何从Linux内核日志中识别系统崩溃的原因? 2.系统重启后如何查看上一次启动的日志? 3. Linux内核模块的加载和卸载日志一般如何记录?</think>### Linux内核日志重启命令分析 当系统日志中出现类似"Say Goodbye to the world..."的消息时,通常表示内核模块在卸载时执行的清理操作。结合您提供的引用代码[^1],这是典型的内核模块生命周期日志: ```c void cleanup_module(void) { printk(KERN_INFO "Goodbye world.\n"); // 对应日志中的"Goodbye world" } ``` #### 系统重启原因分析步骤: 1. **定位关键日志序列** - 在`/var/log/kern.log`或`dmesg`输出中查找: - 模块加载记录:`Hello world. Now we crash.` - 崩溃触发点:`panic("Down we go, panic called!")` - 模块卸载记录:`Goodbye world.` - 使用命令过滤: ```bash journalctl -k | grep -E "panic|Hello|Goodbye" ``` 2. **分析崩溃上下文** - `panic()`调用表明发生了不可恢复的内核级错误 - 典型日志模式: ``` [timestamp] Hello world. Now we crash. [timestamp] Kernel panic - not syncing: Down we go, panic called! [timestamp] CPU: 0 PID: 1 Comm: swapper/0 Not tainted... [timestamp] Rebooting in 10 seconds.. [timestamp] Goodbye world. # 注意此日志可能不会输出 ``` 3. **常见重启原因关联** | 日志特征 | 可能原因 | 验证方法 | |----------|----------|----------| | `panic()`调用 | 内核模块BUG/内存溢出 | 检查`/proc/vmstat`的`oom_kill`值 | | 硬件错误 | CPU/内存故障 | `dmesg -T | grep MCE` | | 内核oops | 驱动兼容性问题 | 检查`/var/log/kern.log`中的Oops编号 | | 正常卸载日志 | 计划内重启 | 检查`last reboot`命令输出 | 4. **关键排查工具** - 崩溃分析: ```bash crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/dumpfile ``` - 模块依赖检查: ```bash lsmod | grep -i crash modinfo kill-kernel # 替换为实际模块名 ``` #### 特殊注意事项: 1. 当`panic()`被调用时,系统通常**不会执行`cleanup_module()`**,因此"Goodbye world"日志可能表明: - 系统经历了有序重启(如`reboot`命令触发) - 内核成功执行了部分清理流程 - 存在延迟日志写入(查看`/sys/module/printk/parameters/async`) 2. 若日志中同时出现: ``` panic_called + reboot_scheduled + goodbye_log ``` 可能指示硬件看门狗超时重启,建议检查: ```bash sudo apt install watchdog sudo service watchdog status ``` #### 推荐诊断流程: ```mermaid graph TD A[发现重启事件] --> B{检查日志时间戳} B -->|定位崩溃点| C[分析panic调用栈] B -->|找到Goodbye日志| D[验证模块卸载顺序] C --> E[检查内核Oops代码] D --> F[审查模块依赖关系] E --> G[更新/回滚驱动] F --> H[测试模块独立加载] ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值