最初于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
+一个空间前缀。这将帮助您区分llnode
和lldb
命令。说到帮助,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转储非常适合用于生产环境。