下载本文的PDF版本 PDF

动态环境中的事后调试

现代动态语言缺乏用于理解软件故障的工具。


David Pacheco


尽管软件工程师尽最大努力生产高质量的软件,但不可避免地,即使是最严格的测试过程也无法避免一些错误,最终由最终用户首次遇到。当这种情况发生时,必须快速理解这些故障,修复潜在的错误,并修补部署以避免另一个用户(或同一个用户)再次遇到相同的问题。早在1951年,现代计算的黎明时期,Stanley Gill6 就写道:“因此,人们已经关注于处理程序尝试后发现失败的错误问题。” Gill 接着描述了软件中“事后技术”的首次使用,即修改正在运行的程序以记录重要的系统状态,以便程序员稍后可以理解发生了什么以及软件为何失败。

自那时以来,事后调试技术得到了发展,并在许多不同的系统中使用,包括所有主要的消费者和企业操作系统,以及这些系统上的本机执行环境。这些环境构成了当今大部分的核心基础设施,从构成每个应用程序基础的操作系统到诸如 DNS(域名系统)之类的核心服务,因此构成了几乎所有更大型系统的构建块。为了实现此类软件所期望的高可靠性水平,这些系统被设计为在每次故障后快速恢复服务,同时保留足够的信息,以便稍后可以完全理解故障本身。

虽然此类软件在历史上是用 C 和其他本机环境编写的,但核心基础设施越来越多地使用动态语言开发,从过去二十年的 Java 到过去 18 个月的服务器端 JavaScript。动态语言之所以具有吸引力,原因有很多,其中最重要的是它们通常可以加速复杂软件的开发。

然而,在许多这些环境中,明显缺乏即使是基本的事后调试的工具,这使得理解生产故障极其困难。动态语言必须弥合这一差距,并提供丰富的工具来理解已部署系统中的故障,以便与它们在软件系统基石中日益增长的角色所要求的可靠性相匹配。

为了理解复杂的事后分析工具的真正潜力,我们将首先回顾当今的调试状态以及事后分析工具在其他环境中的作用。然后,我们将研究为动态环境构建此类工具的独特挑战以及当今此类工具的状态。


大规模调试

为了理解事后调试的独特价值,值得研究一下替代方案。当今的本机和动态环境都提供了原位调试工具,或者在有缺陷的程序仍在运行时对其进行调试。这通常涉及将单独的调试器程序附加到有缺陷的程序,然后以交互方式指导有缺陷的程序的执行,逐条指令或使用断点。用户因此在各个点停止程序以检查程序状态,以便找出程序出错的位置。通常,此过程会重复进行,以测试关于问题的连续理论。

这种技术对于容易复现的错误非常有效,但它有几个缺点。首先,停止程序的行为通常会改变其行为。由并行操作之间意外交互引起的错误(例如竞争条件)可能特别难以通过这种方式分析,因为各种事件的定时会受到调试器本身的显着影响。更重要的是,原位调试在生产系统上通常是站不住脚的:许多调试器依赖于未优化的调试版本,该版本运行速度太慢而无法在生产环境中使用;工程师通常无法访问程序正在运行的系统(就像大多数移动和桌面应用程序以及许多企业系统的情况一样);并且所需的调试工具通常在这些系统上也不可用。

即使在工程师可以使用他们需要的工具访问有缺陷的软件的情况下,在调试器中暂停程序通常也代表着对生产服务不可接受的破坏,以及一个不可接受的风险,即粗心的调试器命令可能会导致程序崩溃。管理员通常不能为了理解导致先前中断的故障而承担现在的停机风险。更重要的是,他们不应该这样做。即使在 1951 年,Gill 也引用了“所涉及的机器时间的过分浪费”,并得出结论:“单步操作是维护工程师的有用工具,但程序员只能将其视为最后的手段。”

