驯服龙:使用llnode调试您的node.js应用程序
#node #调试 #lldb

最初于2018年6月20日发布在Sthima Insights上。在此处重新发布以保存目的。

注意:llnode尚未在几年中进行更新,并且已知在node.js v14上使用的最新node.js版本。

如果您在应用程序中遇到了内存泄漏或无限循环,您会知道找到这些问题的来源有多沮丧。即使Node.js具有由V8提供动力的Garbage Collector,如果我们保留对对象的不必要的引用,仍然有可能具有内存泄漏(或无限内存增长)。

可用于调查此类问题的一种技术是验尸。本地语言的开发人员(例如C ++和GO)可能会熟悉它,尤其是在给定时间的core dump文件的事后调试。

幸运的是,我们可以在Node.js应用程序中使用相同的技术!这是通过llnode:一个LLDB插件可以进行的,它使我们能够检查Node.js核心转储。使用llnode,我们可以检查内存中的对象,并查看程序的完整回溯,包括本机(C ++)帧和JavaScript帧。它可以在运行的node.js应用程序上或通过核心转储使用。

一个人去是危险的。拿着它!

llnode可通过npm获得,并在撰写本文时与Active node.js发行线(v6.x,v8.x,v9.x,v9.x和v10.x一起使用)。由于它是lldb的插件,因此您需要先安装它。 llnode在LLDB 3.9或更高版本上最有效。

在Mac OS上,您可以通过安装Xcode获得lldb。但是,如果您使用Ubuntu,则可以通过运行:
安装LLDB

sudo apt install lldb-4.0 liblldb-4.0-dev

安装lldb后,您可以使用npm install -g llnode在全球安装llnode。请注意,如果您不使用nvm使用node.js或没有global prefix集,则需要运行sudo install -g llnode

生成核心转储

llnode可以连接到进程或读取核心转储文件。在本教程中,我们将重点关注后者,因此我们首先需要能够生成核心转储。

有两种主要方法可以在UNIX系统中生成核心转储:您可以从运行过程中创建核心转储,也可以在进程崩溃时创建一个。

要生成运行过程的核心转储,您可以使用gcore **{PID}**(Linux)或lldb --attach-pid **{PID}** -b -o 'process save-core "core.**{PID}**"'(OS X),用应用程序的PID代替**{PID}**。两种方法都将在当前工作目录中创建一个名为core.$PID的核心转储文件。

但是,如果您要在过程崩溃时生成一个核心转储(例如,在应用程序死亡时对应用程序死亡时进行调试),则您需要通过运行ulimit -c unlimited来设置系统的ULIMIT 。这将为当前终端会话设置ULIMIT,这意味着如果您打开另一个终端,则需要再次设置。以这种方式生成的核心转储保存在Linux的当前工作目录中的名为core的文件中,而OS X内核则存储在/cores/文件夹中。在Mac OS上要小心,因为核心转储可以快速填充您的高清。

当您的流程崩溃时生成核心转储的另一件事是:Node.js由于未知的例外而终止时会崩溃。如果您想在这种情况下生成一个核心转储,则需要使用--abort-on-uncaught-exception运行node.js。

驯服龙

现在我们已经安装了llnode,并且知道如何生成核心转储,让我们学习如何一起使用它们。我们将使用一个小节点。

首先,让我们运行此服务器:

$ node index.js  
server is listening on 3000. Our PID is: **74262**

为了使事情变得有趣,让我们用autocannon向该服务器发送一些请求:

$ # Send requests to localhost:3000 during 10 seconds  
$ npx autocannon -d 10 localhost:3000

现在,我们将生成运行过程的核心转储。在这种情况下,PID为 74262 记住使用node.js process

要在Linux上生成一个核心转储,请运行以下内容:

$ sudo gcore **74262** 

如果您在OS X上,则可以运行以下内容:

$ lldb --attach-pid **74262** -b -o 'process save-core "core.74262"'

两个命令都将在当前工作目录中创建一个核心转储,并使用名称core.**74262**创建一个核心转储,现在我们可以使用llnode打开它。 llnode采用了一个必需的参数,即您要调试的二进制(在这种情况下为node)和各种可选参数。由于我们想调试一个核心转储,因此我们需要告诉llnode -c **{core-file}**的核心转储文件在哪里。最终命令看起来像:

$ llnode node -c core.**74262**  
(llnode)

让我们从打印有关node.js及其依赖项的信息开始。此命令还将帮助我们了解llnode命令的语法。在(llnode)提示下,键入v8 nodeinfo并按Enter:

(llnode) v8 nodeinfo  
Information for process id 74262 (process=0x1ca4ddb89cb9)  
Platform = darwin, Architecture = x64, **Node Version = v8.11.2**  
Component versions (process.versions=0x1ca4b45a0379):  
    cldr = 32.0  
    icu = 60.1  
    tz = 2017c  
    unicode = 10.0  
