查看源代码 如何解读 Erlang 崩溃转储
本节介绍 Erlang 运行时系统异常退出时生成的 erl_crash.dump
文件。
注意
Erlang 崩溃转储在 Erlang/OTP R9C 中进行了重大改版。因此,本节中的信息不直接适用于较旧的转储。但是,如果在较旧的转储上使用
crashdump_viewer
,则崩溃转储将转换为类似于此格式的格式。
系统将崩溃转储写入模拟器的当前目录或环境变量(无论在当前操作系统上意味着什么)ERL_CRASH_DUMP
所指向的文件中。要写入崩溃转储,必须挂载可写文件系统。
写入崩溃转储的主要原因有两个:要么是从正在运行的 Erlang 代码中显式调用内置函数 erlang:halt/1
并带有字符串参数,要么是运行时系统检测到无法处理的错误。系统无法处理错误的最常见原因是外部限制,例如内存不足。内部错误导致的崩溃转储可能是由于系统本身在模拟器中达到限制(例如系统中的原子数量或同时存在的 ETS 表过多)。通常,可以重新配置模拟器或操作系统以避免崩溃,这就是正确解读崩溃转储很重要的原因。
在支持 OS 信号的系统上,也可以通过发送 SIGUSR1
信号来停止运行时系统并生成崩溃转储。
Erlang 崩溃转储是一个可读的文本文件,但可能难以阅读。使用 Observer
应用程序中的崩溃转储查看器工具可以简化任务。这是一个基于 wx-widget 的工具,用于浏览 Erlang 崩溃转储。
基本信息
崩溃转储的第一部分显示以下内容
- 转储的创建时间
- 指示转储原因的标语
- 生成转储的节点的系统版本
- 原子表中的原子数量
- 导致崩溃转储的运行时系统线程
崩溃转储的原因(标语)
转储的原因在文件的开头显示为
Slogan: <reason>
如果系统被 BIF erlang:halt/1
停止,则标语是传递给 BIF 的字符串参数,否则是模拟器或(Erlang)内核生成的描述。通常,该消息足以理解问题,但这里描述了一些消息。请注意,建议的崩溃原因是仅供参考。错误的真正原因可能因本地应用程序和底层操作系统而异。
<A>: 无法分配 <N> 字节的内存(类型为 "<T>") - 系统内存不足。 <A> 是无法分配内存的分配器,<N> 是 <A> 尝试分配的字节数,<T> 是需要内存的内存块类型。最常见的情况是进程存储了大量数据。在这种情况下,<T> 通常是
heap
、old_heap
、heap_frag
或binary
。有关分配器的更多信息,请参阅erts_alloc(3)
。<A>: 无法重新分配 <N> 字节的内存(类型为 "<T>") - 与上述相同,只是系统内存不足时重新分配了内存,而不是分配了内存。
意外的操作码 <N> - 编译代码错误、
beam
文件损坏或编译器错误。模块 <Name> 未定义
|
函数 <Name> 未定义|
没有函数 <Name>:<Name>/1|
没有函数 <Name>:start/2 - Kernel/STDLIB 应用程序已损坏或启动脚本已损坏。使用过大的文件描述符
N
调用 Driver_select - 套接字的文件描述符数量超过 1024(仅限 Unix)。在某些 Unix 版本中,文件描述符的限制可以设置为超过 1024,但 Erlang 同时只能使用 1024 个套接字/管道(因为 Unixselect
调用的限制)。打开的常规文件数量不受此限制的影响。接收到 SIGUSR1 - 向 Erlang 机器(仅限 Unix)发送
SIGUSR1
信号会强制生成崩溃转储。此标语反映了 Erlang 机器因接收到该信号而崩溃转储。内核 pid 终止 (<Who>) (<Exit reason>) - 内核主管检测到故障,通常是
application_controller
已关闭(Who
=application_controller
,Why
=shutdown
)。应用程序控制器可能因多种原因而关闭,最常见的是分布式 Erlang 节点的节点名称已被使用。完整的监管树“崩溃”(即顶层主管已退出)会产生大致相同的结果。此消息来自 Erlang 代码,而不是来自虚拟机本身。它总是由于 OTP 或“用户编写”的应用程序中的某些故障引起的。查看应用程序的错误日志可能是采取的第一步。Init 在 do_boot () 中终止 - 原始 Erlang 启动序列已终止,很可能是因为启动脚本有错误或无法读取。这通常是配置错误;系统可能已使用错误的
-boot
参数或来自错误 OTP 版本的启动脚本启动。无法启动内核 pid (<Who>) () - 其中一个内核进程无法启动。这可能是由于错误的参数(如
-config
参数中的错误)或错误的配置文件引起的。检查所有文件是否位于正确的位置,以及配置文件(如果有)是否损坏。通常,也会将解释错误的提示消息写入控制终端和/或错误日志。
可能会发生除这些错误之外的其他错误,因为 erlang:halt/1
BIF 可以生成任何消息。如果该消息不是由 BIF 生成的,并且没有出现在上面的列表中,则可能是因为模拟器中存在错误。但是,可能会出现此处未提及的异常消息,这些消息仍然与应用程序故障相关。还有更多可用的信息,因此仔细阅读崩溃转储可以揭示崩溃原因。进程的大小、ETS 表的数量以及每个进程堆栈上的 Erlang 数据对于查找问题非常有用。
原子数量
崩溃时系统中的原子数量显示为 Atoms: <number>
。几万个原子是完全正常的,但更多可能表明 BIF erlang:list_to_atom/1
用于动态生成许多不同的原子,这绝不是一个好主意。
调度器信息
在 =scheduler 标签下显示有关运行时系统中调度器的当前状态和统计信息。在允许挂起其他线程的操作系统上,此部分中的数据反映了发生崩溃时运行时系统的外观。
进程可能存在以下字段
=scheduler:id - 标题。声明调度器标识符。
调度器睡眠信息标志 - 如果为空,则调度器正在执行某些工作。如果不为空,则调度器处于某种睡眠状态或已挂起。
调度器睡眠信息辅助工作 - 如果不为空,则会安排调度器内部辅助工作。
当前端口 - 调度器当前执行的端口的端口标识符。
当前进程 - 调度器当前执行的进程的进程标识符。如果有这样的进程,则此条目后跟同一进程的状态、内部状态、程序计数器和 CP。这些条目在 进程信息 部分中描述。
请注意,这是在开始生成崩溃转储时条目的快照。因此,它们很可能与在 =proc 部分中找到的同一进程的条目不同(并且更能说明问题)。如果没有当前正在运行的进程,则仅显示当前进程条目。
当前进程有限堆栈跟踪 - 仅当存在当前进程时才显示此条目。它类似于 =proc_stack,只是仅显示函数帧(即,省略堆栈变量)。此外,仅显示堆栈的顶部和底部部分。如果堆栈很小(< 512 个槽位),则显示整个堆栈。否则,将显示条目跳过 ## 个槽位,其中
##
替换为已跳过的槽位数量。运行队列 - 显示有关此调度器上调度的不同优先级的进程和端口数量的统计信息。
*** 崩溃 *** - 此条目通常不显示。它表示由于某种原因,获取有关此调度器的其余信息失败。
内存信息
在标记 =memory 下显示的信息类似于在活动的节点上使用 erlang:memory()
可以获得的信息。
内部表信息
在标记 =hash_table:<table_name> 和 =index_table:<table_name> 下显示内部表。这些主要对运行时系统开发人员感兴趣。
已分配区域
在标记 =allocated_areas 下显示的信息类似于在活动的节点上使用 erlang:system_info(allocated_areas)
可以获得的信息。
分配器
在标记 =allocator:<A> 下显示有关分配器 <A> 的各种信息。这些信息类似于在活动的节点上使用 erlang:system_info({allocator, <A>})
可以获得的信息。有关更多信息,另请参阅 erts_alloc(3)
。
进程信息
Erlang 崩溃转储包含系统中每个活动 Erlang 进程的列表。以下字段可能存在于进程中
=proc:<pid> - 标题。说明进程标识符。
状态 - 进程的状态。它可以是以下之一
已调度 - 进程已安排运行,但当前未运行(“在运行队列中”)。
等待 - 进程正在等待某些东西(在
receive
中)。运行 - 进程当前正在运行。如果调用了 BIF
erlang:halt/1
,则这是调用它的进程。退出 - 进程正在退出。
垃圾回收中 - 这是运气不好,当写入崩溃转储时,进程正在进行垃圾回收。此进程的其余信息有限。
已暂停 - 进程已暂停,可能是通过 BIF
erlang:suspend_process/1
,或者因为它尝试写入繁忙的端口。
已注册名称 - 进程的已注册名称(如果有)。
作为...生成 - 进程的入口点,即在启动进程的
spawn
或spawn_link
调用中引用的函数。最后一次调度时间 | 当前调用 - 进程的当前函数。这些字段并非总是存在。
由...生成 - 进程的父进程,即执行了
spawn
或spawn_link
的进程。已启动 - 进程启动的日期和时间。
消息队列长度 - 进程消息队列中的消息数量。
堆片段数量 - 已分配的堆片段数量。
堆片段数据 - 碎片化堆数据的大小(以字为单位)。这是由发送到进程的消息或 Erlang BIF 创建的数据。此数量取决于太多因素,因此此字段通常不令人感兴趣。
链接列表 - 链接到此进程的进程 ID。也可以包含端口。如果使用进程监控,则此字段还会告知监控生效的方向。也就是说,链接“到”进程表示“当前”进程正在监控另一个进程,而链接“来自”进程表示另一个进程正在监控当前进程。
归约次数 - 进程消耗的归约次数。
栈+堆 - 栈和堆的大小(以字为单位)(它们共享内存段)。
旧堆 - “旧堆”的大小(以字为单位)。Erlang 虚拟机使用带有两个代际的代际垃圾回收。有一个堆用于新数据项,一个堆用于在两次垃圾回收中幸存的数据。假设(几乎总是正确的)是,经过两次垃圾回收后幸存的数据可以“转入”更少进行垃圾回收的堆中,因为它们将存活很长时间。这是虚拟机中常用的技术。堆和栈的总和构成了进程分配的大部分内存。
堆未使用,旧堆未使用 - 每个堆上未使用的内存量(以字为单位)。此信息通常无用。
内存 - 此进程使用的总内存(以字节为单位)。这包括调用堆栈、堆和内部结构。与
erlang:process_info(Pid,memory)
相同。程序计数器 - 当前指令指针。这仅对运行时系统开发人员感兴趣。程序计数器指向的函数是进程的当前函数。
CP - 继续指针,即当前调用的返回地址。通常对运行时系统开发人员以外的人员无用。可以跟随 CP 指向的函数,这是调用当前函数的函数。
Arity - 活动参数寄存器的数量。如果有任何活动参数寄存器,则将跟随其后。如果函数的参数尚未移动到堆栈,则它们可以包含函数的参数。
内部状态 - 此进程状态的更详细的内部表示。
另请参见 进程数据 部分。
端口信息
此部分列出了打开的端口、其所有者、任何链接的进程以及其驱动程序或外部进程的名称。
ETS 表
此部分包含有关系统中所有 ETS 表的信息。以下字段对于每个表都很有意义
=ets:<owner> - 标题。说明表所有者(进程标识符)。
表 - 表的标识符。如果表是
named_table
,则此为名称。名称 - 表的名称,无论它是否为
named_table
。哈希表,存储桶 - 如果表是哈希表,即如果它不是
ordered_set
。哈希表,链长度 - 如果表是哈希表。包含有关该表的统计信息,例如最大、最小和平均链长度。最大值远大于平均值,并且标准偏差远大于预期标准偏差,这表明术语的哈希处理由于某种原因表现不佳。
有序集(AVL 树),元素 - 如果表是
ordered_set
。(元素的数量与表中的对象数量相同。)已固定 - 如果使用
ets:safe_fixtable/2
或某些内部机制固定了该表。对象 - 表中的对象数量。
字 - 为表中的数据分配的字数。
类型 - 表类型,即
set
、bag
、duplicate_bag
或ordered_set
。已压缩 - 如果该表已压缩。
保护 - 表的保护。
写入并发 - 如果为表启用了
write_concurrency
。读取并发 - 如果为表启用了
read_concurrency
。
计时器
此部分包含有关使用 BIF erlang:start_timer/3
和 erlang:send_after/3
启动的所有计时器的信息。以下字段存在于每个计时器
=timer:<owner> - 标题。说明计时器所有者(进程标识符),即计时器到期时接收消息的进程。
消息 - 要发送的消息。
剩余时间 - 剩余的毫秒数,直到发送消息。
分布信息
如果 Erlang 节点是活动的,即设置为与其他节点通信,则此部分列出活动的连接。以下字段可能存在
=node:<node_name> - 节点名称。
no_distribution - 如果节点未分布式。
=visible_node:<channel> - 可见节点的标题,即可与崩溃的节点建立连接的活动节点。说明节点的通道号。
=hidden_node:<channel> - 隐藏节点的标题。隐藏节点与可见节点相同,只是它使用
"-hidden"
标志启动。说明节点的通道号。=not_connected:<channel> - 以前连接到崩溃节点的节点的标题。在崩溃时存在对未连接节点的引用(即进程或端口标识符)。说明节点的通道号。
名称 - 远程节点的名称。
控制器 - 控制与远程节点通信的端口。
创建 - 一个整数 (1-3),与节点名称一起标识节点的特定实例。
远程监控:<local_proc> <remote_proc> - 在崩溃时,本地进程正在监控远程进程。
被远程监控:<local_proc> <remote_proc> - 在崩溃时,远程进程正在监控本地进程。
远程链接:<local_proc> <remote_proc> - 在崩溃时,本地进程和远程进程之间存在链接。
已加载模块信息
此部分包含有关所有已加载模块的信息。
首先,总结已加载代码的内存使用情况
当前代码 - 模块的最新版本代码。
旧代码 - 系统中存在较新版本,但旧版本尚未清除的代码。
然后,列出所有已加载的模块。存在以下字段
=mod:<module_name> - 标题。声明模块名称。
当前大小 - 已加载代码的内存使用量,以字节为单位。
旧大小 - 旧代码的内存使用量,以字节为单位。
当前属性 - 当前代码的模块属性。当通过 Crashdump Viewer 工具查看时,将解码此字段。
旧属性 - 旧代码的模块属性(如果有)。当通过 Crashdump Viewer 工具查看时,将解码此字段。
当前编译信息 - 当前代码的编译信息(选项)。当通过 Crashdump Viewer 工具查看时,将解码此字段。
旧编译信息 - 旧代码的编译信息(选项)(如果有)。当通过 Crashdump Viewer 工具查看时,将解码此字段。
Fun 信息
此部分列出所有 fun。 每个 fun 存在以下字段
=fun - 标题。
模块 - 定义 fun 的模块名称。
Uniq, Index - 标识符。
地址 - fun 代码的地址。
Refc - 对 fun 的引用次数。
进程数据
对于每个进程,至少有一个 =proc_stack 和一个 =proc_heap 标签,后跟该进程的堆栈和堆的原始内存信息。
对于每个进程,如果进程消息队列非空,则还有一个 =proc_messages 标签;如果进程字典(put/2
和 get/1
功能)非空,则还有一个 =proc_dictionary 标签。
原始内存信息可以通过 Crashdump Viewer 工具进行解码。然后,你可以看到堆栈转储、消息队列(如果有)和字典(如果有)。
堆栈转储是 Erlang 进程堆栈的转储。大多数活动数据(即当前正在使用的变量)都放置在堆栈上;因此,这可能很有趣。必须“猜测”是什么,但由于信息是符号化的,因此彻底阅读此信息可能很有用。例如,我们可以找到以下示例中 Erlang 原语加载器的在线状态变量 (5)
和 (6)
(1) 3cac44 Return addr 0x13BF58 (<terminate process normally>)
(2) y(0) ["/view/siri_r10_dev/clearcase/otp/erts/lib/kernel/ebin",
(3) "/view/siri_r10_dev/clearcase/otp/erts/lib/stdlib/ebin"]
(4) y(1) <0.1.0>
(5) y(2) {state,[],none,#Fun<erl_prim_loader.6.7085890>,undefined,#Fun<erl_prim_loader.7.9000327>,
(6) #Fun<erl_prim_loader.8.116480692>,#Port<0.2>,infinity,#Fun<erl_prim_loader.9.10708760>}
(7) y(3) infinity
在解释进程的数据时,了解匿名函数对象(fun)具有以下特点会有所帮助
- 一个由创建它们的函数名称构造的名称
- 一个数字(从 0 开始),表示该函数中 fun 的数量
原子
此部分显示系统中的所有原子。仅当怀疑动态生成原子可能是问题时,此部分才值得关注,否则可以忽略此部分。
请注意,最后创建的原子首先显示。
免责声明
崩溃转储的格式在 OTP 版本之间不断演变。此处描述的某些信息可能不适用于你的版本。这样的描述永远不会是完整的;它旨在作为对崩溃转储的总体解释,并在尝试查找应用程序错误时提供帮助,而不是作为完整的规范。