原位调试最严重的缺陷是它只能用于理解可复现的问题。许多生产问题要么非常罕见,要么涉及许多系统的复杂交互,这些系统通常很难在开发环境中复制。此类问题的罕见性并不意味着它们不重要:恰恰相反,每周只发生一次的操作系统崩溃在停机时间方面可能会非常昂贵,但是任何每周只能发生一次的错误都很难进行实时调试。同样,在数千人使用的应用程序中,每周发生一次的致命错误可能会导致许多用户每天遇到该错误,但工程师无法在每个用户的系统上附加调试器。

所谓的 printf 调试是处理可复现性问题的常用技术。在这种方法中,工程师修改软件以在代码的关键点记录相关的程序状态位。这会导致在没有人为干预的情况下收集数据,以便在问题发生后可以检查数据以了解发生了什么。通过自动化数据收集,这种技术通常会对生产服务产生显着较小的影响,因为当程序崩溃时,系统可以立即重新启动它,而无需等待工程师登录并以交互方式调试问题。

然而,从日志文件中提取关于致命故障的足够信息通常非常困难,并且经常需要经历多次迭代,包括插入额外的日志记录、部署修改后的程序以及检查输出。对于生产系统来说,这也是站不住脚的,因为临时的代码更改通常是不切实际的(在桌面和移动应用程序的情况下)或被变更控制策略(和常识)所禁止。

解决方案是构建一个在程序崩溃时捕获所有程序状态的工具。1980 年,格拉斯哥斯特拉斯克莱德大学的 Douglas R. McGregor 和 Jon R. Malone9 观察到,使用这种方法,“在空间或速度方面几乎没有运行时开销”,并且“不需要额外的跟踪例程”,但该工具“在程序已投入生产使用时仍然有效。” 最重要的是,在系统保存所有程序状态后,它可以立即重新启动程序以快速恢复服务。有了这样的系统,即使是罕见的错误也通常可以在第一次发生时(无论是在开发、测试还是生产中)被找出根本原因并修复。这使软件供应商能够在太多用户遇到错误之前修复错误。

总而言之,为了找出从开发到生产中发生的故障的根本原因,事后调试工具必须满足以下几个约束条件

* 应用程序软件不得要求进行无法在生产中使用的修改以支持事后调试,例如未优化的代码或会显着影响性能(或完全影响正确性)的额外调试数据。

* 该工具必须始终处于开启状态:它不得要求管理员在问题发生之前附加调试器或以其他方式启用事后支持。

* 该工具必须是完全自动的:它应检测崩溃、保存程序状态,然后立即允许系统重新启动失败的组件,以尽快恢复服务。

* 转储(保存的状态)必须是全面的:堆栈跟踪虽然可能是最有价值的信息,但通常不足以从单次发生中找出问题的根本原因。通常,工程师既需要全局状态,也需要每个线程的状态(包括堆栈跟踪和每个堆栈帧的参数和变量)。当然,在这个维度上可能存在各种各样的结果;“约束”(如果可以这样称呼)是该工具必须提供足够的信息才能用于重要的难题。转储中包含的信息越多,工程师就越有可能仅根据一次发生来识别根本原因。

* 转储必须可传输到其他系统进行分析。这允许工程师在熟悉的环境中使用他们需要的任何工具来分析数据,并且在许多情况下无需工程师访问生产系统。


本机环境中的事后调试

为了理解动态语言中事后调试的潜在价值,了解事后分析技术得到良好发展和广泛使用的领域也很有帮助。最好的例子是操作系统及其本机执行环境。从历史上看,这种软件构成了核心基础设施;此堆栈级别的故障通常非常昂贵,要么是因为系统本身是业务关键功能所必需的(就像运行业务关键软件的操作系统的情况一样),要么是因为它们被上游的业务关键系统所依赖(就像 DNS 等基础设施服务的情况一样)。