Release Info (process.release=0x1ca4b45a0451):  
Executable Path = /Users/mmarchini/.nvm/versions/node/v8.11.2/bin/node  
Command line arguments (process.argv=0x1ca4b45a0411):  
    \[0\] = '/Users/mmarchini/.nvm/versions/node/v8.11.2/bin/node'  
    \[1\] = '/Users/mmarchini/workspace/blog-posts/index.js'  
Node.js Comamnd line arguments (process.execArgv=0x1ca4b45a0579):

llnode命令由v8 +一个空间前缀。这将帮助您区分llnodelldb命令。说到帮助,v8 help从现在开始是您最好的朋友:

(llnode) v8 help  
     Node.js helpersSyntax:The following subcommands are supported: **bt**              -- Show a backtrace with node.js JavaScript   
                         functions and their args. An optional   
                         argument is accepted; if that argument is a   
                         number, it specifies the number of frames   
                         to display. Otherwise all frames will be   
                         dumped.  
                         Syntax: v8 bt \[number\]  
      **findjsinstances** -- List every object with the specified type   
                         name. Use -v or --verbose to display   
                         detailed \`v8 inspect\` output for each   
                         object. Accepts the same options as   
                         \`v8 inspect\`  
      **findjsobjects**   -- List all object types and instance counts       
                         grouped by type name and sorted by instance   
                         count. Use -d or --detailed to get an   
                         output grouped by type name, properties,   
                         and array length, as well as more   
                         information regarding each type.  
      **findrefs**        -- Finds all the object properties which meet   
                         the search criteria. The default is to list   
                         all the object properties that reference   
                         the specified value.  
                         Flags:  
                         \* -v, --value expr     - all properties   
                                                  that refer to the   
                                                  specified   
                                                  JavaScript object   
                                                  (default)  
                         \* -n, --name  name     - all properties   
                                                  with the specified       
                                                  name  
                         \* -s, --string string  - all properties   
                                                  that refer to the   
                                                  specified   
                                                  JavaScript string   
                                                  value  
      **inspect**         -- Print detailed description and contents of   
                         the JavaScript value.  
                         Possible flags (all optional):  
                         \* -F, --full-string    - print whole string   
                                                  without adding   
                                                  ellipsis  
                         \* -m, --print-map      - print object's map   
                                                  address  
                         \* -s, --print-source   - print source code   
                                                  for function   
                                                  objects  
                         \* -l num, --length num - print maximum of   
                                                  \`num\` elements   
                                                  from string/array  
                         Syntax: v8 inspect \[flags\] expr  
      **nodeinfo**        -- Print information about Node.js  
      **print**           -- Print short description of the JavaScript   
                         value.  
                         Syntax: v8 print expr  
      **source**          -- Source code informationFor more help on any particular subcommand, type 'help <command> <subcommand>'.

是的,那里有很多信息。但是不用担心,我们现在会通过有趣的命令。

V8 FindjSobjects

v8 findjsobjects列出了所有对象类型及其大小和数量。这对于找出哪些对象类型都使用您的大多数内存很有用。您第一次运行此命令可能需要几分钟才能进行处理,所以请不要担心是否发生。

(llnode) v8 findjsobjects  
 Instances  Total Size Name  
 ---------- ---------- ----  
          1         24 AssertionError  
          1         24 AsyncResource  
          1         24 Control  
          1         24 FastBuffer  
          1         24 Loader  
          1         24 ModuleJob  
          1         24 ModuleMap  
          1         24 Performance  
          1         24 PerformanceObserver  
          1         24 SafeMap  
          1         24 SafePromise  
          1         24 SafeSet  
          1         24 SocketListReceive  
          1         24 SocketListSend  
          1         24 TextDecoder  
          1         24 TextEncoder  
          1         24 URL  
          1         24 URLContext  
          1         24 URLSearchParams  
          1         24 WebAssembly  
          1         32 (Object)  
          1         32 ContextifyScript  
          1        104 ImmediateList  
          1        104 Stack  
          1        128 Server  
          1        168 Agent  
          2         48 (anonymous)  
          2         48 process  
          2         64 ChannelWrap  
          2         64 Signal  
          2        120 Resolver  
          2        128 PerformanceNodeTiming  
          2        136 NextTickQueue  
          2        144 FreeList  
          2        200 PerformanceObserverEntryList  
          2        208 EventEmitter  
          2        208 WriteStream  
          2        224 Console  
          2        272 Module  
          3         72 NodeError  
          3         96 TTY  
          3        280 AsyncHook  
          4        128 Timer  
          6        432 TimersList  
         10       2480 Socket  
         11        352 HTTPParser  
         11        352 WriteWrap  
         12        384 TCP  
         12       2688 WritableState  
         15       1360 (ArrayBufferView)  
         74       4736 NativeModule  
       5715    1234440 IncomingMessage  
       5744     781184 ServerResponse  
       5747    1103424 ReadableState  
       5748     275880 BufferList  
      45980    2942680 TickObject  
      69344    2219008 (Array)  
     **235515    9420584 Visit**  
     293720   15437744 Object  
     615411    3750984 (String)  
 ---------- ----------  
    1283140   37182200

如果我们查看输出,我们将看到许多由autocannon运行生成的Visit对象。我们还会看到访问是使用更多内存的第三种类型。在实际情况下,此信息是追踪内存泄漏的第一步。

此命令的详细版本也可以用v8 findjsobjects -d调用。结果将对具有相同属性和元素数量的类型进行分组,还将提供该类型的示例对象的地址。

V8 Findjsinstances

v8 findjsinstances为您提供了一个给定类型的所有对象的列表。它还具有详细的版本,可以使用v8 findjsinstances -d调用。这将打印给定类型的所有对象,其属性和元素。

(llnode) v8 findjsinstances -d Visit  
0x0000176d04402201:<Object: Visit properties {  
    .visit\_id=<Smi: 82704>,  
    .headers=0x0000176d7d99f1c9:<Object: Object>}>  
0x0000176d04402229:<Object: Visit properties {  
    .visit\_id=<Smi: 82705>,  
    .headers=0x0000176d7d99f191:<Object: Object>}>  
0x0000176d04402251:<Object: Visit properties {  
    .visit\_id=<Smi: 82706>,  
    .headers=0x0000176d7d99f159:<Object: Object>}>  
0x0000176d04402279:<Object: Visit properties {  
    .visit\_id=<Smi: 82707>,  
    .headers=0x0000176d7d99f121:<Object: Object>}>  
0x0000176d044022a1:<Object: Visit properties {  
    .visit\_id=<Smi: 82708>,  
    .headers=0x0000176d7d99f0e9:<Object: Object>}>  
0x0000176d044022c9:<Object: Visit properties {  
    .visit\_id=<Smi: 82709>,  
    .headers=0x0000176d7d99f0b1:<Object: Object>}>  
_// A thousand miles later...  
_0x0000176dffba62d9:<Object: Visit properties {  
    .visit\_id=<Smi: 156026>,  
    .headers=0x0000176dffbef559:<Object: Object>}>  
0x0000176dffba6301:<Object: Visit properties {  
    .visit\_id=<Smi: 156027>,  
    .headers=0x0000176dffbef8a9:<Object: Object>}>  
**0x0000176dffba6329**:<Object: Visit properties {  
    .visit\_id=<Smi: 156028>,  
    .headers=0x0000176dffb82481:<Object: Object>}>

v8 findjsinstances -d接受可以传递给v8 inspect的相同参数,我们将看到下一步。

V8检查

v8 inspect打印给定对象的所有属性和元素。它还可以打印其他信息,例如对象映射的地址。如果您想查看对象的地图地址,则应运行v8 inspect -m。您可以使用v8 inspect检查地图。

(llnode) v8 inspect -m **0x0000176dffba6329**  
0x0000176dffba6329(map=**_0x0000176d689cec29_**):<Object: Visit properties {  
    .visit\_id=<Smi: 156028>,  
    .headers=0x0000176dffb82481:<Object: Object>}>  
(llnode) v8 inspect **_0x0000176d689cec29_**  
0x0000176d689cec29:<Map own\_descriptors=2 in\_object\_size=2   
  instance\_size=40   
  descriptors=0x0000176d7f284569:<FixedArray, len=8 contents={  
    \[0\]=<Smi: 2>,  
    \[1\]=<Smi: 0>,  
    \[2\]=0x0000176dd8566a11:<String: "visit\_id">,  
    \[3\]=<Smi: 320>,  
    \[4\]=<Smi: 1>,  
    \[5\]=0x0000176dd8566a31:<String: "headers">,  
    \[6\]=<Smi: 1050112>,  
    \[7\]=0x0000176d117509f9:<unknown>}>>

V8 Findrefs

现在,我们知道如何使用大量内存,如何找到这些类型的对象以及如何检查这些对象的属性。但是,如果我们想找到内存泄漏,我们需要找到将这些对象保留在内存中的原因。换句话说,我们需要找到引用它们的其他对象。有一个完美的命令:v8 findrefs。此命令将返回引用另一个对象的所有对象。让我们尝试一下:

(llnode) v8 findrefs **0x0000176dffba6329  
**0x176d1f4fac41: (Array)\[156027\]=0x176dffba6329

结果表明,有一个阵列持有很多(156027)对象,这可能是我们在内存中拥有这么多Visit对象的原因(扰流板:它,请查看服务器的第13和16行)。不幸的是,llnode无法分辨该数组在哪里 ,但是有一个开放的issue可以在以后添加此功能。

结论

即使具有有限的功能,llnode也是一个强大的调试工具,可以在我们的Node.js Diagnostics Arsenal上使用。与传统工具相比,核心转储崩溃的轻巧性质可以帮助我们在较少的努力中识别记忆问题,因此可以立即检查应用程序的状态。它还使LNODE和CORE转储非常适合用于生产环境。