大多数现代操作系统都可以配置为,当它们崩溃时,它们会立即保存所有状态的“崩溃转储”,然后重新启动。同样,这些系统也可以配置为,当用户应用程序崩溃时,操作系统会将所有程序状态的“核心转储”保存到文件中,然后重新启动应用程序。在大多数情况下,这些机制允许操作系统或用户应用程序快速恢复服务,同时保留足够的信息以便稍后找出故障的根本原因。

作为一个例子,让我们看看 Illumos(一个基于 Solaris 的开源系统)下的核心转储。   考虑以下有缺陷的程序


 
   1 int
   2 main(int argc, char *argv[])
   3 {
   4       func();
   5       return (0);
   6 }    
   7      
   8 int
   9 func(void)
  10 {
  11       int ii;
  12       int *ptrs[100];
  13      
  14       for (ii = -1; ii < 100; ii++)
  15                 *(ptrs[ii]) = 0;
  16      
  17       return (0);
  18 }

这个简单的程序有一个致命的缺陷:在尝试清除第 14-15 行中 ptrs 数组中的每个项目时,它在数组之前(ii = -1 的位置)清除了一个额外的元素。运行此程序时,您会看到



  $ gcc -o example1 example1.c


  $ ./example1


  段错误(已转储核心)


系统生成一个名为 core 的文件。Illumos MDB(模块化调试器)可以帮助检查此文件



  $ mdb core


  正在加载模块:[ ld.so.1 ]


  > ::status


  正在调试来自 solaron 的 example1 (32 位) 的核心文件


  文件:/export/home/dap/tmp/example1


  初始 argv:./example1


  线程模型:本机线程


  状态:进程因 SIGSEGV(段错误)终止,地址=10


 


  > ::walk thread | ::findstack -v


  线程 1 的堆栈指针:8047b98


  [ 08047b98 func+0x20() ]


    08047bbc main+0x21(1, 8047bdc, 8047be4)


    08047bd0 _start+0x80(1, 8047cc4, 0, 8047ccf, 8047cdc, 8047ced)


 


  > func+0x20::dis


  ...


  func+0x20:                     movl $0x0,(%eax)


  ...


对于新用户来说,MDB 的语法可能显得晦涩难懂,但这个例子相当基础。首先,::status 命令生成了发生情况的摘要:进程因试图访问内存地址 0x10 的段错误而终止。接下来,::walk thread | ::findstack -v 命令用于检查线程堆栈(在本例中,只有一个),它显示程序在程序文本中偏移量为 0x20 的 func 函数中死亡。然后,文件转储出此指令,以查看进程在将 0 存储到寄存器 %eax 中包含的地址时死亡。

虽然这个例子确实是人为设计的,但它说明了事后调试的基本方法。请注意,与原位调试不同,这种方法可以很好地随着被调试程序的复杂性而扩展。如果不是一个进程中的一个线程,而是跨越数十个组件的数千个线程(就像操作系统的情况一样),全面的转储将包括关于所有线程的信息。下一个挑战将是如何理解如此多的信息,但至少找出错误的根本原因是可以解决的,因为所有信息都是可用的。

在这种情况下,下一步是构建自定义工具,用于提取、分析和总结特定的组件状态。全面的事后工具使工程师能够构建此类工具。例如,gdb 支持用户定义的宏。这些宏可以与源代码一起分发,以便所有开发人员都可以在原位(通过将 gdb 附加到正在运行的进程)和事后(通过使用 gdb 打开核心文件)中使用它们。例如,Python 解释器提供了此类宏,允许解释器和本机模块开发人员解析 Python 级别对象的 C 表示形式。

MDB 将这个想法提升到了一个新的水平:它的设计专门围绕构建自定义工具,用于原位和事后理解系统的特定组件。在 Illumos 系统上,内核附带了 MDB 模块,这些模块提供了 1000 多个命令来迭代和检查内核的各种组件。最常用的命令之一是 ::stacks 命令,该命令迭代所有内核线程,可以选择根据堆栈跟踪中是否存在特定的内核模块或函数来过滤它们,然后转储出按频率排序的唯一线程堆栈列表。

这是一个来自正在进行一些轻量级 I/O 的系统的示例



> ::stacks -m zfs


线程         状态   SOBJ                     计数


ffffff0007c0fc60 SLEEP   CV                     2


                swtch+0x147


            cv_wait+0x61


                txg_thread_wait+0x5f


                txg_quiesce_thread+0x94


                thread_start+8


 


ffffff0007f51c60 FREE   <NONE>                 1


                cpu_decay+0x2f


                bitset_atomic_del+0x38


                apic_setspl+0x5c


                do_splx+0x50


                disp_lock_exit+0x55


                cv_signal+0x96


                taskq_dispatch+0x351


                zio_taskq_dispatch+0x6b


                zio_interrupt+0x1a


            vdev_disk_io_intr+0x6b


                biodone+0x84


                dadk_iodone+0xe7


                dadk_pktcb+0xc6


                ata_disk_complete+0x119


                ata_hba_complete+0x38


                ghd_doneq_process+0xb3


             0x16


                dispatch_softint+0x3f


 


ffffff0007b25c60 SLEEP   CV                     1


                swtch+0x147


                cv_timedwait+0xba


                arc_reclaim_thread+0x17b


                thread_start+8


 


ffffff0007b2bc60 SLEEP   CV                     1


                swtch+0x147


                cv_timedwait+0xba


                l2arc_feed_thread+0xa5


                thread_start+8


 


ffffff0009b95c60 SLEEP   CV                     1


                swtch+0x147


                cv_timedwait+0xba


                txg_thread_wait+0x7b


                txg_sync_thread+0x114


                thread_start+8


 


ffffff01e26d08e0 SLEEP   CV                     1


                swtch+0x147


                cv_wait+0x61


                txg_wait_synced+0x7f


                spa_sync_allpools+0x76


                zfs_sync+0xce


                vfs_sync+0x9c


                syssync+0xb


                sys_syscall32+0x101


 


ffffff0007c15c60 SLEEP   CV                   1


                swtch+0x147


                cv_wait+0x61      


                zio_wait+0x5d


                dsl_pool_sync+0xe1


                spa_sync+0x32a


                txg_sync_thread+0x265


                thread_start+8


此调用将此系统上 600 多个线程的复杂性简化为仅约七个与 ZFS 文件系统相关的唯一线程堆栈。您可以快速查看每个组中线程的状态(例如,休眠在条件变量上),并检查代表性线程以获取更多信息。数十个其他操作系统组件提供了他们自己的 MDB 命令,用于检查特定的组件状态,包括网络堆栈、NFS 服务器、DTrace 和 ZFS。

其中一些更高级别的分析工具非常复杂。例如,::typegraph 命令3 分析整个生产崩溃转储(没有调试数据),并构建对象引用及其类型的图。使用此图,用户可以查询任意内存对象的类型。这对于理解内存损坏问题非常有用,其中主要问题是识别哪个组件覆盖了特定的内存块。了解损坏对象的类型可以将调查范围从整个内核缩小到负责该类型的组件。

此类工具绝不仅限于生产环境。在大多数系统上,也可以从正在运行的进程生成核心转储,这使得核心转储分析在开发期间也很有吸引力。当测试人员或其他工程师提交关于应用程序崩溃的错误时,让他们包含核心转储通常比尝试猜测他们采取了哪些步骤导致崩溃,然后从这些步骤重现问题更容易。检查核心转储也是确保您发现的问题与错误报告者遇到的问题相同的唯一方法。

也可以显式地为开发构建更高级别的转储分析工具。Libumem 是 malloc(3c) 及其友元的即插即用替代品,它(在其他功能中)提供了一个 MDB 模块,用于迭代和检查与分配器相关的对象。结合记录每个分配器操作的堆栈跟踪的可选功能,::findleaks MDB 命令可用于非常快速地识别各种类型的内存泄漏,而无需在应用程序本身中添加任何显式支持。::findleaks 命令实际上输出了泄漏对象列表以及每个对象分配位置的堆栈跟踪——直接指向每个泄漏的位置。Libumem 基于内核内存分配器,后者为内核提供了许多相同的功能。2


动态环境中的事后调试

虽然操作系统和本机环境具有高度发达的工具来处理崩溃、保存转储并在事后分析它们,但事后分析问题(以及更普遍的软件可观察性)在 Java、Python 和 JavaScript 等动态环境领域中远未解决。在过去,事后分析对于这些语言来说可能不太重要,因为这些环境中的崩溃不太重要:大多数最终用户应用程序无论如何都会频繁保存工作,并且操作系统或浏览器通常会在崩溃后重新启动应用程序。然而,这些崩溃仍然代表着对用户体验的破坏,而事后调试是理解此类故障的唯一希望。

更重要的是,Node.js 等动态语言作为更大型分布式系统的构建块正在爆炸式增长,在这些系统中,看似微不足道的崩溃可能会导致堆栈上的级联故障。因此,就像操作系统和核心服务一样,完全理解每次故障对于实现此类基础软件所期望的可靠性水平至关重要。

然而,为动态环境提供事后工具并非易事。虽然本机程序可以利用操作系统对核心转储的支持,但动态语言必须使用其开发人员熟悉的相同更高级别的抽象来呈现事后状态。C 程序的事后环境可以简单地呈现全局符号列表、指向线程堆栈的指针以及进程的所有虚拟内存(所有这些都是操作系统必须维护的),但 Java 的类似工具必须使用类似的 Java 抽象来扩充(或替换)这些。当 Java 程序崩溃时,Java 开发人员希望查看 Java 线程堆栈、局部变量和对象,而不是(不一定)JVM(Java 虚拟机)实现使用的线程、变量和原始内存。此外,由于动态语言中的程序在解释器或 VM 内部运行,因此当用户程序“崩溃”时,解释器或 VM 本身不会崩溃。例如,当 Python 程序使用未定义的变量(相当于 C 中的 NULL 指针)时,解释器会检测到这种情况并正常退出。因此,为了支持事后调试,解释器需要显式触发核心转储工具,而不是依赖操作系统来检测崩溃。

在某些情况下,呈现有用的事后状态需要形式化在语言中根本不存在的抽象。JavaScript 在这方面提出了一个特别有趣的挑战。除了通常的全局状态和堆栈详细信息之外,JavaScript 还维护一个待处理事件队列,以及稍后可能发生的一系列事件——这两者都仅作为具有关联上下文的函数存在,这些函数将在稍后的某个时间由运行时调用。例如,Web 浏览器可能有很多未完成的异步 HTTP 请求。对于每个请求,都有一个具有关联上下文的函数,该函数可能无法从全局范围访问,因此不会包含在所有全局状态和线程状态的简单转储中。然而,理解哪些请求是未完成的以及与它们关联的状态对于理解致命故障可能非常关键。

对于服务器上的 Node.js 来说,这个问题更加严重,Node.js 经常用于管理与许多不同类型组件的数千个并发连接。单个 Node 程序可能具有数百个未完成的 HTTP 请求,每个请求都等待数据库查询完成。程序可能会在处理其中一个数据库查询结果时崩溃,因为它遇到了由其他未完成查询之一导致的无效数据库状态。此类问题需要事后调试,因为每个实例都相对较少见;它们基本上不可能仅从堆栈跟踪中理解,但如果从崩溃时获得足够的信息,它们通常可以从第一次发生时识别出来。挑战在于以有意义的方式向 JavaScript 开发人员呈现有关未完成的异步事件(即,将在未来某个时间调用的回调)的信息,这些开发人员通常无法直接访问事件队列或未完成事件的集合;这些抽象在底层 API 中是隐式的,因此公开这些需要首先弄清楚如何表达这些抽象。

最后,面向用户的应用程序还存在将事后状态从用户的计算机传输给可以找出错误根本原因的开发人员的额外问题(同时保护用户隐私)。正如 Eric Schrock11 详细介绍的那样,对于当今最重要的动态环境之一:JavaScript Web 应用程序,这个问题在很大程度上仍未解决。没有基于浏览器的工具可以自动将事后程序状态上传回服务器。

尽管存在这些困难,但一些动态环境确实提供了事后工具。例如,Oracle Java HotSpot VM 支持从 JVM 本机核心转储中提取 Java 级别状态。当 JVM 崩溃时,或者当使用操作系统工具(如 gcore(1))手动创建核心文件时,您可以使用 jdb(1) 工具来检查生成核心文件时 Java 程序的状态(而不是 JVM 本身)。核心文件也可以由名为 jmap(1) 的工具处理,以创建 Java 堆转储,然后可以使用多个不同的程序对其进行分析。

然而,这些工具仅仅是一个开始:首先设置应用程序以在崩溃时触发核心转储并非易事。此外,这些工具非常特定于 HotSpot VM。有一个活跃的 Java 社区规范提案,旨在创建一个通用的 API 来访问调试信息,但在撰写本文时,该项目因 Oracle 对该项目的承诺尚不明确而停滞不前。8

虽然 Java 工具存在一些重要的局限性,但许多其他动态环境似乎根本没有事后工具——至少没有任何工具能够满足刚刚描述的约束条件。

Python10 和 Ruby4 各自都有一个名为事后调试器的工具,但这些工具指的是在调试器下启动程序,并在程序崩溃时让程序中断到交互式调试器会话中。由于几个原因,这不适用于生产环境,其中最重要的是它不是完全自动的。如前所述,在工程师登录以交互方式诊断问题时中断生产服务是站不住脚的。

Erlang5 为 Erlang 运行时本身提供了一个丰富的崩溃转储工具。它的工作方式很像本机崩溃转储,即在发生故障时,它会将全面的状态转储保存到文件中,然后退出,从而允许操作系统看到程序已退出并立即重新启动它。然后可以稍后分析崩溃转储文件。

bash shell1 很有趣,因为它的部署模型与其他动态环境甚至其他环境都非常不同。Bash 提供了一种名为 xtrace 的机制,用于生成一个全面的跟踪文件,描述 shell 在执行过程中评估的几乎每个表达式。这对于理解 shell 脚本故障非常有用,但即使对于简单的脚本也可能产生大量输出。输出随着程序的运行而无限增长,这通常会使其无法在服务器或应用程序的生产环境中使用,但由于大多数 bash 脚本的生命周期非常有限,因此只要可以合理地存储和管理输出(即,在成功运行后自动删除),这种机制就是一种有效的事后工具。

JavaScript 与上述许多语言不同,它被广泛部署在几种完全不同的运行时环境中,例如 Mozilla 的 Spidermonkey、Google 的 V8(Chrome 和 Node.js 均使用)以及 WebKit JavaScript 引擎。尽管近年来 JavaScript in situ 调试工具在运行时程序检查的浏览器支持方面得到了显著改进,但 JavaScript 仍然没有被广泛使用的事后调试工具。


Node.js 的原始事后调试工具

尽管缺乏 JavaScript 语言支持,我们还是开发了一种粗糙但有效的事后调试工具,用于 Joyent 的 Node.js 生产部署中。回想一下,Node 通常运行在服务器而不是 Web 浏览器上,并且通常用于实现可扩展到数百或数千个网络连接的服务。我们使用 Node 和底层 V8 虚拟机提供的以下原语来构建一个简单的实现

* 一个 uncaughtException 事件,它允许程序注册一个函数,以便在该程序抛出一个异常并一直冒泡到顶层时(通常会导致程序崩溃)调用该函数。

* 用于将简单的 JavaScript 对象序列化/反序列化为文本字符串的内置机制(JSON.stringify() 和 JSON.parse())。

* 用于写入文件的同步函数。

首要挑战实际上是确定要转储哪个状态。JavaScript 提供了一种内省全局状态的方法,但是声明变量的 Node.js 程序并不使用全局状态本身。看起来像顶层作用域的东西实际上包含在函数作用域内,而函数作用域无法被内省。为了解决这个问题,使用我们事后调试工具的程序必须提前显式注册调试状态。虽然这个解决方案非常令人不满,因为总是很难提前知道在调试时哪些信息会有用,但它在实践中证明是有效的,因为我们的每个程序本质上只是实例化一个代表程序本身的单例对象,然后将其注册到事后调试工具。大多数相关的程序状态都以某种方式被这个伪全局对象引用。

下一个挑战是序列化循环对象。JSON.stringify() 由于显而易见的原因不支持这一点,因此我们的实现通过在序列化调试对象之前修剪所有循环引用来避免这个问题。虽然这使得在转储中查找信息更加困难,但我们知道每个对象的至少一个副本将存在于转储中的某个位置。

考虑到所有这些,实现非常简单:在 uncaughtException 事件时,我们从调试状态中修剪循环引用,使用内置的 JSON.stringify() 例程对其进行序列化,并将结果保存到名为 core 的文件中。为了分析 core 文件,我们使用一个工具,该工具使用 JSON.parse() 读取 core,并呈现序列化的状态供工程师检查。该实现是开源的,可在 GitHub 上获得。7

除了刚才描述的实现挑战之外,这种方法还有几个明显的局限性。首先,它只能保存程序员可以提前注册的状态,但正如已经讨论过的,JavaScript 程序内部还有大量其他重要的状态,例如调用堆栈中的函数参数以及与待处理和未来事件相关的上下文,这些都无法从全局作用域访问。

其次,由于这个系统的全部意义是在崩溃事件中捕获程序状态,因此它必须高度可靠。此实现对大多数运行时故障都具有鲁棒性,但它仍然需要额外的内存,首先是执行转储代码和序列化程序状态。额外的内存很容易与整个堆一样大,这使得它对于由内存压力引起的故障(动态环境中常见的故障原因)来说是站不住脚的。

第三,由于该实现会在序列化程序数据之前删除循环引用,因此生成的转储更难浏览,并且该工具无法支持不用于事后分析的转储(例如实时转储)。

尽管存在这些缺陷,但该实现已被证明非常有效,因为它满足了前面提出的要求:它在生产环境中始终开启、完全自动化、结果可传输到其他系统进行分析,并且足够全面以解决复杂的问题。然而,为了解决此处描述的许多范围、鲁棒性和丰富性问题,并为语言的所有用户提供这样的工具,事后调试工具必须集成到 VM 本身中。这样的实现原则上会类似地工作,但它可以包含绝对所有的程序状态,使其在程序本身发生故障时可靠地工作,流式传输输出以避免使用过多额外的内存,并使用一种保留底层内存结构的格式,以便于理解转储。最重要的是,开箱即用地包含事后分析工具将大大有助于在这些环境中采用事后调试技术。


结论

长期以来,事后调试工具使操作系统工程师和本机应用程序开发人员能够从已部署系统中首次发生的复杂软件故障中理解问题。此类工具构成了企业系统支持流程的支柱,并且对于复杂软件环境核心的软件组件至关重要。即使是用于记录事后状态的简单平台也使工程师能够开发复杂的分析工具,帮助他们快速找出许多类型问题的根本原因。

与此同时,现代动态语言越来越受欢迎,因为它们非常有效地促进了快速开发。诸如 Node.js 之类的环境也促进了可良好扩展的编程模型,尤其是在面对延迟波动时。这在当今的实时系统中变得越来越重要。

动态环境的事后调试仍处于起步阶段。大多数此类环境,即使是那些被认为是成熟的环境,也没有提供任何记录事后状态的工具,更不用说对这些故障进行更高级别分析的工具了。那些确实存在的工具在其各自的环境中也不是一流的工具,因此没有被广泛使用。随着动态语言在构建关键软件组件方面越来越受欢迎,这种差距正变得越来越重要。那些忽视与调试生产系统相关问题的语言将越来越被降级到解决更简单、界限明确、易于理解的问题,而那些提供丰富工具来理解事后故障的语言将构成下一代软件基石的基础。


致谢

非常感谢 Bryan Cantrill、Peter Memishian 和 Theo Schlossnagle 审阅了本文的早期草稿,并感谢 Adam Cath、Ryan Dahl、Robert Mustacchi 和许多其他人就此主题进行了有益的讨论。


参考文献

1. Bash 参考手册。2009. https://gnu.ac.cn/s/bash/manual/bash.html.

2. Bonwick, J. 1994. Slab 分配器:一种对象缓存内核内存分配器。Usenix 1994 年夏季技术会议。

3. Cantrill, B. M. 2003. 事后对象类型识别。第五届国际自动化和算法调试研讨会。

4. 使用 ruby-debug 进行调试。2011; http://bashdb.sourceforge.net/ruby-debug.html#Post_002dMortem-Debugging.

5. Erlang 运行时系统应用程序用户指南,版本 5.8.4。2011. 如何解释 Erlang 崩溃转储; https://erlang.org.cn/doc/apps/erts/crash_dump.html.

6. Gill, S. 1951. EDSAC 程序中错误的诊断。英国皇家学会学报 A 206: 538-554.

7. joyent GitHub 项目。2011. https://github.com/joyent/node

8. Incubator Wiki。2011. 2011 年 3 月董事会报告。Kato 项目; http://wiki.apache.org/incubator/March2011.

9. McGregor, D. R., Malone, J. R. 1980. Stabdump - 一个用于辅助调试的转储解释器程序。软件实践与经验 10(4): 329-332.

10. Python 标准库。2011. Python v2.7.2 文档。pdb - Python 调试器; https://docs.pythonlang.cn/library/pdb.html.

11. Schrock, E. 2009. 在生产环境中调试 AJAX。 7 (1); https://queue.org.cn/detail.cfm?id=1515745.

喜欢或讨厌?请告诉我们

[email protected]

David Pacheco 是 Joyent 的一名工程师,他在那里领导 Cloud Analytics 的设计和实施,Cloud Analytics 是一个基于实时的 Node.js/DTrace 系统,用于可视化云中的服务器和应用程序性能。他之前是 Sun Microsystems Fishworks 团队的成员,曾致力于 Sun Storage 7000 设备的多项功能,包括基于 ZFS 的远程复制、故障管理、闪存设备支持以及 HTTP/WebDAV 分析。

© 2011 1542-7730/11/0900 $10.00

acmqueue

最初发表于 Queue vol. 9, no. 10
数字图书馆 中评论本文





更多相关文章

Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息方面需要权衡:这可能会使编译速度变慢。链接时优化等技术可以为您提供两全其美的效果。编译器中的优化在不断改进,即将到来的间接调用和虚函数分派方面的改进可能很快就会带来更快的多态性。


Ulan Degenbaev, Michael Lippautz, Hannes Payer - 作为合资企业的垃圾回收
跨组件跟踪是一种解决跨组件边界的引用循环问题的方法。只要组件可以形成任意对象图,并且跨 API 边界具有重要的所有权,就会出现此问题。CCT 的增量版本已在 V8 和 Blink 中实现,从而能够以安全有效的方式回收内存。


David Chisnall - C 语言不是一种低级语言
在最近的 Meltdown 和 Spectre 漏洞之后,值得花一些时间来研究根本原因。这两种漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能被添加进来,是为了让 C 程序员继续相信他们正在使用低级语言编程,而这种情况已经几十年没有发生了。


Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知存在漏洞。了解问题的范围以及包含库的许多意外方式,只是改进情况的第一步。这里的目标是本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。





© 保留所有权利。

© . All rights reserved.