├── 1.1 perf + FlameGraph.md ├── 1.2 v8-profiler.md ├── 1.3 Tick Processor.md ├── 2.1 gcore + llnode.md ├── 2.2 heapdump.md ├── 2.3 memwatch-next.md ├── 2.4 cpu-memory-monitor.md ├── 3.1 Promise.md ├── 3.2 Async + Await.md ├── 3.3 Error Stack.md ├── 3.4 Node@8.md ├── 3.5 Rust Addons.md ├── 3.6 Event Loop.md ├── 3.7 uncaughtException + llnode.md ├── 4.1 Source Map.md ├── 4.2 Chrome DevTools.md ├── 4.3 Visual Studio Code.md ├── 4.4 debug + repl2 + power-assert.md ├── 4.5 supervisor-hot-reload.md ├── 5.1 NewRelic.md ├── 5.2 Elastic APM.md ├── 6.1 koa-await-breakpoint.md ├── 6.2 async_hooks.md ├── 6.3 ELK.md ├── 6.4 OpenTracing + Jaeger.md ├── 6.5 Sentry.md ├── 7.1 Telegraf + InfluxDB + Grafana(上).md ├── 7.2 Telegraf + InfluxDB + Grafana(下).md ├── 8.1 node-clinic.md ├── 8.2 alinode.md ├── README.md └── assets ├── 1.1.1.png ├── 1.1.2.png ├── 1.1.3.jpg ├── 1.2.1.png ├── 1.2.2.png ├── 1.2.3.png ├── 1.3.1.png ├── 2.1.1.png ├── 2.2.1.png ├── 2.2.2.png ├── 2.2.3.png ├── 2.2.4.png ├── 2.3.1.png ├── 3.1.1.png ├── 3.1.2.png ├── 3.1.3.png ├── 3.1.4.png ├── 3.4.1.jpg ├── 3.4.2.jpg ├── 3.4.3.jpg ├── 3.4.4.jpg ├── 3.4.5.jpg ├── 3.6.1.png ├── 3.7.1.png ├── 4.2.1.png ├── 4.2.2.png ├── 4.2.3.png ├── 4.2.4.png ├── 4.3.1.png ├── 4.3.2.png ├── 4.3.3.png ├── 4.3.4.png ├── 4.3.5.png ├── 4.4.1.jpg ├── 4.4.2.jpg ├── 5.1.1.png ├── 5.1.2.png ├── 5.1.3.png ├── 5.2.1.png ├── 5.2.2.png ├── 5.2.3.png ├── 5.2.4.png ├── 5.2.5.png ├── 5.2.6.png ├── 6.3.1.png ├── 6.3.2.png ├── 6.3.3.png ├── 6.3.4.png ├── 6.4.1.jpg ├── 6.4.2.jpg ├── 6.4.3.jpg ├── 6.4.4.jpg ├── 6.4.5.jpg ├── 6.4.6.jpg ├── 6.5.1.jpg ├── 6.5.2.jpg ├── 6.5.3.png ├── 6.5.4.png ├── 6.5.5.png ├── 6.5.6.png ├── 6.5.7.png ├── 7.1.1.png ├── 7.1.2.png ├── 7.1.3.png ├── 7.1.4.png ├── 7.1.5.png ├── 7.1.6.png ├── 7.1.7.png ├── 7.1.8.png ├── 7.1.9.png ├── 7.2.1.png ├── 7.2.2.png ├── 7.2.3.png ├── 7.2.4.png ├── 7.2.5.png ├── 7.2.6.png ├── 7.2.7.png ├── 7.2.8.png ├── 8.1.1.png ├── 8.1.2.png ├── 8.2.1.png ├── 8.2.2.png ├── 8.2.3.png ├── 8.2.4.png ├── 8.2.5.png ├── 8.2.6.png ├── alipay.png ├── book.jpg └── wechat.jpeg /1.1 perf + FlameGraph.md: -------------------------------------------------------------------------------- 1 | 当程序出现性能瓶颈时,我们通常通过表象(比如请求某个接口时 CPU 使用率飙涨)然后结合代码去推测可能出问题的地方,却不知道问题到底是什么引起的。如果有个一可视化的工具直观地展现程序的性能瓶颈就好了,幸好 [Brendan D. Gregg](http://www.brendangregg.com/) 发明了火焰图。 2 | 3 | [火焰图](http://www.brendangregg.com/flamegraphs.html)(Flame Graph)看起来就像一团跳动的火焰,因此得名。火焰图可以将 CPU 的使用情况可视化,使我们直观地了解到程序的性能瓶颈,通常要结合操作系统的性能分析工具(profiling tracer)使用,常见的操作系统的性能分析工具如下: 4 | 5 | - Linux:perf, eBPF, SystemTap, and ktap。 6 | - Solaris, illumos, FreeBSD:DTrace。 7 | - Mac OS X:DTrace and Instruments。 8 | - Windows:Xperf.exe。 9 | 10 | ## 1.1.1 perf 11 | 12 | [perf_events](http://www.brendangregg.com/linuxperf.html)(简称 perf)是 Linux Kernal 自带的系统性能分析工具,能够进行函数级与指令级的热点查找。它基于事件采样原理,以性能事件为基础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析,常用于查找性能瓶颈及定位热点代码。 13 | 14 | 测试机器: 15 | 16 | ```sh 17 | $ uname -a 18 | Linux nswbmw-VirtualBox 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 19 | ``` 20 | 21 | **注意**:非 Linux 用户需要用虚拟机安装 Ubuntu 16.04 和 node@8.9.4 后进行后面的操作。 22 | 23 | 安装 perf: 24 | 25 | ```sh 26 | $ sudo apt install linux-tools-common 27 | $ perf # 根据提示安装对应的内核版本的 tools, 如下 28 | $ sudo apt install linux-tools-4.10.0-28-generic linux-cloud-tools-4.10.0-28-generic 29 | ``` 30 | 31 | 创建测试目录 ~/test 和测试代码: 32 | 33 | **app.js** 34 | 35 | ```js 36 | const crypto = require('crypto') 37 | const Paloma = require('paloma') 38 | const app = new Paloma() 39 | const users = {} 40 | 41 | app.route({ method: 'GET', path: '/newUser', controller (ctx) { 42 | const username = ctx.query.username || 'test' 43 | const password = ctx.query.password || 'test' 44 | 45 | const salt = crypto.randomBytes(128).toString('base64') 46 | const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') 47 | 48 | users[username] = { salt, hash } 49 | 50 | ctx.status = 204 51 | }}) 52 | 53 | app.route({ method: 'GET', path: '/auth', controller (ctx) { 54 | const username = ctx.query.username || 'test' 55 | const password = ctx.query.password || 'test' 56 | 57 | if (!users[username]) { 58 | ctx.throw(400) 59 | } 60 | const hash = crypto.pbkdf2Sync(password, users[username].salt, 10000, 64, 'sha512').toString('hex') 61 | 62 | if (users[username].hash === hash) { 63 | ctx.status = 204 64 | } else { 65 | ctx.throw(403) 66 | } 67 | }}) 68 | 69 | app.listen(3000) 70 | ``` 71 | 72 | 添加 --perf_basic_prof(或者 --perf-basic-prof)参数运行此程序,会对应生成一个 /tmp/perf-\.map 的文件。命令如下: 73 | 74 | ```sh 75 | $ node --perf_basic_prof app.js & 76 | [1] 3590 77 | $ tail /tmp/perf-3590.map 78 | 51b87a7b93e 18 Function:~emitListeningNT net.js:1375 79 | 51b87a7b93e 18 LazyCompile:~emitListeningNT net.js:1375 80 | 51b87a7bad6 39 Function:~emitAfterScript async_hooks.js:443 81 | 51b87a7bad6 39 LazyCompile:~emitAfterScript async_hooks.js:443 82 | 51b87a7bcbe 77 Function:~tickDone internal/process/next_tick.js:88 83 | 51b87a7bcbe 77 LazyCompile:~tickDone internal/process/next_tick.js:88 84 | 51b87a7bf36 12 Function:~clear internal/process/next_tick.js:42 85 | 51b87a7bf36 12 LazyCompile:~clear internal/process/next_tick.js:42 86 | 51b87a7c126 b8 Function:~emitPendingUnhandledRejections internal/process/promises.js:86 87 | 51b87a7c126 b8 LazyCompile:~emitPendingUnhandledRejections internal/process/promises.js:86 88 | ``` 89 | 90 | **map 文件内容三列依次为**:16进制的符号地址(symbol addresses)、大小(sizes)和符号名(symbol names)。perf 会尝试查找 /tmp/perf-\.map 文件,用来做符号转换,即把 16 进制的符号地址转换成人能读懂的符号名。 91 | 92 | **注意**:使用 --perf_basic_prof_only_functions 参数也可以,但经尝试后发现生成的火焰图信息不全(不全的地方显示 [perf-\.map]),所以这里使用 --perf_basic_prof。但是,使用 --perf_basic_prof 有个缺点,就是会导致 map 文件一直增大,这是由于符号(symbols)地址不断变换导致的,用 --perf_basic_prof_only_functions 可以缓解这个问题。关于如何取舍,还请读者自行尝试。 93 | 94 | 接下来 clone 用来生成火焰图的工具: 95 | 96 | ```sh 97 | $ git clone http://github.com/brendangregg/FlameGraph ~/FlameGraph 98 | ``` 99 | 100 | 我们先用 ab 压测: 101 | 102 | ```sh 103 | $ curl "http://localhost:3000/newUser?username=admin&password=123456" 104 | $ ab -k -c 10 -n 2000 "http://localhost:3000/auth?username=admin&password=123456" 105 | ``` 106 | 107 | 新开另一个终端,在 ab 开始压测后立即运行: 108 | 109 | ```sh 110 | $ sudo perf record -F 99 -p 3590 -g -- sleep 30 111 | $ sudo chown root /tmp/perf-3590.map 112 | $ sudo perf script > perf.stacks 113 | $ ~/FlameGraph/stackcollapse-perf.pl --kernel < ~/perf.stacks | ~/FlameGraph/flamegraph.pl --color=js --hash> ~/flamegraph.svg 114 | ``` 115 | 116 | **注意**:第 1 次生成的 svg 可能不太准确,最好重复几次以上步骤,使用第 2 次及以后生成的 flamegraph.svg。 117 | 118 | 有几点需要解释一下: 119 | 120 | - perf record 121 | - -F 指定了采样频率 99Hz(即每秒 99 次,如果 99 次都返回同一个函数名,那就说明 CPU 在这一秒钟都在执行同一个函数,可能存在性能问题)。 122 | - -p 指定进程的 pid。 123 | - -g 启用 call-graph 记录。 124 | - -- sleep 30 指定记录 30s。 125 | 126 | - sudo chown root /tmp/perf-3009.map,将 map 文件更改为 root 权限,否则会报如下错误: 127 | 128 | > File /tmp/perf-PID.map not owned by current user or root, ignoring it (use -f to override). 129 | > Failed to open /tmp/perf-PID.map, continuing without symbols 130 | 131 | - perf record 会将记录的信息保存到当前执行目录的 perf.data 文件中,然后使用 perf script 读取 perf.data 的 trace 信息写入 perf.stacks。 132 | 133 | - --color=js 指定生成针对 JavaScript 配色的 svg,即: 134 | 135 | - green:JavaScript。 136 | - blue:Builtin。 137 | - yellow:C++。 138 | - red:System(native user-level, and kernel)。 139 | 140 | ab 压测用了 30s 左右,用浏览器打开 flamegraph.svg,截取关键的部分如下图所示: 141 | ![](./assets/1.1.1.png) 142 | 143 | ## 1.1.2 理解火焰图 144 | 145 | 火焰图含义: 146 | 147 | - 每一个小块代表了一个函数在栈中的位置(即一个栈帧)。 148 | - Y 轴代表栈的深度(栈上的帧数),顶端的小块显示了占据 CPU 的函数。每个小块的下面是它的祖先(即父函数)。 149 | - X 轴代表总的样例群体。它不像绝大多数图表那样从左到右表示时间的流逝,其左右顺序没有特殊含义,仅仅按照字母表的顺序排列。 150 | - 小块的宽度代表 CPU 的使用时间,或者说相对于父函数而言使用 CPU 的比例(基于所有样例),越宽则代表占用 CPU 的时间越长,或者使用 CPU 很频繁。 151 | - 如果采取多线程并发运行取样,则取样数量会超过运行时间。 152 | 153 | **从上图可以看出**:最上面的绿色小块(即 JavaScript 代码)指向 test/app.js 第 18 行,即 `GET /auth` 这个路由。再往上看,黄色的小块(即 C++ 代码) node::crypto::PBKDF2 占用了大量的 CPU 时间。 154 | 155 | **解决方法**:将同步改为异步,即将 crypto.pbkdf2Sync 改为 crypto.pbkdf2。修改如下: 156 | 157 | ```js 158 | app.route({ method: 'GET', path: '/auth', async controller (ctx) { 159 | const username = ctx.query.username || 'test' 160 | const password = ctx.query.password || 'test' 161 | 162 | if (!users[username]) { 163 | ctx.throw(400) 164 | } 165 | const hash = await new Promise((resolve, reject) => { 166 | crypto.pbkdf2(password, users[username].salt, 10000, 64, 'sha512', (err, derivedKey) => { 167 | if (err) { 168 | return reject(err) 169 | } 170 | resolve(derivedKey.toString('hex')) 171 | }) 172 | }) 173 | 174 | if (users[username].hash === hash) { 175 | ctx.status = 204 176 | } else { 177 | ctx.throw(403) 178 | } 179 | }}) 180 | ``` 181 | 182 | 用 ab 重新压测,结果用了 16s。重新生成的火焰图如下: 183 | 184 | ![](./assets/1.1.2.png) 185 | 186 | **可以看出**:只有在左侧极窄的绿色小块可以看到 JavaScript 代码,红色的部分我们不关心也无法优化。那么,为什么异步比同步的 QPS 要高呢?原因是 Node.js 底层的 libuv 用了多个线程进行计算,这里就不再深入介绍了。 187 | 188 | svg 火焰图的其他小技巧如下: 189 | 190 | 1. 单击任意一个小块即可展开,即被单击的小块宽度变宽,它的子函数也按比例变宽,方便查看。 191 | 2. 可单击 svg 右上角的 search 按钮进行搜索,被搜索的关键词会高亮显示,在有目的地查找某个函数时比较有用。 192 | 193 | ## 1.1.3 红蓝差分火焰图 194 | 195 | 虽然我们有了火焰图,但要处理性能回退问题,还需要在修改代码前后的火焰图之间,不断切换和对比,来找出问题所在,很不方便。于是 [Brendan D. Gregg](http://www.brendangregg.com/index.html) 又发明了红蓝差分火焰图(Red/Blue Differential Flame Graphs)。 196 | 197 | **如下所示**:红色表示增长,蓝色表示衰减。 198 | 199 | ![](./assets/1.1.3.jpg) 200 | 201 | 红蓝差分火焰图的工作原理如下: 202 | 203 | 1. 抓取修改前的栈 profile1 文件。 204 | 2. 抓取修改后的栈 profile2 文件。 205 | 3. 使用 profile2 来生成火焰图,这样栈帧的宽度就是以 profile2 文件为基准的。 206 | 4. 使用 profile2 - profile1 的差异来对火焰图重新上色。上色的原则是:如果栈帧在 profile2 中出现出现的次数更多,则标为红色,否则标为蓝色。色彩是根据修改前后的差异来填充的。 207 | 208 | 这样,通过红蓝差分火焰图,我们就可以清楚地看到系统性能的差异之处。 209 | 210 | 生成红蓝差分火焰图的流程如下: 211 | 212 | 1. 修改代码前运行: 213 | 214 | ```sh 215 | $ sudo perf record -F 99 -p -g -- sleep 30 216 | $ sudo chown root /tmp/perf-.map 217 | $ sudo perf script > perf_before.stacks 218 | ``` 219 | 220 | 2. 修改代码后运行: 221 | 222 | ```sh 223 | $ sudo perf record -F 99 -p -g -- sleep 30 224 | $ sudo chown root /tmp/perf-.map 225 | $ sudo perf script > perf_after.stacks 226 | ``` 227 | 228 | 3. 将 profile 文件进行折叠(fold),然后生成差分火焰图: 229 | 230 | ```sh 231 | $ ~/FlameGraph/stackcollapse-perf.pl ~/perf_before.stacks > perf_before.folded 232 | $ ~/FlameGraph/stackcollapse-perf.pl ~/perf_after.stacks > perf_after.folded 233 | $ ./FlameGraph/difffolded.pl perf_before.folded perf_after.folded | ./FlameGraph/flamegraph.pl > flamegraph_diff.svg 234 | ``` 235 | 236 | **如上缺点是**:如果一个代码执行路径完全消失了,那么在火焰图中就找不到地方来标注蓝色,我们只能看到当前的 CPU 使用情况,却不知道为什么会变成这样。 237 | 238 | 一种解决办法是:生成一个相反的差分火焰图,即基于 profile1 生成 profile1 - profile2 的差分火焰图。对应命令如下: 239 | 240 | ```sh 241 | $ ./FlameGraph/difffolded.pl perf_after.folded perf_before.folded | ./FlameGraph/flamegraph.pl --negate > flamegraph_diff2.svg 242 | ``` 243 | 244 | 其中,--negate 用于颠倒红/蓝配色。最终我们得到: 245 | 246 | - flamegraph_diff.svg:宽度是以修改前的 profile 文件为基准,颜色表明将要发生的情况。 247 | - flamegraph_diff2.svg:宽度是以修改后的 profile 文件为基准,颜色表明已经发生的情况。 248 | 249 | 总之,红蓝差分火焰图可能只在代码变化不大的情况下使用时效果明显,在代码变化较大的情况下使用时效果可能就不明显了。 250 | 251 | ## 1.1.4 参考链接 252 | 253 | - https://yunong.io/2015/11/23/generating-node-js-flame-graphs/ 254 | - http://www.brendangregg.com/perf.html 255 | - http://www.brendangregg.com/blog/2014-09-17/node-flame-graphs-on-linux.html 256 | - https://linux.cn/article-4670-1.html 257 | - http://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html 258 | - http://www.ruanyifeng.com/blog/2017/09/flame-graph.html 259 | 260 | 下一节:[1.2 v8-profiler](https://github.com/nswbmw/node-in-debugging/blob/master/1.2%20v8-profiler.md) 261 | -------------------------------------------------------------------------------- /1.2 v8-profiler.md: -------------------------------------------------------------------------------- 1 | 我们知道 Node.js 是基于 V8 引擎的,V8 暴露了一些 profiler API,我们可以通过 [v8-profiler](https://github.com/node-inspector/v8-profiler) 收集一些运行时数据(例如:CPU 和内存)。本节将介绍如何使用 v8-profiler 分析 CPU 的使用情况。 2 | 3 | ## 1.2.1 使用 v8-profiler 4 | 5 | 创建测试代码: 6 | 7 | **app.js** 8 | 9 | ```js 10 | const fs = require('fs') 11 | const crypto = require('crypto') 12 | const Bluebird = require('bluebird') 13 | const profiler = require('v8-profiler') 14 | const Paloma = require('paloma') 15 | const app = new Paloma() 16 | 17 | app.route({ method: 'GET', path: '/encrypt', controller: function encryptRouter (ctx) { 18 | const password = ctx.query.password || 'test' 19 | const salt = crypto.randomBytes(128).toString('base64') 20 | const encryptedPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') 21 | 22 | ctx.body = encryptedPassword 23 | }}) 24 | 25 | app.route({ method: 'GET', path: '/cpuprofile', async controller (ctx) { 26 | //Start Profiling 27 | profiler.startProfiling('CPU profile') 28 | await Bluebird.delay(30000) 29 | //Stop Profiling after 30s 30 | const profile = profiler.stopProfiling() 31 | profile.export() 32 | .pipe(fs.createWriteStream(`cpuprofile-${Date.now()}.cpuprofile`)) 33 | .on('finish', () => profile.delete()) 34 | ctx.status = 204 35 | }}) 36 | 37 | app.listen(3000) 38 | ``` 39 | 40 | `GET /encrypt` 有一个 CPU 密集型的计算函数 crypto.pbkdf2Sync,`GET /cpuprofile` 用来收集 30s 的 V8 log 然后将其 dump 到一个文件中。 41 | 42 | 运行该程序,打开两个终端窗口。一个终端运行: 43 | 44 | ```sh 45 | $ curl localhost:3000/cpuprofile 46 | ``` 47 | 48 | 来触发 CPU profiling,然后另一个终端立即运行: 49 | 50 | ```sh 51 | $ ab -c 20 -n 2000 "http://localhost:3000/encrypt?password=123456" 52 | ``` 53 | 54 | 来触发 CPU 密集计算。 55 | 56 | 最后生成 cpuprofile-xxx.cpuprofile 文件,该文件的内容其实就是一个大的 JSON 对象,大体如下: 57 | 58 | ```json 59 | { 60 | "typeId": "CPU", 61 | "uid": "1", 62 | "title": "CPU profile", 63 | "head": 64 | { "functionName": "(root)", 65 | "url": "", 66 | "lineNumber": 0, 67 | "callUID": 154, 68 | "bailoutReason": "", 69 | "id": 1, 70 | "scriptId": 0, 71 | "hitCount": 0, 72 | "children": [ ... ] }, 73 | "startTime": 276245, 74 | "endTime": 276306, 75 | "samples": [ ... ], 76 | "timestamps": [ ... ] 77 | } 78 | ``` 79 | 80 | 这个 JSON 对象记录了函数调用栈、路径、时间戳和一些其他信息,samples 节点数组与 timestamps 节点数组中的时间戳是一一对应的,并且 samples 节点数组中的每一个值其实对应了 head 节点的深度优先遍历 ID。这里我们不深究每个字段的含义,先来看看如何可视化这些数据。 81 | 82 | ## 1.2.2 方法 1——Chrome DevTools 83 | 84 | Chrome 自带了分析 CPU profile 日志的工具。打开 Chrome -> 调出开发者工具(DevTools) -> 单击右上角三个点的按钮 -> More tools -> JavaScript Profiler -> Load,加载刚才生成的 cpuprofile 文件。左上角的下拉菜单可以选择如下三种模式: 85 | 86 | 1. Chart:显示按时间顺序排列的火焰图。 87 | 2. Heavy (Bottom Up):按照函数对性能的影响排列,同时可以检查函数的调用路径。 88 | 3. Tree (Top Down):显示调用结构的总体状况,从调用堆栈的顶端开始。 89 | 90 | 这里我们选择 Tree (Top Down) 模式,按 Total Time 降序排列。可以看到有如下三列: 91 | 92 | 1. Self Time:函数调用所耗费的时间,仅包含函数本身的声明,不包含任何子函数的执行时间。 93 | 2. Total Time:函数调用所耗费的总时间,包含函数本身的声明及所有子函数执行时间。即:父函数的 Total Time = 父函数的 Self Time + 所有子函数的 Total Time。 94 | 3. Function:函数名及路径,可展开查看子函数。 95 | 96 | 我们不断地展开,并定位到了 encryptRouter,如下图所示: 97 | 98 | ![](./assets/1.2.1.png) 99 | 100 | **可以看出**:我们定位到了 encryptRouter 这个路由,并且这个路由中 exports.pbkdf2Sync 占据了绝大部分 CPU 时间。 101 | 102 | ## 1.2.3 方法 2——火焰图 103 | 104 | 我们也可以用火焰图来展示 cpuprofile 数据。首先全局安装 flamegraph 模块: 105 | 106 | ```sh 107 | $ npm i flamegraph -g 108 | ``` 109 | 110 | 运行以下命令将 cpuprofile 文件生成 svg 文件: 111 | 112 | ```sh 113 | $ flamegraph -t cpuprofile -f cpuprofile-xxx.cpuprofile -o cpuprofile.svg 114 | ``` 115 | 116 | 用浏览器打开 cpuprofile.svg,如下所示: 117 | 118 | ![](./assets/1.2.2.png) 119 | 120 | **可以看出**:我们定位到了 app.js 的第 8 行,即 encryptRouter 这个路由,并且这个路由中 exports.pbkdf2Sync 占据了绝大部分 CPU 时间。 121 | 122 | ## 1.2.4 方法 3——v8-analytics 123 | 124 | [v8-analytics](https://github.com/hyj1991/v8-analytics) 是社区开源的一个解析 v8-profiler 和 heapdump 等模块生成的 CPU 和 heap-memory 日志的工具。它提供以下功能: 125 | 126 | - 将 V8 引擎逆优化或者优化失败的函数标红展示,并展示优化失败的原因。 127 | - 在函数执行时长超过预期时标红展示。 128 | - 展示当前项目中可疑的内存泄漏点。 129 | 130 | 我们以上述第 2 个功能为例,使用 v8-analytics 分析 CPU 的使用情况。 131 | 132 | 首先,全局安装 v8-analytics: 133 | 134 | ```sh 135 | $ npm i v8-analytics -g 136 | ``` 137 | 138 | 使用以下命令查看执行时间大于 200ms 的函数: 139 | 140 | ```sh 141 | $ va timeout cpuprofile-xxx.cpuprofile 200 --only 142 | ``` 143 | 144 | 结果截图如下: 145 | 146 | ![](./assets/1.2.3.png) 147 | 148 | **可以看出**:我们依然能够定位到 encryptRouter 和 exports.pbkdf2Sync。 149 | 150 | ## 1.2.5 参考链接 151 | 152 | - https://developers.google.com/web/tools/chrome-devtools/rendering-tools/js-execution 153 | - http://www.ebaytechblog.com/2016/06/15/igniting-node-js-flames/ 154 | - https://cnodejs.org/topic/58b562f97872ea0864fee1a7 155 | - https://github.com/hyj1991/v8-analytics/blob/master/README_ZH.md 156 | 157 | 上一节:[1.1 perf + FlameGraph](https://github.com/nswbmw/node-in-debugging/blob/master/1.1%20perf%20%2B%20FlameGraph.md) 158 | 159 | 下一节:[1.3 Tick Processor](https://github.com/nswbmw/node-in-debugging/blob/master/1.3%20Tick%20Processor.md) 160 | -------------------------------------------------------------------------------- /1.3 Tick Processor.md: -------------------------------------------------------------------------------- 1 | V8 内置了一个性能分析工具——Tick Processor,可以记录 JavaScript/C/C++ 代码的堆栈信息,该功能默认是关闭的,可以通过添加命令行参数 `--prof` 开启。 2 | 3 | ## 1.3.1 Tick Processor 4 | 5 | 创建测试代码: 6 | 7 | **app.js** 8 | 9 | ```js 10 | const crypto = require('crypto') 11 | 12 | function hash (password) { 13 | const salt = crypto.randomBytes(128).toString('base64') 14 | const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512') 15 | return hash 16 | } 17 | 18 | console.time('pbkdf2Sync') 19 | for (let i = 0; i < 100; i++) { 20 | hash('random_password') 21 | } 22 | console.timeEnd('pbkdf2Sync') 23 | ``` 24 | 25 | 运行: 26 | 27 | ```sh 28 | $ node --prof app 29 | pbkdf2Sync: 1375.582ms 30 | ``` 31 | 32 | 可以看出,执行 100 次 hash 函数总共用了 1375.585ms,并且当前目录下多了一个 isolate-xxx-v8.log 文件,该文件记录了 V8 的性能日志,内容如下: 33 | 34 | ``` 35 | v8-version,6,1,534,50,0 36 | shared-library,"/usr/local/bin/node",0x100001800,0x100bbb69a,0 37 | ... 38 | code-creation,Function,18,111912,0x37d07c7246a8,144,"hash /Users/nswbmw/Desktop/test/app.js:3:15",0x37d07c7076d0,~ 39 | code-creation,LazyCompile,18,111927,0x37d07c7246a8,144,"hash /Users/nswbmw/Desktop/test/app.js:3:15",0x37d07c7076d0,~ 40 | code-creation,Function,18,112058,0x37d07c725690,80,"exports.pbkdf2Sync crypto.js:686:30",0x37d07c70cb58,~ 41 | code-creation,LazyCompile,18,112074,0x37d07c725690,80,"exports.pbkdf2Sync crypto.js:686:30",0x37d07c70cb58,~ 42 | ... 43 | ``` 44 | 45 | 早期我们需要借助 [node-tick-processor](https://www.npmjs.com/package/node-tick-processor) 这样的工具解析 v8.log,但 Node.js 在 v5.2.0 之后包含了 v8.log 处理器,添加命令行参数 `--prof-process` 开启。 46 | 47 | 运行: 48 | 49 | ```sh 50 | $ node --prof-process isolate-0x103000000-v8.log 51 | ``` 52 | 53 | 结果如下: 54 | 55 | ``` 56 | Statistical profiling result from isolate-0x103000000-v8.log, (1152 ticks, 44 unaccounted, 0 excluded). 57 | 58 | [Shared libraries]: 59 | ticks total nonlib name 60 | 61 | [JavaScript]: 62 | ticks total nonlib name 63 | 1 0.1% 0.1% Function: ~Uint8Array native typedarray.js:158:31 64 | 1 0.1% 0.1% Function: ~NativeModule.cache bootstrap_node.js:604:42 65 | 1 0.1% 0.1% Function: ~Buffer.toString buffer.js:609:37 66 | 67 | [C++]: 68 | ticks total nonlib name 69 | 1023 88.8% 88.8% T node::crypto::PBKDF2(v8::FunctionCallbackInfo const&) 70 | 27 2.3% 2.3% t node::(anonymous namespace)::ContextifyScript::New(v8::FunctionCallbackInfo const&) 71 | ... 72 | 73 | [Summary]: 74 | ticks total nonlib name 75 | 3 0.3% 0.3% JavaScript 76 | 1105 95.9% 95.9% C++ 77 | 3 0.3% 0.3% GC 78 | 0 0.0% Shared libraries 79 | 44 3.8% Unaccounted 80 | 81 | [C++ entry points]: 82 | ticks cpp total name 83 | 1062 98.2% 92.2% T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) 84 | 13 1.2% 1.1% T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*) 85 | ... 86 | 87 | [Bottom up (heavy) profile]: 88 | Note: percentage shows a share of a particular caller in the total 89 | amount of its parent calls. 90 | Callers occupying less than 1.0% are not shown. 91 | 92 | ticks parent name 93 | 1023 88.8% T node::crypto::PBKDF2(v8::FunctionCallbackInfo const&) 94 | 1023 100.0% T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) 95 | 1023 100.0% Function: ~pbkdf2 crypto.js:691:16 96 | 1023 100.0% Function: ~exports.pbkdf2Sync crypto.js:686:30 97 | 1023 100.0% Function: ~hash /Users/nswbmw/Desktop/test/app.js:3:15 98 | 1023 100.0% Function: ~ /Users/nswbmw/Desktop/test/app.js:1:11 99 | ... 100 | ``` 101 | 102 | 打印结果包含六部分:Shared libraries、JavaScript、C++、Summary、C++ entry points 和 Bottom up (heavy) profile。[JavaScript] 部分列出了 JavaScript 代码执行所占用的 CPU ticks(CPU 时钟周期),[C++] 部分列出了 C++ 代码执行所占用的 CPU ticks,[Summary] 列出了各个部分的占比,[Bottom up] 列出了所有 CPU 占用时间从大到小的函数及堆栈信息,小于 1% 的则不予显示。 103 | 104 | **可以看出**:88.8%的 CPU 时间都花在了 crypto.js 文件的 pbkdf2Sync 函数上,该函数在 app.js 第 3 行被调用,即我们的 hash 函数。 105 | 106 | **解决方法**:将同步的 pbkdf2Sync 改为异步的 pbkdf2。修改代码如下: 107 | 108 | ```js 109 | const crypto = require('crypto') 110 | 111 | function hash (password, cb) { 112 | const salt = crypto.randomBytes(128).toString('base64') 113 | crypto.pbkdf2(password, salt, 10000, 64, 'sha512', cb) 114 | } 115 | 116 | let count = 0 117 | console.time('pbkdf2') 118 | for (let i = 0; i < 100; i++) { 119 | hash('random_password', () => { 120 | count++ 121 | if (count === 100) { 122 | console.timeEnd('pbkdf2') 123 | } 124 | }) 125 | } 126 | ``` 127 | 128 | 运行结果: 129 | 130 | ```sh 131 | $ node --prof app 132 | pbkdf2: 656.332ms 133 | ``` 134 | 135 | 可以看出,程序运行了 656.332ms,相比较于之前的 1375.585ms,性能提升了 1 倍。我们继续看下 v8.log 的分析结果,运行: 136 | 137 | ```sh 138 | $ node --prof-process isolate-0x102802400-v8.log 139 | Statistical profiling result from isolate-0x103001a00-v8.log, (198 ticks, 19 unaccounted, 0 excluded). 140 | 141 | [Shared libraries]: 142 | ticks total nonlib name 143 | 144 | [JavaScript]: 145 | ticks total nonlib name 146 | 1 0.5% 0.5% StoreIC: A store IC from the snapshot 147 | 1 0.5% 0.5% Function: ~set native collection.js:149:4 148 | 1 0.5% 0.5% Function: ~pbkdf2 crypto.js:691:16 149 | 1 0.5% 0.5% Function: ~inherits util.js:962:18 150 | 1 0.5% 0.5% Builtin: ArrayIteratorPrototypeNext 151 | 152 | [C++]: 153 | ticks total nonlib name 154 | 83 41.9% 41.9% T ___kdebug_trace_string 155 | 31 15.7% 15.7% t node::(anonymous namespace)::ContextifyScript::New(v8::FunctionCallbackInfo const&) 156 | 14 7.1% 7.1% T ___pthread_sigmask 157 | ... 158 | 159 | [Summary]: 160 | ticks total nonlib name 161 | 5 2.5% 2.5% JavaScript 162 | 174 87.9% 87.9% C++ 163 | 3 1.5% 1.5% GC 164 | 0 0.0% Shared libraries 165 | 19 9.6% Unaccounted 166 | 167 | [C++ entry points]: 168 | ticks cpp total name 169 | 41 60.3% 20.7% T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) 170 | 17 25.0% 8.6% T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*) 171 | ... 172 | 173 | [Bottom up (heavy) profile]: 174 | Note: percentage shows a share of a particular caller in the total 175 | amount of its parent calls. 176 | Callers occupying less than 1.0% are not shown. 177 | 178 | ticks parent name 179 | 83 41.9% T ___kdebug_trace_string 180 | 181 | 31 15.7% t node::(anonymous namespace)::ContextifyScript::New(v8::FunctionCallbackInfo const&) 182 | 31 100.0% T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) 183 | 31 100.0% Function: ~runInThisContext bootstrap_node.js:495:28 184 | 31 100.0% Function: ~NativeModule.compile bootstrap_node.js:584:44 185 | 31 100.0% Function: ~NativeModule.require bootstrap_node.js:516:34 186 | ... 187 | ``` 188 | 189 | 可以看出,[Bottom up] 没有很多 ticks,而且不再有 pbkdf2 这种堆栈信息。 190 | 191 | ## 1.3.2 Web UI 192 | 193 | V8 还提供了一个 Web 可视化工具来查看生成的 v8 日志。首先,将代码还原到使用 pbkdf2Sync 的版本,运行: 194 | 195 | ```sh 196 | $ node --prof app # 生成 isolate-0x103000000-v8.log 197 | $ node --prof-process --preprocess isolate-0x103000000-v8.log > v8.json # 格式化成 JSON 文件 198 | $ git clone https://github.com/v8/v8.git # 克隆 v8 仓库 199 | $ open v8/tools/profview/index.html # 打开 V8 profiling log processor 200 | ``` 201 | 202 | 点击 “选择文件”,选择刚才生成的 v8.json 文件,点击 “Bottom up” 视图,如下所示: 203 | 204 | ![](./assets/1.3.1.png) 205 | 206 | 有以下两点需要解释: 207 | 208 | 1. 图中的上半部分展示了 CPU 的 timeline,X 轴代表时间的流逝,Y 轴代表当前时间点不同部分占用 CPU 的比例,可以在 timeline 图表上单击左键不放,然后拖动,选择时间区间。 209 | 2. 图中的下半部分展示了当前时间段内 CPU 占用比从大到小降序排列的函数,展开可查看堆栈信息。不同的颜色代表了不同的部分,点击任意一个函数,timeline 底部会展示该函数的执行时间分布。 210 | 211 | ## 1.3.3 参考链接 212 | 213 | - https://github.com/v8/v8/wiki/V8-Profiler 214 | - https://blog.ghaiklor.com/profiling-nodejs-applications-1609b77afe4e 215 | - https://stackoverflow.com/questions/23934451/how-to-read-nodejs-internal-profiler-tick-processor-output 216 | 217 | 上一节:[1.2 v8-profiler](https://github.com/nswbmw/node-in-debugging/blob/master/1.2%20v8-profiler.md) 218 | 219 | 下一节:[2.1 gcore + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/2.1%20gcore%20%2B%20llnode.md) 220 | -------------------------------------------------------------------------------- /2.1 gcore + llnode.md: -------------------------------------------------------------------------------- 1 | ## 2.1.1 Core & Core Dump 2 | 3 | 在开始之前,我们先了解下什么是 Core 和 Core Dump。 4 | 5 | **什么是 Core?** 6 | 7 | > 在使用半导体作为内存材料前,人类用线圈作为内存的材料,线圈就叫作 core ,用线圈做的内存就叫作 core memory。如今,半导体工业蓬勃发展,已经没有人用 core memory 了,不过在许多情况下,人们还是把记忆体叫作 core 。 8 | 9 | **什么是 Core Dump?** 10 | 11 | > 当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫作 Core Dump(中文翻译成 “核心转储”)。我们可以认为 Core Dump 是 “内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。Core Dump 对于编程人员诊断和调试程序是非常有帮助的,因为有些程序中的错误是很难重现的,例如指针异常,而 Core Dump 文件可以再现程序出错时的情景。 12 | 13 | **测试环境** 14 | 15 | ```sh 16 | $ uname -a 17 | Linux nswbmw-VirtualBox 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux 18 | ``` 19 | 20 | **开启 Core Dump** 21 | 22 | 在终端中输入: 23 | 24 | ```sh 25 | $ ulimit -c 26 | ``` 27 | 28 | 查看允许 Core Dump 生成的文件的大小,如果是 0 则表示关闭了 Core Dump。使用以下命令开启 Core Dump,并且不限制 Core Dump 生成的文件大小: 29 | 30 | ```sh 31 | $ ulimit -c unlimited 32 | ``` 33 | 34 | 以上命令只在当前终端环境下有效,如果想永久生效,就需要修改 /etc/security/limits.conf 文件,如下: 35 | 36 | ![](./assets/2.1.1.png) 37 | 38 | ## 2.1.2 [gcore](http://man7.org/linux/man-pages/man1/gcore.1.html) 39 | 40 | 使用 gcore 可以不重启程序而 dump 出特定进程的 core 文件。gcore 使用方法如下: 41 | 42 | ```sh 43 | $ gcore [-o filename] pid 44 | ``` 45 | 46 | 在 Core Dump 时,默认会在执行 gcore 命令的目录生成 core.\ 文件。 47 | 48 | ## 2.1.3 llnode 49 | 50 | 什么是 llnode? 51 | 52 | > Node.js v4.x+ C++ plugin for [LLDB](http://lldb.llvm.org/) - a next generation, high-performance debugger. 53 | 54 | 什么是 LLDB? 55 | 56 | > LLDB is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler. 57 | 58 | 安装 llnode + lldb: 59 | 60 | ```sh 61 | $ sudo apt-get update 62 | 63 | # Clone llnode 64 | $ git clone https://github.com/nodejs/llnode.git ~/llnode && cd ~/llnode 65 | 66 | # Install lldb and headers 67 | $ sudo apt-get install lldb-4.0 liblldb-4.0-dev 68 | 69 | # Initialize GYP 70 | $ git clone https://github.com/bnoordhuis/gyp.git tools/gyp 71 | 72 | # Configure 73 | $ ./gyp_llnode -Dlldb_dir=/usr/lib/llvm-4.0/ 74 | 75 | # Build 76 | $ make -C out/ -j9 77 | 78 | # Install 79 | $ sudo make install-linux 80 | ``` 81 | 82 | **注意**:如果 `sudo apt-get update` 遇到这种错误: 83 | 84 | ``` 85 | W: GPG error: xxx stable Release: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 6DA62DE462C7DA6D 86 | ``` 87 | 88 | 可以用以下命令解决: 89 | 90 | ```sh 91 | $ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6DA62DE462C7DA6D 92 | ``` 93 | 94 | --recv-keys 后面跟的是前面报错提示的 PUBKEY。 95 | 96 | ## 2.1.4 测试 Core Dump 97 | 98 | 下面用一个典型的全局变量缓存导致的内存泄漏的例子来测试 llnode 的用法。代码如下: 99 | 100 | **app.js** 101 | 102 | ```js 103 | const leaks = [] 104 | 105 | function LeakingClass() { 106 | this.name = Math.random().toString(36) 107 | this.age = Math.floor(Math.random() * 100) 108 | } 109 | 110 | setInterval(() => { 111 | for (let i = 0; i < 100; i++) { 112 | leaks.push(new LeakingClass) 113 | } 114 | 115 | console.warn('Leaks: %d', leaks.length) 116 | }, 1000) 117 | ``` 118 | 119 | 运行该程序: 120 | 121 | ```sh 122 | $ node app.js 123 | ``` 124 | 125 | 等待几秒,打开另一个终端运行 gcore: 126 | 127 | ```sh 128 | $ ulimit -c unlimited 129 | $ sudo gcore `pgrep -n node` 130 | ``` 131 | 132 | 生成 core.2763 文件。 133 | 134 | ## 2.1.5 分析 Core 文件 135 | 136 | 使用 lldb 加载刚才生成的 Core 文件: 137 | 138 | ```sh 139 | $ lldb-4.0 -c ./core.2763 140 | (lldb) target create --core "./core.2763" 141 | Core file '/home/nswbmw/test/./core.2763' (x86_64) was loaded. 142 | (lldb) 143 | ``` 144 | 145 | 输入 v8 查看使用文档,有以下几条命令: 146 | 147 | - bt 148 | - findjsinstances 149 | - findjsobjects 150 | - findrefs 151 | - inspect 152 | - nodeinfo 153 | - print 154 | - source 155 | 156 | 运行 `v8 findjsobjects` 查看所有对象实例及总共占内存大小: 157 | 158 | ``` 159 | (lldb) v8 findjsobjects 160 | Instances Total Size Name 161 | ---------- ---------- ---- 162 | ... 163 | 2100 84000 LeakingClass 164 | 8834 39792 (String) 165 | ---------- ---------- 166 | 12088 181320 167 | ``` 168 | 169 | **可以看出**:LeakingClass 有 2100 个实例,占内存 84000 byte。使用 `v8 findjsinstances` 查看所有 LeakingClass 实例: 170 | 171 | ``` 172 | (lldb) v8 findjsinstances LeakingClass 173 | 0x000022aaa118ab19: 174 | 0x000022aaa118acf9: 175 | 0x000022aaa118ade1: 176 | ... 177 | ``` 178 | 179 | 使用 `v8 i` 检索实例的具体内容: 180 | 181 | ``` 182 | (lldb) v8 i 0x000022aaa118ab19 183 | 0x000022aaa118ab19:, 185 | .age=}> 186 | (lldb) v8 i 0x000022aaa118acf9 187 | 0x000022aaa118acf9:, 189 | .age=}> 190 | (lldb) v8 i 0x000022aaa118ade1 191 | 0x000022aaa118ade1:, 193 | .age=}> 194 | ``` 195 | 196 | 可以看到每个 LeakingClass 实例的 name 和 age 字段的值。 197 | 198 | 使用 `v8 findrefs` 查看引用: 199 | 200 | ``` 201 | (lldb) v8 findrefs 0x000022aaa118ab19 202 | 0x22aaa1189729: (Array)[0]=0x22aaa118ab19 203 | (lldb) v8 i 0x22aaa1189729 204 | 0x000022aaa1189729:, 206 | [1]=0x000022aaa118acf9:, 207 | [2]=0x000022aaa118ade1:, 208 | [3]=0x000022aaa118aea1:, 209 | [4]=0x000022aaa118af61:, 210 | [5]=0x000022aaa118b021:, 211 | [6]=0x000022aaa118b0e1:, 212 | [7]=0x000022aaa118b1a1:, 213 | [8]=0x000022aaa118b221:, 214 | [9]=0x000022aaa118b2a1:, 215 | [10]=0x000022aaa118b321:, 216 | [11]=0x000022aaa118b3a1:, 217 | [12]=0x000022aaa118b421:, 218 | [13]=0x000022aaa118b4a1:, 219 | [14]=0x000022aaa118b521:, 220 | [15]=0x000022aaa118b5a1:}> 221 | ``` 222 | 223 | **可以看出**:通过一个 LeakingClass 实例的内存地址,我们使用 `v8 findrefs` 找到了引用它的数组的内存地址,然后通过这个地址去检索数组,得到这个数组长度为 2100,每一项都是一个 LeakingClass 实例,这不就是我们代码中的 leaks 数组吗? 224 | 225 | **小提示**: `v8 i` 是 `v8 inspect` 的缩写,`v8 p` 是 `v8 print` 的缩写。 226 | 227 | ## 2.1.6 --abort-on-uncaught-exception 228 | 229 | 在 Node.js 程序启动时添加 --abort-on-uncaught-exception 参数,当程序 crash 的时候,会自动 Core Dump,方便 “死后验尸”。 230 | 231 | 添加 --abort-on-uncaught-exception 参数,启动测试程序: 232 | 233 | ```sh 234 | $ ulimit -c unlimited 235 | $ node --abort-on-uncaught-exception app.js 236 | ``` 237 | 238 | 启动另外一个终端运行: 239 | 240 | ```sh 241 | $ kill -BUS `pgrep -n node` 242 | ``` 243 | 244 | 第 1 个终端会显示: 245 | 246 | ```sh 247 | Leaks: 100 248 | Leaks: 200 249 | Leaks: 300 250 | Leaks: 400 251 | Leaks: 500 252 | Leaks: 600 253 | Leaks: 700 254 | Leaks: 800 255 | Bus error (core dumped) 256 | ``` 257 | 258 | 调试步骤与上面一致: 259 | 260 | ```sh 261 | $ lldb-4.0 -c ./core 262 | (lldb) target create --core "./core" 263 | Core file '/home/nswbmw/test/./core' (x86_64) was loaded. 264 | (lldb) v8 findjsobjects 265 | Instances Total Size Name 266 | ---------- ---------- ---- 267 | ... 268 | 800 32000 LeakingClass 269 | 7519 38512 (String) 270 | ---------- ---------- 271 | 9440 126368 272 | ``` 273 | 274 | ## 2.1.7 总结 275 | 276 | 我们的测试代码很简单,没有引用任何第三方模块,如果项目较大且引用的模块较多,则 `v8 findjsobjects` 的结果将难以甄别,这时可以多次使用 gcore 进行 Core Dump,对比发现增长的对象,再进行诊断。 277 | 278 | ## 2.1.8 参考链接 279 | 280 | - http://www.cnblogs.com/Anker/p/6079580.html 281 | - http://www.brendangregg.com/blog/2016-07-13/llnode-nodejs-memory-leak-analysis.html 282 | 283 | 上一节:[1.3 Tick Processor](https://github.com/nswbmw/node-in-debugging/blob/master/1.3%20Tick%20Processor.md) 284 | 285 | 下一节:[2.2 heapdump](https://github.com/nswbmw/node-in-debugging/blob/master/2.2%20heapdump.md) 286 | -------------------------------------------------------------------------------- /2.2 heapdump.md: -------------------------------------------------------------------------------- 1 | [heapdump](https://github.com/bnoordhuis/node-heapdump) 是一个 dump V8 堆信息的工具。[v8-profiler](https://github.com/node-inspector/v8-profiler) 也包含了这个功能,这两个工具的原理都是一致的,都是 v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(title, control),但是 heapdump 的使用简单些。下面我们以 heapdump 为例讲解如何分析 Node.js 的内存泄漏。 2 | 3 | ## 2.2.1 使用 heapdump 4 | 5 | 这里以一段经典的内存泄漏代码作为测试代码: 6 | 7 | **app.js** 8 | 9 | ```js 10 | const heapdump = require('heapdump') 11 | let leakObject = null 12 | let count = 0 13 | 14 | setInterval(function testMemoryLeak() { 15 | const originLeakObject = leakObject 16 | const unused = function () { 17 | if (originLeakObject) { 18 | console.log('originLeakObject') 19 | } 20 | } 21 | leakObject = { 22 | count: String(count++), 23 | leakStr: new Array(1e7).join('*'), 24 | leakMethod: function () { 25 | console.log('leakMessage') 26 | } 27 | } 28 | }, 1000) 29 | ``` 30 | 31 | 为什么这段程序会发生内存泄漏呢?首先我们要明白闭包的原理:**同一个函数内部的闭包作用域只有一个,所有闭包共享。在执行函数的时候,如果遇到闭包,则会创建闭包作用域的内存空间,将该闭包所用到的局部变量添加进去,然后再遇到闭包时,会在之前创建好的作用域空间添加此闭包会用到而前闭包没用到的变量。函数结束时,会清除没有被闭包作用域引用的变量。** 32 | 33 | **这段代码内存泄露原因是**:在 testMemoryLeak 函数内有两个闭包:unused 和 leakMethod。unused 这个闭包引用了父作用域中的 originLeakObject 变量,如果没有后面的 leakMethod,则会在函数结束后被清除,闭包作用域也跟着被清除了。因为后面的 leakObject 是全局变量,即 leakMethod 是全局变量,它引用的闭包作用域(包含了 unused 所引用的 originLeakObject)不会释放。而随着 testMemoryLeak 不断的调用,originLeakObject 指向前一次的 leakObject,下次的 leakObject.leakMethod 又会引用之前的 originLeakObject,从而形成一个闭包引用链,而 leakStr 是一个大字符串,得不到释放,从而造成了内存泄漏。 34 | 35 | **解决方法**:在 testMemoryLeak 函数内部的最后添加 `originLeakObject = null` 即可。 36 | 37 | 运行测试代码: 38 | 39 | ```sh 40 | $ node app 41 | ``` 42 | 43 | 然后先后执行**两**次: 44 | 45 | ```sh 46 | $ kill -USR2 `pgrep -n node` 47 | ``` 48 | 49 | 在当前目录下生成了两个 heapsnapshot 文件: 50 | 51 | ``` 52 | heapdump-100427359.61348.heapsnapshot 53 | heapdump-100438986.797085.heapsnapshot 54 | ``` 55 | 56 | ## 2.2.2 Chrome DevTools 57 | 58 | 我们使用 Chrome DevTools 来分析前面生成的 heapsnapshot 文件。调出 Chrome DevTools -> Memory -> Load,按顺序依次加载前面生成的 heapsnapshot 文件。单击第 2 个堆快照,在左上角有个下拉菜单,有如下 4 个选项: 59 | 60 | 1. Summary:以构造函数名分类显示。 61 | 2. Comparison:比较多个快照之间的差异。 62 | 3. Containment:查看整个 GC 路径。 63 | 4. Statistics:以饼状图显示内存占用信息。 64 | 65 | 通常我们只会用前两个选项;第 3 个选项一般用不到,因为在展开 Summary 和 Comparison 中的每一项时,都可以看到从 GC roots 到这个对象的路径;第 4 个选项只能看到内存占用比,如下图所示: 66 | 67 | ![](./assets/2.2.1.png) 68 | 69 | 切换到 Summary 页,可以看到有如下 5 个属性: 70 | 71 | 1. Contructor:构造函数名,例如 Object、Module、Socket,(array)、(string)、(regexp) 等加了括号的分别代表内置的 Array、String 和 Regexp。 72 | 2. Distance:到 GC roots (GC 根对象)的距离。GC 根对象在浏览器中一般是 window 对象,在 Node.js 中是 global 对象,距离越大,则说明引用越深。 73 | 3. Objects Count:对象个数。 74 | 4. Shallow Size:对象自身的大小,不包括它引用的对象。 75 | 5. Retained Size:对象自身的大小和它引用的对象的大小,即该对象被 GC 之后所能回收的内存大小。 76 | 77 | **小提示**:一个对象的 Retained Size = 该对象的 Shallow Size + 该对象支配树上其子节点的 Retained Size 之和。Shallow Size == Retained Size 的有 (boolean)、(number)、(string),它们无法引用其他值,并且始终是叶子节点。 78 | 79 | 单击 Retained Size 选择降序展示,可以看到 (closure) 这一项引用的内容达到 98%,继续展开如下: 80 | 81 | ![](./assets/2.2.2.png) 82 | 83 | **可以看出**:一个 leakStr 占了 8% 的内存,而 leakMethod 引用了 81% 的内存。对象保留树(Retainers,老版本 Chrome 中叫 Object's retaining tree)展示了对象的 GC path,单击如上图中的 leakStr(Distance 是 13),Retainers 会自动展开,Distance 从 13 递减到 1。 84 | 85 | 继续展开 leakMethod,如下所示: 86 | 87 | ![](./assets/2.2.3.png) 88 | 89 | **可以看出**:有一个 count="10" 的 originLeakObject 的 leakMethod 函数的 context(即上下文) 引用了一个 count="9" 的 originLeakObject 对象,而这个 originLeakObject 对象的 leakMethod 函数的 context 又引用了 count="8" 的 originLeakObject 对象,以此类推。而每个 originLeakObject 对象上都有一个大字符串 leakStr(占用 8% 的内存),从而造成内存泄漏,符合我们之前的推断。 90 | 91 | **小提示**:如果背景色是黄色的,则表示这个对象在 JavaScript 中还存在引用,所以可能没有被清除。如果背景色是红色的,则表示这个对象在 JavaScript 中不存在引用,但是依然存活在内存中,一般常见于 DOM 对象,它们存放的位置和 JavaScript 中的对象还是有不同的,在 Node.js 中很少遇见。 92 | 93 | ## 2.2.3 对比快照 94 | 95 | 切换到 Comparison 视图下,可以看到 #New、#Deleted、#Delta 等属性,+ 和 - 表示相对于比较的堆快照而言。我们对比第 2 个快照和第 1 个快照,如下所示: 96 | 97 | ![](./assets/2.2.4.png) 98 | 99 | **可以看出**:(string) 增加了 10 个,每个 string 大小为 10000024 字节。 100 | 101 | ## 2.2.4 参考链接 102 | 103 | - https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156 104 | - https://www.zhihu.com/question/56806069 105 | - http://taobaofed.org/blog/2016/04/15/how-to-find-memory-leak/ 106 | - https://developers.google.com/web/tools/chrome-devtools/memory-problems/memory-101 107 | 108 | 上一节:[2.1 gcore + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/2.1%20gcore%20%2B%20llnode.md) 109 | 110 | 下一节:[2.3 memwatch-next](https://github.com/nswbmw/node-in-debugging/blob/master/2.3%20memwatch-next.md) 111 | -------------------------------------------------------------------------------- /2.3 memwatch-next.md: -------------------------------------------------------------------------------- 1 | [memwatch-next](https://github.com/marcominetti/node-memwatch)(以下简称 memwatch)是一个用来监测 Node.js 的内存泄漏和堆信息比较的模块。下面我们以一段事件监听器导致内存泄漏的代码为例,讲解如何使用 memwatch。 2 | 3 | ## 2.3.1 使用 memwatch-next 4 | 5 | 测试代码如下: 6 | 7 | **app.js** 8 | 9 | ```js 10 | let count = 1 11 | const memwatch = require('memwatch-next') 12 | memwatch.on('stats', (stats) => { 13 | console.log(count++, stats) 14 | }) 15 | memwatch.on('leak', (info) => { 16 | console.log('---') 17 | console.log(info) 18 | console.log('---') 19 | }) 20 | 21 | const http = require('http') 22 | const server = http.createServer((req, res) => { 23 | for (let i = 0; i < 10000; i++) { 24 | server.on('request', function leakEventCallback() {}) 25 | } 26 | res.end('Hello World') 27 | global.gc() 28 | }).listen(3000) 29 | ``` 30 | 31 | 在每个请求到来时,在 server 上注册 10000 个 request 事件的监听函数(大量的事件监听函数存储到内存中,从而造成了内存泄漏),然后手动触发一次 GC。 32 | 33 | 运行该程序: 34 | 35 | ```sh 36 | $ node --expose-gc app.js 37 | ``` 38 | 39 | **注意**:这里添加 --expose-gc 参数启动程序,这样我们才可以在程序中手动触发 GC。 40 | 41 | memwatch 监听以下两个事件: 42 | 43 | 1. stats:GC 事件,每执行一次 GC,都会触发该函数,打印 heap 相关的信息。如下: 44 | 45 | ```js 46 | { 47 | num_full_gc: 1,// 完整的垃圾回收次数 48 | num_inc_gc: 1,// 增长的垃圾回收次数 49 | heap_compactions: 1,// 内存压缩次数 50 | usage_trend: 0,// 使用趋势 51 | estimated_base: 5350136,// 预期基数 52 | current_base: 5350136,// 当前基数 53 | min: 0,// 最小值 54 | max: 0// 最大值 55 | } 56 | ``` 57 | 58 | 2. leak:可疑的内存泄露事件,触发该事件的条件是:内存在连续 5 次 GC 后都是增长的。打印如下: 59 | 60 | ```js 61 | { 62 | growth: 4051464, 63 | reason: 'heap growth over 5 consecutive GCs (2s) - -2147483648 bytes/hr' 64 | } 65 | ``` 66 | 67 | 运行: 68 | 69 | ```sh 70 | $ ab -c 1 -n 5 http://localhost:3000/ 71 | ``` 72 | 73 | 输出: 74 | 75 | ```js 76 | (node:20989) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit 77 | 1 { num_full_gc: 1, 78 | num_inc_gc: 1, 79 | heap_compactions: 1, 80 | usage_trend: 0, 81 | estimated_base: 5720064, 82 | current_base: 5720064, 83 | min: 0, 84 | max: 0 } 85 | 2 { num_full_gc: 2, 86 | num_inc_gc: 1, 87 | heap_compactions: 2, 88 | usage_trend: 0, 89 | estimated_base: 7073824, 90 | current_base: 7073824, 91 | min: 0, 92 | max: 0 } 93 | 3 { num_full_gc: 3, 94 | num_inc_gc: 1, 95 | heap_compactions: 3, 96 | usage_trend: 0, 97 | estimated_base: 7826368, 98 | current_base: 7826368, 99 | min: 7826368, 100 | max: 7826368 } 101 | 4 { num_full_gc: 4, 102 | num_inc_gc: 1, 103 | heap_compactions: 4, 104 | usage_trend: 0, 105 | estimated_base: 8964784, 106 | current_base: 8964784, 107 | min: 7826368, 108 | max: 8964784 } 109 | --- 110 | { growth: 3820272, 111 | reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' } 112 | --- 113 | 5 { num_full_gc: 5, 114 | num_inc_gc: 1, 115 | heap_compactions: 5, 116 | usage_trend: 0, 117 | estimated_base: 9540336, 118 | current_base: 9540336, 119 | min: 7826368, 120 | max: 9540336 } 121 | ``` 122 | 123 | **可以看出**:Node.js 已经警告我们事件监听器超过了 11 个,可能造成内存泄露。连续 5 次内存增长触发 leak 事件打印出增长了多少内存(bytes)和预估每小时增长多少 bytes。 124 | 125 | ## 2.3.2 Heap Diffing 126 | 127 | memwatch 有一个 HeapDiff 函数,用来对比并计算出两次堆快照的差异。修改测试代码如下: 128 | 129 | ```js 130 | const memwatch = require('memwatch-next') 131 | const http = require('http') 132 | const server = http.createServer((req, res) => { 133 | for (let i = 0; i < 10000; i++) { 134 | server.on('request', function leakEventCallback() {}) 135 | } 136 | res.end('Hello World') 137 | global.gc() 138 | }).listen(3000) 139 | 140 | const hd = new memwatch.HeapDiff() 141 | memwatch.on('leak', (info) => { 142 | const diff = hd.end() 143 | console.dir(diff, { depth: 10 }) 144 | }) 145 | ``` 146 | 147 | 运行这段代码并执行同样的 ab 命令,打印如下: 148 | 149 | ```js 150 | { before: { nodes: 35727, size_bytes: 4725128, size: '4.51 mb' }, 151 | after: { nodes: 87329, size_bytes: 8929792, size: '8.52 mb' }, 152 | change: 153 | { size_bytes: 4204664, 154 | size: '4.01 mb', 155 | freed_nodes: 862, 156 | allocated_nodes: 52464, 157 | details: 158 | [ ... 159 | { what: 'Array', 160 | size_bytes: 530200, 161 | size: '517.77 kb', 162 | '+': 1023, 163 | '-': 510 }, 164 | { what: 'Closure', 165 | size_bytes: 3599856, 166 | size: '3.43 mb', 167 | '+': 50001, 168 | '-': 3 }, 169 | ... 170 | ] 171 | } 172 | } 173 | ``` 174 | 175 | **可以看出**:内存由 4.51mb 涨到了 8.52mb,其中 Closure 和 Array 涨了绝大部分,而我们知道注册事件监听函数的本质就是将事件函数(Closure)push 到相应的数组(Array)里。 176 | 177 | ## 2.3.3 结合 heapdump 178 | 179 | memwatch 在结合 heapdump 使用时才能发挥更好的作用。通常用 memwatch 监测到内存泄漏,用 heapdump 导出多份堆快照,然后用 Chrome DevTools 分析和比较,定位内存泄漏的元凶。 180 | 181 | 修改代码如下: 182 | 183 | ```js 184 | const memwatch = require('memwatch-next') 185 | const heapdump = require('heapdump') 186 | 187 | const http = require('http') 188 | const server = http.createServer((req, res) => { 189 | for (let i = 0; i < 10000; i++) { 190 | server.on('request', function leakEventCallback() {}) 191 | } 192 | res.end('Hello World') 193 | global.gc() 194 | }).listen(3000) 195 | 196 | dump() 197 | memwatch.on('leak', () => { 198 | dump() 199 | }) 200 | 201 | function dump() { 202 | const filename = `${__dirname}/heapdump-${process.pid}-${Date.now()}.heapsnapshot` 203 | 204 | heapdump.writeSnapshot(filename, () => { 205 | console.log(`${filename} dump completed.`) 206 | }) 207 | } 208 | ``` 209 | 210 | 以上程序在启动后先执行一次 heap dump,当触发 leak 事件时再执行一次 heap dump。运行这段代码并执行同样的 ab 命令,生成两个 heapsnapshot 文件: 211 | 212 | ``` 213 | heapdump-21126-1519545957879.heapsnapshot 214 | heapdump-21126-1519545975702.heapsnapshot 215 | ``` 216 | 217 | 用 Chrome DevTools 加载这两个 heapsnapshot 文件,选择 comparison 比较视图,如下所示: 218 | 219 | ![](./assets/2.3.1.png) 220 | 221 | **可以看出**:增加了 5 万个 leakEventCallback 函数,单击其中任意一个,可以从 Retainers 中看到更详细的信息,例如 GC path 和所在的文件等信息。 222 | 223 | ## 2.3.4 参考链接 224 | 225 | - https://github.com/marcominetti/node-memwatch 226 | 227 | 上一节:[2.2 heapdump](https://github.com/nswbmw/node-in-debugging/blob/master/2.2%20heapdump.md) 228 | 229 | 下一节:[2.4 cpu-memory-monitor](https://github.com/nswbmw/node-in-debugging/blob/master/2.4%20cpu-memory-monitor.md) 230 | -------------------------------------------------------------------------------- /2.4 cpu-memory-monitor.md: -------------------------------------------------------------------------------- 1 | 前面介绍了 heapdump 和 memwatch-next 的用法,但在实际使用时并不那么方便,我们总不能一直盯着服务器的状况,在发现内存持续增长并超过心里的阈值时,再手动去触发 Core Dump 吧?在大多数情况下发现问题时,就已经错过了现场。所以,我们可能需要 [cpu-memory-monitor](https://github.com/nswbmw/cpu-memory-monitor)。顾名思义,这个模块可以用来监控 CPU 和 Memory 的使用情况,并可以根据配置策略自动 dump CPU 的使用情况(cpuprofile)和内存快照(heapsnapshot)。 2 | 3 | ## 2.4.1 使用 cpu-memory-monitor 4 | 5 | 我们先来看看如何使用 cpu-memory-monitor,其实很简单,只需在进程启动的入口文件中引入以下代码: 6 | 7 | ```js 8 | require('cpu-memory-monitor')({ 9 | cpu: { 10 | interval: 1000, 11 | duration: 30000, 12 | threshold: 60, 13 | profileDir: '/tmp', 14 | counter: 3, 15 | limiter: [5, 'hour'] 16 | } 17 | }) 18 | ``` 19 | 20 | **上述代码的作用是**:每 **1000ms**(interval)检查一次 CPU 的使用情况,如果发现连续 **3**(counter)次 CPU 使用率大于 **60%**(threshold),则 dump **30000ms**(duration) CPU 的使用情况,生成 `cpu-${process.pid}-${Date.now()}.cpuprofile` 到 **/tmp**(profileDir) 目录下,**1**(limiter[1]) 小时最多 dump **5**(limiter[0]) 次。 21 | 22 | 以上是自动 dump CPU 使用情况的策略。dump Memory 使用情况的策略同理: 23 | 24 | ```js 25 | require('cpu-memory-monitor')({ 26 | memory: { 27 | interval: 1000, 28 | threshold: '1.2gb', 29 | profileDir: '/tmp', 30 | counter: 3, 31 | limiter: [3, 'hour'] 32 | } 33 | }) 34 | ``` 35 | 36 | **上述代码的作用是**:每 **1000ms**(interval) 检查一次 Memory 的使用情况,如果发现连续 **3**(counter) 次 Memory 大于 **1.2gb**(threshold),则 dump 一次 Memory,生成 `memory-${process.pid}-${Date.now()}.heapsnapshot` 到 **/tmp**(profileDir) 目录下,**1**(limiter[1]) 小时最多 dump **3**(limiter[0]) 次。 37 | 38 | **注意**:memory 的配置没有 duration 参数,因为 Memroy 的 dump 只是某一时刻的,而不是一段时间的。 39 | 40 | 聪明的你肯定会问了:能不能将 cpu 和 memory 配置一块使用?比如: 41 | 42 | ```js 43 | require('cpu-memory-monitor')({ 44 | cpu: { 45 | interval: 1000, 46 | duration: 30000, 47 | threshold: 60, 48 | ... 49 | }, 50 | memory: { 51 | interval: 10000, 52 | threshold: '1.2gb', 53 | ... 54 | } 55 | }) 56 | ``` 57 | 58 | 答案是:可以,但不要这么做。因为这样做可能会出现这种情况:内存高了且达到设定的阈值 -> 触发 Memory Dump/GC -> 导致 CPU 使用率高且达到设定的阈值 -> 触发 CPU Dump -> 导致堆积的请求越来越多(比如内存中堆积了很多 SQL 查询)-> 触发 Memory Dump -> 导致雪崩。 59 | 60 | 通常情况下,只使用其中一种就可以了。 61 | 62 | ## 2.4.2 源码解读 63 | 64 | cpu-memory-monitor 的源代码不过百余行,大体逻辑如下: 65 | 66 | ```js 67 | ... 68 | const processing = { 69 | cpu: false, 70 | memory: false 71 | } 72 | 73 | const counter = { 74 | cpu: 0, 75 | memory: 0 76 | } 77 | 78 | function dumpCpu(cpuProfileDir, cpuDuration) { ... } 79 | function dumpMemory(memProfileDir) { ... } 80 | 81 | module.exports = function cpuMemoryMonitor(options = {}) { 82 | ... 83 | if (options.cpu) { 84 | const cpuTimer = setInterval(() => { 85 | if (processing.cpu) { 86 | return 87 | } 88 | pusage.stat(process.pid, (err, stat) => { 89 | if (err) { 90 | clearInterval(cpuTimer) 91 | return 92 | } 93 | if (stat.cpu > cpuThreshold) { 94 | counter.cpu += 1 95 | if (counter.cpu >= cpuCounter) { 96 | memLimiter.removeTokens(1, (limiterErr, remaining) => { 97 | if (limiterErr) { 98 | return 99 | } 100 | if (remaining > -1) { 101 | dumpCpu(cpuProfileDir, cpuDuration) 102 | counter.cpu = 0 103 | } 104 | }) 105 | } else { 106 | counter.cpu = 0 107 | } 108 | } 109 | }) 110 | }, cpuInterval) 111 | } 112 | 113 | if (options.memory) { 114 | ... 115 | memwatch.on('leak', () => { 116 | dumpMemory(...) 117 | }) 118 | } 119 | } 120 | ``` 121 | 122 | **可以看出**:cpu-memory-monitor 没有用到什么新鲜的东西,还是之前讲解过的 v8-profiler、heapdump、memwatch-next 的组合使用而已。 123 | 124 | 有以下几点需要注意: 125 | 126 | 1. 只有传入了 cpu 或者 memory 的配置,才会去监听相应的 CPU 或者 Memory。 127 | 2. 在传入 memory 配置时,因为用 memwatch-next 额外监听了 leak 事件,也会 dump Memory,格式是 `leak-memory-${process.pid}-${Date.now()}.heapsnapshot`。 128 | 3. 顶部引入了 heapdump,所以即使没有 memory 配置,也可以通过 `kill -USR2 ` 手动触发 Memory Dump。 129 | 130 | ## 2.4.3 参考链接 131 | 132 | - https://github.com/node-inspector/v8-profiler 133 | - https://github.com/bnoordhuis/node-heapdump 134 | - https://github.com/marcominetti/node-memwatch 135 | 136 | 上一节:[2.3 memwatch-next](https://github.com/nswbmw/node-in-debugging/blob/master/2.3%20memwatch-next.md) 137 | 138 | 下一节:[3.1 Promise](https://github.com/nswbmw/node-in-debugging/blob/master/3.1%20Promise.md) 139 | -------------------------------------------------------------------------------- /3.2 Async + Await.md: -------------------------------------------------------------------------------- 1 | 笔者在很长一段时间内都在使用 koa@1 +(generator|bluebird)+ sequelize 这个组合,这个组合并没有什么问题,也很常见,但是到了滥用的地步,导致后来维护和调试起来都很痛苦。若排除 sequelize 这个我们不得不用的模块,从调试 cpuprofile 的角度讲讲为什么笔者认为应该用 async/await + Promise 替代 co + generator|bluebird。 2 | 3 | 笔者的观点是:**使用原生模块具有更清晰的调用栈**。 4 | 5 | 下面用 4 个例子进行对比,看看实现相同逻辑的不同代码生成的 cpuprofile 中调用栈的信息。 6 | 7 | ## 3.2.1 async + await 8 | 9 | **async.js** 10 | 11 | ```js 12 | const fs = require('fs') 13 | const profiler = require('v8-profiler') 14 | 15 | async function A () { 16 | return await Promise.resolve('A') 17 | } 18 | 19 | async function B () { 20 | return await A() 21 | } 22 | 23 | (async function asyncWrap () { 24 | const start = Date.now() 25 | profiler.startProfiling() 26 | while (Date.now() - start < 10000) { 27 | await B() 28 | } 29 | const profile = profiler.stopProfiling() 30 | profile.export() 31 | .pipe(fs.createWriteStream('async.cpuprofile')) 32 | .on('finish', () => { 33 | profile.delete() 34 | console.error('async.cpuprofile export success') 35 | }) 36 | })() 37 | ``` 38 | 39 | 加载运行后生成的 async.cpuprofile,如下所示: 40 | 41 | ![](./assets/3.1.1.png) 42 | 43 | **可以看出**:asyncWrap 中调用了 B 函数,B 函数调用了 A 函数,A 函数中 resolve 了一个值。在 asyncWrap 中还调用了 stopProfiling 函数。 44 | 45 | ## 3.2.2 co + yield 46 | 47 | **co.js** 48 | 49 | ```js 50 | const fs = require('fs') 51 | const co = require('co') 52 | const profiler = require('v8-profiler') 53 | 54 | function * A () { 55 | return yield Promise.resolve('A') 56 | } 57 | 58 | function * B () { 59 | return yield A() 60 | } 61 | 62 | co(function * coWrap () { 63 | const start = Date.now() 64 | profiler.startProfiling() 65 | while (Date.now() - start < 10000) { 66 | yield B() 67 | } 68 | const profile = profiler.stopProfiling() 69 | profile.export() 70 | .pipe(fs.createWriteStream('co.cpuprofile')) 71 | .on('finish', () => { 72 | profile.delete() 73 | console.error('co.cpuprofile export success') 74 | }) 75 | }) 76 | ``` 77 | 78 | 加载运行后生成的 co.cpuprofile,如下所示: 79 | 80 | ![](./assets/3.1.2.png) 81 | 82 | **可以看出**:调用栈非常深,有太多没有用的 co 相关的调用栈。如果 n 个 generator 层层嵌套,就会出现 n 倍的 (anonymous)->onFullfiled->next->toPromise->co->Promise->(anonymous) 调用栈。如果你读过 co 的源码,就可能知道,这是 co 将 generator 解包的过程。其实这个可以通过将 `yield generator` 替换成 `yield* generator` 来优化。 83 | 84 | ## 3.2.3 co + yield* 85 | 86 | **co_better.js** 87 | 88 | ```js 89 | const fs = require('fs') 90 | const co = require('co') 91 | const profiler = require('v8-profiler') 92 | 93 | function * A () { 94 | return yield Promise.resolve('A') 95 | } 96 | 97 | function * B () { 98 | return yield * A() 99 | } 100 | 101 | co(function * coWrap () { 102 | const start = Date.now() 103 | profiler.startProfiling() 104 | while (Date.now() - start < 10000) { 105 | yield * B() 106 | } 107 | const profile = profiler.stopProfiling() 108 | profile.export() 109 | .pipe(fs.createWriteStream('co_better.cpuprofile')) 110 | .on('finish', () => { 111 | profile.delete() 112 | console.error('co_better.cpuprofile export success') 113 | }) 114 | }) 115 | ``` 116 | 117 | 加载运行后生成的 co_better.cpuprofile,如下所示: 118 | 119 | ![](./assets/3.1.3.png) 120 | 121 | **可以看出**:与 co.js 相比,调用栈清晰了很多,不过与使用 async/await 相比,还是多了些 onFulfilled、next。 122 | 123 | ## 3.2.4 co + bluebird 124 | 125 | **co_bluebird.js** 126 | 127 | ```js 128 | const fs = require('fs') 129 | const co = require('co') 130 | const Promise = require('bluebird') 131 | const profiler = require('v8-profiler') 132 | 133 | function * A () { 134 | return yield Promise.resolve('A') 135 | } 136 | 137 | function * B () { 138 | return yield * A() 139 | } 140 | 141 | co(function * coBluebirdWrap () { 142 | const start = Date.now() 143 | profiler.startProfiling() 144 | while (Date.now() - start < 10000) { 145 | yield * B() 146 | } 147 | const profile = profiler.stopProfiling() 148 | profile.export() 149 | .pipe(fs.createWriteStream('co_bluebird.cpuprofile')) 150 | .on('finish', () => { 151 | profile.delete() 152 | console.error('co_bluebird.cpuprofile export success') 153 | }) 154 | }) 155 | ``` 156 | 157 | 加载运行后生成的 co_bluebird.cpuprofile,如下所示: 158 | 159 | ![](./assets/3.1.4.png) 160 | 161 | **可以看出**:与 co_better.js 相比,调用栈中多了许多 bluebird 模块的无用信息。而且这只是非常简单的示例代码,要是在复杂的业务逻辑中大量使用 bluebird 代码生成的 cpuprofile,就几乎没法看了。 162 | 163 | **结论**:使用 async/await + Promise + 命名函数,具有更清晰的调用栈,让分析 cpuprofile 时不再痛苦。 164 | 165 | 聪明的你可能会问: 166 | 167 | 1. 为什么不建议用 bluebird?因为: 168 | 1. 随着 V8 不断优化,原生 Promise 性能逐渐提高,bluebird 的性能优势不明显。 169 | 2. 原生 Promise 的 API 足够用,至少能覆盖大部分使用场景,而且还在不断完善,未来还会添加新的 API,例如:Promise.prototype.finally。 170 | 3. 具有更清晰的调用栈。 171 | 172 | 2. 由于历史遗留原因,现在代码中大量使用了 yield + generator 怎么办?可以: 173 | 1. 将所有 yield generator 替换成 yield * generator。 174 | 2. 升级到 node@8+,逐步用 async/await 替换,毕竟 async 函数调用后返回的也是一个 promise,也是 yieldable 的。 175 | 3. 性能比较呢? 176 | 1. node@8+ 下 async/await 完胜 co。 177 | 178 | ## 3.2.5 yield -> yield* 遇到的坑 179 | 180 | 上面讲到,可以将 yield generator 改成 yield * generator,这里面有一个坑,是由于不明白 co 的原理而滥用 co 导致的。代码如下: 181 | 182 | ```js 183 | const co = require('co') 184 | 185 | function * genFunc () { 186 | return Promise.resolve('genFunc') 187 | } 188 | 189 | co(function * () { 190 | console.log(yield genFunc()) // => genFunc 191 | console.log(yield * genFunc()) // => Promise { 'genFunc' } 192 | }) 193 | ``` 194 | 195 | **可以看出**:genFunc 这个 generatorFunction 在执行后会返回一个 promise,当使用 `yield genFunc()` 的时候,co 判断返回了一个 promise 会继续帮我们调用它的 then 从而得到真正的字符串。如果使用 `yield * genFunc()`,就用了语言原生的特性而不经过 co,直接返回一个 promise。 196 | 197 | **解决方法(任选其一)**: 198 | 199 | 1. `function * genFunc` -> `function genFunc`,用 `yield genFunc()`。 200 | 2. `return Promise.resolve('genFunc')` -> `return yield Promise.resolve('genFunc')`,用 `yield* genFunc()`。 201 | 202 | 不过,建议最终转换到 async/await + Promise 上来,毕竟 co + generator 只是一个过渡产物。 203 | 204 | ## 3.2.6 async + bluebird 205 | 206 | 如果是使用 async/await + bluebird 的情况呢?代码如下: 207 | 208 | **async_bluebird.js** 209 | 210 | ```js 211 | const fs = require('fs') 212 | const profiler = require('v8-profiler') 213 | const Promise = require('bluebird') 214 | 215 | async function A () { 216 | return await Promise.resolve('A') 217 | } 218 | 219 | async function B () { 220 | return await A() 221 | } 222 | 223 | (async function asyncBluebirdWrap () { 224 | const start = Date.now() 225 | profiler.startProfiling() 226 | while (Date.now() - start < 10000) { 227 | await B() 228 | } 229 | const profile = profiler.stopProfiling() 230 | profile.export() 231 | .pipe(fs.createWriteStream('async_bluebird.cpuprofile')) 232 | .on('finish', () => { 233 | profile.delete() 234 | console.error('async_bluebird.cpuprofile export success') 235 | }) 236 | })() 237 | ``` 238 | 239 | **结论**:调用栈比 co_blueblird.js 的还乱。 240 | 241 | ## 3.2.7 参考链接 242 | 243 | - https://medium.com/@markherhold/generators-vs-async-await-performance-806d8375a01a 244 | 245 | 上一节:[3.1 Promise](https://github.com/nswbmw/node-in-debugging/blob/master/3.1%20Promise.md) 246 | 247 | 下一节:[3.3 Error Stack](https://github.com/nswbmw/node-in-debugging/blob/master/3.3%20Error%20Stack.md) 248 | -------------------------------------------------------------------------------- /3.3 Error Stack.md: -------------------------------------------------------------------------------- 1 | 对于 JavaScript 中的 Error,想必大家已经很熟悉了,毕竟天天与它打交道。 2 | 3 | Node.js 内置的 Error 类型有: 4 | 5 | 1. [Error](https://nodejs.org/api/errors.html%23errors_class_error):通用的错误类型,例如:`new Error('error!!!')`。 6 | 2. [SyntaxError](https://nodejs.org/api/errors.html%23errors_class_syntaxerror):语法错误,例如:`require('vm').runInThisContext('binary ! isNotOk')`。 7 | 3. [ReferenceError](https://nodejs.org/api/errors.html%23errors_class_referenceerror):引用错误,如引用一个未定义的变量,例如:`doesNotExist`。 8 | 4. [TypeError](https://nodejs.org/api/errors.html%23errors_class_typeerror):类型错误,例如:`require('url').parse(() => {})`。 9 | 5. [URIError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError):全局的 URI 处理函数抛出的错误,例如:`encodeURI('\uD800')`。 10 | 6. [AssertError](https://nodejs.org/api/errors.html%23errors_class_assertionerror):使用 assert 模块时抛出的错误,例如:`assert(false)`。 11 | 12 | 每个 Error 对象通常有 name、message、stack、constructor 等属性。当程序抛出异常时,我们需要根据错误栈(error.stack)定位到出错代码。希望本节能够帮助读者理解并玩转错误栈,写出错误栈清晰的代码,方便调试。 13 | 14 | ## 3.3.1 Stack Trace 15 | 16 | 错误栈本质上就是调用栈(或者叫:堆栈追踪)。所以我们先复习一下 JavaScript 中调用栈的概念。 17 | 18 | **调用栈**:每当有一个函数调用,就会将其压入栈顶,在调用结束的时候再将其从栈顶移出。 19 | 20 | 来看一段代码: 21 | 22 | ```js 23 | function c () { 24 | console.log('c') 25 | console.trace() 26 | } 27 | 28 | function b () { 29 | console.log('b') 30 | c() 31 | } 32 | 33 | function a () { 34 | console.log('a') 35 | b() 36 | } 37 | 38 | a() 39 | ``` 40 | 41 | 执行后打印出: 42 | 43 | ```js 44 | a 45 | b 46 | c 47 | Trace 48 | at c (/Users/nswbmw/Desktop/test/app.js:3:11) 49 | at b (/Users/nswbmw/Desktop/test/app.js:8:3) 50 | at a (/Users/nswbmw/Desktop/test/app.js:13:3) 51 | at Object. (/Users/nswbmw/Desktop/test/app.js:16:1) 52 | at ... 53 | ``` 54 | 55 | **可以看出**:c 函数中 console.trace() 打印出的堆栈追踪依次为 c、b、a,即 a 调用了 b,b 调用了 c。 56 | 57 | 稍微修改下上面的例子: 58 | 59 | ```js 60 | function c () { 61 | console.log('c') 62 | } 63 | 64 | function b () { 65 | console.log('b') 66 | c() 67 | console.trace() 68 | } 69 | 70 | function a () { 71 | console.log('a') 72 | b() 73 | } 74 | 75 | a() 76 | ``` 77 | 78 | 执行后打印出: 79 | 80 | ``` 81 | a 82 | b 83 | c 84 | Trace 85 | at b (/Users/nswbmw/Desktop/test/app.js:8:11) 86 | at a (/Users/nswbmw/Desktop/test/app.js:13:3) 87 | at Object. (/Users/nswbmw/Desktop/test/app.js:16:1) 88 | at ... 89 | ``` 90 | 91 | **可以看出**:c() 在 console.trace() 之前执行完毕,从栈中移除,所以栈中从上往下为 b、a。 92 | 93 | 上面示例的代码过于简单,在实际情况下错误栈并没有这么直观。以常用的 [mongoose](https://www.npmjs.com/package/mongoose) 为例,mongoose 的错误栈并不友好: 94 | 95 | ```js 96 | const mongoose = require('mongoose') 97 | const Schema = mongoose.Schema 98 | mongoose.connect('mongodb://localhost/test') 99 | 100 | const UserSchema = new Schema({ 101 | id: mongoose.Schema.Types.ObjectId 102 | }) 103 | const User = mongoose.model('User', UserSchema) 104 | User 105 | .create({ id: 'xxx' }) 106 | .then(console.log) 107 | .catch(console.error) 108 | ``` 109 | 110 | 运行后打印出: 111 | 112 | ```js 113 | { ValidationError: User validation failed: id: Cast to ObjectID failed for value "xxx" at path "id" 114 | at ValidationError.inspect (/Users/nswbmw/Desktop/test/node_modules/mongoose/lib/error/validation.js:56:24) 115 | at ... 116 | errors: 117 | { id: 118 | { CastError: Cast to ObjectID failed for value "xxx" at path "id" 119 | at new CastError (/Users/nswbmw/Desktop/test/node_modules/mongoose/lib/error/cast.js:27:11) 120 | at model.$set (/Users/nswbmw/Desktop/test/node_modules/mongoose/lib/document.js:792:7) 121 | at ... 122 | message: 'Cast to ObjectID failed for value "xxx" at path "id"', 123 | name: 'CastError', 124 | stringValue: '"xxx"', 125 | kind: 'ObjectID', 126 | value: 'xxx', 127 | path: 'id', 128 | reason: [Object] } }, 129 | _message: 'User validation failed', 130 | name: 'ValidationError' } 131 | ``` 132 | 133 | 从 mongoose 给出的 error.stack 中看不到任何有用的信息,error.message 告诉我们 "xxx" 不匹配 User 这个 Model 的 id(ObjectID)的类型,其他的字段基本上也是这个结论的补充,却没有给出我们最关心的问题:**我写的代码中,到底哪一行出了问题?** 134 | 135 | 如何解决这个问题呢?我们先看看 Error.captureStackTrace 的用法。 136 | 137 | ## 3.3.2 Error.captureStackTrace 138 | 139 | Error.captureStackTrace 是 V8 提供的一个 API,可以传入两个参数: 140 | 141 | ```js 142 | Error.captureStackTrace(targetObject[, constructorOpt]) 143 | ``` 144 | 145 | Error.captureStackTrace 会在 targetObject 中添加一个 stack 属性,对该属性进行访问时,将以字符串的形式返回 Error.captureStackTrace() 语句被调用时的代码位置信息(即:调用栈历史)。 146 | 147 | 举个简单的例子: 148 | 149 | ```js 150 | const myObject = {} 151 | Error.captureStackTrace(myObject) 152 | console.log(myObject.stack) 153 | // 输出 154 | Error 155 | at Object. (/Users/nswbmw/Desktop/test/app.js:2:7) 156 | at ... 157 | ``` 158 | 159 | 除了 targetObject,captureStackTrace 还接收一个类型为 function 的可选参数 constructorOpt,当传递该参数时,调用栈中所有 constructorOpt 函数之上的信息(包括 constructorOpt 函数自身),都会在访问 targetObject.stack 时被忽略。当需要对终端用户隐藏内部的实现细节时,constructorOpt 参数会很有用。传入第 2 个参数通常用于自定义错误,例如: 160 | 161 | ```js 162 | function MyError() { 163 | Error.captureStackTrace(this, MyError) 164 | this.name = this.constructor.name 165 | this.message = 'you got MyError' 166 | } 167 | 168 | const myError = new MyError() 169 | console.log(myError) 170 | console.log(myError.stack) 171 | // 输出 172 | MyError { name: 'MyError', message: 'you got MyError' } 173 | Error 174 | at Object. (/Users/nswbmw/Desktop/test/app.js:7:17) 175 | at ... 176 | ``` 177 | 178 | 如果去掉 captureStackTrace 的第 2 个参数: 179 | 180 | ```js 181 | function MyError() { 182 | Error.captureStackTrace(this) 183 | this.name = this.constructor.name 184 | this.message = 'you got MyError' 185 | } 186 | 187 | const myError = new MyError() 188 | console.log(myError) 189 | console.log(myError.stack) 190 | // 输出 191 | MyError { name: 'MyError', message: 'you got MyError' } 192 | Error 193 | at new MyError (/Users/nswbmw/Desktop/test/app.js:2:9) 194 | at Object. (/Users/nswbmw/Desktop/test/app.js:7:17) 195 | at ... 196 | ``` 197 | 198 | **可以看出**:出现了 MyError 相关的调用栈,但我们并不关心 MyError 及其内部是如何实现的。 199 | 200 | captureStackTrace 的第 2 个参数可以传入调用链上的其他函数,不一定是当前函数,例如: 201 | 202 | ```js 203 | const myObj = {} 204 | 205 | function c () { 206 | Error.captureStackTrace(myObj, b) 207 | } 208 | 209 | function b () { 210 | c() 211 | } 212 | 213 | function a () { 214 | b() 215 | } 216 | 217 | a() 218 | console.log(myObj.stack) 219 | // 输出 220 | Error 221 | at a (/Users/nswbmw/Desktop/test/app.js:12:3) 222 | at Object. (/Users/nswbmw/Desktop/test/app.js:15:1) 223 | at ... 224 | ``` 225 | 226 | **可以看出**:captureStackTrace 的第 2 个参数传入了函数 b,调用栈中隐藏了 b 函数及其以上所有的堆栈帧。 227 | 228 | 讲到这里,相信读者都明白了 captureStackTrace 的用法。但这具体有什么用呢?其实上面提到了:**隐藏内部的实现细节,优化错误栈**。 229 | 230 | 下面以笔者写的一个模块 [Mongolass](https://github.com/mongolass/mongolass) 为例,讲解如何应用 captureStackTrace。 231 | 232 | > [Mongolass](https://github.com/mongolass/mongolass) 是一个轻量且优雅的连接 MongoDB 的模块。 233 | 234 | ## 3.3.3 captureStackTrace 在 Mongolass 中的应用 235 | 236 | 这里先大体讲讲 Mongolass 的用法。Mongolass 与 Mongoose 类似,有 Model 的概念,Model 上挂载的方法对应对 MongoDB 的 collections 的操作,例如:`User.insert`。User 是一个 Model 实例,`User.insert` 方法返回的是一个 Query 实例。Query 的代码如下: 237 | 238 | ```js 239 | class Query { 240 | constructor(op, args) { 241 | Error.captureStackTrace(this, this.constructor); 242 | ... 243 | } 244 | } 245 | ``` 246 | 247 | 这里用 Error.captureStackTrace 隐藏了 Query 内部的错误栈细节,但这样带来一个问题:丢失了原来的 error.stack,在 Mongolass 中可以自定义插件,而插件函数的执行是在 Query 内部,假如在插件中抛错,则会丢失相关错误栈信息。 248 | 249 | 如何弥补呢?Mongolass 的做法是:当 Query 内部抛出错误(error)时,截取有用的 error.stack,然后拼接到 Query 实例通过 Error.captureStackTrace 生成的 stack 上。 250 | 251 | 来看一段 Mongolass 的代码: 252 | 253 | ```js 254 | const Mongolass = require('mongolass') 255 | const Schema = Mongolass.Schema 256 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 257 | 258 | const UserSchema = new Schema('UserSchema', { 259 | name: { type: 'string' }, 260 | age: { type: 'number' } 261 | }) 262 | const User = mongolass.model('User', UserSchema) 263 | 264 | User 265 | .insertOne({ name: 'nswbmw', age: 'wrong age' }) 266 | .exec() 267 | .then(console.log) 268 | .catch(console.error) 269 | ``` 270 | 271 | 运行后打印的错误信息如下: 272 | 273 | ```js 274 | { TypeError: ($.age: "wrong age") ✖ (type: number) 275 | at Model.insertOne (/Users/nswbmw/Desktop/test/node_modules/mongolass/lib/query.js:104:16) 276 | at Object. (/Users/nswbmw/Desktop/test/app.js:12:4) 277 | at ... 278 | validator: 'type', 279 | actual: 'wrong age', 280 | expected: { type: 'number' }, 281 | path: '$.age', 282 | schema: 'UserSchema', 283 | model: 'User', 284 | op: 'insertOne', 285 | args: [ { name: 'nswbmw', age: 'wrong age' } ], 286 | pluginName: 'MongolassSchema', 287 | pluginOp: 'beforeInsertOne', 288 | pluginArgs: [] } 289 | ``` 290 | 291 | **可以看出**:app.js 第 12 行的 insertOne 报错,报错原因是 age 字段是字符串 "wrong age",而我们期望的是 number 类型的值。 292 | 293 | ## 3.3.4 Error.prepareStackTrace 294 | 295 | V8 暴露了另外一个接口——Error.prepareStackTrace。简单来讲,它的作用就是:**定制 stack**。用法如下: 296 | 297 | ```js 298 | Error.prepareStackTrace(error, structuredStackTrace) 299 | ``` 300 | 301 | 第 1 个参数是个 Error 对象,第 2 个参数是一个数组,每一项都是一个 CallSite 对象,包含错误的函数名、行数等信息。对比以下两种代码: 302 | 303 | 正常的 throw error: 304 | 305 | ```js 306 | function c () { 307 | throw new Error('error!!!') 308 | } 309 | 310 | function b () { 311 | c() 312 | } 313 | 314 | function a () { 315 | b() 316 | } 317 | 318 | try { 319 | a() 320 | } catch (e) { 321 | console.log(e.stack) 322 | } 323 | // 输出 324 | Error: error!!! 325 | at c (/Users/nswbmw/Desktop/test/app.js:2:9) 326 | at b (/Users/nswbmw/Desktop/test/app.js:6:3) 327 | at a (/Users/nswbmw/Desktop/test/app.js:10:3) 328 | at Object. (/Users/nswbmw/Desktop/test/app.js:14:3) 329 | at ... 330 | ``` 331 | 332 | 使用 Error.prepareStackTrace 格式化 stack: 333 | 334 | ```js 335 | Error.prepareStackTrace = function (error, callSites) { 336 | return error.toString() + '\n' + callSites.map(callSite => { 337 | return ' -> ' + callSite.getFunctionName() + ' (' 338 | + callSite.getFileName() + ':' 339 | + callSite.getLineNumber() + ':' 340 | + callSite.getColumnNumber() + ')' 341 | }).join('\n') 342 | } 343 | 344 | function c () { 345 | throw new Error('error!!!') 346 | } 347 | 348 | function b () { 349 | c() 350 | } 351 | 352 | function a () { 353 | b() 354 | } 355 | 356 | try { 357 | a() 358 | } catch (e) { 359 | console.log(e.stack) 360 | } 361 | // 输出 362 | Error: error!!! 363 | -> c (/Users/nswbmw/Desktop/test/app.js:11:9) 364 | -> b (/Users/nswbmw/Desktop/test/app.js:15:3) 365 | -> a (/Users/nswbmw/Desktop/test/app.js:19:3) 366 | -> null (/Users/nswbmw/Desktop/test/app.js:23:3) 367 | -> ... 368 | ``` 369 | 370 | **可以看出**:我们自定义了一个 Error.prepareStackTrace 格式化了 stack 并打印出来。 371 | 372 | CallSite 对象还有许多 API,例如:getThis、getTypeName、getFunction、getFunctionName、getMethodName、getFileName、getLineNumber、getColumnNumber、getEvalOrigin、isToplevel、isEval、isNative 和 isConstructor,这里不一一介绍了,有兴趣的读者可查看参考链接。 373 | 374 | 在使用 Error.prepareStackTrace 时需要注意两点: 375 | 376 | 1. 这个方法是 V8 暴露出来的,所以只能在基于 V8 的 Node.js 或者 Chrome 里才能使用。 377 | 2. 这个方法会修改全局 Error 的行为。 378 | 379 | ## 3.3.5 Error.prepareStackTrace 的其他用法 380 | 381 | Error.prepareStackTrace 除了格式化错误栈外还有什么作用呢?[sindresorhus](https://github.com/sindresorhus) 大神还写了一个 [callsites](https://github.com/sindresorhus/callsites) 的模块,可以用来获取函数调用相关的信息,例如获取执行该函数所在的文件名: 382 | 383 | ```js 384 | const callsites = require('callsites') 385 | 386 | function getFileName() { 387 | console.log(callsites()[0].getFileName()) 388 | //=> '/Users/nswbmw/Desktop/test/app.js' 389 | } 390 | 391 | getFileName() 392 | ``` 393 | 394 | 我们来看一下源代码: 395 | 396 | ```js 397 | module.exports = () => { 398 | const _ = Error.prepareStackTrace 399 | Error.prepareStackTrace = (_, stack) => stack 400 | const stack = new Error().stack.slice(1) 401 | Error.prepareStackTrace = _ 402 | return stack 403 | } 404 | ``` 405 | 406 | 注意以下几点: 407 | 408 | 1. 因为修改 Error.prepareStackTrace 会全局生效,所以将原来的 Error.prepareStackTrace 存到一个变量中,函数执行完后再重置回去,避免影响全局的 Error。 409 | 2. Error.prepareStackTrace 函数直接返回 CallSite 对象数组,而不是格式化后的 stack 字符串。 410 | 3. new 一个 Error,stack 是返回的 CallSite 对象数组,因为第 1 项是 callsites,它总是这个模块的 CallSite,所以通过 slice(1) 去掉。 411 | 412 | 假如我们想获取当前函数的父函数名,则可以这样用: 413 | 414 | ```js 415 | const callsites = require('callsites') 416 | 417 | function b () { 418 | console.log(callsites()[1].getFunctionName()) 419 | // => 'a' 420 | } 421 | 422 | function a () { 423 | b() 424 | } 425 | a() 426 | ``` 427 | 428 | ## 3.3.6 Error.stackTraceLimit 429 | 430 | Node.js 还暴露了一个 Error.stackTraceLimit 的设置,可以通过设置这个值来改变输出的 stack 的行数,默认值是 10。 431 | 432 | ## 3.3.7 Long Stack Trace 433 | 434 | stack trace 也有短板,问题出在异步操作上。若在异步回调中抛错,就会丢失绑定回调前的调用栈信息,来看个例子: 435 | 436 | ```js 437 | const foo = function () { 438 | throw new Error('error!!!') 439 | } 440 | const bar = function () { 441 | setTimeout(foo) 442 | } 443 | bar() 444 | // 输出 445 | /Users/nswbmw/Desktop/test/app.js:2 446 | throw new Error('error!!!') 447 | ^ 448 | 449 | Error: error!!! 450 | at Timeout.foo [as _onTimeout] (/Users/nswbmw/Desktop/test/app.js:2:9) 451 | at ontimeout (timers.js:469:11) 452 | at tryOnTimeout (timers.js:304:5) 453 | at Timer.listOnTimeout (timers.js:264:5) 454 | ``` 455 | 456 | **可以看出**:丢失了 bar 的调用栈。 457 | 458 | 在实际开发过程中,异步回调的例子数不胜数,如果不能知道异步回调之前的触发位置,则会给 debug 带来很大的难度。这时,出现了一个叫 long Stack Trace 的概念。 459 | 460 | long Stack Trace 并不是 JavaScript 原生就支持的功能,所以要拥有这样的功能,就需要我们做一些 hack,幸好在 V8 环境下,所有 hack 所需的 API,V8 都已经提供了。 461 | 462 | 对于异步回调,目前能做的就是在所有会产生异步操作的 API 上做一些手脚,这些 API 包括: 463 | 464 | - setTimeout, setInterval, setImmediate。 465 | - nextTick, nextDomainTick。 466 | - EventEmitter.addEventListener。 467 | - EventEmitter.on。 468 | - Ajax XHR。 469 | 470 | Long Stack Trace 相关的库可以参考: 471 | 472 | 1. [AndreasMadsen/trace](https://github.com/AndreasMadsen/trace) 473 | 2. [mattinsler/longjohn](https://github.com/mattinsler/longjohn) 474 | 3. [tlrobinson/long-stack-traces](https://github.com/tlrobinson/long-stack-traces) 475 | 476 | node@8+ 提供了强大的 async_hooks 模块,在本书的后面章节会介绍如何使用。 477 | 478 | ## 3.3.8 参考链接 479 | 480 | - https://zhuanlan.zhihu.com/p/25338849 481 | - https://segmentfault.com/a/1190000007076507 482 | - https://github.com/v8/v8/wiki/Stack-Trace-API 483 | - https://www.jianshu.com/p/1d5120ad62bb 484 | 485 | 上一节:[3.2 Async + Await](https://github.com/nswbmw/node-in-debugging/blob/master/3.2%20Async%20%2B%20Await.md) 486 | 487 | 下一节:[3.4 Node@8](https://github.com/nswbmw/node-in-debugging/blob/master/3.4%20Node%408.md) 488 | -------------------------------------------------------------------------------- /3.4 Node@8.md: -------------------------------------------------------------------------------- 1 | 如果你想以最简单的方式提升 Node.js 程序的性能,那就升级到 node@8+ 吧。这不是一个玩笑,多少 JavaScript 前辈们以血的教训总结出了一长列 “Optimization killers”,典型的有: 2 | 3 | 1. 在 try 里面不要写过多代码,V8 无法优化,最好将这些代码放到一个函数里,然后 try 这个函数。 4 | 2. 少用 delete。 5 | 3. 少用 arguments。 6 | 4. ... 7 | 8 | 然而,随着 V8 彻底换上了新的 JIT 编译器—— Turbofan,大多数 “Optimization killers” 都已经成了过去时。所以在本节中我们来看看哪些过去常见的 “Optimization killers” 已经可以被 V8 优化。 9 | 10 | ## 3.4.1 Ignition + Turbofan 11 | 12 | 之前 V8 使用的是名为 Crankshaft 的编译器,这个编译器后来逐渐暴露出一些缺点: 13 | 14 | 1. Doesn’t scale to full, modern JavaScript (try-catch, for-of, generators, async/await, …) 15 | 2. Defaults to deoptimization (performance cliffs, deoptimization loops) 16 | 3. Graph construction, inlining and optimization all mixed up 17 | 4. Tight coupling to fullcodegen / brittle environment tracking 18 | 5. Limited optimization potential / limited static analysis (i.e. type propagation) 19 | 6. High porting overhead 20 | 7. Mixed low-level and high-level semantics of instructions 21 | 22 | 而引入 Turbofan 的好处是: 23 | 24 | 1. Full ESnext language support (try-catch/-finally, class literals, eval, generators, async functions, modules, destructuring, etc.) 25 | 2. Utilize and propagate (static) type information 26 | 3. Separate graph building from optimization / inlining 27 | 4. No deoptimization loops / deoptimization only when really beneficial 28 | 5. Sane environment tracking (also for lazy deoptimization) 29 | 6. Predictable peak performance 30 | 31 | Ignition 是 V8 新引入的解释器,用来将代码编译成简洁的字节码,而不是之前的机器码,这大大减少了结果代码,减少了系统的内存使用。由于字节码较小,所以可以编译全部源代码,而不用避免编译未使用的代码。也就是说,脚本只需要解析一次,而不是像之前的编译过程那样解析多次。 32 | 33 | Ignition 与 TurboFan 的关系为:Ignition 解释器使用低级的、体系结构无关的 TurboFan 宏汇编指令为每个操作码生成字节码处理程序,TurboFan 将这些指令编译成目标平台的代码,并在这个过程中执行低级的指令选择和机器寄存器分配。 34 | 35 | 补充一点,之前的 V8 将代码编译成机器码执行,而新的 V8 将代码编译成字节码解释执行,动机是什么呢?可能是: 36 | 37 | 1. 减少机器码占用的内存空间,即牺牲时间换空间(主要动机)。 38 | 2. 加快代码的启动速度。 39 | 3. 对 V8 的代码进行重构,降低 V8 的代码复杂度。 40 | 41 | ## 3.4.2 版本对应关系 42 | 43 | ``` 44 | node@6 -> V8@5.1 -> Crankshaft 45 | node@8.0-8.2 -> V8@5.8 -> Crankshaft + Turbofan 46 | V8@5.9 -> Turbofan 47 | node@8.3-8.4 -> V8@6.0 -> Turbofan 48 | ``` 49 | 50 | ## 3.4.3 try/catch 51 | 52 | 最著名的去优化之一是使用 try/catch 代码块。下面通过 4 种场景比较在不同的 V8 版本下执行的效率: 53 | 54 | ```js 55 | var benchmark = require('benchmark') 56 | var suite = new benchmark.Suite() 57 | 58 | function sum (base, max) { 59 | var total = 0 60 | 61 | for (var i = base; i < max; i++) { 62 | total += i 63 | } 64 | } 65 | 66 | suite.add('sum with try catch', function sumTryCatch () { 67 | try { 68 | var base = 0 69 | var max = 65535 70 | 71 | var total = 0 72 | 73 | for (var i = base; i < max; i++) { 74 | total += i 75 | } 76 | } catch (err) { 77 | console.log(err.message) 78 | } 79 | }) 80 | 81 | suite.add('sum without try catch', function noTryCatch () { 82 | var base = 0 83 | var max = 65535 84 | 85 | var total = 0 86 | 87 | for (var i = base; i < max; i++) { 88 | total += i 89 | } 90 | }) 91 | 92 | suite.add('sum wrapped', function wrapped () { 93 | var base = 0 94 | var max = 65535 95 | 96 | try { 97 | sum(base, max) 98 | } catch (err) { 99 | console.log(err.message) 100 | } 101 | }) 102 | 103 | suite.add('sum function', function func () { 104 | var base = 0 105 | var max = 65535 106 | 107 | sum(base, max) 108 | }) 109 | 110 | suite.on('complete', require('./print')) 111 | suite.run() 112 | ``` 113 | 114 | 运行结果如下: 115 | 116 | ![](./assets/3.4.1.jpg) 117 | 118 | **结论**:在 node@8.3 及以上版本中,在 try 块内写代码的性能损耗可以忽略不计。 119 | 120 | ## 3.4.4 delete 121 | 122 | 多年以来,delete 对于任何希望编写高性能 JavaScript 的人来说都是受限制的,我们通常用赋值 undefined 替代。delete 的问题归结为 V8 处理 JavaScript 对象的动态特性和原型链方式,使得属性查找在实现上变得复杂。下面通过 3 种场景比较在不同的 V8 版本下执行的效率: 123 | 124 | ```js 125 | var benchmark = require('benchmark') 126 | var suite = new benchmark.Suite() 127 | 128 | function MyClass (x, y) { 129 | this.x = x 130 | this.y = y 131 | } 132 | 133 | function MyClassLast (x, y) { 134 | this.y = y 135 | this.x = x 136 | } 137 | 138 | suite.add('setting to undefined', function undefProp () { 139 | var obj = new MyClass(2, 3) 140 | obj.x = undefined 141 | 142 | JSON.stringify(obj) 143 | }) 144 | 145 | suite.add('delete', function deleteProp () { 146 | var obj = new MyClass(2, 3) 147 | delete obj.x 148 | 149 | JSON.stringify(obj) 150 | }) 151 | 152 | suite.add('delete last property', function deleteProp () { 153 | var obj = new MyClassLast(2, 3) 154 | delete obj.x 155 | 156 | JSON.stringify(obj) 157 | }) 158 | 159 | suite.add('setting to undefined literal', function undefPropLit () { 160 | var obj = { x: 2, y: 3 } 161 | obj.x = undefined 162 | 163 | JSON.stringify(obj) 164 | }) 165 | 166 | suite.add('delete property literal', function deletePropLit () { 167 | var obj = { x: 2, y: 3 } 168 | delete obj.x 169 | 170 | JSON.stringify(obj) 171 | }) 172 | 173 | suite.add('delete last property literal', function deletePropLit () { 174 | var obj = { y: 3, x: 2 } 175 | delete obj.x 176 | 177 | JSON.stringify(obj) 178 | }) 179 | 180 | suite.on('complete', require('./print')) 181 | suite.run() 182 | ``` 183 | 184 | 运行结果如下: 185 | 186 | ![](./assets/3.4.2.jpg) 187 | 188 | **结论**:在 node@8 及以上版本中,delete 一个对象上的属性比 node@6 快了一倍。在 node@8.3 及以上版本中,delete 一个对象上最后一个属性几乎与赋值 undefined 同样快了。 189 | 190 | ## 3.4.5 arguments 191 | 192 | 我们知道 arguments 是个类数组,所以通常我们要使用 `Array.prototype.slice.call(arguments)` 将它转化成数组再使用,这样会有一定的性能损耗。下面通过 4 种场景比较在不同的 V8 版本下执行的效率: 193 | 194 | ```js 195 | var benchmark = require('benchmark') 196 | var suite = new benchmark.Suite() 197 | 198 | function leakyArguments () { 199 | return other(arguments) 200 | } 201 | 202 | function copyArgs () { 203 | var array = new Array(arguments.length) 204 | 205 | for (var i = 0; i < array.length; i++) { 206 | array[i] = arguments[i] 207 | } 208 | 209 | return other(array) 210 | } 211 | 212 | function sliceArguments () { 213 | var array = Array.prototype.slice.apply(arguments) 214 | return other(array) 215 | } 216 | 217 | function spreadOp(...args) { 218 | return other(args) 219 | } 220 | 221 | function other (toSum) { 222 | var total = 0 223 | for (var i = 0; i < toSum.length; i++) { 224 | total += toSum[i] 225 | } 226 | return total 227 | } 228 | 229 | suite.add('leaky arguments', () => { 230 | leakyArguments(1, 2, 3) 231 | }) 232 | 233 | suite.add('Array.prototype.slice arguments', () => { 234 | sliceArguments(1, 2, 3) 235 | }) 236 | 237 | suite.add('for-loop copy arguments', () => { 238 | copyArgs(1, 2, 3) 239 | }) 240 | 241 | suite.add('spread operator', () => { 242 | spreadOp(1, 2, 3) 243 | }) 244 | 245 | suite.on('complete', require('./print')) 246 | suite.run() 247 | ``` 248 | 249 | 运行结果如下: 250 | 251 | ![](./assets/3.4.3.jpg) 252 | 253 | **结论**:在 node@8.3 及以上版本中,使用对象展开运算符是除直接使用 arguments 外最快的方案,对于 node@8.2 及以下的版本,我们应该使用一个 for 循环将 key 从 arguments 复制到一个新的(预先分配的)数组中。总之,是时候抛弃 Array.prototype.slice.call 了。 254 | 255 | ## 3.4.6 async 性能提升 256 | 257 | 在 V8@5.7 发布后,原生的 async 函数与 Promise 一样快了,同时,Promise 的性能也比 V8@5.6 快了一倍。如图所示: 258 | 259 | ![](./assets/3.4.4.jpg) 260 | 261 | ## 3.4.7 不会优化的特性 262 | 263 | 并不是说上了 Turbofan 就能优化所有的 JavaScript 语法,有些语法 V8 是不会去优化的(也没有必要),例如: 264 | 265 | 1. debugger 266 | 2. eval 267 | 3. with 268 | 269 | 我们以 debugger 为例,比较使用和不使用 debugger 时的性能: 270 | 271 | ```js 272 | var benchmark = require('benchmark') 273 | var suite = new benchmark.Suite() 274 | 275 | suite.add('with debugger', function withDebugger () { 276 | var base = 0 277 | var max = 65535 278 | 279 | var total = 0 280 | 281 | for (var i = base; i < max; i++) { 282 | debugger 283 | total += i 284 | } 285 | }) 286 | 287 | suite.add('without debugger', function withoutDebugger () { 288 | var base = 0 289 | var max = 65535 290 | 291 | var total = 0 292 | 293 | for (var i = base; i < max; i++) { 294 | total += i 295 | } 296 | }) 297 | 298 | suite.on('complete', require('./print')) 299 | suite.run() 300 | ``` 301 | 302 | 运行结果如下: 303 | 304 | ![](./assets/3.4.5.jpg) 305 | 306 | **结论**:在所有测试的 V8 版本中,debugger 一直都很慢,所以记得在打断点测试完后一定要删掉 debugger。 307 | 308 | ## 3.4.8 总结 309 | 310 | 1. 使用最新 LTS 版本的 Node.js。 311 | 2. 关注 V8 团队的博客——[https://v8project.blogspot.com](https://v8project.blogspot.com/),了解第一手资讯。 312 | 3. 清晰的代码远比使用一些奇技淫巧提升的一点性能重要得多。 313 | 314 | ## 3.4.9 参考链接 315 | 316 | - https://github.com/davidmarkclements/v8-perf 317 | - http://www.infoq.com/cn/news/2016/08/v8-ignition-javascript-inteprete 318 | - 319 | - https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan 320 | - https://zhuanlan.zhihu.com/p/26669846 321 | 322 | 上一节:[3.3 Error Stack](https://github.com/nswbmw/node-in-debugging/blob/master/3.3%20Error%20Stack.md) 323 | 324 | 下一节:[3.5 Rust Addons](https://github.com/nswbmw/node-in-debugging/blob/master/3.5%20Rust%20Addons.md) 325 | -------------------------------------------------------------------------------- /3.5 Rust Addons.md: -------------------------------------------------------------------------------- 1 | 我们知道,Node.js 不适合 CPU 密集型计算的场景,通常的解决方法是用 C/C++ 编写 Node.js 的扩展(Addons)。以前只能用 C/C++,现在我们有了新的选择——Rust。 2 | 3 | ## 3.5.1 环境 4 | 5 | - node@8.9.4 6 | - rust@1.26.0-nightly 7 | 8 | ## 3.5.2 Rust 9 | 10 | Rust 是 Mozilla 开发的注重安全、性能和并发的现代编程语言。相比较于其他常见的编程语言,它有 3 个独特的概念: 11 | 12 | 1. 所有权 13 | 2. 借用 14 | 3. 生命周期 15 | 16 | 正是这 3 个特性保证了 Rust 是内存安全的,这里不会展开讲解,有兴趣的读者可以去了解一下。 17 | 18 | 接下来,我们通过三种方式使用 Rust 编写 Node.js 的扩展。 19 | 20 | ## 3.5.3 [FFI](https://github.com/node-ffi/node-ffi) 21 | 22 | FFI 的全称是 Foreign Function Interface,即可以用 Node.js 调用动态链接库。 23 | 24 | 运行以下命令: 25 | 26 | ```sh 27 | $ cargo new ffi-demo && cd ffi-demo 28 | $ npm init -y 29 | $ npm i ffi --save 30 | $ touch index.js 31 | ``` 32 | 33 | 部分文件修改如下: 34 | 35 | **src/lib.rs** 36 | 37 | ```rust 38 | #[no_mangle] 39 | pub extern fn fib(n: i64) -> i64 { 40 | return match n { 41 | 1 | 2 => 1, 42 | n => fib(n - 1) + fib(n - 2) 43 | } 44 | } 45 | ``` 46 | 47 | **Cargo.toml** 48 | 49 | ```toml 50 | [package] 51 | name = "ffi-demo" 52 | version = "0.1.0" 53 | 54 | [lib] 55 | name = "ffi" 56 | crate-type = ["dylib"] 57 | ``` 58 | 59 | Cargo.toml 是 Rust 项目的配置文件,相当于 Node.js 中的 package.json。这里指定编译生成的类型是 dylib(动态链接库),名字在 *inux 下是 libffi,Windows 下是 ffi。 60 | 61 | 使用 cargo 编译代码: 62 | 63 | ```sh 64 | $ cargo build #开发环境用 65 | 或者 66 | $ cargo build --release #生产环境用,编译器做了更多优化,但编译慢 67 | ``` 68 | 69 | cargo 是 Rust 的构建工具和包管理工具,负责构建代码、下载依赖库并编译它们。此时会生成一个 target 的目录,该目录下会有 debug(不加 --release)或者 release(加 --release)目录,存放了生成的动态链接库。 70 | 71 | **index.js** 72 | 73 | ```js 74 | const ffi = require('ffi') 75 | const isWin = /^win/.test(process.platform) 76 | 77 | const rust = ffi.Library('target/debug/' + (!isWin ? 'lib' : '') + 'ffi', { 78 | fib: ['int', ['int']] 79 | }) 80 | 81 | function fib(n) { 82 | if (n === 1 || n === 2) { 83 | return 1 84 | } 85 | return fib(n - 1) + fib(n - 2) 86 | } 87 | 88 | // js 89 | console.time('node') 90 | console.log(fib(40)) 91 | console.timeEnd('node') 92 | 93 | // rust 94 | console.time('rust') 95 | console.log(rust.fib(40)) 96 | console.timeEnd('rust') 97 | ``` 98 | 99 | 运行 index.js: 100 | 101 | ```sh 102 | $ node index.js 103 | 102334155 104 | node: 1053.743ms 105 | 102334155 106 | rust: 1092.570ms 107 | ``` 108 | 109 | 将 index.js 中 debug 改为 release,运行: 110 | 111 | ```sh 112 | $ cargo build --release 113 | $ node index.js 114 | 102334155 115 | node: 1050.467ms 116 | 102334155 117 | rust: 273.508ms 118 | ``` 119 | 120 | **可以看出**:添加了 --release 编译后的代码,执行效率提升十分明显。 121 | 122 | ## 3.5.4 [Neon](https://github.com/neon-bindings/neon) 123 | 124 | 官方介绍: 125 | 126 | > Rust bindings for writing safe and fast native Node.js modules. 127 | 128 | 使用方法如下: 129 | 130 | ```sh 131 | $ npm i neon-cli -g 132 | $ neon new neon-demo 133 | $ cd neon-demo 134 | $ tree . 135 | . 136 | ├── README.md 137 | ├── lib 138 | │ └── index.js 139 | ├── native 140 | │ ├── Cargo.toml 141 | │ ├── build.rs 142 | │ └── src 143 | │ └── lib.rs 144 | └── package.json 145 | 146 | 3 directories, 6 files 147 | $ npm i #触发 neon build 148 | $ node lib/index.js 149 | hello node 150 | ``` 151 | 152 | 接下来我们看看关键的代码文件。 153 | 154 | **lib/index.js** 155 | 156 | ```js 157 | var addon = require('../native'); 158 | console.log(addon.hello()); 159 | ``` 160 | 161 | **native/src/lib.rs** 162 | 163 | ```rust 164 | #[macro_use] 165 | extern crate neon; 166 | 167 | use neon::vm::{Call, JsResult}; 168 | use neon::js::JsString; 169 | 170 | fn hello(call: Call) -> JsResult { 171 | let scope = call.scope; 172 | Ok(JsString::new(scope, "hello node").unwrap()) 173 | } 174 | 175 | register_module!(m, { 176 | m.export("hello", hello) 177 | }); 178 | ``` 179 | 180 | **native/build.rs** 181 | 182 | ```rust 183 | extern crate neon_build; 184 | 185 | fn main() { 186 | neon_build::setup(); // must be called in build.rs 187 | 188 | // add project-specific build logic here... 189 | } 190 | ``` 191 | 192 | **native/Cargo.toml** 193 | 194 | ```toml 195 | [package] 196 | name = "neon-demo" 197 | version = "0.1.0" 198 | authors = ["nswbmw"] 199 | license = "MIT" 200 | build = "build.rs" 201 | 202 | [lib] 203 | name = "neon_demo" 204 | crate-type = ["dylib"] 205 | 206 | [build-dependencies] 207 | neon-build = "0.1.22" 208 | 209 | [dependencies] 210 | neon = "0.1.22" 211 | ``` 212 | 213 | 在运行 `neon build` 时,会根据 native/Cargo.toml 中 build 字段指定的文件(这里是 build.rs)编译,并且生成的类型是 dylib(动态链接库)。native/src/lib.rs 存放了扩展的代码逻辑,通过 register_module 注册了一个 hello 方法,返回 hello node 字符串。 214 | 215 | 接下来测试原生 Node.js 和 Neon 编写的扩展运行斐波那契数列的执行效率。 216 | 217 | 修改对应文件如下: 218 | 219 | **native/src/lib.rs** 220 | 221 | ```rust 222 | #[macro_use] 223 | extern crate neon; 224 | 225 | use neon::vm::{Call, JsResult}; 226 | use neon::mem::Handle; 227 | use neon::js::JsInteger; 228 | 229 | fn fib(call: Call) -> JsResult { 230 | let scope = call.scope; 231 | let index: Handle = try!(try!(call.arguments.require(scope, 0)).check::()); 232 | let index: i32 = index.value() as i32; 233 | let result: i32 = fibonacci(index); 234 | Ok(JsInteger::new(scope, result)) 235 | } 236 | 237 | fn fibonacci(n: i32) -> i32 { 238 | match n { 239 | 1 | 2 => 1, 240 | _ => fibonacci(n - 1) + fibonacci(n - 2) 241 | } 242 | } 243 | 244 | register_module!(m, { 245 | m.export("fib", fib) 246 | }); 247 | ``` 248 | 249 | **lib/index.js** 250 | 251 | ```js 252 | const rust = require('../native') 253 | 254 | function fib (n) { 255 | if (n === 1 || n === 2) { 256 | return 1 257 | } 258 | return fib(n - 1) + fib(n - 2) 259 | } 260 | 261 | // js 262 | console.time('node') 263 | console.log(fib(40)) 264 | console.timeEnd('node') 265 | 266 | // rust 267 | console.time('rust') 268 | console.log(rust.fib(40)) 269 | console.timeEnd('rust') 270 | ``` 271 | 272 | 运行: 273 | 274 | ```sh 275 | $ neon build 276 | $ node lib/index.js 277 | 102334155 278 | node: 1030.681ms 279 | 102334155 280 | rust: 270.417ms 281 | ``` 282 | 283 | 接下来看一个复杂点的例子,用 Neon 编写一个 User 类,可传入一个含有 first_name 和 last_name 的对象,暴露出一个 get_full_name 方法。 284 | 285 | 修改对应文件如下: 286 | 287 | **native/src/lib.rs** 288 | 289 | ```rust 290 | #[macro_use] 291 | extern crate neon; 292 | 293 | use neon::js::{JsFunction, JsString, Object, JsObject}; 294 | use neon::js::class::{Class, JsClass}; 295 | use neon::mem::Handle; 296 | use neon::vm::Lock; 297 | 298 | pub struct User { 299 | first_name: String, 300 | last_name: String, 301 | } 302 | 303 | declare_types! { 304 | pub class JsUser for User { 305 | init(call) { 306 | let scope = call.scope; 307 | let user = try!(try!(call.arguments.require(scope, 0)).check::()); 308 | let first_name: Handle = try!(try!(user.get(scope, "first_name")).check::()); 309 | let last_name: Handle = try!(try!(user.get(scope, "last_name")).check::()); 310 | 311 | Ok(User { 312 | first_name: first_name.value(), 313 | last_name: last_name.value(), 314 | }) 315 | } 316 | 317 | method get_full_name(call) { 318 | let scope = call.scope; 319 | let first_name = call.arguments.this(scope).grab(|user| { user.first_name.clone() }); 320 | let last_name = call.arguments.this(scope).grab(|user| { user.last_name.clone() }); 321 | Ok(try!(JsString::new_or_throw(scope, &(first_name + &last_name))).upcast()) 322 | } 323 | } 324 | } 325 | 326 | register_module!(m, { 327 | let class: Handle> = try!(JsUser::class(m.scope)); 328 | let constructor: Handle> = try!(class.constructor(m.scope)); 329 | try!(m.exports.set("User", constructor)); 330 | Ok(()) 331 | }); 332 | ``` 333 | 334 | **lib/index.js** 335 | 336 | ```js 337 | const rust = require('../native') 338 | const User = rust.User 339 | 340 | const user = new User({ 341 | first_name: 'zhang', 342 | last_name: 'san' 343 | }) 344 | 345 | console.log(user.get_full_name()) 346 | ``` 347 | 348 | 运行: 349 | 350 | ```sh 351 | $ neon build 352 | $ node lib/index.js 353 | zhangsan 354 | ``` 355 | 356 | ## 3.5.5 [NAPI](https://nodejs.org/api/n-api.html) 357 | 358 | 不少 Node.js 开发者可能都遇到过升级 Node.js 版本导致程序运行不起来的情况,需要重新安装依赖解决,比如:node-sass 模块。因为之前编写 Node.js 扩展严重依赖于 V8 暴露的 API,而不同版本的 Node.js 依赖的 V8 版本可能不同,一旦升级 Node.js 版本,原先运行正常的 Node.js 的扩展就可能失效了。 359 | 360 | NAPI 是 node@8 新添加的用于原生模块开发的接口,相较于以前的开发方式,NAPI 提供了稳定的 ABI 接口,消除了 Node.js 版本差异、引擎差异等编译后不兼容的问题,解决了编写 Node.js 插件最头疼的问题。 361 | 362 | 目前 NAPI 还处于试验阶段,所以相关资料并不多,笔者写了一个 demo 放到了 GitHub 上,这里直接 clone 下来运行: 363 | 364 | ```sh 365 | $ git clone https://github.com/nswbmw/rust-napi-demo 366 | ``` 367 | 368 | 主要文件代码如下: 369 | 370 | **src/lib.rs** 371 | 372 | ```rust 373 | #[macro_use] 374 | extern crate napi; 375 | #[macro_use] 376 | extern crate napi_derive; 377 | 378 | use napi::{NapiEnv, NapiNumber, NapiResult}; 379 | 380 | #[derive(NapiArgs)] 381 | struct Args<'a> { 382 | n: NapiNumber<'a> 383 | } 384 | 385 | fn fibonacci<'a>(env: &'a NapiEnv, args: &Args<'a>) -> NapiResult> { 386 | let number = args.n.to_i32()?; 387 | NapiNumber::from_i32(env, _fibonacci(number)) 388 | } 389 | 390 | napi_callback!(export_fibonacci, fibonacci); 391 | 392 | fn _fibonacci(n: i32) -> i32 { 393 | match n { 394 | 1 | 2 => 1, 395 | _ => _fibonacci(n - 1) + _fibonacci(n - 2) 396 | } 397 | } 398 | ``` 399 | 400 | **index.js** 401 | 402 | ```js 403 | const rust = require('./build/Release/example.node') 404 | 405 | function fib (n) { 406 | if (n === 1 || n === 2) { 407 | return 1 408 | } 409 | return fib(n - 1) + fib(n - 2) 410 | } 411 | 412 | // js 413 | console.time('node') 414 | console.log(fib(40)) 415 | console.timeEnd('node') 416 | 417 | // rust 418 | console.time('rust') 419 | console.log(rust.fibonacci(40)) 420 | console.timeEnd('rust') 421 | ``` 422 | 423 | 运行结果: 424 | 425 | ```sh 426 | $ npm start 427 | 102334155 428 | node: 1087.650ms 429 | 102334155 430 | rust: 268.395ms 431 | (node:33302) Warning: N-API is an experimental feature and could change at any time. 432 | ``` 433 | 434 | ## 3.5.6 参考链接 435 | 436 | - https://github.com/neon-bindings/neon 437 | - https://github.com/napi-rs/napi 438 | - https://zhuanlan.zhihu.com/p/27650526 439 | 440 | 上一节:[3.4 Node@8](https://github.com/nswbmw/node-in-debugging/blob/master/3.4%20Node%408.md) 441 | 442 | 下一节:[3.6 Event Loop](https://github.com/nswbmw/node-in-debugging/blob/master/3.6%20Event%20Loop.md) 443 | -------------------------------------------------------------------------------- /3.6 Event Loop.md: -------------------------------------------------------------------------------- 1 | 事件循环(Event Loop)是 Node.js 最核心的概念,所以理解 Event Loop 如何运作对于写出正确的代码和调试是非常重要的。比如考虑以下代码: 2 | 3 | ```js 4 | setTimeout(() => { 5 | console.log('hi') 6 | }, 1000) 7 | ... 8 | ``` 9 | 10 | 我们期望程序运行 1s 后打印出 hi,但是实际情况可能是远大于 1s 后才打印出 hi。这个时候如果理解 Event Loop 就可以轻易发现问题,否则任凭怎么调试都是发现不了问题的。 11 | 12 | ## 3.6.1 什么是 Event Loop? 13 | 14 | Event Loop 可以简单理解为: 15 | 16 | 1. 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)。 17 | 2. 主线程之外,还存在一个 “任务队列”(Task Queue)。系统把异步任务放到 “任务队列” 之中,然后主线程继续执行后续的任务。 18 | 3. 一旦 “执行栈” 中的所有任务执行完毕,系统就会读取 “任务队列”。如果这个时候,异步任务已经结束了等待状态,就会从 “任务队列” 进入执行栈,恢复执行。 19 | 4. 主线程不断重复上面的第三步。 20 | 21 | **小提示**:我们常说 Node.js 是单线程的,但为何能达到高并发呢?原因就在于底层的 Libuv 维护一个 I/O 线程池(即上述的 “任务队列”),结合 Node.js 异步 I/O 的特性,单线程也能达到高并发啦。 22 | 23 | 上面提到了 “读取任务队列”,这样讲有点笼统,其实 Event Loop 的 “读取任务队列” 有 6 个阶段,如下所示: 24 | 25 | ``` 26 | ┌───────────────────────┐ 27 | ┌─>│ timers │ 28 | │ └──────────┬────────────┘ 29 | │ ┌──────────┴────────────┐ 30 | │ │ I/O callbacks │ 31 | │ └──────────┬────────────┘ 32 | │ ┌──────────┴────────────┐ 33 | │ │ idle, prepare │ 34 | │ └──────────┬────────────┘ ┌───────────────┐ 35 | │ ┌──────────┴────────────┐ │ incoming: │ 36 | │ │ poll │<─────┤ connections, │ 37 | │ └──────────┬────────────┘ │ data, etc. │ 38 | │ ┌──────────┴────────────┐ └───────────────┘ 39 | │ │ check │ 40 | │ └──────────┬────────────┘ 41 | │ ┌──────────┴────────────┐ 42 | └──┤ close callbacks │ 43 | └───────────────────────┘ 44 | ``` 45 | 46 | 每个阶段都有一个 **FIFO** 的回调队列(queue),当 Event Loop 执行到这个阶段时,会从当前阶段的队列里拿出一个任务放到栈中执行,当队列任务清空,或者执行的回调数量达到上限后,Event Loop 会进入下个阶段。 47 | 48 | 每个阶段(phase)的作用: 49 | 50 | - timers:执行 setTimeout() 和 setInterval() 中到期的 callback。 51 | - I/O callbacks:上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。 52 | - idle, prepare:仅内部使用。 53 | - poll:最重要的阶段,执行 I/O callback,在适当的条件下 node 会阻塞在这个阶段。 54 | - check:执行 setImmediate() 的 callback。 55 | - close callbacks:执行 close 事件的 callback,例如 socket.on('close',func)。 56 | 57 | ## 3.6.2 poll 阶段 58 | 59 | poll 阶段主要有两个功能: 60 | 61 | 1. 当 timers 的定时器到期后,执行定时器(setTimeout 和 setInterval)的 callback。 62 | 2. 执行 poll 队列里面的 I/O callback。 63 | 64 | 如果 Event Loop 进入了 poll 阶段,且代码未设定 timer,可能发生以下情况: 65 | 66 | - 如果 poll queue 不为空,Event Loop 将同步的执行 queue 里的 callback,直至 queue 为空,或者执行的 callback 到达系统上限。 67 | - 如果 poll queue 为空,可能发生以下情况: 68 | - 如果代码使用 setImmediate() 设定了 callback,Event Loop 将结束 poll 阶段进入 check 阶段,并执行 check 阶段的 queue。 69 | - 如果代码没有使用 setImmediate(),Event Loop 将阻塞在该阶段等待 callbacks 加入 poll queue,如果有 callback 进来则立即执行。 70 | 71 | 一旦 poll queue 为空,Event Loop 将检查 timers,如果有 timer 的时间到期,Event Loop 将回到 timers 阶段,然后执行 timer queue。 72 | 73 | ## 3.6.3 process.nextTick() 74 | 75 | 上面的 6 个阶段并没有出现 process.nextTick(),process.nextTick() 不在 Event Loop 的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。这里还需要提一下 macrotask 和 microtask 的概念,macrotask(宏任务)指 Event Loop 每个阶段执行的任务,microtask(微任务)指每个阶段之间执行的任务。即上述 6 个阶段都属于 macrotask,process.nextTick() 属于 microtask。 76 | 77 | **小提示**:process.nextTick() 的实现和 v8 的 microtask 并无关系,是 Node.js 层面的东西,应该说 process.nextTick() 的行为接近为 microtask。Promise.then 也属于 microtask 的一种。 78 | 79 | 最后,放出一张关于 Event Loop 非常直观的图: 80 | 81 | ![](./assets/3.6.1.png) 82 | 83 | 绿色小块表示 Event Loop 的各个阶段,执行的是 macrotask,macrotask 中间的粉红箭头表示执行的是 microtask。 84 | 85 | ## 3.6.4 六道题 86 | 87 | 下面我们以六道题巩固一下前面讲到的 Event Loop 的知识。 88 | 89 | ### 题目一 90 | 91 | ```js 92 | setTimeout(() => { 93 | console.log('setTimeout') 94 | }, 0) 95 | 96 | setImmediate(() => { 97 | console.log('setImmediate') 98 | }) 99 | ``` 100 | 101 | 运行结果: 102 | 103 | ``` 104 | setImmediate 105 | setTimeout 106 | ``` 107 | 108 | 或者: 109 | 110 | ``` 111 | setTimeout 112 | setImmediate 113 | ``` 114 | 115 | 为什么结果不确定呢? 116 | 117 | **解释**:setTimeout/setInterval 的第 2 个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况: 118 | 119 | 1. timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数。 120 | 2. timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数。 121 | 122 | 再看个例子: 123 | 124 | ```js 125 | setTimeout(() => { 126 | console.log('setTimeout') 127 | }, 0) 128 | 129 | setImmediate(() => { 130 | console.log('setImmediate') 131 | }) 132 | 133 | const start = Date.now() 134 | while (Date.now() - start < 10); 135 | ``` 136 | 137 | 运行结果一定是: 138 | 139 | ``` 140 | setTimeout 141 | setImmediate 142 | ``` 143 | 144 | ### 题目二 145 | 146 | ```js 147 | const fs = require('fs') 148 | 149 | fs.readFile(__filename, () => { 150 | setTimeout(() => { 151 | console.log('setTimeout') 152 | }, 0) 153 | 154 | setImmediate(() => { 155 | console.log('setImmediate') 156 | }) 157 | }) 158 | ``` 159 | 160 | 运行结果: 161 | 162 | ``` 163 | setImmediate 164 | setTimeout 165 | ``` 166 | 167 | **解释**:fs.readFile 的回调函数执行完后: 168 | 169 | 1. 注册 setTimeout 的回调函数到 timer 阶段。 170 | 2. 注册 setImmediate 的回调函数到 check 阶段。 171 | 3. event loop 从 pool 阶段出来继续往下一个阶段执行,恰好是 check 阶段,所以 setImmediate 的回调函数先执行。 172 | 4. 本次 event loop 结束后,进入下一次 event loop,执行 setTimeout 的回调函数。 173 | 174 | 所以,在 I/O Callbacks 中注册的 setTimeout 和 setImmediate,永远都是 setImmediate 先执行。 175 | 176 | ### 题目三 177 | 178 | ```js 179 | setInterval(() => { 180 | console.log('setInterval') 181 | }, 100) 182 | 183 | process.nextTick(function tick () { 184 | process.nextTick(tick) 185 | }) 186 | ``` 187 | 188 | 运行结果:setInterval 永远不会打印出来。 189 | 190 | **解释**:process.nextTick 会无限循环,将 event loop 阻塞在 microtask 阶段,导致 event loop 上其他 macrotask 阶段的回调函数没有机会执行。 191 | 192 | 解决方法通常是用 setImmediate 替代 process.nextTick,如下: 193 | 194 | ```js 195 | setInterval(() => { 196 | console.log('setInterval') 197 | }, 100) 198 | 199 | setImmediate(function immediate () { 200 | setImmediate(immediate) 201 | }) 202 | ``` 203 | 204 | 运行结果:每 100ms 打印一次 setInterval。 205 | 206 | **解释**:process.nextTick 内执行 process.nextTick 仍然将 tick 函数注册到当前 microtask 的尾部,所以导致 microtask 永远执行不完; setImmediate 内执行 setImmediate 会将 immediate 函数注册到下一次 event loop 的 check 阶段,而不是当前正在执行的 check 阶段,所以给了 event loop 上其他 macrotask 执行的机会。 207 | 208 | 再看个例子: 209 | 210 | ```js 211 | setImmediate(() => { 212 | console.log('setImmediate1') 213 | setImmediate(() => { 214 | console.log('setImmediate2') 215 | }) 216 | process.nextTick(() => { 217 | console.log('nextTick') 218 | }) 219 | }) 220 | 221 | setImmediate(() => { 222 | console.log('setImmediate3') 223 | }) 224 | ``` 225 | 226 | 运行结果: 227 | 228 | ``` 229 | setImmediate1 230 | setImmediate3 231 | nextTick 232 | setImmediate2 233 | ``` 234 | 235 | **注意**:并不是说 setImmediate 可以完全代替 process.nextTick,process.nextTick 在特定场景下还是无法被代替的,比如我们就想将一些操作放到最近的 microtask 里执行。 236 | 237 | ### 题目四 238 | 239 | ```js 240 | const promise = Promise.resolve() 241 | .then(() => { 242 | return promise 243 | }) 244 | promise.catch(console.error) 245 | ``` 246 | 247 | 运行结果: 248 | 249 | ``` 250 | TypeError: Chaining cycle detected for promise # 251 | at 252 | at process._tickCallback (internal/process/next_tick.js:188:7) 253 | at Function.Module.runMain (module.js:667:11) 254 | at startup (bootstrap_node.js:187:16) 255 | at bootstrap_node.js:607:3 256 | ``` 257 | 258 | **解释**:Promise A+ 的规范里规定 promise 不能返回自己。仔细想想,即使规范里不规定,promise.then 类似于 process.nextTick,都会将回调函数注册到 microtask 阶段。上面代码也会导致死循环,类似前面提到的: 259 | 260 | ```js 261 | process.nextTick(function tick () { 262 | process.nextTick(tick) 263 | }) 264 | ``` 265 | 266 | 再看个例子: 267 | 268 | ```js 269 | const promise = Promise.resolve() 270 | 271 | promise.then(() => { 272 | console.log('promise') 273 | }) 274 | 275 | process.nextTick(() => { 276 | console.log('nextTick') 277 | }) 278 | ``` 279 | 280 | 运行结果: 281 | 282 | ``` 283 | nextTick 284 | promise 285 | ``` 286 | 287 | **解释**:promise.then 虽然和 process.nextTick 一样,都将回调函数注册到 microtask,但优先级不一样。process.nextTick 的 microtask queue 总是优先于 promise 的 microtask queue 执行。 288 | 289 | ### 题目五 290 | 291 | ```js 292 | setTimeout(() => { 293 | console.log(1) 294 | }, 0) 295 | new Promise((resolve, reject) => { 296 | console.log(2) 297 | for (let i = 0; i < 10000; i++) { 298 | i === 9999 && resolve() 299 | } 300 | console.log(3) 301 | }).then(() => { 302 | console.log(4) 303 | }) 304 | console.log(5) 305 | ``` 306 | 307 | 运行结果: 308 | 309 | ``` 310 | 2 311 | 3 312 | 5 313 | 4 314 | 1 315 | ``` 316 | 317 | **解释**:Promise 构造函数是同步执行的,所以先打印 2、3,然后打印 5,接下来 event loop 进入执行 microtask 阶段,执行 promise.then 的回调函数打印出 4,然后执行下一个 macrotask,恰好是 timer 阶段的 setTimeout 的回调函数,打印出 1。 318 | 319 | ### 题目六 320 | 321 | ```js 322 | setImmediate(() => { 323 | console.log(1) 324 | setTimeout(() => { 325 | console.log(2) 326 | }, 100) 327 | setImmediate(() => { 328 | console.log(3) 329 | }) 330 | process.nextTick(() => { 331 | console.log(4) 332 | }) 333 | }) 334 | process.nextTick(() => { 335 | console.log(5) 336 | setTimeout(() => { 337 | console.log(6) 338 | }, 100) 339 | setImmediate(() => { 340 | console.log(7) 341 | }) 342 | process.nextTick(() => { 343 | console.log(8) 344 | }) 345 | }) 346 | console.log(9) 347 | ``` 348 | 349 | 运行结果: 350 | 351 | ``` 352 | 9 353 | 5 354 | 8 355 | 1 356 | 7 357 | 4 358 | 3 359 | 6 360 | 2 361 | ``` 362 | 363 | process.nextTick、setTimeout 和 setImmediate 的组合,请读者自行推理吧。 364 | 365 | ## 3.6.5 参考链接 366 | 367 | - https://cnodejs.org/topic/57d68794cb6f605d360105bf 368 | - https://cnodejs.org/topic/5a9108d78d6e16e56bb80882 369 | - https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 370 | - https://medium.com/the-node-js-collection/what-you-should-know-to-really-understand-the-node-js-event-loop-and-its-metrics-c4907b19da4c 371 | 372 | 上一节:[3.5 Rust Addons](https://github.com/nswbmw/node-in-debugging/blob/master/3.5%20Rust%20Addons.md) 373 | 374 | 下一节:[3.7 uncaughtException + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/3.7%20uncaughtException%20%2B%20llnode.md) 375 | -------------------------------------------------------------------------------- /3.7 uncaughtException + llnode.md: -------------------------------------------------------------------------------- 1 | 相信所有 Node.js 开发者都对 `TypeError: Cannot read property 'xxx' of undefined/null` 这种错误并不陌生,这是因为期望从一个对象上获取 xxx 属性,结果这个对象的值是 undefined 或者 null。 2 | 3 | ## 3.7.1 uncaughtException 4 | 5 | 看一段代码: 6 | 7 | ```js 8 | const article = { title: 'Node.js', content: 'Hello, Node.js' } 9 | setImmediate(() => { 10 | console.log(article.author.name) 11 | }) 12 | ``` 13 | 14 | 运行以上代码打印出: 15 | 16 | ``` 17 | /Users/nswbmw/Desktop/test/app.js:3 18 | console.log(article.author.name) 19 | ^ 20 | TypeError: Cannot read property 'name' of undefined 21 | at Timeout.setInterval [as _onTimeout] (/Users/nswbmw/Desktop/test/app.js:3:30) 22 | at ontimeout (timers.js:475:11) 23 | at tryOnTimeout (timers.js:310:5) 24 | at Timer.listOnTimeout (timers.js:270:5) 25 | ``` 26 | 27 | article 是一个文章对象有 title 和 content 属性,没有 author 属性,所以 article.author 是 undefined,调用 article.author.name 会报错。而且这个运行时错误是在一个异步函数(setImmediate)内抛出的,所以这个错误是一个 “uncaught exception”,如果没有 process.on('uncaughtException', () => {}) 事件监听器的话程序会 crash。 28 | 29 | 调试这种错误没有比较好的方法,通常只能添加 console.log 打印出 article 的值。但是我们前面介绍过 llnode 的用法,是否可以使用 llnode 调试这类问题呢?答案是肯定的。 30 | 31 | ## 3.7.2 llnode 32 | 33 | 我们添加 --abort-on-uncaught-exception 参数重新运行程序,当程序 crash 的时候,会自动 Core Dump。 34 | 35 | ```sh 36 | $ ulimit -c unlimited 37 | $ node --abort-on-uncaught-exception app.js 38 | Uncaught TypeError: Cannot read property 'name' of undefined 39 | 40 | FROM 41 | Immediate.setImmediate (/home/nswbmw/test/app.js:1:1) 42 | runCallback (timers.js:1:1) 43 | tryOnImmediate (timers.js:1:1) 44 | processImmediate [as _immediateCallback] (timers.js:1:1) 45 | Illegal instruction (core dumped) 46 | ``` 47 | 48 | 此时生成一个 core 文件,我们使用 llnode 加载并诊断这个 core 文件。 49 | 50 | ```sh 51 | $ lldb-4.0 -c ./core 52 | (lldb) target create --core "./core" 53 | Core file '/home/nswbmw/test/./core' (x86_64) was loaded. 54 | (lldb) 55 | ``` 56 | 57 | 使用 `v8 bt` 查看最近的 backtrace。 58 | 59 | ```sh 60 | (lldb) v8 bt 61 | * thread #1: tid = 4750, 0x00007ffd905d5b39 node`v8::base::OS::Abort() + 9, name = 'node', stop reason = signal SIGILL 62 | * frame #0: 0x00007ffd905d5b39 node`v8::base::OS::Abort() + 9 63 | frame #1: 0x00007ffd900a4d19 node`v8::internal::Isolate::Throw(v8::internal::Object*, v8::internal::MessageLocation*) + 489 64 | frame #2: 0x00007ffd9005e7f9 node`v8::internal::LoadIC::Load(v8::internal::Handle, v8::internal::Handle) + 569 65 | frame #3: 0x00007ffd9005f759 node`v8::internal::Runtime_LoadIC_Miss(int, v8::internal::Object**, v8::internal::Isolate*) + 633 66 | frame #4: 0x000018e6e710463d 67 | frame #5: 0x000018e6e71ecce4 68 | frame #6: 0x000018e6e71bf9ce setImmediate(this=0x0000154a3ae09429:) at /home/nswbmw/test/app.js:2:14 fn=0x0000154a3ae09281 69 | ... 70 | ``` 71 | 72 | **可以看出**:在 frame #4 处程序触发 exit,往上追溯到 frame #6 有一个 setImmediate 抛出了错误,在 app.js 第 2 行,符合打印出的错误信息。setImmediate 的回调函数的地址为 0x0000154a3ae09281,我们使用 `v8 i` 检索这个函数。 73 | 74 | ```sh 75 | (lldb) v8 i 0x0000154a3ae09281 76 | 0x0000154a3ae09281:}, 80 | article=0x0000154a3ae091d1:}> 81 | (lldb) v8 i 0x0000154a3ae091d1 82 | 0x0000154a3ae091d1:, 84 | .content=0x000014117e04ca09:}> 85 | ``` 86 | 87 | setImmediate 函数内有一个 article 对象,然后我们继续通过 `v8 i` 检索得知 article 的值为 { title: "Node.js", content: "Hello, Node.js" },并没有 author 属性,真相大白。 88 | 89 | ## 3.7.3 ReDoS 90 | 91 | DoS(Denial of Service)全称是拒绝服务攻击,ReDoS(RegExp Denial of Service)即是正则表达式拒绝服务攻击。ReDoS 是由于正则表达式写得有缺陷,所以使用正则匹配时,会出现大量占用 CPU 的情况,导致服务不可用,而导致正则表达式匹配 “卡住” 的原因正是正则表达式的 “回溯” 特性。 92 | 93 | 看一个简单的例子: 94 | 95 | ```js 96 | /a.*b/g.test('aaaf') 97 | ``` 98 | 99 | 匹配过程如下: 100 | 101 | ![](./assets/3.7.1.png) 102 | 103 | **可以看出**:因为 * 是贪婪匹配,所以第 3 步 `.*` 匹配了字符串末尾,由于剩下一个 `b` 无法匹配所以 “吐” 出一个字符再尝试匹配(第 4 步),仍然不匹配(第 5 步),继续 “吐” 出一个字符…这个 “吐” 一个字符的过程就是回溯(backtrack)。 104 | 105 | 再看个例子: 106 | 107 | ```js 108 | const reg = /(a*)+b/ 109 | 110 | console.time('reg') 111 | reg.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaf') // reg: 2572.022ms 112 | // reg.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaf') // reg: 5048.735ms 113 | // reg.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaf') // reg: 10710.070ms 114 | console.timeEnd('reg') 115 | ``` 116 | 117 | 运行以上代码,每添加一个字母 a,程序的运行时间就翻倍,这正是由于正则表达式的回溯导致的,这个正则表达式的时间复杂度为 O(2^n)。 118 | 119 | 平时写出具有回溯的正则表达式是比较常见的,这个时候程序会 “卡住”,使用 llnode 也可以调试这类问题。 120 | 121 | 运行以下代码: 122 | 123 | ```sh 124 | $ echo "/(a*)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaf')" > app.js 125 | $ node --abort-on-uncaught-exception app.js & 126 | $ kill -BUS `pgrep -n node` 127 | ``` 128 | 129 | 生成 core 文件,使用 llnode 调试。 130 | 131 | ```sh 132 | $ lldb-4.0 -c ./core 133 | (lldb) target create --core "./core" 134 | Core file '/home/nswbmw/test/./core' (x86_64) was loaded. 135 | (lldb) v8 bt 136 | * thread #1: tid = 5381, 0x000036a6db804f6b, name = 'node', stop reason = signal SIGBUS 137 | * frame #0: 0x000036a6db804f6b 138 | ... 139 | frame #6: 0x000036a6db68463d 140 | frame #7: 0x000036a6db7135f4 test(this=0x0000038344709119:, 0x0000098ccdb4c9e9:) at (no script) fn=0x0000183ca0c134a1 141 | ... 142 | (lldb) v8 i -F 0x0000098ccdb4c9e9 143 | 0x0000098ccdb4c9e9: 144 | ``` 145 | 146 | **可以看出**:在程序退出前,程序在执行一个正则表达式(`/(a*)+b/`)的 test 方法,参数是字符串(`aaaaaaaaaaaaaaaaaaaaaaaaaaaaaf`)。 147 | 148 | 减少正则表达式回溯的简单方法就是合并不必要的量词,如将上面的正则表达式 `/(a*)+b/` 修改为 `/a*b/`。 149 | 150 | ## 3.7.4 参考链接 151 | 152 | - https://www.rawidn.com/posts/ddos-and-ddos-in-regular-expression.html 153 | 154 | 上一节:[3.6 Event Loop](https://github.com/nswbmw/node-in-debugging/blob/master/3.6%20Event%20Loop.md) 155 | 156 | 下一节:[4.1 Source Map](https://github.com/nswbmw/node-in-debugging/blob/master/4.1%20Source%20Map.md) 157 | -------------------------------------------------------------------------------- /4.1 Source Map.md: -------------------------------------------------------------------------------- 1 | ## 4.1.1 什么是 Source Map? 2 | 3 | 对于 Source Map,想必大家并不陌生,在前端开发中通常要压缩 JavaScript,CSS,以减小体积,加快网页显示。但带来的后果是如果出现错误,就会导致无法定位错误,这时 Source Map 应运而生。举个例子, jQuery 1.9 引入了 Source Map,打开 [http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js](http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js),最后一行是这样的: 4 | 5 | ```js 6 | //@ sourceMappingURL=jquery.min.map 7 | ``` 8 | 9 | 这就是 Source Map。它是一个独立的 map(其实就是 JSON) 文件,通常与源码在同一个目录下。 10 | 11 | Source Map 常用于以下几个场景: 12 | 13 | 1. 压缩代码,减小体积。比如 jQuery 1.9 的源码,压缩前是 252KB,压缩后是 32KB。 14 | 2. 多个文件合并,减少 HTTP 请求数,仅用于前端。 15 | 3. 将其他语言编译成 JavaScript,例如:CoffeeScript、TypeScript 等。 16 | 17 | 本节只讲解如何使用 Source Map,关于 map 文件中字段的含义本节不会解释,有兴趣的读者可以查看参考链接中的文章。接下来我们在 Node.js 环境下以场景 1、3 为例,分别介绍如何将 uglify-es 和 TypeScript 结合 Source Map 使用。 18 | 19 | ## 4.1.2 uglify-es 20 | 21 | uglify-js 是最常用的 JavaScript 代码压缩工具,但只支持到 ES5,uglify-es 支持 ES6+ 并且兼容 uglify-js,所以本节使用 uglify-es。 22 | 23 | source-map-support 是一个在 Node.js 环境下支持 Source Map 的模块。 24 | 25 | 安装 uglify-es 和 source-map-support: 26 | 27 | ```sh 28 | $ npm i uglify-es -g 29 | $ npm i source-map-support 30 | ``` 31 | 32 | 创建测试代码: 33 | 34 | **app.js** 35 | 36 | ```js 37 | require('source-map-support').install() 38 | 39 | function sayHello (name) { 40 | throw new Error('error!!!') 41 | console.log(`Hello, ${name}`) 42 | } 43 | 44 | sayHello('World') 45 | ``` 46 | 47 | 使用 uglify-es 压缩代码文件并生成 map 文件: 48 | 49 | ```sh 50 | $ uglifyjs app.js -o app.min.js --source-map "url=app.min.js.map" 51 | ``` 52 | 53 | 生成 app.min.js 和 app.min.js.map 文件,内容分别如下: 54 | 55 | **app.min.js** 56 | 57 | ```js 58 | require("source-map-support").install();function sayHello(name){throw new Error("error!!!");console.log(`Hello, ${name}`)}sayHello("World"); 59 | //# sourceMappingURL=app.min.js.map 60 | ``` 61 | 62 | **app.min.js.map** 63 | 64 | ```json 65 | {"version":3,"sources":["app.js"],"names":["require","install","sayHello","name","Error","console","log"],"mappings":"AAAAA,QAAQ,sBAAsBC,UAE9B,SAASC,SAAUC,MACjB,MAAM,IAAIC,MAAM,YAChBC,QAAQC,cAAcH,QAGxBD,SAAS"} 66 | ``` 67 | 68 | 此时运行 app.min.js 可以显示正确的错误栈: 69 | 70 | ```js 71 | $ node app.min.js 72 | 73 | /Users/nswbmw/Desktop/test/app.js:4 74 | throw new Error('error!!!') 75 | ^ 76 | Error: error!!! 77 | at sayHello (/Users/nswbmw/Desktop/test/app.js:4:9) 78 | ``` 79 | 80 | 如果删除 app.min.js 最后那行注释,重新运行则无法显示正确的错误栈: 81 | 82 | ```js 83 | $ node app.min.js 84 | 85 | /Users/nswbmw/Desktop/test/app.min.js:1 86 | require("source-map-support").install();function sayHello(name){throw new Error("error!!!");console.log(`Hello, ${name}`)}sayHello("World"); 87 | ^ 88 | Error: error!!! 89 | at sayHello (/Users/nswbmw/Desktop/test/app.min.js:1:71) 90 | ``` 91 | 92 | source-map-support 是通过 Error.prepareStackTrace 实现的,前面讲解过它的用法,这里不再赘述。 93 | 94 | ## 4.1.3 TypeScript 95 | 96 | 全局安装 TypeScript: 97 | 98 | ```sh 99 | $ npm i typescript -g 100 | ``` 101 | 102 | 创建测试代码: 103 | 104 | **app_ts.ts** 105 | 106 | ```typescript 107 | declare function require(name: string) 108 | require('source-map-support').install() 109 | 110 | function sayHello (name: string): any { 111 | throw new Error('error!!!') 112 | } 113 | 114 | sayHello('World') 115 | ``` 116 | 117 | 运行: 118 | 119 | ```sh 120 | $ tsc --sourceMap app_ts.ts 121 | ``` 122 | 123 | 生成 app_ts.js 和 app_ts.js.map,运行 app_ts.js 如下: 124 | 125 | ```js 126 | $ node app_ts.js 127 | 128 | /Users/nswbmw/Desktop/test/app_ts.ts:5 129 | throw new Error('error!!!') 130 | ^ 131 | Error: error!!! 132 | at sayHello (/Users/nswbmw/Desktop/test/app_ts.ts:5:9) 133 | ``` 134 | 135 | ## 4.1.4 source-map-support 高级用法 136 | 137 | 我们可以在调用 install 方法时传入一个 retrieveSourceMap 参数,用来自定义处理 Source Map: 138 | 139 | ```js 140 | require('source-map-support').install({ 141 | retrieveSourceMap: function(source) { 142 | if (source === 'compiled.js') { 143 | return { 144 | url: 'original.js', 145 | map: fs.readFileSync('compiled.js.map', 'utf8') 146 | } 147 | } 148 | return null 149 | } 150 | }) 151 | ``` 152 | 153 | 比如将所有 map 文件缓存到内存中,而不是磁盘上。 154 | 155 | ## 4.1.5 参考链接 156 | 157 | - http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html 158 | - https://yq.aliyun.com/articles/73529 159 | - https://github.com/v8/v8/wiki/Stack-Trace-API 160 | - https://github.com/evanw/node-source-map-support 161 | 162 | 上一节:[3.7 uncaughtException + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/3.7%20uncaughtException%20%2B%20llnode.md) 163 | 164 | 下一节:[4.2 Chrome DevTools](https://github.com/nswbmw/node-in-debugging/blob/master/4.2%20Chrome%20DevTools.md) 165 | -------------------------------------------------------------------------------- /4.2 Chrome DevTools.md: -------------------------------------------------------------------------------- 1 | 调试是每个程序员必备的技能,因此选择合适的调试工具能极大地方便我们调试代码。Node.js 的调试方式也有很多,常见的有: 2 | 3 | 1. 万能的 console.log 4 | 2. debugger 5 | 3. node-inspector 6 | 7 | 以上本节都不会讲解,因为: 8 | 9 | 1. console.log 就不用说了。 10 | 2. debugger 不推荐使用,因为: 11 | 1. 使用繁琐,需手动打点。 12 | 2. 若忘记删除 debugger,还会引起性能问题。 13 | 3. node-inspector 已经退出历史舞台。node@6.3 以后内置了一个调试器,可以结合 Chrome DevTools 使用,而且比 node-inspector 更强大。 14 | 15 | 下面就讲讲 Chrome DevTools 的用法。 16 | 17 | ## 4.2.1 使用 Chrome DevTools 18 | 19 | 创建示例代码: 20 | 21 | **app.js** 22 | 23 | ```js 24 | const Paloma = require('paloma') 25 | const app = new Paloma() 26 | 27 | app.use(ctx => { 28 | ctx.body = 'hello world!' 29 | }) 30 | 31 | app.listen(3000) 32 | ``` 33 | 34 | 运行: 35 | 36 | ```sh 37 | $ node --inspect app.js 38 | ``` 39 | 40 | **注意**:如果想让代码在第 1 行就暂停执行,需要使用 --inspect-brk 参数启动,即 `node --inspect-brk app.js`。 41 | 42 | 打开 Chrome 浏览器,访问 chrome://inspect,如下所示: 43 | 44 | ![](./assets/4.2.1.png) 45 | 46 | 单击 Remote Target 下的 inspect,选择 Sources,如下所示: 47 | 48 | ![](./assets/4.2.2.png) 49 | 50 | 使用方式与 node-inspector 类似,可以添加断点,然后在 Console 里面直接输入变量名来打印该变量的值。如下所示,在第 6 行添加断点,然后通过 `curl localhost:3000?name=nswbmw`,代码执行到第 6 行暂停执行,在 Console 里打印 ctx.query 的值: 51 | 52 | ![](./assets/4.2.3.png) 53 | 54 | **小提示**:将鼠标悬浮在某个变量上,也会显示它的值,例如:ctx。 55 | 56 | 展开右侧的 debugger 有更多的功能,例如单步执行、单步进入、单步退出等等,这里不再详细讲解。 57 | 58 | ## 4.2.2 [NIM](https://chrome.google.com/webstore/detail/nim-node-inspector-manage/gnhhdgbaldcilmgcpfddgdbkhjohddkj) 59 | 60 | 每次调试 Node.js 都要打开隐藏那么深的入口是不是很烦?还好我们有 [NIM](https://chrome.google.com/webstore/detail/nim-node-inspector-manage/gnhhdgbaldcilmgcpfddgdbkhjohddkj)。NIM(Node Inspector Manager)是一个 Chrome 插件,可以帮助我们快捷地打开 DevTools,也可以设置自动发现并打开 DevTools。 61 | 62 | ![](./assets/4.2.4.png) 63 | 64 | ## 4.2.3 [inspect-process](https://github.com/jaridmargolin/inspect-process) 65 | 66 | 如果你觉得 NIM 用起来也麻烦,那你可能需要 [inspect-process](https://github.com/jaridmargolin/inspect-process)。 67 | 68 | 全局安装: 69 | 70 | ```sh 71 | $ npm i inspect-process -g 72 | ``` 73 | 74 | 使用: 75 | 76 | ```sh 77 | $ inspect app.js 78 | ``` 79 | 80 | inspect-process 会自动调起 Chrome DevTools,然后定位到 app.js,其余用法与 Chrome DevTools 一致。 81 | 82 | ## 4.2.4 process._debugProcess 83 | 84 | 如果一个 Node.js 进程已经启动,没有添加 --inspect 参数,我们不想重启(会丢失现场)又想调试怎么办?这时可以用 process._debugProcess。使用方法如下: 85 | 86 | 1. 通过 ps 命令或者 `pgrep -n node` 查看当前启动的 Node.js 进程的 pid,例如:53911。 87 | 2. 打开新的终端,运行:`node -e "process._debugProcess(53911)"`,原来的 Node.js 进程会打印出:Debugger listening on ws://127.0.0.1:9229/2331fa07-32af-45eb-a1a8-bead7a0ab905。 88 | 3. 调出 Chrome DevTools 进行调试。 89 | 90 | ## 4.2.5 参考链接 91 | 92 | - https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27 93 | 94 | 上一节:[4.1 Source Map](https://github.com/nswbmw/node-in-debugging/blob/master/4.1%20Source%20Map.md) 95 | 96 | 下一节:[4.3 Visual Studio Code](https://github.com/nswbmw/node-in-debugging/blob/master/4.3%20Visual%20Studio%20Code.md) 97 | -------------------------------------------------------------------------------- /4.3 Visual Studio Code.md: -------------------------------------------------------------------------------- 1 | Visual Studio Code(简称 VS Code)是一款微软开源的现代化、跨平台、轻量级的代码编辑器。VS Code 很好很强大,本节将介绍如何使用 VS Code 来调试 Node.js 代码。 2 | 3 | ## 4.3.1 基本调试 4 | 5 | 示例代码如下: 6 | 7 | **app.js** 8 | 9 | ```js 10 | const Paloma = require('paloma') 11 | const app = new Paloma() 12 | 13 | app.use(ctx => { 14 | ctx.body = 'hello world!' 15 | }) 16 | 17 | app.listen(3000) 18 | ``` 19 | 20 | 用 VS Code 加载 test 文件夹,打开 app.js,然后进行如下操作: 21 | 22 | 1. 单击左侧第 4 个 tab,切换到调试模式。 23 | 2. 单击代码第 5 行 `ctx.body='hello world!'` 左侧空白处添加断点。 24 | 3. 单击左上角 ”调试“ 的绿色三角按钮启动调试。 25 | 4. 单击左上角的终端图标打开调试控制台。 26 | 27 | 最终如下所示: 28 | 29 | ![](./assets/4.3.1.png) 30 | 31 | 从 “调试控制台“ 切换到 ”终端“,运行: 32 | 33 | ```sh 34 | $ curl localhost:3000 35 | ``` 36 | 37 | 如下所示: 38 | 39 | ![](./assets/4.3.2.png) 40 | 41 | 可以看出,VS Code 基本覆盖了 Chrome DevTools 的所有功能,并且有两个额外的优点: 42 | 43 | 1. 集成了终端,不用再打开新的终端输入命令了。 44 | 2. 调试动作里添加了 ”重启“ 和 ”停止“ 按钮,不用每次修改完代码后切回终端去重启了。 45 | 46 | 但 VS Code 的强大远不止如此,通过 launch.json 可以配置详细的调试功能。 47 | 48 | ## 4.3.2 launch.json 49 | 50 | 上图可以看出,”调试“ 按钮右边有一个下拉菜单,默认是 ”没有配置“。单击右侧的齿轮状图标,会在项目根目录下创建 .vscode 文件夹及 launch.json 文件。launch.json 的内容如下: 51 | 52 | ![](./assets/4.3.3.png) 53 | 54 | 这个默认配置的意思是执行: 55 | 56 | ```sh 57 | $ node ${workspaceFolder}/app.js 58 | ``` 59 | 60 | launch.json 其实就是存储了一些调试相关的配置,VS Code 在启动调试时,会读取 launch.json 决定以何种方式调试。launch.json 有以下常用选项: 61 | 62 | 必需字段如下: 63 | 64 | - type:调试器类型。这里是 node(内置的调试器),如果安装了 Go 和 PHP 的扩展后,则对应的 type 分别为 go 和 php。 65 | - request:请求的类型,支持 launch 和 attach。launch 就是以 debug 模式启动调试,attach 就是附加到已经启动的进程开启 debug 模式并调试,跟在上一小节中提到的用 `node -e "process._debugProcess(PID)"` 作用一样。 66 | - name:下拉菜单显示的名字。 67 | 68 | 可选字段(括号里表示适用的类型)如下: 69 | 70 | - program:可执行文件或者调试器要运行的文件 (launch)。 71 | - args:要传递给调试程序的参数 (launch)。 72 | - env:环境变量 (launch)。 73 | - cwd:当前执行目录 (launch)。 74 | - address:IP 地址 (launch & attach)。 75 | - port:端口号 (launch & attach)。 76 | - skipFiles:想要忽略的文件,数组类型 (launch & attach)。 77 | - processId:进程 PID (attach)。 78 | - ... 79 | 80 | 变量替换: 81 | 82 | - ${workspaceFolder}:当前打开工程的路径。 83 | - ${file}:当前打开文件的路径。 84 | - ${fileBasename}:当前打开文件的名字,包含后缀名。 85 | - ${fileDirname}:当前打开文件所在的文件夹的路径。 86 | - ${fileExtname}:当前打开文件的后缀名。 87 | - ${cwd}:当前执行目录。 88 | - ... 89 | 90 | 如果当前打开的文件是 app.js,则以下配置与默认配置是等效的: 91 | 92 | ```json 93 | { 94 | "version": "0.2.0", 95 | "configurations": [ 96 | { 97 | "type": "node", 98 | "request": "launch", 99 | "name": "启动程序", 100 | "program": "${file}" 101 | } 102 | ] 103 | } 104 | ``` 105 | 106 | 若想了解更多的 launch.json 选项,则请查阅: 107 | 108 | - [Debugging in Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes)。 109 | - [Debug Node.js Apps using VS Code](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_launch-configuration-attributes)。 110 | 111 | 下面以 5 个实用的技巧讲解部分 launch.json 配置的作用。 112 | 113 | ## 4.3.3 技巧 1——条件断点 114 | 115 | VS Code 可以添加条件断点,即执行到该行代码满足特定条件后程序才会中断。在断点小红点上右键选择 ”编辑断点“,可以选择以下两种条件: 116 | 117 | 1. 表达式:当表达式计算结果为 true 时中断,例如设置:`ctx.query.name === 'nswbmw'`,表示当访问 `localhost:3000?name=nswbmw` 时断点才会生效,其余请求断点无效。 118 | 2. 命中次数:同样当表达式计算结果为 true 时中断,支持运算符 <、<=、==、>、>=、%。例如: 119 | 1. \>10:执行 10 次以后,断点才会生效。 120 | 2. <3:只有前 2 次断点会生效。 121 | 3. 10:等价于 >=10。 122 | 4. %2:隔一次中断一次。 123 | 124 | **注意**:可以组合表达式和命中次数条件一起使用。在切换条件类型时,需要将原来的条件清空,否则会添加两种条件。将鼠标悬浮在断点上,可以查看设置了哪些条件。 125 | 126 | ## 4.3.4 技巧 2——skipFiles 127 | 128 | 从上面图中可以看到,在 VS Code 左侧有一个 ”调用堆栈“ 面板,显示了当前断点的调用堆栈,但无法直观地看出哪些是我们项目的代码,哪些是 node_modules 里模块的代码,而且在单步调试时会进入到 node_modules 里。总之,我们不关心 node_modules 里的代码,我们只关心项目本身的代码。这时,skipFiles 就派上用场了。 129 | 130 | skipFiles 顾名思义就是忽略我们不关心的文件。修改 launch.json 如下: 131 | 132 | ```json 133 | { 134 | "version": "0.2.0", 135 | "configurations": [ 136 | { 137 | "type": "node", 138 | "request": "launch", 139 | "name": "启动程序", 140 | "program": "${workspaceFolder}/app.js", 141 | "skipFiles": [ 142 | "${workspaceFolder}/node_modules/**/*.js", 143 | "/**/*.js" 144 | ] 145 | } 146 | ] 147 | } 148 | ``` 149 | 150 | 有以下几点需要解释: 151 | 152 | 1. 支持 ${xxx} 这种变量替换。 153 | 2. 支持 glob 模式匹配。 154 | 3. 用来忽略 Node.js 核心模块。 155 | 156 | 重启调试后,如下所示: 157 | 158 | ![](./assets/4.3.4.png) 159 | 160 | **可以看出**:在左侧 ”调用堆栈“ 中,我们不关心的调用栈都变灰了,而且单步调试也不会进入到 skipFiles 所匹配的文件里。 161 | 162 | ## 4.3.5 技巧 3——自动重启 163 | 164 | 在每次修改代码保存后都要手动重启,否则修改后的代码和断点都不会生效。VS Code 开发者们想到了这一点,通过添加配置可以实现修改代码保存后会自动重启调试,需要结合 [nodemon](https://nodemon.io/) 一起使用。 165 | 166 | 首先,全局安装 nodemon: 167 | 168 | ```sh 169 | $ npm i nodemon -g 170 | ``` 171 | 172 | 然后,修改 launch.json: 173 | 174 | ```json 175 | { 176 | "version": "0.2.0", 177 | "configurations": [ 178 | { 179 | "type": "node", 180 | "request": "launch", 181 | "name": "启动程序", 182 | "runtimeExecutable": "nodemon", 183 | "program": "${workspaceFolder}/app.js", 184 | "restart": true, 185 | "console": "integratedTerminal", 186 | "skipFiles": [ 187 | "${workspaceFolder}/node_modules/**/*.js", 188 | "/**/*.js" 189 | ] 190 | } 191 | ] 192 | } 193 | ``` 194 | 195 | 当前的 launch.json 相比较上一个版本的 launch.json,多了以下几个字段: 196 | 197 | - runtimeExecutable:用什么命令执行 app.js,这里设置为 nodemon。 198 | - restart:设置为 true,修改代码并保存后会自动重启调试。 199 | - console:当单击停止按钮或者修改代码并保存后自动重启调试,而 nodemon 是仍然在运行的,通过设置为 console 为 integratedTerminal 可以解决这个问题。此时 VS Code 终端将会打印 nodemon 的 log,可以在终端右侧的下拉菜单中选择返回第 1 个终端,然后运行 `curl localhost:3000` 进行调试。 200 | 201 | 对于已经使用 nodemon 运行的程序,例如: 202 | 203 | ```sh 204 | $ nodemon --inspect app.js 205 | ``` 206 | 207 | 可使用 attach 模式启动调试,launch.json 如下: 208 | 209 | ```json 210 | { 211 | "version": "0.2.0", 212 | "configurations": [ 213 | { 214 | "name": "Attach to node", 215 | "type": "node", 216 | "request": "attach", 217 | "restart": true, 218 | "processId": "${command:PickProcess}" 219 | } 220 | ] 221 | } 222 | ``` 223 | 224 | 运行 Attach to node 配置进行调试时,VS Code 会列出正在执行的 node 进程及对应的 PID 以供选择。也可以通过 address 和 port 参数设置 attach 到具体的进程开启调试。 225 | 226 | ## 4.3.6 技巧 4——特定操作系统设置 227 | 228 | 针对不同的操作系统,可能会用到不同的调试配置。可选的参数为: 229 | 230 | - windows 231 | - linux 232 | - osx 233 | 234 | 示例如下: 235 | 236 | ```json 237 | { 238 | "version": "0.2.0", 239 | "configurations": [ 240 | { 241 | "type": "node", 242 | "request": "launch", 243 | "name": "启动调试", 244 | "program": "./node_modules/gulp/bin/gulpfile.js", 245 | "args": ["/path/to/app.js"], 246 | "windows": { 247 | "args": ["\\path\\to\\app.js"] 248 | } 249 | } 250 | ] 251 | } 252 | ``` 253 | 254 | ## 4.3.7 技巧 5——多配置 255 | 256 | configurations 是个数组而不是个对象,这样设计就是为了可以添加多个调试配置。打开 launch.json,单击右下角的 ”添加配置…“,会弹出配置模板,如下所示: 257 | 258 | ![](./assets/4.3.5.png) 259 | 260 | configurations 可以用来配置不同的调试规则,比如最终将 launch.json 修改如下: 261 | 262 | ```json 263 | { 264 | "version": "0.2.0", 265 | "configurations": [ 266 | { 267 | "type": "node", 268 | "request": "attach", 269 | "name": "Attach to node", 270 | "restart": true, 271 | "processId": "${command:PickProcess}" 272 | }, 273 | { 274 | "type": "node", 275 | "request": "launch", 276 | "name": "启动程序", 277 | "runtimeExecutable": "nodemon", 278 | "program": "${workspaceFolder}/app.js", 279 | "restart": true, 280 | "console": "integratedTerminal", 281 | "skipFiles": [ 282 | "${workspaceFolder}/node_modules/**/*.js", 283 | "/**/*.js" 284 | ] 285 | } 286 | ] 287 | } 288 | ``` 289 | 290 | ## 4.3.8 总结 291 | 292 | VS Code 的调试功能十分强大,本节只讲解了一些常用的调试功能,对于其余的调试功能,还请读者自行尝试。 293 | 294 | ## 4.3.9 参考链接 295 | 296 | - https://code.visualstudio.com/docs/editor/debugging 297 | - https://code.visualstudio.com/docs/nodejs/nodejs-debugging 298 | 299 | 上一节:[4.2 Chrome DevTools](https://github.com/nswbmw/node-in-debugging/blob/master/4.2%20Chrome%20DevTools.md) 300 | 301 | 下一节:[4.4 debug + repl2 + power-assert](https://github.com/nswbmw/node-in-debugging/blob/master/4.4%20debug%20%2B%20repl2%20%2B%20power-assert.md) 302 | -------------------------------------------------------------------------------- /4.4 debug + repl2 + power-assert.md: -------------------------------------------------------------------------------- 1 | 上一小节讲解了如何用使用 VS Code 调试 Node.js 代码,但调试不只是打断点,比如: 2 | 3 | - 如何快速地切换输出的日志类型(或级别)? 4 | - 我想用 moment 打印出年份,是使用 `moment().format('YYYY')`,还是 `moment().format('yyyy')`,还是两种写法都可以? 5 | - 断言报错:AssertionError: false == true,没啥有用信息,黑人问号??? 6 | 7 | 本节将介绍 3 款实用的调试工具,分别解决以上 3 种情况,来提高我们的调试效率。 8 | 9 | ## 4.4.1 debug 10 | 11 | debug 是一个小巧却非常实用的日志模块,可以根据环境变量决定打印不同类型(或级别)的日志。代码如下: 12 | 13 | **app.js** 14 | 15 | ```js 16 | const normalLog = require('debug')('log') 17 | const errorLowLog = require('debug')('error:low') 18 | const errorNormalLog = require('debug')('error:normal') 19 | const errorHighLog = require('debug')('error:high') 20 | 21 | setInterval(() => { 22 | const value = Math.random() 23 | switch (true) { 24 | case value < 0.5: normalLog(value); break 25 | case value >= 0.5 && value < 0.7: errorLowLog(value); break 26 | case value >= 0.7 && value < 0.9: errorNormalLog(value); break 27 | case value >= 0.9: errorHighLog(value); break 28 | default: normalLog(value) 29 | } 30 | }, 1000) 31 | ``` 32 | 33 | 运行上面的代码,每一秒生成一个随机数,根据随机数的值模拟不同级别的日志输出: 34 | 35 | - < 0.5:正常日志。 36 | - 0.5~0.7:低级别的错误日志。 37 | - 0.7~0.9:一般级别的错误日志。 38 | - \>= 0.9:严重级别的错误日志。 39 | 40 | 运行: 41 | 42 | ```sh 43 | $ DEBUG=* node app.js 44 | ``` 45 | 46 | 打印如下: 47 | 48 | ![](./assets/4.4.1.jpg) 49 | 50 | 可以看出,debug 模块打印的日志与 console.log 相比,有以下几个特点: 51 | 52 | 1. 不同的日志类型分配了不同的颜色加以区分,更直观。 53 | 2. 添加了日志类型的前缀。 54 | 3. 添加了自上一次该类型日志打印到这次日志打印经历了多长时间的后缀。 55 | 56 | debug 模块支持以下用法: 57 | 58 | - DEBUG=*:打印所有类型的日志。 59 | - DEBUG=log:只打印 log 类型的日志。 60 | - DEBUG=error:*:打印所有以 error: 开头的日志。 61 | - DEBUG=error:*,-error:low:打印所有以 error: 开头的并且过滤掉 error:low 类型的日志。 62 | 63 | 下面演示一下第 4 种的用法,运行: 64 | 65 | ```sh 66 | $ DEBUG=error:*,-error:low node app.js 67 | ``` 68 | 69 | 打印如下: 70 | 71 | ![](./assets/4.4.2.jpg) 72 | 73 | ## 4.4.2 repl2 74 | 75 | 我们在写代码时,有时可能记不清某个模块的某个方法的具体用法,比如:用 moment 格式化年份是用 `moment().format('YYYY')` 还是用 `moment().format('yyyy')` 还是两种写法都可以?lodash 的 `_.pick` 方法能否能接收数组作为参数?这个时候相对于翻阅官方文档,在 REPL 里试一下可能会更快,通常步骤是: 76 | 77 | ```sh 78 | $ npm i moment 79 | $ node 80 | > const moment = require('moment') 81 | > moment().format('YYYY') 82 | '2017' 83 | > moment().format('yyyy') 84 | 'yyyy' 85 | ``` 86 | 87 | 一次还好,次数多了也略微烦琐。repl2 模块便是为了解决这个问题而生的。 88 | 89 | repl2 顾名思义是 REPL 的增强版,repl2 会根据一个用户配置(~/.noderc),预先加载模块到 REPL 中,省下了我们手动在 REPL 中 require 模块的过程。 90 | 91 | 全局安装 repl2: 92 | 93 | ```sh 94 | $ npm i repl2 -g 95 | ``` 96 | 97 | 使用方式很简单: 98 | 99 | 1. 将常用的模块全局安装,例如: 100 | 101 | ```sh 102 | $ npm i lodash validator moment -g 103 | ``` 104 | 105 | 2. 添加配置到 ~/.noderc: 106 | 107 | ```json 108 | { 109 | "lodash": "__", 110 | "moment": "moment", 111 | "validator": "validator" 112 | } 113 | ``` 114 | 115 | 3. 运行 noder: 116 | 117 | ```sh 118 | $ noder 119 | __ = lodash@4.17.4 -> local 120 | moment = moment@2.18.1 -> global 121 | validator = validator@7.0.0 -> global 122 | > moment().format('YYYY') 123 | '2017' 124 | > __.random(0, 5) 125 | 3 126 | > validator.isEmail('foo@bar.com') 127 | true 128 | ``` 129 | 130 | 需要讲解以下几点: 131 | 132 | 1. ~/.noderc 是一个 JSON 文件,key 是模块的名字,value 是 require 这个模块后加载到 REPL 中的变量名。这里给 lodash 命名的变量名是 __ 而不是 _,是因为 REPL 中 _ 有特殊含义,表示上一个表达式的结果。 133 | 2. repl2 会优先加载当前目录下的模块,没有找到然后再去加载全局安装的模块。上面结果显示 lodash 是从本地目录加载的,因为 test 目录下已经安装了 lodash,其余的模块没有从本地目录找到则尝试从全局 npm 目录加载。如果都没有找到,则不会加载。 134 | 135 | ## 4.4.3 power-assert 136 | 137 | 我们常用的断言库有: 138 | 139 | - [should.js](https://github.com/shouldjs/should.js) 140 | - [expect.js](https://github.com/Automattic/expect.js) 141 | - [chai](https://github.com/chaijs/chai) 142 | 143 | 但这类断言库都有一些通病: 144 | 145 | 1. 过分追求语义化,API 复杂。 146 | 2. 错误信息不足。 147 | 148 | 先看一段代码: 149 | 150 | **test.js** 151 | 152 | ```js 153 | const assert = require('assert') 154 | const should = require('should') 155 | const expect = require('expect.js') 156 | 157 | const tom = { id: 1, age: 18 } 158 | const bob = { id: 2, age: 20 } 159 | 160 | describe('app.js', () => { 161 | it('assert', () => { 162 | assert(tom.age > bob.age) 163 | }) 164 | it('should.js', () => { 165 | tom.age.should.be.above(bob.age) 166 | }) 167 | it('expect.js', () => { 168 | expect(tom.age).be.above(bob.age) 169 | }) 170 | }) 171 | ``` 172 | 173 | 运行: 174 | 175 | ```sh 176 | $ mocha 177 | ``` 178 | 179 | 结果如下: 180 | 181 | ```js 182 | app.js 183 | 1) assert 184 | 2) should.js 185 | 3) expect.js 186 | 187 | 188 | 0 passing (13ms) 189 | 3 failing 190 | 191 | 1) app.js 192 | assert: 193 | 194 | AssertionError [ERR_ASSERTION]: false == true 195 | + expected - actual 196 | 197 | -false 198 | +true 199 | 200 | at Context.it (test.js:10:5) 201 | 202 | 2) app.js 203 | should.js: 204 | AssertionError: expected 18 to be above 20 205 | at Assertion.fail (node_modules/should/cjs/should.js:275:17) 206 | at Assertion.value (node_modules/should/cjs/should.js:356:19) 207 | at Context.it (test.js:13:23) 208 | 209 | 3) app.js 210 | expect.js: 211 | Error: expected 18 to be above 20 212 | at Assertion.assert (node_modules/expect.js/index.js:96:13) 213 | at Assertion.greaterThan.Assertion.above (node_modules/expect.js/index.js:297:10) 214 | at Function.above (node_modules/expect.js/index.js:499:17) 215 | at Context.it (test.js:16:24) 216 | ``` 217 | 218 | 可以看出,基本没有有用的信息。这时,power-assert 粉墨登场。 219 | 220 | power-assert 使用起来很简单,理论上只用一个 assert 就可以了,而且可以无缝迁移。 221 | 222 | **注意**:在使用 intelli-espower-loader 时,要求必须将测试文件放到 test/ 目录下,所以我们在 test 目录下创建 test/app.js,将原来的 test.js 代码粘贴过去。 223 | 224 | 安装 power-assert 和 intelli-espower-loader,然后运行测试: 225 | 226 | ```sh 227 | $ npm i power-assert intelli-espower-loader --save-dev 228 | $ mocha -r intelli-espower-loader 229 | ``` 230 | 231 | 结果如下: 232 | 233 | ```js 234 | app.js 235 | 1) assert 236 | 2) should.js 237 | 3) expect.js 238 | 239 | 240 | 0 passing (42ms) 241 | 3 failing 242 | 243 | 1) app.js 244 | assert: 245 | 246 | AssertionError [ERR_ASSERTION]: # test/app.js:10 247 | 248 | assert(tom.age > bob.age) 249 | | | | | | 250 | | | | | 20 251 | | | | Object{id:2,age:20} 252 | | 18 false 253 | Object{id:1,age:18} 254 | 255 | + expected - actual 256 | 257 | -false 258 | +true 259 | ... 260 | ``` 261 | 262 | 错误信息非常直观,有以下两点需要说明: 263 | 264 | 1. mocha 需要引入 intelli-espower-loader,主要是转译代码,转译之后 `require('assert')` 都不需要改。 265 | 2. intelli-espower-loader 可选择地在 package.json 中添加 directories.test 配置,例如: 266 | 267 | ```json 268 | "directories": { 269 | "test": "mytest/" 270 | } 271 | ``` 272 | 273 | 如果没有 directories.test 配置,则默认是 `test/`。 274 | 275 | ## 4.4.4 参考链接 276 | 277 | - https://zhuanlan.zhihu.com/p/25956323 278 | - https://www.npmjs.com/package/intelli-espower-loader 279 | 280 | 上一节:[4.3 Visual Studio Code](https://github.com/nswbmw/node-in-debugging/blob/master/4.3%20Visual%20Studio%20Code.md) 281 | 282 | 下一节:[4.5 supervisor-hot-reload](https://github.com/nswbmw/node-in-debugging/blob/master/4.5%20supervisor-hot-reload.md) 283 | -------------------------------------------------------------------------------- /4.5 supervisor-hot-reload.md: -------------------------------------------------------------------------------- 1 | 我们在本地开发 Node.js 程序时通常会使用 [nodemon](https://github.com/remy/nodemon) 或者 [supervisor](https://github.com/petruisfan/node-supervisor) 这种进程管理工具,当有文件修改时自动重启应用。小项目还好,项目大了(尤其是前端应用)每次重启应用都用几秒到几十秒的时间,大部分时间都花在了加载及编译代码上。 2 | 3 | 这让笔者联想到前端比较火的一个名词——Hot Reload(热加载),比如 React 静态资源的热加载通过 webpack-dev-server 和 react-hot-loader 实现,webpack-dev-server 负责重新编译代码,react-hot-loader 负责热加载。 4 | 5 | 那在 Node.js 应用中,如何实现 Hot Reload 呢?最好能实现不重启应用便使新代码生效。幸好 ES6 引入了一个新特性——Proxy。 6 | 7 | ## 4.5.1 Proxy 8 | 9 | Proxy 用于修改对象的默认行为,等同于在语言层面做出修改,属于一种 “元编程”。Proxy 在要访问的对象之前架设一层拦截,在访问该对象成员时必须先经过这层拦截。示例代码如下: 10 | 11 | ```js 12 | const obj = new Proxy({}, { 13 | get: function (target, key) { 14 | console.log(`getting ${key}!`) 15 | return 'haha' 16 | } 17 | }) 18 | 19 | console.log(obj.name) 20 | // getting name! 21 | // haha 22 | console.log(obj.age) 23 | // getting age! 24 | // haha 25 | ``` 26 | 27 | **可以看出**:我们并没有在 obj 上定义 name 和 age 属性,所有获取 obj 上属性都会执行 get 方法然后打印 getting xxx! 和返回 haha。 28 | 29 | 这里 Proxy 的第 1 个参数是一个空对象,也可以是一个其他的对象,比如函数(毕竟在 JavaScript 中函数也是对象)。 30 | 31 | ```js 32 | function user () {} 33 | 34 | const obj = new Proxy(user, { 35 | get: function (target, key) { 36 | console.log(`getting ${key}!`) 37 | return 'haha' 38 | } 39 | }) 40 | 41 | console.log(user.name) 42 | // user 43 | console.log(user.age) 44 | // undefined 45 | console.log(obj.name) 46 | // getting name! 47 | // haha 48 | console.log(obj.age) 49 | // getting age! 50 | // haha 51 | new Proxy(1, {}) 52 | // TypeError: Cannot create proxy with a non-object as target or handler 53 | ``` 54 | 55 | ## 4.5.2 Proxy 实现 Hot Reload 56 | 57 | **核心原理**:使用 Proxy 将模块导出的对象包装一层 “代理”,即 module.exports 导出的是一个 Proxy 实例,定义一个 get 方法,使得获取实例上的属性其实是去获取最新的 require.cache 中的对象上的属性。同时,监听代码文件,如果有修改,则更新 require.cache。 58 | 59 | **简而言之**:我们在获取对象的属性时,中间加了一层代理,通过代理间接获取原有属性的值,如果属性值有更新,则会更新 require.cache 的缓存,那么下次再获取对象的属性时,通过代理将获取该属性最新的值。可见,Proxy 可以实现属性访问拦截,也可实现断开强引用的作用。 60 | 61 | 笔者发布了一个 [proxy-hot-reload](https://www.npmjs.com/package/proxy-hot-reload) 模块,核心代码如下: 62 | 63 | ```js 64 | module.exports = function proxyHotReload(opts) { 65 | const includes = [ ... ] 66 | const excludes = [ ... ] 67 | const filenames = _.difference(includes, excludes) 68 | 69 | chokidar 70 | .watch(filenames, { 71 | usePolling: true 72 | }) 73 | .on('change', (path) => { 74 | try { 75 | if (require.cache[path]) { 76 | const _exports = require.cache[path].exports 77 | if (_.isPlainObject(_exports) && !_.isEmpty(_exports)) { 78 | delete require.cache[path] 79 | require(path) 80 | } 81 | } 82 | } catch (e) { ... } 83 | }) 84 | .on('error', (error) => console.error(error)) 85 | 86 | shimmer.wrap(Module.prototype, '_compile', function (__compile) { 87 | return function proxyHotReloadCompile(content, filename) { 88 | if (!_.includes(filenames, filename)) { 89 | try { 90 | return __compile.call(this, content, filename) 91 | } catch (e) { ... } 92 | } else { 93 | const result = __compile.call(this, content, filename) 94 | this._exports = this.exports 95 | // non-object return original compiled code 96 | if (!_.isPlainObject(this._exports)) { 97 | return result 98 | } 99 | try { 100 | this.exports = new Proxy(this._exports, { 101 | get: function (target, key, receiver) { 102 | try { 103 | if (require.cache[filename]) { 104 | return require.cache[filename]._exports[key] 105 | } else { 106 | return Reflect.get(target, key, receiver) 107 | } 108 | } catch (e) { ... } 109 | } 110 | }) 111 | } catch (e) { ... } 112 | } 113 | } 114 | }) 115 | } 116 | ``` 117 | 118 | 简单讲解一下: 119 | 120 | 1. 可传入 includes 和 excludes 参数,支持 glob 写法,用来设置监听哪些代码文件。 121 | 2. 用 chokidar 模块监听文件,如果有改动则重新加载该文件。这里只针对 module.exports 导出的是纯对象的模块有用,做这个限制的原因是:对于非对象比如函数,一般我们导出一个函数会直接调用执行而不是获取函数上的属性或方法,这种导出非纯对象模块即使重建缓存也不会生效,所以干脆忽略。幸运的是,module.exports 导出对象占了大多数场景。 122 | 3. 用 shimmer 模块重载 Module.prototype._compile 方法,如果是被监听的文件并且导出的是纯对象,则尝试将导出的对象包装成 Proxy 实例。这样,在获取该对象上的属性时,将从 require.cache 中读取最新的值。 123 | 124 | 使用示例: 125 | 126 | **user.js** 127 | 128 | ```js 129 | module.exports = { 130 | id: 1, 131 | name: 'nswbmw' 132 | } 133 | ``` 134 | 135 | **app.js** 136 | 137 | ```js 138 | if (process.env.NODE_ENV !== 'production') { 139 | require('proxy-hot-reload')({ 140 | includes: '**/*.js' 141 | }) 142 | } 143 | 144 | const Paloma = require('paloma') 145 | const app = new Paloma() 146 | const user = require('./user') 147 | 148 | app.route({ method: 'GET', path: '/', controller (ctx) { 149 | ctx.body = user 150 | }}) 151 | 152 | app.listen(3000) 153 | ``` 154 | 155 | 浏览器访问 localhost:3000 查看结果,修改 user.js 中字段的值,然后刷新浏览器查看结果。 156 | 157 | proxy-hot-reload 有个非常明显的**缺点**:只支持对导出的是纯对象的文件做代理,而且程序入口文件不会生效,比如上面的 app.js,修改端口号只能重启才会生效。Proxy 再怎么黑魔法也只能做到这个地步了,退一步想,如果修改了 proxy-hot-reload 覆盖不到的文件(例如:app.js)降级成自动重启就好了,如果将 proxy-hot-reload 和 supervisor 结合,会怎么样呢? 158 | 159 | ## 4.5.3 [supervisor-hot-reload](https://github.com/nswbmw/supervisor-hot-reload) 160 | 161 | 如果要将 proxy-hot-reload 结合 supervisor 使用,需要解决以下几个难点: 162 | 163 | 1. 非侵入式。即代码里不再写: 164 | 165 | ```js 166 | if (process.env.NODE_ENV !== 'production') { 167 | require('proxy-hot-reload')({ 168 | includes: '**/*.js' 169 | }) 170 | } 171 | ``` 172 | 173 | 2. 参数统一。supervisor 可接受 -w 参数表明监听哪些文件,-i 参数表明忽略哪些文件,这两个参数怎么与 proxy-hot-reload 的 includes 和 excludes 参数整合。 174 | 175 | 3. 职责分明。修改代码文件并保存后,优先尝试 proxy-hot-reload 的热更新,如果 proxy-hot-reload 热更新不了,则使用 supervisor 重启。 176 | 177 | 首先,我们来看下 supervisor 的源码(lib/supervisor.js),源码中有这么一段代码: 178 | 179 | ```js 180 | var watchItems = watch.split(','); 181 | watchItems.forEach(function (watchItem) { 182 | watchItem = path.resolve(watchItem); 183 | 184 | if ( ! ignoredPaths[watchItem] ) { 185 | log("Watching directory '" + watchItem + "' for changes."); 186 | if(interactive) { 187 | log("Press rs for restarting the process."); 188 | } 189 | findAllWatchFiles(watchItem, function(f) { 190 | watchGivenFile( f, poll_interval ); 191 | }); 192 | } 193 | }); 194 | ``` 195 | 196 | 以上代码的作用是:遍历找到所有需要监听的文件,然后调用 watchGivenFile 监听文件。watchGivenFile 代码如下: 197 | 198 | ```js 199 | function watchGivenFile (watch, poll_interval) { 200 | if (isWindowsWithoutWatchFile || forceWatchFlag) { 201 | fs.watch(watch, { persistent: true, interval: poll_interval }, crashWin); 202 | } else { 203 | fs.watchFile(watch, { persistent: true, interval: poll_interval }, function(oldStat, newStat) { 204 | // we only care about modification time, not access time. 205 | if ( newStat.mtime.getTime() !== oldStat.mtime.getTime() ) { 206 | if (verbose) { 207 | log("file changed: " + watch); 208 | } 209 | } 210 | crash(); 211 | }); 212 | } 213 | if (verbose) { 214 | log("watching file '" + watch + "'"); 215 | } 216 | } 217 | ``` 218 | 219 | watchGivenFile 的作用是:用 fs.watch/fs.watchFile 监听文件,如果有改动则调用 crashWin/crash 程序退出。supervisor 使用 child_process.spawn 将程序运行在子进程,子进程退出后会被 supervisor 重新启动。相关代码如下: 220 | 221 | ```js 222 | function startProgram (prog, exec) { 223 | var child = exports.child = spawn(exec, prog, {stdio: 'inherit'}); 224 | ... 225 | child.addListener("exit", function (code) { 226 | ... 227 | startProgram(prog, exec); 228 | }); 229 | } 230 | ``` 231 | 232 | 大体理清 supervisor 的关键源码后,我们就知道如何解决上面提到的几个难点了。 233 | 234 | 首先需要修改 proxy-hot-reload,添加以下几个功能: 235 | 236 | 1. 添加 includeFiles 和 excludeFiles 选项,值为数组,用来接收 supervisor 传来的文件列表。 237 | 2. 添加 watchedFileChangedButNotReloadCache 参数。proxy-hot-reload 可以知道哪些代码文件可以热更新,哪些不可以。当监听到不能热更新的文件有修改时,则调用 watchedFileChangedButNotReloadCache 函数,这个函数里有 process.exit() 使进程退出。 238 | 239 | 难点及解决方案如下: 240 | 241 | 1. 非侵入式。因为真正的程序试运行在 supervisor 创建的子进程中,所以我们无法在 supervisor 进程中引入 proxy-hot-reload,只能通过子进程用 `node -r xxx` 提前引入并覆盖 Module.prototype._compile。解决方案:将 supervisor 需要监听的文件数组(watchFiles)和 proxy-hot-reload 配置写到一个文件(例如:proxy-hot-reload.js)里,子进程通过 `node -r proxy-hot-reload.js app.js` 预加载此文件启动。 242 | 243 | supervisor 相关代码如下: 244 | 245 | ```js 246 | // 获取 watchFiles 247 | fs.writeFileSync(path.join(__dirname, 'proxy-hot-reload.js'), ` 248 | require('${path.join(__dirname, "..", "node_modules", "proxy-hot-reload")}')({ 249 | includeFiles: ${JSON.stringify(watchFiles)}, 250 | excludeFiles: [], 251 | watchedFileChangedButNotReloadCache: function (filename) { 252 | console.log(filename + ' changed, restarting...'); 253 | setTimeout(function () { 254 | process.exit(); 255 | }, ${poll_interval}); 256 | } 257 | });`); 258 | // startChildProcess() 259 | ``` 260 | 261 | 2. 参数统一。将上面的 watchItems.forEach 内异步遍历需要监听的文件列表修改为同步,代码如下: 262 | 263 | ```js 264 | var watchFiles = [] 265 | var watchItems = watch.split(','); 266 | watchItems.forEach(function (watchItem) { 267 | watchItem = path.resolve(watchItem); 268 | if ( ! ignoredPaths[watchItem] ) { 269 | log("Watching directory '" + watchItem + "' for changes."); 270 | if(interactive) { 271 | log("Press rs for restarting the process."); 272 | } 273 | findAllWatchFiles(watchItem, function(f) { 274 | watchFiles.push(f) 275 | // watchGivenFile( f, poll_interval ); 276 | }); 277 | } 278 | }); 279 | ``` 280 | 281 | **注意**:这里 findAllWatchFiles 虽然有回调函数,但却是同步的。将 findAllWatchFiles 内的 fs.lstat/fs.stat/fs.readdir 分别改为 fs.lstatSync/fs.statSync/fs.readdirSync,这里就不贴代码了。 282 | 283 | 3. 职责分明。子进程使用 `node -r proxy-hot-reload.js app.js` 启动后,能热更新的则热更新,不能热更新的执行 watchedFileChangedButNotReloadCache,子进程退出,supervisor 会启动一个新的子进程,实现了职责分明。 284 | 285 | 笔者将改进后的 supervisor 发布成一个新的包——[supervisor-hot-reload](https://github.com/nswbmw/supervisor-hot-reload) 。使用如下: 286 | 287 | **user.js** 288 | 289 | ```js 290 | module.exports = { 291 | id: 1, 292 | name: 'nswbmw' 293 | } 294 | ``` 295 | 296 | **app.js** 297 | 298 | ```js 299 | const Paloma = require('paloma') 300 | const app = new Paloma() 301 | const user = require('./user') 302 | 303 | app.route({ method: 'GET', path: '/', controller (ctx) { 304 | ctx.body = user 305 | }}) 306 | 307 | app.listen(3000) 308 | ``` 309 | 310 | 全局安装并使用 supervisor-hot-reload: 311 | 312 | ```sh 313 | $ npm i supervisor-hot-reload -g 314 | $ DEBUG=proxy-hot-reload supervisor-hot-reload app.js 315 | ``` 316 | 317 | 修改 user.js,程序不会重启,打印: 318 | 319 | ``` 320 | proxy-hot-reload Reload file: /Users/nswbmw/Desktop/test/user.js 321 | ``` 322 | 323 | 修改 app.js,程序会重启,打印: 324 | 325 | ``` 326 | /Users/nswbmw/Desktop/test/app.js changed, restarting... 327 | Program node app.js exited with code 0 328 | 329 | Starting child process with 'node app.js' 330 | ... 331 | ``` 332 | 333 | ## 4.5.4 内存泄露问题 334 | 335 | 这里需要声明一下,虽然修改 require.cache + Proxy 实现了我们想要的功能,但这样做存在内存泄漏问题,因为即使删除了一个模块的缓存,但父模块的缓存中还引用着旧的模块导出的对象。这个问题可以不用太关心,知道为什么就好,因为我们只是在开发环境使用 proxy-hot-reload。 336 | 337 | ## 4.5.5 参考链接 338 | 339 | - https://nodejs.org/dist/latest-v8.x/docs/api/async_hooks.html 340 | 341 | 上一节:[4.4 debug + repl2 + power-assert](https://github.com/nswbmw/node-in-debugging/blob/master/4.4%20debug%20%2B%20repl2%20%2B%20power-assert.md) 342 | 343 | 下一节:[5.1 NewRelic](https://github.com/nswbmw/node-in-debugging/blob/master/5.1%20NewRelic.md) 344 | -------------------------------------------------------------------------------- /5.1 NewRelic.md: -------------------------------------------------------------------------------- 1 | [NewRelic](https://newrelic.com/) 是一个老牌的应用性能监测工具,提供 14 天免费试用,本节将讲解如何使用 NewRelic 监控 Node.js 程序的性能。 2 | 3 | 测试代码如下: 4 | 5 | **app.js** 6 | 7 | ```js 8 | require('newrelic') 9 | 10 | const crypto = require('crypto') 11 | const express = require('express') 12 | const app = express() 13 | const createUser = require('./routes/users').createUser 14 | 15 | app.get('/', (req, res) => { 16 | const salt = crypto.randomBytes(128).toString('base64') 17 | const hash = crypto.pbkdf2Sync(String(Math.random()), salt, 10000, 512, 'sha512').toString('hex') 18 | res.json({ salt, hash }) 19 | }) 20 | 21 | app.get('/error', (req, res, next) => { 22 | next(new Error('error!!!')) 23 | }) 24 | 25 | app.post('/users/:user', async (req, res) => { 26 | const user = await createUser(req.params.user, 18) 27 | res.json(user) 28 | }) 29 | 30 | app.listen(3000) 31 | ``` 32 | 33 | **routes/users.js** 34 | 35 | ```js 36 | const Mongolass = require('mongolass') 37 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 38 | const User = mongolass.model('User') 39 | 40 | exports.createUser = async function (ctx) { 41 | const name = ctx.query.name || 'default' 42 | const age = +ctx.query.age || 18 43 | const user = await createUser(name, age) 44 | ctx.status = user 45 | } 46 | 47 | async function createUser (name, age) { 48 | const user = (await User.create({ 49 | name, 50 | age 51 | })).ops[0] 52 | return user 53 | } 54 | ``` 55 | 56 | ## 5.1.1 使用 NewRelic 57 | 58 | 首先,注册一个 [NewRelic](https://newrelic.com/signup) 账号。创建一个应用,如下所示: 59 | 60 | ![](./assets/5.1.1.png) 61 | 62 | 选择 APM,进入下一步,选择 Node.js 应用,并拿到 license key: 63 | 64 | ![](./assets/5.1.2.png) 65 | 66 | 在 Node.js 中使用 NewRelic 的步骤如下: 67 | 68 | ```sh 69 | $ npm i newrelic --save # 安装 NewRelic 的 Node.js SDK 70 | $ cp node_modules/newrelic/newrelic.js . # 将默认配置文件拷贝到项目根目录下 71 | ``` 72 | 73 | 修改 newrelic.js,app_name 填写我们的应用名(例如:api),license_key 填写刚才生成的 license key。 74 | 75 | 启动测试程序,并发起几个请求,稍等几分钟,NewRelic 的后台将会收到并展示一些数据(例如:吞吐量,请求的 Urls,错误率、Apdex score 等),如下所示: 76 | 77 | ![](./assets/5.1.3.png) 78 | 79 | 试用版的功能有限,升级到付费版可解锁更多功能,例如:数据库分析、错误分析甚至 Node.js VM 监控(CPU、内存、GC、Event Loop)等等。 80 | 81 | 类似的其他 APM 有: 82 | 83 | - [AppDynamics](https://www.appdynamics.com/) 84 | - [OneAPM](oneapm.com) 85 | - [DataDog](https://www.datadoghq.com/monitor-nodejs/) 86 | - [atatus](https://www.atatus.com/) 87 | - [opbeat](https://opbeat.com/nodejs ) 88 | 89 | 用法大同小异,这里就不一一介绍了。 90 | 91 | ## 5.1.2 参考链接 92 | 93 | - https://newrelic.com/ 94 | 95 | 上一节:[4.5 supervisor-hot-reload](https://github.com/nswbmw/node-in-debugging/blob/master/4.5%20supervisor-hot-reload.md) 96 | 97 | 下一节:[5.2 Elastic APM](https://github.com/nswbmw/node-in-debugging/blob/master/5.2%20Elastic%20APM.md) 98 | -------------------------------------------------------------------------------- /5.2 Elastic APM.md: -------------------------------------------------------------------------------- 1 | ## 5.2.1 什么是 Elastic APM? 2 | 3 | Elastic APM 是 Elastic 公司开源的一款 APM 工具,目前还处于 Beta 阶段,它有以下几个优势: 4 | 5 | 1. 开源。我们可以免费使用,像使用 ELK 一样。 6 | 2. 功能完善。API 比较完善,有 Agent、Transaction 和 Trace,默认创建响应时间和每分钟请求数两种图表,且可以使用 Kibana 的 Filter 过滤生成关心的数据的图表。 7 | 3. 监控与日志统一。Elastic APM 依赖 ElasticSearch + Kibana,所以可以结合 ELK 使用,可在 Kibana 中查看监控,然后直接查询日志。 8 | 9 | Elastic APM 架构如下: 10 | 11 | ![](./assets/5.2.1.png) 12 | 13 | APM Agent(即在应用端引入的探针)将收集的日志发送到 APM Server(Go 写的 HTTP 服务),APM Server 将数据存储到 ElasticSearch 中,然后通过 Kibana 展示。 14 | 15 | Kibana 展示如下: 16 | 17 | ![](./assets/5.2.2.png) 18 | 19 | ## 5.2.2 启动 ELK 20 | 21 | 我们使用 Docker 安装并启动 ELK,运行如下命令: 22 | 23 | ```sh 24 | $ docker run -p 5601:5601 \ 25 | -p 9200:9200 \ 26 | -p 5044:5044 \ 27 | -it --name elk sebp/elk 28 | ``` 29 | 30 | ## 5.2.3 启动 APM Server 31 | 32 | 首先,下载 [APM Server](https://www.elastic.co/downloads/apm/apm-server) 解压。然后运行以下命令: 33 | 34 | ```sh 35 | $ ./apm-server setup # 导入 APM 仪表盘到 Kibana 36 | $ ./apm-server -e # 启动 APM Server,默认监听 8200 端口 37 | ``` 38 | 39 | 用浏览器打开 localhost:5601,进入 Dashboard 页,如下所示: 40 | 41 | ![](./assets/5.2.3.png) 42 | 43 | ## 5.2.4 使用 Elastic APM 44 | 45 | 测试代码如下: 46 | 47 | ```js 48 | const apm = require('elastic-apm-node').start({ 49 | appName: 'test' 50 | }) 51 | 52 | const Paloma = require('paloma') 53 | const app = new Paloma() 54 | 55 | app.route({ method: 'GET', path: '/', controller (ctx) { 56 | apm.setTransactionName(`${ctx.method} ${ctx._matchedRoute}`) 57 | ctx.status = 200 58 | }}) 59 | 60 | app.route({ method: 'GET', path: '/:name', controller (ctx) { 61 | apm.setTransactionName(`${ctx.method} ${ctx._matchedRoute}`) 62 | ctx.status = 200 63 | }}) 64 | 65 | app.listen(3000) 66 | ``` 67 | 68 | 运行该程序,发起两个请求: 69 | 70 | ```sh 71 | $ curl localhost:3000/ 72 | $ curl localhost:3000/nswbmw 73 | ``` 74 | 75 | 等待一会,Kibana 展示如下: 76 | 77 | ![](./assets/5.2.4.png) 78 | 79 | 在 Elastic APM 中,有两个术语: 80 | 81 | - transaction:一组 traces 的集合,例如:一个 HTTP 请求。 82 | - trace:一个事件及持续时间,例如:一个 SQL 查询。 83 | 84 | ## 5.2.5 错误日志 85 | 86 | 现在,我们来测试下 Elastic APM 的错误收集功能。修改测试代码为: 87 | 88 | ```js 89 | const apm = require('elastic-apm-node').start({ 90 | appName: 'test' 91 | }) 92 | 93 | const Paloma = require('paloma') 94 | const app = new Paloma() 95 | 96 | app.use(async (ctx, next) => { 97 | try { 98 | await next() 99 | } catch (e) { 100 | apm.captureError(e) 101 | ctx.status = 500 102 | ctx.message = e.message 103 | } 104 | }) 105 | 106 | app.route({ method: 'GET', path: '/', controller: function indexRouter (ctx) { 107 | apm.setTransactionName(`${ctx.method} ${ctx._matchedRoute}`) 108 | throw new Error('error!!!') 109 | }}) 110 | 111 | app.listen(3000) 112 | ``` 113 | 114 | 重启测试程序,并发起一次请求。回到 Kibana,单击 Dashboard -> [APM] Errors 可以看到错误日志记录(自动聚合)和图表,如下所示: 115 | 116 | ![](./assets/5.2.5.png) 117 | 118 | 单击 View Error Details 进入错误详情页,如下所示: 119 | 120 | ![](./assets/5.2.6.png) 121 | 122 | **可以看出**:在错误日志中展示了错误代码及行数、上下几行代码、父级函数名和所在文件等信息。 123 | 124 | ## 5.2.6 参考链接 125 | 126 | - https://www.elastic.co/guide/en/apm/agent/nodejs/0.x/index.html 127 | - https://www.elastic.co/guide/en/apm/agent/nodejs/0.x/custom-stack.html 128 | 129 | 上一节:[5.1 NewRelic](https://github.com/nswbmw/node-in-debugging/blob/master/5.1%20NewRelic.md) 130 | 131 | 下一节:[6.1 koa-await-breakpoint](https://github.com/nswbmw/node-in-debugging/blob/master/6.1%20koa-await-breakpoint.md) 132 | -------------------------------------------------------------------------------- /6.1 koa-await-breakpoint.md: -------------------------------------------------------------------------------- 1 | 日志打点一直是个调试最头疼的问题。如果直接在代码中插入埋点代码,不仅侵入性强而且工作量大也不够灵活,要是能做到智能打点就好了,koa-await-breakpoint 正是我们需要的。 2 | 3 | ## 6.1.1 什么是 [koa-await-breakpoint](https://github.com/nswbmw/koa-await-breakpoint)? 4 | 5 | koa-await-breakpoint 是一个 Koa 的中间件,是一个在 routes/controllers 里(作用域包含 ctx)的 await 表达式前后自动打点的工具,不用插入一行日志打点代码,只需要在引入时配置一下,就可以记录每个请求到来时 await 表达式前后的现场,例如: 6 | 7 | 1. await 表达式所在的文件及行列号(filename)。 8 | 2. await 表达式是执行的第几步(step)。 9 | 3. await 表达式字符串形式(fn)。 10 | 4. 执行 await 表达式所花费的毫秒(take)。 11 | 5. 执行 await 表达式的结果(result)。 12 | 6. 当前请求的 ctx。 13 | 14 | 使用方法如下: 15 | 16 | ```js 17 | // On top of the main file 18 | const koaAwaitBreakpoint = require('koa-await-breakpoint')({ 19 | name: 'api', 20 | files: ['./routes/*.js'] 21 | }) 22 | 23 | const Koa = require('koa') 24 | const app = new Koa() 25 | 26 | // Generally, above other middlewares 27 | app.use(koaAwaitBreakpoint) 28 | ... 29 | 30 | app.listen(3000) 31 | ``` 32 | 33 | ## 6.1.2 实现原理 34 | 35 | 1. 重载 Module.prototype._compile,相当于 hack 了 require,如果发现是 require 了配置里指定的文件,则进行下一步,否则返回原始代码的内容,相关源代码如下: 36 | ```js 37 | shimmer.wrap(Module.prototype, '_compile', function (__compile) { 38 | return function koaBreakpointCompile(content, filename) { 39 | if (!_.includes(filenames, filename)) { 40 | return __compile.call(this, content, filename); 41 | } 42 | ... 43 | }; 44 | }); 45 | ``` 46 | 47 | 2. 用 esprima 解析代码,生成 AST。例如: 48 | ```js 49 | const Mongolass = require('mongolass') 50 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 51 | const User = mongolass.model('users') 52 | 53 | exports.getUsers = async function getUsers(ctx) { 54 | await User.create({ 55 | name: 'xx', 56 | age: 18 57 | }) 58 | const users = await User.find() 59 | return users 60 | } 61 | ``` 62 | 会生成如下 AST,只截取了 `await User.create(...)` 相关的 AST: 63 | 64 | ```js 65 | Script { 66 | ... 67 | AwaitExpression { 68 | type: 'AwaitExpression', 69 | argument: 70 | CallExpression { 71 | type: 'CallExpression', 72 | callee: 73 | StaticMemberExpression { 74 | type: 'MemberExpression', 75 | computed: false, 76 | object: 77 | Identifier { 78 | type: 'Identifier', 79 | name: 'User', 80 | loc: { start: { line: 6, column: 10 }, end: { line: 6, column: 14 } } }, 81 | property: 82 | Identifier { 83 | type: 'Identifier', 84 | name: 'create', 85 | loc: { start: { line: 6, column: 15 }, end: { line: 6, column: 21 } } }, 86 | loc: { start: { line: 6, column: 10 }, end: { line: 6, column: 21 } } }, 87 | arguments: 88 | [ ObjectExpression { 89 | type: 'ObjectExpression', 90 | properties: 91 | [ Property { 92 | type: 'Property', 93 | key: 94 | Identifier { 95 | type: 'Identifier', 96 | name: 'name', 97 | loc: { start: { line: 7, column: 6 }, end: { line: 7, column: 10 } } }, 98 | computed: false, 99 | value: 100 | Literal { 101 | type: 'Literal', 102 | value: 'xx', 103 | raw: '\'xx\'', 104 | loc: { start: { line: 7, column: 12 }, end: { line: 7, column: 16 } } }, 105 | kind: 'init', 106 | method: false, 107 | shorthand: false, 108 | loc: { start: { line: 7, column: 6 }, end: { line: 7, column: 16 } } }, 109 | Property { 110 | type: 'Property', 111 | key: 112 | Identifier { 113 | type: 'Identifier', 114 | name: 'age', 115 | loc: { start: { line: 8, column: 6 }, end: { line: 8, column: 9 } } }, 116 | computed: false, 117 | value: 118 | Literal { 119 | type: 'Literal', 120 | value: 18, 121 | raw: '18', 122 | loc: { start: { line: 8, column: 11 }, end: { line: 8, column: 13 } } }, 123 | kind: 'init', 124 | method: false, 125 | shorthand: false, 126 | loc: { start: { line: 8, column: 6 }, end: { line: 8, column: 13 } } } ], 127 | loc: { start: { line: 6, column: 22 }, end: { line: 9, column: 5 } } } ], 128 | loc: { start: { line: 6, column: 10 }, end: { line: 9, column: 6 } } }, 129 | loc: { start: { line: 6, column: 4 }, end: { line: 9, column: 6 } } }, 130 | ... 131 | ``` 132 | 133 | 3. 遍历找到 awaitExpression 节点,进行以下包装后生成 AST,替换掉原来的节点。 134 | ```js 135 | global.logger( 136 | (typeof ctx !== 'undefined' ? ctx : this), 137 | function(){ 138 | return awaitExpression 139 | }, 140 | awaitExpressionString, 141 | filename 142 | ) 143 | ``` 144 | 相关源代码如下: 145 | ```js 146 | findAwaitAndWrapLogger(parsedCodes) 147 | try { 148 | content = escodegen.generate(parsedCodes, { 149 | format: { indent: { style: ' ' } }, 150 | sourceMap: filename, 151 | sourceMapWithCode: true 152 | }) 153 | } catch (e) { 154 | console.error('cannot generate code for file: %s', filename) 155 | console.error(e.stack) 156 | process.exit(1) 157 | } 158 | debug('file %s regenerate codes:\n%s', filename, content.code) 159 | ``` 160 | findAwaitAndWrapLogger 的作用就是遍历 AST,将 awaitExpression 替换成用日志函数包裹后新的 awaitExpression 的 AST。最后用 escodegen 将 AST 生成代码(支持 soucemap,所以错误栈对应的行数是正确的)。 161 | 162 | **核心**:每个请求到来时,生成一个 requestId(可自定义,默认为 uuid)挂载到 ctx 上,这样就可以通过 requestId 将日志串起来了。 163 | 164 | **特点**:可以记录每个请求的每一步(await 表达式)的现场及返回值,方便查日志。 165 | 166 | ## 6.1.3 使用 koa-await-breakpoint 167 | 168 | 测试代码如下: 169 | 170 | **app.js** 171 | 172 | ```js 173 | const koaAwaitBreakpoint = require('koa-await-breakpoint')({ 174 | name: 'api', 175 | files: ['./routes/*.js'] 176 | }) 177 | 178 | const Paloma = require('paloma') 179 | const app = new Paloma() 180 | const userRouter = require('./routes/user') 181 | 182 | app.use(koaAwaitBreakpoint) 183 | app.route({ method: 'GET', path: '/users', controller: userRouter.getUsers }) 184 | 185 | app.listen(3000) 186 | ``` 187 | 188 | **routes/user.js** 189 | 190 | ```js 191 | const Mongolass = require('mongolass') 192 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 193 | const User = mongolass.model('users') 194 | 195 | exports.getUsers = async function getUsers (ctx) { 196 | await User.create({ 197 | name: 'xx', 198 | age: 18 199 | }) 200 | 201 | const users = await User.find() 202 | ctx.body = users 203 | } 204 | ``` 205 | 206 | 运行: 207 | 208 | ```sh 209 | $ DEBUG=koa-await-breakpoint node app.js 210 | ``` 211 | 212 | 终端打印出转换后的代码,可以看出 routes/users.js 被转换成了: 213 | 214 | ```js 215 | const Mongolass = require('mongolass'); 216 | const mongolass = new Mongolass('mongodb://localhost:27017/test'); 217 | const User = mongolass.model('users'); 218 | exports.getUsers = async function getUsers(ctx) { 219 | await global.logger(typeof ctx !== 'undefined' ? ctx : this, function () { 220 | return User.create({ 221 | name: 'xx', 222 | age: 18 223 | }); 224 | }, 'User.create({\n name: \'xx\',\n age: 18\n})', '/Users/nswbmw/Desktop/test/routes/user.js:6:2'); 225 | const users = await global.logger(typeof ctx !== 'undefined' ? ctx : this, function () { 226 | return User.find(); 227 | }, 'User.find()', '/Users/nswbmw/Desktop/test/routes/user.js:11:16'); 228 | ctx.body = users; 229 | }; 230 | ``` 231 | 232 | 访问 localhost:3000/users,终端打印出: 233 | 234 | ```js 235 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","timestamp":"2018-02-26T06:31:31.100Z","this":...,"type":"start","step":1,"take":0} 236 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","step":2,"filename":"/Users/nswbmw/Desktop/test/routes/user.js:6:2","timestamp":"2018-02-26T06:31:31.104Z","this":...,"type":"beforeAwait","fn":"User.create({\n name: 'xx',\n age: 18\n})","take":4} 237 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","step":3,"filename":"/Users/nswbmw/Desktop/test/routes/user.js:6:2","timestamp":"2018-02-26T06:31:31.175Z","this":...,"type":"afterAwait","fn":"User.create({\n name: 'xx',\n age: 18\n})","result":{"result":{"ok":1,"n":1},"ops":[{"name":"xx","age":18,"_id":"5a93a9c3cf8c8797c9b47482"}],"insertedCount":1,"insertedIds":["5a93a9c3cf8c8797c9b47482"]},"take":71} 238 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","step":4,"filename":"/Users/nswbmw/Desktop/test/routes/user.js:11:16","timestamp":"2018-02-26T06:31:31.175Z","this":...,"type":"beforeAwait","fn":"User.find()","take":0} 239 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","step":5,"filename":"/Users/nswbmw/Desktop/test/routes/user.js:11:16","timestamp":"2018-02-26T06:31:31.180Z","this":...,"type":"afterAwait","fn":"User.find()","result":[{"_id":"5a93a9c3cf8c8797c9b47482","name":"xx","age":18}],"take":5} 240 | {"name":"api","requestId":"50dbda0c-9e13-4659-acce-b237bc5178b7","timestamp":"2018-02-26T06:31:31.181Z","this":...,"type":"end","step":6,"take":1} 241 | ``` 242 | 243 | **注意**:type 是以下其中一种,take 的单位是 ms。 244 | 245 | 1. start:请求到来时第 1 次打点。 246 | 2. beforeAwait:上一个 awaitExpression 之后到这一个 awaitExpression 之前。 247 | 3. afterAwait:这个 awaitExpression 开始到结束。 248 | 4. error:错误日志,包含了错误信息。 249 | 5. end:请求结束时打点。 250 | 251 | ## 6.1.4 自定义日志存储 252 | 253 | store 参数最好自己定义(默认打印日志到 stdout),该参数是一个对象并且有一个 save 方法即可。在 save 方法内可做一些逻辑修改或者日志策略,比如: 254 | 255 | 1. 添加日志标识(例如:name)方便区分不同服务的日志。 256 | 2. 针对错误日志,添加一些额外字段方便追踪现场。 257 | 3. 将日志发送到 Logstash 或其他日志服务。 258 | 4. 限制日志频率,比如:只有响应时间大于 500ms 的请求日志才会被记录。 259 | 260 | **koa_await_breakpoint_store.js** 261 | 262 | ```js 263 | exports.save = function save(record, ctx) { 264 | record.name = 'app name' 265 | record.env = process.env.NODE_ENV 266 | 267 | if (record.error) { 268 | record.error = { 269 | message: record.error.message, 270 | stack: record.error.stack, 271 | status: record.error.status || record.error.statusCode || 500 272 | } 273 | } 274 | ... 275 | logstash.send(record) 276 | } 277 | ``` 278 | 279 | ## 6.1.5 参考链接 280 | 281 | - https://github.com/jquery/esprima 282 | - https://github.com/estools/escodegen 283 | 284 | 上一节:[5.2 Elastic APM](https://github.com/nswbmw/node-in-debugging/blob/master/5.2%20Elastic%20APM.md) 285 | 286 | 下一节:[6.2 async_hooks](https://github.com/nswbmw/node-in-debugging/blob/master/6.2%20async_hooks.md) 287 | -------------------------------------------------------------------------------- /6.2 async_hooks.md: -------------------------------------------------------------------------------- 1 | 上一小节讲解了 koa-await-breakpoint 的用法,但 koa-await-breakpoint 仍然有一个很大的缺憾,即无法记录除 routes/controllers 外的函数的执行时间(因为获取不到当前请求的 ctx)。举个通俗的例子:在一个路由的 controller 里面调用了 A ,A 调用了其他文件的 B ,B 又调用了其他文件的 C...这是非常常见的用法,但之前使用 koa-await-breakpoint 只能获取 A 的执行时间,无法获取 B 和 C 的执行时间。 2 | 3 | **根本原因在于**:无法知道函数之间的调用关系,即 B 不知道是 A 调用的它,即便知道也不知道是哪次请求到来时执行的 A 调用的它。 4 | 5 | 但是,node@8.1 引入了一个黑魔法——Async Hooks。 6 | 7 | ## 6.2.1 Async Hooks 8 | 9 | 我们先看看 async_hooks 是什么。Node.js 官网对 async_hooks 的介绍为: 10 | 11 | > The `async_hooks` module provides an API to register callbacks tracking the lifetime of asynchronous resources created inside a Node.js application. 12 | 13 | **一句话概括**:async_hooks 用来追踪 Node.js 中异步资源的生命周期。 14 | 15 | 我们来看段测试代码: 16 | 17 | ```js 18 | const fs = require('fs') 19 | const async_hooks = require('async_hooks') 20 | 21 | async_hooks.createHook({ 22 | init (asyncId, type, triggerAsyncId, resource) { 23 | fs.writeSync(1, `${type}(${asyncId}): trigger: ${triggerAsyncId}\n`) 24 | }, 25 | destroy (asyncId) { 26 | fs.writeSync(1, `destroy: ${asyncId}\n`); 27 | } 28 | }).enable() 29 | 30 | async function A () { 31 | fs.writeSync(1, `A -> ${async_hooks.executionAsyncId()}\n`) 32 | setTimeout(() => { 33 | fs.writeSync(1, `A in setTimeout -> ${async_hooks.executionAsyncId()}\n`) 34 | B() 35 | }) 36 | } 37 | 38 | async function B () { 39 | fs.writeSync(1, `B -> ${async_hooks.executionAsyncId()}\n`) 40 | process.nextTick(() => { 41 | fs.writeSync(1, `B in process.nextTick -> ${async_hooks.executionAsyncId()}\n`) 42 | C() 43 | C() 44 | }) 45 | } 46 | 47 | function C () { 48 | fs.writeSync(1, `C -> ${async_hooks.executionAsyncId()}\n`) 49 | Promise.resolve().then(() => { 50 | fs.writeSync(1, `C in promise.then -> ${async_hooks.executionAsyncId()}\n`) 51 | }) 52 | } 53 | 54 | fs.writeSync(1, `top level -> ${async_hooks.executionAsyncId()}\n`) 55 | A() 56 | ``` 57 | 58 | async_hooks.createHook 可以注册 4 个方法来跟踪所有异步资源的初始化(init)、回调之前(before)、回调之后(after)、销毁后(destroy)事件,并通过调用 .enable() 启用,调用 .disable() 关闭。 59 | 60 | 这里我们只关心异步资源的初始化和销毁的事件,并使用 `fs.writeSync(1, msg)` 打印到标准输出,writeSync 的第 1 个参数接收文件描述符,1 表示标准输出。为什么不使用 console.log 呢?因为 console.log 是一个异步操作,如果在 init、before、after 和 destroy 事件处理函数中出现,就会导致无限循环,同理也不能使用任何其他的异步操作。 61 | 62 | 运行该程序,打印如下: 63 | 64 | ``` 65 | top level -> 1 66 | PROMISE(6): trigger: 1 67 | A -> 1 68 | Timeout(7): trigger: 1 69 | TIMERWRAP(8): trigger: 1 70 | A in setTimeout -> 7 71 | PROMISE(9): trigger: 7 72 | B -> 7 73 | TickObject(10): trigger: 7 74 | B in process.nextTick -> 10 75 | C -> 10 76 | PROMISE(11): trigger: 10 77 | PROMISE(12): trigger: 11 78 | C -> 10 79 | PROMISE(13): trigger: 10 80 | PROMISE(14): trigger: 13 81 | C in promise.then -> 12 82 | C in promise.then -> 14 83 | destroy: 7 84 | destroy: 10 85 | destroy: 8 86 | ``` 87 | 88 | 这段程序的打印结果包含了很多信息,下面逐一进行解释: 89 | 90 | 1. 为了实现对异步资源的跟踪,Node.js 对每一个函数(不论异步还是同步)提供了一个 async scope,我们可以通过调用 `async_hooks.executionAsyncId()` 来获取函数当前的 async scope 的 id(称为 asyncId),通过调用 `async_hooks.triggerAsyncId()` 来获取当前函数调用者的 asyncId。 91 | 2. 异步资源在创建时触发 init 事件函数,init 函数中的第 1 个参数代表该异步资源的 asyncId,type 表示异步资源的类型(例如 TCPWRAP、PROMISE、Timeout、Immediate、TickObject 等等),triggerAsyncId 表示该异步资源的调用者的 asyncId。异步资源在销毁时触发 destroy 事件函数,该函数只接收一个参数,即该异步资源的 asyncId。 92 | 3. 函数调用关系明确。我们通过上面的打印结果可以很容易地看出(从下往上看) :C(asyncId: 10)被 B(asyncId: 7)调用,B(asyncId: 7)被 A(asyncId: 1)调用。而且 C 的 promise.then 里面的 asyncId(值为 12/14)也可以通过 12/14 -> 11/13 -> 10 定位到 C 的 asyncId(值为 10)。 93 | 4. 同步函数每次调用的 asyncId 都一样,如上所示,C 调用了两次,都打印了 C -> 10,与调用方的作用域的 asyncId 一致,即如上所示打印的 B in process.nextTick -> 10。异步函数每次调用的 asyncId 都不一样,即如上所示打印的 C in promise.then -> 12 和 C in promise.then -> 14。 94 | 5. 最外层作用域的 asyncId 总是 1,每个异步资源在创建时 asyncId 全局递增。 95 | 96 | 上面 5 条结论非常重要。接下来我们看看如何使用 async_hooks 改造 koa-await-breakpoint。 97 | 98 | ## 6.2.2 改造 koa-await-breakpoint 99 | 100 | 我们通过前面的结论已经知道,使用 async_hooks 时可以通过 asyncId 串起函数的调用关系,但是如何将这些函数的调用链与 koa 接收的每个请求关联起来呢? 101 | 102 | 首先,定义一个全局 Map,存储函数的调用关系: 103 | 104 | ```js 105 | const async_hooks = require('async_hooks') 106 | const asyncIdMap = new Map() 107 | 108 | async_hooks.createHook({ 109 | init (asyncId, type, triggerAsyncId) { 110 | const ctx = getCtx(triggerAsyncId) 111 | if (ctx) { 112 | asyncIdMap.set(asyncId, ctx) 113 | } else { 114 | asyncIdMap.set(asyncId, triggerAsyncId) 115 | } 116 | }, 117 | destroy (asyncId) { 118 | asyncIdMap.delete(asyncId) 119 | } 120 | }).enable() 121 | 122 | function getCtx (asyncId) { 123 | if (!asyncId) { 124 | return 125 | } 126 | if (typeof asyncId === 'object' && asyncId.app) { 127 | return asyncId 128 | } 129 | return getCtx(asyncIdMap.get(asyncId)) 130 | } 131 | ``` 132 | 133 | 有以下三点需要解释: 134 | 135 | 1. 定义了一个全局 Map 来存储函数的调用关系,在适当的地方(下面会讲到)将当前请求的 ctx 存储到 Map 中,key 是 asyncId。 136 | 2. 每个异步资源在初始化时,会尝试通过 asyncId 向上寻找祖先的 value 是否是 ctx(koa 应用中每个请求的 ctx),如果有,则直接将 value 设置为 ctx,否则将 value 设置为调用者的 asyncId(即 triggerAsyncId)。 137 | 3. 在 destroy 事件函数里直接删除调用关系,保证了不会引起内存泄漏,即杜绝引用了 ctx 但没有释放的情况。 138 | 139 | 然后,修改 global[loggerName] 如下: 140 | 141 | ```js 142 | global[loggerName] = async function (ctx, fn, fnStr, filename) { 143 | const originalContext = ctx 144 | let requestId = _getRequestId() 145 | 146 | const asyncId = async_hooks.executionAsyncId() 147 | if (!requestId) { 148 | const _ctx = getCtx(asyncId) 149 | if (_ctx) { 150 | ctx = _ctx 151 | requestId = _getRequestId() 152 | } 153 | } else { 154 | asyncIdMap.set(asyncId, ctx) 155 | } 156 | 157 | if (requestId) { 158 | _logger('beforeAwait') 159 | } 160 | const result = await fn.call(originalContext) 161 | if (requestId) { 162 | _logger('afterAwait', result) 163 | } 164 | return result 165 | 166 | function _getRequestId () { 167 | return ctx && ctx.app && _.get(ctx, requestIdPath) 168 | } 169 | 170 | function _logger (type, result) { 171 | ... 172 | } 173 | } 174 | ``` 175 | 176 | 有以下两点需要解释: 177 | 178 | 1. logger 函数传入的第 1 个参数 ctx,之前是每个请求的 ctx,现在可能是当前执行上下文的 this,所以先将 ctx 赋值给 originalContext,然后通过 `await fn.call(originalContext)` 让函数在执行时有正确的上下文。 179 | 2. 如果传入的 ctx 是来自请求的 ctx 且能拿到 requestId,那么将当前 asyncId 和 ctx 写入 Map,如果不是来自请求的 ctx,则尝试从 Map 里向上寻找祖先的 value 是否是 ctx,如果找到,则覆盖当前的 ctx 并拿到 requestId。 180 | 181 | 至此,koa-await-breakpoint 全部改造完毕。接下来我们通过一个例子验证下升级后的 koa-await-breakpoint: 182 | 183 | **app.js** 184 | 185 | ```js 186 | const koaAwaitBreakpoint = require('koa-await-breakpoint')({ 187 | files: ['./routes/*.js'] 188 | }) 189 | 190 | const Paloma = require('paloma') 191 | const app = new Paloma() 192 | 193 | app.use(koaAwaitBreakpoint) 194 | app.route({ method: 'POST', path: '/users', controller: require('./routes/user').createUser }) 195 | 196 | app.listen(3000) 197 | ``` 198 | 199 | **routes/users.js** 200 | 201 | ```js 202 | const Mongolass = require('mongolass') 203 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 204 | const User = mongolass.model('User') 205 | const Post = mongolass.model('Post') 206 | const Comment = mongolass.model('Comment') 207 | 208 | exports.createUser = async function (ctx) { 209 | const name = ctx.query.name || 'default' 210 | const age = +ctx.query.age || 18 211 | await createUser(name, age) 212 | ctx.status = 204 213 | } 214 | 215 | async function createUser (name, age) { 216 | const user = (await User.create({ 217 | name, 218 | age 219 | })).ops[0] 220 | await createPost(user) 221 | } 222 | 223 | async function createPost (user) { 224 | const post = (await Post.create({ 225 | uid: user._id, 226 | title: 'post', 227 | content: 'post' 228 | })).ops[0] 229 | 230 | await createComment(user, post) 231 | } 232 | 233 | async function createComment (user, post) { 234 | await Comment.create({ 235 | userId: user._id, 236 | postId: post._id, 237 | content: 'comment' 238 | }) 239 | } 240 | ``` 241 | 242 | 这段代码的意思是:在访问创建用户接口时,调用 createUser,createUser 里面又调用了 createPost,createPost 里面又调用了 createComment。运行: 243 | 244 | ```sh 245 | $ curl -XPOST localhost:3000/users 246 | ``` 247 | 248 | 打印如下: 249 | 250 | ```js 251 | { type: 'start', 252 | step: 1, 253 | take: 0 ... } 254 | { type: 'beforeAwait', 255 | step: 2, 256 | fn: 'createUser(name, age)', 257 | take: 1 ... } 258 | { type: 'beforeAwait', 259 | step: 3, 260 | fn: 'User.create(...)', 261 | take: 1 ... } 262 | { type: 'afterAwait', 263 | step: 4, 264 | fn: 'User.create(...)', 265 | take: 36 ... } 266 | { type: 'beforeAwait', 267 | step: 5, 268 | fn: 'createPost(user)', 269 | take: 1 ... } 270 | { type: 'beforeAwait', 271 | step: 6, 272 | fn: 'Post.create(...)', 273 | take: 0 ... } 274 | { type: 'afterAwait', 275 | step: 7, 276 | fn: 'Post.create(...)', 277 | take: 3 ... } 278 | { type: 'beforeAwait', 279 | step: 8, 280 | fn: 'createComment(user, post)', 281 | take: 1 ... } 282 | { type: 'beforeAwait', 283 | step: 9, 284 | fn: 'Comment.create(...)', 285 | take: 0 ... } 286 | { type: 'afterAwait', 287 | step: 10, 288 | fn: 'Comment.create(...)', 289 | take: 1 ... } 290 | { type: 'afterAwait', 291 | step: 11, 292 | fn: 'createComment(user, post)', 293 | take: 1 ... } 294 | { type: 'afterAwait', 295 | step: 12, 296 | fn: 'createPost(user)', 297 | take: 6 ... } 298 | { type: 'afterAwait', 299 | step: 13, 300 | fn: 'createUser(name, age)', 301 | take: 44 ... } 302 | { type: 'end', 303 | step: 14, 304 | take: 0 ... } 305 | ``` 306 | 307 | 至此,一个全链路、无侵入、强大的日志打点工具就完成了。 308 | 309 | **注意**:使用 async_hooks 在目前有较严重的性能损耗,见 [https://github.com/bmeurer/async-hooks-performance-impact](https://github.com/bmeurer/async-hooks-performance-impact),请慎重在生产环境中使用。 310 | 311 | ## 6.2.3 参考链接 312 | 313 | - https://nodejs.org/dist/latest-v8.x/docs/api/async_hooks.html 314 | - https://zhuanlan.zhihu.com/p/27394440 315 | - https://www.jianshu.com/p/4a568dac41ed 316 | 317 | 上一节:[6.1 koa-await-breakpoint](https://github.com/nswbmw/node-in-debugging/blob/master/6.1%20koa-await-breakpoint.md) 318 | 319 | 下一节:[6.3 ELK](https://github.com/nswbmw/node-in-debugging/blob/master/6.3%20ELK.md) 320 | -------------------------------------------------------------------------------- /6.3 ELK.md: -------------------------------------------------------------------------------- 1 | ELK 是 ElasticSearch + Logstash + Kibana 这套组合工具的简称,是一个常用的日志系统。 2 | 3 | - ElasticSearch:是一款开源的基于 Lucene 之上实现的一个分布式搜索引擎,也是一个存储引擎(例如:日志),它的特点有:分布式、零配置、自动发现、索引自动分片、索引副本机制、Restful 风格的接口、多数据源和自动搜索负载等。 4 | - Logstash:是一款开源的日志收集工具,它可以对日志进行收集、分析、过滤,并将其存储(例如:ElasticSearch)起来供以后使用。 5 | - Kibana:是一款开源的可视化工具,可以为 ElasticSearch 提供的日志分析友好的 Web 界面,可以汇总、分析和搜索重要的数据日志。 6 | 7 | ## 6.3.1 安装 ELK 8 | 9 | 我们使用 Docker 安装 ELK,运行如下命令: 10 | 11 | ```sh 12 | $ docker run -p 5601:5601 \ 13 | -p 9200:9200 \ 14 | -p 5044:5044 \ 15 | -p 15044:15044/udp \ 16 | -it --name elk sebp/elk 17 | ``` 18 | 19 | 进入容器: 20 | 21 | ```sh 22 | $ docker exec -it elk /bin/bash 23 | ``` 24 | 25 | 运行以下命令设置 logstash 的 input 和 output: 26 | 27 | ```sh 28 | # /opt/logstash/bin/logstash --path.data /tmp/logstash/data \ 29 | -e 'input { udp { codec => "json" port => 15044 } } output { elasticsearch { hosts => ["localhost"] } }' 30 | ``` 31 | 32 | 这里我们启动一个 15044 的 UDP 端口,用来接收通过 UDP 发送到 Logstash 的日志。 33 | 34 | 用浏览器打开 localhost:5601,如下所示: 35 | 36 | ![](./assets/6.3.1.png) 37 | 38 | 目前还没有指定 index(ElasticSearch 的 index 类似于 MySQL/MongoDB 中的 database),即日志来源。下面我们尝试向 ELK 中写入一些日志。 39 | 40 | ## 6.3.2 使用 ELK 41 | 42 | 这里仍然以使用 koa-await-breakpoint 为例,来演示如何将日志发送到 ELK。 43 | 44 | **app.js** 45 | 46 | ```js 47 | const koaAwaitBreakpoint = require('koa-await-breakpoint')({ 48 | name: 'api', 49 | files: ['./routes/*.js'], 50 | store: require('./logger') 51 | }) 52 | 53 | const Paloma = require('paloma') 54 | const app = new Paloma() 55 | 56 | app.use(koaAwaitBreakpoint) 57 | app.route({ method: 'POST', path: '/users', controller: require('./routes/user').createUser }) 58 | 59 | app.listen(3000) 60 | ``` 61 | 62 | **logger.js** 63 | 64 | ```js 65 | const Logstash = require('logstash-client') 66 | 67 | const logstash = new Logstash({ 68 | type: 'udp', 69 | host: 'localhost', 70 | port: 15044 71 | }) 72 | 73 | module.exports = { 74 | save (log) { 75 | if (log.error) { 76 | log.errMsg = log.error.message 77 | log.errStack = log.error.stack 78 | } 79 | logstash.send(log) 80 | } 81 | } 82 | ``` 83 | 84 | **routes/user.js** 85 | 86 | ```js 87 | const Mongolass = require('mongolass') 88 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 89 | const User = mongolass.model('User') 90 | const Post = mongolass.model('Post') 91 | const Comment = mongolass.model('Comment') 92 | 93 | exports.createUser = async function (ctx) { 94 | const name = ctx.query.name || 'default' 95 | const age = +ctx.query.age || 18 96 | await createUser(name, age) 97 | ctx.status = 204 98 | } 99 | 100 | async function createUser (name, age) { 101 | const user = (await User.create({ 102 | name, 103 | age 104 | })).ops[0] 105 | await createPost(user) 106 | } 107 | 108 | async function createPost (user) { 109 | const post = (await Post.create({ 110 | uid: user._id, 111 | title: 'post', 112 | content: 'post' 113 | })).ops[0] 114 | 115 | await createComment(user, post) 116 | } 117 | 118 | async function createComment (user, post) { 119 | await Comment.create({ 120 | userId: user._id, 121 | postId: post._id, 122 | content: 'comment' 123 | }) 124 | } 125 | ``` 126 | 127 | 运行: 128 | 129 | ```sh 130 | $ curl -XPOST localhost:3000/users 131 | ``` 132 | 133 | 此时刷新 Kibana,如下所示: 134 | 135 | ![](./assets/6.3.2.png) 136 | 137 | 在初次使用 Kibana 时,需要配置 Kibana 从 ElasticSearch 的哪些 index 中搜索日志,我们在 `Index pattern` 处填 `logstash-*`,然后单击 Next step 按钮,在 `Time Filter field name` 中选择 timestamp,单击 Create index pattern 完成配置。 138 | 139 | **注意**:我们选择 timestamp 而不是默认的 @timestamp,是因为在 koa-await-breakpoint 的日志中有 timestamp 字段。 140 | 141 | 单击左侧目录的 Discover,我们发现已经有日志了。分别单击左侧出现的 Available Fields 的 fn、type、step、take,然后按 step 升序展示,如下所示: 142 | 143 | ![](./assets/6.3.3.png) 144 | 145 | 是不是一目了然!我们把每个请求的每一步的函数及其执行时间都记录下来了。 146 | 147 | 修改 routes/users.js 的 createComment,throw 一个 `new Error('test')`。重启程序并发起一个请求,ELK 显示如下: 148 | 149 | ![](./assets/6.3.4.png) 150 | 151 | **小提示**:在实际应用中会有非常多的日志,我们可以通过 requestId 找到一个请求的所有日志,在 7.2 小节会讲解。 152 | 153 | ELK 非常强大,基本能满足所有日志查询需求,Kibana 的查询使用 lucene 语法,用 10 分钟左右就能大体上手。Kibana 还能创建各种仪表盘和聚合图表,读者可自行尝试。 154 | 155 | ## 6.3.3 参考链接 156 | 157 | - http://blog.51cto.com/baidu/1676798 158 | - http://elk-docker.readthedocs.io 159 | 160 | 上一节:[6.2 async_hooks](https://github.com/nswbmw/node-in-debugging/blob/master/6.2%20async_hooks.md) 161 | 162 | 下一节:[6.4 OpenTracing + Jaeger](https://github.com/nswbmw/node-in-debugging/blob/master/6.4%20OpenTracing%20%2B%20Jaeger.md) 163 | -------------------------------------------------------------------------------- /6.4 OpenTracing + Jaeger.md: -------------------------------------------------------------------------------- 1 | ## 6.4.1 什么是 OpenTracing? 2 | 3 | [OpenTracing](http://opentracing.io/) 是一个分布式追踪规范。OpenTracing 通过提供平台无关、厂商无关的 API,为分布式追踪提供统一的概念和数据标准,使得开发人员能够方便的添加(或更换)追踪系统的实现。OpenTracing 定义了如下几个术语: 4 | 5 | - Span:代表了系统中的一个逻辑工作单元,它具有操作名、操作开始时间以及持续时长。Span 可能会有嵌套或排序,从而对因果关系建模。 6 | - Tags:每个 Span 可以有多个键值对(key: value)形式的 Tags,Tags 是没有时间戳的,支持简单地对 Span 进行注解和补充。 7 | - Logs:每个 Span 可以进行多次 Log 操作,每一次 Log 操作,都需要一个带时间戳的时间名称,以及可选的任意大小的存储结构。 8 | - Trace:代表了系统中的一个数据/执行路径(一个或多个 Span),可以将其理解为 Span 的有向无环图。 9 | 10 | OpenTracing 还有其他一些概念,这里不过多解释。我们看个传统的调用关系例子,如下所示: 11 | 12 | ![](./assets/6.4.1.jpg) 13 | 14 | > 在一个分布式系统中,追踪一个事务或者调用流一般如上图所示。虽然这种图对于看清各组件的组合关系是很有用的,但是,它不能很好显示组件的调用时间,以及是串行调用还是并行调用。如果展现更复杂的调用关系,会更加复杂,甚至无法画出这样的图。另外,这种图也无法显示调用间的时间间隔以及是否通过定时调用来启动调用。一种更有效的展现一个典型的 trace 过程,如下图所示: 15 | 16 | ![](./assets/6.4.2.jpg) 17 | 18 | > 这种展现方式增加了执行时间的上下文,相关服务间的层次关系,进程或者任务的串行或并行调用关系。这样的视图有助于发现系统调用的关键路径。通过关注关键路径的执行过程,项目团队可能专注于优化路径中的关键位置,最大幅度地提升系统的性能。例如:可以通过追踪一个资源定位的调用情况,明确底层的调用情况,发现哪些操作有阻塞的情况。 19 | 20 | ## 6.4.2 什么是 Jaeger? 21 | 22 | Jaeger 是 OpenTracing 的一个实现,是 Uber 开源的一个分布式追踪系统,其灵感来源于Dapper 和 OpenZipkin。从 2016 年开始,该系统已经在 Uber 内部得到了广泛的应用,它可以用于微服务架构应用的监控,特性包括分布式上下文传播(Distributed context propagation)、分布式事务监控、根原因分析、服务依赖分析以及性能优化。该项目已经被云原生计算基金会(Cloud Native Computing Foundation,CNCF)接纳为第 12 个项目。 23 | 24 | ## 6.4.3 启动 Jaeger + Jaeger UI 25 | 26 | 我们使用 Docker 启动 Jaeger + Jaeger UI(Jaeger 可视化 web 控制台),运行如下命令: 27 | 28 | ```sh 29 | $ docker run -d -p5775:5775/udp \ 30 | -p 6831:6831/udp \ 31 | -p 6832:6832/udp \ 32 | -p 5778:5778 \ 33 | -p 16686:16686 \ 34 | -p 14268:14268 \ 35 | jaegertracing/all-in-one:latest 36 | ``` 37 | 38 | 用浏览器打开 localhost:16686,如下所示: 39 | 40 | ![](./assets/6.4.3.jpg) 41 | 42 | 现在并没有任何数据,接下来我们看看如何使用 Jaeger 接收并查询日志。 43 | 44 | ## 6.4.4 如何使用 OpenTracing + Jaeger? 45 | 46 | OpenTracing 和 Jaeger 分别提供了 JavaScript/Node.js 的 SDK: 47 | 48 | - [opentracing/opentracing-javascript](https://github.com/opentracing/opentracing-javascript) 49 | - [jaegertracing/jaeger-client-node](https://github.com/jaegertracing/jaeger-client-node) 50 | 51 | opentracing 示例代码如下: 52 | 53 | ```js 54 | const http = require('http') 55 | const opentracing = require('opentracing') 56 | 57 | // NOTE: the default OpenTracing tracer does not record any tracing information. 58 | // Replace this line with the tracer implementation of your choice. 59 | const tracer = new opentracing.Tracer() 60 | 61 | const span = tracer.startSpan('http_request') 62 | const opts = { 63 | host : 'example.com', 64 | method: 'GET', 65 | port : '80', 66 | path: '/', 67 | } 68 | http.request(opts, res => { 69 | res.setEncoding('utf8') 70 | res.on('error', err => { 71 | // assuming no retries, mark the span as failed 72 | span.setTag(opentracing.Tags.ERROR, true) 73 | span.log({'event': 'error', 'error.object': err, 'message': err.message, 'stack': err.stack}) 74 | span.finish() 75 | }) 76 | res.on('data', chunk => { 77 | span.log({'event': 'data_received', 'chunk_length': chunk.length}) 78 | }) 79 | res.on('end', () => { 80 | span.log({'event': 'request_end'}) 81 | span.finish() 82 | }) 83 | }).end() 84 | ``` 85 | 86 | 有以下两点需要解释: 87 | 88 | 1. 需要将上面的 `const tracer = new opentracing.Tracer()` 替换成自己的 tracer 实现,即 Jaeger 的实现。 89 | 2. 通过 tracer.startSpan 启动一个 Span,span.setTag 用来设置 Tags,span.log 用来设置 Logs,span.finish 用来结束一个 Span。 90 | 91 | 这有点类似于我们的手动埋点,只不过变成了一个规范而已。但 OpenTracing 的功能不止如此,上面只是一个 Span 的用法,Span 之间还可以关联调用关系,最后得到一个 DAG(有向无环图)。 92 | 93 | **举个例子**:假如我们正在做微服务,多个服务之间有调用关系(不管是 HTTP 还是 RPC 等),每次调用服务在内部可能产生多个 Span,最终会在 Jaeger 控制台页面看到一个完整的 Trace 和 DAG 图(微服务之间的调用关系)。 94 | 95 | jaeger-client-node 使用如下: 96 | 97 | ```js 98 | const tracer = new jaeger.Tracer( 99 | serviceName, 100 | new jaeger.RemoteReporter(new UDPSender()), 101 | new jaeger.RateLimitingSampler(1) 102 | ) 103 | ``` 104 | 105 | 创建一个 tracer,可以接收 3 个参数: 106 | 107 | 1. serviceName:服务名。 108 | 2. Reporter:上报器,即往哪发日志,如上述代码是通过 UDP 发送日志,默认地址 localhost:6832。 109 | 3. Sampler:采样器,即日志如何采样,如上述代码是限制 1 秒采样一次。 110 | 111 | 这里不再详细介绍其它选项,读者可自行去查阅 jaeger-client-node 的文档。 112 | 113 | ## 6.4.5 koa-await-breakpoint-jaeger 114 | 115 | 通过上面的例子我们知道,在使用 Jaeger 时需要手动埋点。前面我们介绍了 koa-await-breakpoint 日志自动打点,可自定义 store,koa-await-breakpoint-jaeger 是为 koa-await-breakpoint 写的 store 的 adaptor,在实现上有一些小技巧,有兴趣的读者可以去读下源码。 116 | 117 | 还是以 koa-await-breakpoint 的 example 举例,只添加了两行代码引入 jaeger 的使用。代码如下: 118 | 119 | **app.js** 120 | 121 | ```js 122 | const JaegerStore = require('koa-await-breakpoint-jaeger') 123 | const koaAwaitBreakpoint = require('koa-await-breakpoint')({ 124 | name: 'api', 125 | files: ['./routes/*.js'], 126 | store: new JaegerStore() 127 | }) 128 | 129 | const Paloma = require('paloma') 130 | const app = new Paloma() 131 | 132 | app.use(koaAwaitBreakpoint) 133 | app.route({ method: 'POST', path: '/users', controller: require('./routes/user').createUser }) 134 | 135 | app.listen(3000) 136 | ``` 137 | 138 | 运行: 139 | 140 | ```sh 141 | $ curl -XPOST localhost:3000/users 142 | ``` 143 | 144 | 刷新 localhost:16686,可以看到已经有日志了,如下所示: 145 | 146 | ![](./assets/6.4.4.jpg) 147 | 148 | 选择 Sercice -> api,Operation -> POST /users,单击 Find Traces 查看所有结果,右侧展示了一条日志,点进去如下所示: 149 | 150 | ![](./assets/6.4.5.jpg) 151 | 152 | **小提示**:可以根据 tags 过滤结果。 153 | 154 | **注意**:Jaeger 是分布式追踪系统,通常用来追踪多个服务之间的调用关系,而这里用来追踪一个服务的多个函数之间的调用关系。 155 | 156 | 修改 routes/user.js 的 createComment 函数 throw 一个 `new Error('test')`,重新运行,如下所示: 157 | 158 | ![](./assets/6.4.6.jpg) 159 | 160 | 可以看出,Jaeger 完美地展现了在一个请求到来时,函数之间的调用关系、层级关系及耗时,甚至函数体和错误栈都有!然后,我们可以用 requestId 去 ELK 中查询日志了。 161 | 162 | ## 6.4.6 参考链接 163 | 164 | - https://wu-sheng.gitbooks.io/opentracing-io/content/ 165 | - https://segmentfault.com/a/1190000008895129 166 | - http://www.infoq.com/cn/news/2017/11/Uber-open-spurce-Jaeger 167 | 168 | 上一节:[6.3 ELK](https://github.com/nswbmw/node-in-debugging/blob/master/6.3%20ELK.md) 169 | 170 | 下一节:[6.5 Sentry](https://github.com/nswbmw/node-in-debugging/blob/master/6.5%20Sentry.md) 171 | -------------------------------------------------------------------------------- /6.5 Sentry.md: -------------------------------------------------------------------------------- 1 | ## 6.5.1 什么是 Sentry? 2 | 3 | Sentry[官网](sentry.io)的介绍: 4 | 5 | > Sentry’s real-time error tracking gives you insight into production deployments and information to reproduce and fix crashes. 6 | 7 | **简而言之**:Sentry 是一个开源的实时错误日志收集平台。 8 | 9 | ## 6.5.2 安装 Sentry 10 | 11 | 我们使用 Docker 安装并启动 Sentry,步骤如下: 12 | 13 | 1. 启动一个 Redis 容器,命名为 sentry-redis: 14 | 15 | ```sh 16 | $ docker run -d --name sentry-redis redis 17 | ``` 18 | 19 | 2. 启动一个 Postgres 容器,命名为 sentry-postgres: 20 | 21 | ```sh 22 | $ docker run -d \ 23 | --name sentry-postgres \ 24 | -e POSTGRES_PASSWORD=secret \ 25 | -e POSTGRES_USER=sentry \ 26 | postgres 27 | ``` 28 | 29 | 3. 生成一个 Sentry 的 secret key: 30 | 31 | ```sh 32 | $ docker run --rm sentry config generate-secret-key 33 | ``` 34 | 35 | 将下面的 \ 都替换成上面生成的 secret key。 36 | 37 | 4. 如果是新的数据库(第 1 次运行),则需要运行 upgrade: 38 | 39 | ```sh 40 | $ docker run -it --rm \ 41 | -e SENTRY_SECRET_KEY='' \ 42 | --link sentry-postgres:postgres \ 43 | --link sentry-redis:redis \ 44 | sentry upgrade 45 | ``` 46 | 47 | 按步骤填写自己的信息: 48 | 49 | ![](./assets/6.5.1.jpg) 50 | 51 | 最终创建了一个超级管理员和一个默认的名为 sentry 的组织(organization)。 52 | 53 | 5. 启动 Sentry,并对外暴露 9000 端口: 54 | 55 | ```sh 56 | $ docker run -d \ 57 | --name my-sentry \ 58 | -e SENTRY_SECRET_KEY='' \ 59 | --link sentry-redis:redis \ 60 | --link sentry-postgres:postgres \ 61 | -p 9000:9000 \ 62 | sentry 63 | ``` 64 | 65 | 6. 启动 Celery cron 和 Celery workers: 66 | 67 | ```sh 68 | $ docker run -d \ 69 | --name sentry-cron \ 70 | -e SENTRY_SECRET_KEY='' \ 71 | --link sentry-postgres:postgres \ 72 | --link sentry-redis:redis \ 73 | sentry run cron 74 | ``` 75 | 76 | ```sh 77 | $ docker run -d \ 78 | --name sentry-worker-1 \ 79 | -e SENTRY_SECRET_KEY='' \ 80 | --link sentry-postgres:postgres \ 81 | --link sentry-redis:redis \ 82 | sentry run worker 83 | ``` 84 | 85 | **小提示**:Celery 是用 Python 写的一个分布式任务调度模块。 86 | 87 | 7. 完成! 88 | 89 | 用浏览器打开 localhost:9000,就能看到 Sentry 的登录页面了,如下所示: 90 | 91 | ![](./assets/6.5.2.jpg) 92 | 93 | 首次登录时需要填写一些必要信息 ,如下所示: 94 | 95 | ![](./assets/6.5.3.png) 96 | 97 | 单击 Continue 进入 Sentry 仪表盘(Dashboard)。单击右上角的 New Project 按钮创建一个项目,选择 Node.js 并填写项目名称为 API,然后单击 Create Project 按钮创建项目。如下所示: 98 | 99 | ![](./assets/6.5.4.png) 100 | 101 | 创建成功后进入 Node.js 使用示例页面,我们选择使用 Koa 测试,在右侧选择 Koa: 102 | 103 | ![](./assets/6.5.5.png) 104 | 105 | 上图所示是 koa@1 的示例代码,我们以 Paloma(基于 koa@2)为例,编写测试代码: 106 | 107 | ```js 108 | const Raven = require('raven') 109 | const Paloma = require('paloma') 110 | const app = new Paloma() 111 | 112 | Raven.config(DSN).install() 113 | 114 | app.on('error', (err) => { 115 | Raven.captureException(err, (err, eventId) => { 116 | console.log('Reported error ' + eventId) 117 | }) 118 | }) 119 | 120 | app.use((ctx) => { 121 | throw new Error('test') 122 | }) 123 | 124 | app.listen(3000) 125 | ``` 126 | 127 | raven 是 Node.js 版的 Sentry SDK,用来收集和发送错误日志。 128 | 129 | **小提示**:将 DSN 替换为上图中的 `http://xxx@localhost:9000/2`,DSN 既告诉客户端 Sentry 服务器的地址,也用来当做身份认证的 token。 130 | 131 | 运行以上测试代码,访问 localhost:3000,错误信息会发送给 Sentry。Sentry 展示如下: 132 | 133 | ![](./assets/6.5.6.png) 134 | 135 | 点进去可以看到详细的信息: 136 | 137 | ![](./assets/6.5.7.png) 138 | 139 | Sentry 还有许多功能,比如:错误归类、展示错误的频率柱状图、将错误指派给组织中的某个人、给错误添加标签、查看这类错误事件的历史、标记错误为已解决、在错误下发表评论、警报等等功能。 140 | 141 | ## 6.5.3 koa-raven 142 | 143 | 笔者将 raven 封装成 Koa 的一个中间件。使用如下: 144 | 145 | ```js 146 | const raven = require('koa-raven') 147 | const Paloma = require('paloma') 148 | const app = new Paloma() 149 | 150 | app.use(raven(DSN)) 151 | 152 | app.use((ctx) => { 153 | throw new Error('test') 154 | }) 155 | 156 | app.listen(3000) 157 | ``` 158 | 159 | 或者使用 ctx.raven: 160 | 161 | ```js 162 | const raven = require('koa-raven') 163 | const Paloma = require('paloma') 164 | const app = new Paloma() 165 | 166 | app.use(raven(DSN)) 167 | 168 | app.use((ctx) => { 169 | try { 170 | throw new Error('test') 171 | } catch (e) { 172 | ctx.raven.captureException(e, { extra: { name: 'tom' } }) 173 | ctx.status = 500 174 | ctx.body = e.stack 175 | } 176 | }) 177 | 178 | app.listen(3000) 179 | ``` 180 | 181 | ## 6.5.4 参考链接 182 | 183 | - https://sentry.io/ 184 | 185 | 上一节:[6.4 OpenTracing + Jaeger](https://github.com/nswbmw/node-in-debugging/blob/master/6.4%20OpenTracing%20%2B%20Jaeger.md) 186 | 187 | 下一节:[7.1 Telegraf + InfluxDB + Grafana(上)](https://github.com/nswbmw/node-in-debugging/blob/master/7.1%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8A).md) 188 | -------------------------------------------------------------------------------- /7.1 Telegraf + InfluxDB + Grafana(上).md: -------------------------------------------------------------------------------- 1 | 本节将会讲解如何使用 Telegraf(StatsD) + InfluxDB + Grafana 搭建一套完整的监控系统。 2 | 3 | ## 7.1.1 Telegraf(StatsD) + InfluxDB + Grafana 简介 4 | 5 | [Telegraf](https://github.com/influxdata/telegraf) 是一个使用 Go 语言开发的代理程序,可收集系统和服务或者其他来源(inputs)的数据,并将其写入 InfluxDB(outputs)数据库,支持多种 inputs 和 outputs 插件。[StatsD](https://github.com/etsy/statsd) 是一个使用 Node.js 开发的网络守护进程,通过 UDP 或者 TCP 方式收集各种统计信息,包括计数器和定时器等。 6 | 7 | [InfluxDB](https://github.com/influxdata/influxdb) 是一个使用 Go 语言开发的开源的分布式时序、事件和指标数据库,无需外部依赖,其设计目标是实现分布式和水平伸缩扩展。 8 | 9 | [Grafana](https://github.com/grafana/grafana) 是一个使用 Angular + Go 语言开发的开源的、功能齐全的、漂亮的仪表盘和图表的编辑器,可用来做日志的分析与展示曲线图(如 api 的请求日志),支持多种 backend,如 ElasticSearch、InfluxDB、OpenTSDB 等等。 10 | 11 | **工作流程**:Telegraf 将 StatsD(inputs)和 InfluxDB(outputs)结合起来,即发往 StatsD 的数据,最终通过 Telegraf 写入了 InfluxDB,然后 Grafana 读取 InfluxDB 的数据展示成图表。 12 | 13 | ## 7.1.2 启动 docker-statsd-influxdb-grafana 14 | 15 | 我们使用 Docker 一键启动 Telegraf(StatsD)+ InfluxDB + Grafana,节省搭建环境的时间。 16 | 17 | ```sh 18 | $ docker run -d \ 19 | --name docker-statsd-influxdb-grafana \ 20 | -p 3003:3003 \ 21 | -p 3004:8083 \ 22 | -p 8086:8086 \ 23 | -p 22022:22 \ 24 | -p 8125:8125/udp \ 25 | samuelebistoletti/docker-statsd-influxdb-grafana:latest 26 | ``` 27 | 28 | 端口映射关系如下: 29 | 30 | ``` 31 | Host Container Service 32 | ----------------------------------- 33 | 3003 3003 grafana 34 | 3004 8083 influxdb-admin 35 | 8086 8086 influxdb 36 | 8125 8125 statsd 37 | 22022 22 sshd 38 | ``` 39 | 40 | ## 7.1.3 熟悉 InfluxDB 41 | 42 | 容器启动后,浏览器访问 localhost:3004(以下称为 influxdb-admin),如下所示: 43 | 44 | ![](./assets/7.1.1.png) 45 | 46 | InfluxDB 的基本概念如下: 47 | 48 | - database:数据库。 49 | - measurement:数据库中的表。 50 | - point:表里面的一行数据,由时间戳(time)、数据(field)和标签(tag)组成 51 | - time:每条数据记录的时间戳,是数据库中的主索引(会自动生成)。 52 | - field:各种记录的值(没有索引的属性)。 53 | - tag:各种有索引的属性。 54 | - ... 55 | 56 | InfluxDB 采用了类 SQL 的查询语法,例如: 57 | 58 | - show databases:列出所有数据库。 59 | - show measurements:列出当前数据库的所有表。 60 | - select * from xxx:列出 xxx 表的所有数据。 61 | - ... 62 | 63 | 我们在 Query 中输入: 64 | 65 | ```sql 66 | show databases 67 | ``` 68 | 69 | 查询结果如下: 70 | 71 | ![](./assets/7.1.2.png) 72 | 73 | _interal 是 InfluxDB 内部使用的数据库,telegraf 是我们当前 Docker 容器启动后默认创建的测试数据库。 74 | 75 | ## 7.1.4 配置 Grafana 76 | 77 | 用浏览器打开 localhost:3003,如下所示: 78 | 79 | ![](./assets/7.1.3.png) 80 | 81 | 输入用户名 root 和密码 root 登录,进入初始化配置页,单击 “Add data source”,如下填写: 82 | 83 | ![](./assets/7.1.4.png) 84 | 85 | 单击 “Save & Test” 保存配置。目前配置好了 Grafana 默认的 datasource 是名为 api 的 InfluxDB,接下来创建测试代码,产生测试数据。 86 | 87 | ## 7.1.5 node-statsd 88 | 89 | [node-statsd](https://github.com/sivy/node-statsd) 是一个 statsd 的 Node.js client。创建以下测试代码: 90 | 91 | ```js 92 | const StatsD = require('node-statsd') 93 | const statsdClient = new StatsD({ 94 | host: 'localhost', 95 | port: 8125 96 | }) 97 | 98 | setInterval(() => { 99 | const responseTime = Math.floor(Math.random() * 100) 100 | statsdClient.timing('api', responseTime, function (error, bytes) { 101 | if (error) { 102 | console.error(error) 103 | } else { 104 | console.log(`Successfully sent ${bytes} bytes, responseTime ${responseTime}ms`) 105 | } 106 | }) 107 | }, 1000) 108 | ``` 109 | 110 | 运行以上代码,每一秒钟会产生一个 0~99 之间的随机值(模拟响应时间,单位为毫秒),发送到 StatsD,StatsD 会通过 Telegraf 将这些数据写入 InfluxDB 的 telegraf 数据库。 111 | 112 | 回到 influxdb-admin,单击右上角的下拉菜单切换到 telegraf 数据库,然后输入 `show measurements ` 查看已经存在 api 表了,然后输入: 113 | 114 | ```sql 115 | select * from api 116 | ``` 117 | 118 | 查询结果如下: 119 | 120 | ![](./assets/7.1.5.png) 121 | 122 | 可以看出 api 表有以下几个字段: 123 | 124 | - time:InfluxDB 默认添加的时间戳。 125 | - 90_percentile:所有记录中从小到大 90% 那个点的值。 126 | - count:一次收集的日志数量,可以看出每条记录(point)的 count 值接近或等于 10,而我们的测试代码是 1s 发送一条数据,也就说明 Telegraf 默认设置是 10s 收集一次数据,默认配置也的确是这样的,见:[https://github.com/samuelebistoletti/docker-statsd-influxdb-grafana/blob/master/telegraf/telegraf.conf](https://github.com/samuelebistoletti/docker-statsd-influxdb-grafana/blob/master/telegraf/telegraf.conf)。 127 | - host:机器地址。 128 | - lower:最小的那条记录的值。 129 | - mean:所有记录的平均值。 130 | - metric_type:metric 类型。 131 | - stddev:所有记录的标准差。 132 | - upper:最大的那条记录的值。 133 | 134 | ## 7.1.6 创建 Grafana 图表 135 | 136 | 回到 Grafana,单击左上角 Grafana 图标的下拉菜单,单击 Dashboards 回到仪表盘页继续完成配置,单击 “New dashboard”,然后单击创建 Graph 类型的图表,就创建了一个空的图表,如下所示: 137 | 138 | ![](./assets/7.1.6.png) 139 | 140 | 单击当前的图表,选择 Edit,修改如下几个地方: 141 | 142 | 1. Metrics 配置中选择 FROM -> api 表,SELECT -> field(mean) 字段。 143 | 2. Display 配置中 “Null value” 选择 connected,将每个点连成折线。 144 | 145 | 效果如下所示: 146 | 147 | ![](./assets/7.1.7.png) 148 | 149 | ## 7.1.7 模拟真实环境 150 | 151 | **middlewares/statsd.js** 152 | 153 | ```js 154 | const StatsD = require('node-statsd') 155 | const statsdClient = new StatsD({ 156 | host: 'localhost', 157 | port: 8125 158 | }) 159 | 160 | module.exports = function (routerName) { 161 | return async function statsdMiddleware (ctx, next) { 162 | const start = Date.now() 163 | 164 | try { 165 | await next() 166 | const spent = Date.now() - start 167 | statsdClient.timing(`api_${routerName}`, spent) 168 | } catch (e) { 169 | statsdClient.increment(`api_${routerName}_${e.status || (ctx.status !== 404 ? ctx.status : 500)}`) 170 | throw e 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | **server.js** 177 | 178 | ```js 179 | const Bluebird = require('bluebird') 180 | const Paloma = require('paloma') 181 | const app = new Paloma() 182 | const statsd = require('./middlewares/statsd') 183 | 184 | app.route({ method: 'GET', path: '/', controller: [ 185 | statsd('getHome'), 186 | async (ctx) => { 187 | // 模拟十分之一出错概率 188 | if (Math.random() < 0.1) { 189 | console.error('error') 190 | ctx.throw(400) 191 | } 192 | // 模拟 1-100 毫秒响应时间 193 | const responseTime = Math.floor(Math.random() * 100 + 1) 194 | await Bluebird.delay(responseTime) 195 | console.log(`Spent ${responseTime}ms`) 196 | ctx.status = 200 197 | } 198 | ]}) 199 | 200 | app.listen(3000) 201 | ``` 202 | 203 | **client.js** 204 | 205 | ```js 206 | const axios = require('axios') 207 | 208 | setInterval(() => { 209 | // 模拟 1-10 的 tps 210 | const tps = Math.floor(Math.random() * 10 + 1) 211 | for (let i = 0; i < tps; i++) { 212 | axios.get('http://localhost:3000') 213 | } 214 | }, 1000) 215 | ``` 216 | 217 | 打开两个终端,分别运行: 218 | 219 | ```sh 220 | $ node server.js 221 | $ node client.js 222 | ``` 223 | 224 | 回到 influxdb-admin,输入: 225 | 226 | ```sql 227 | show measurements 228 | ``` 229 | 230 | 可以看到已经有 api_getHome 和 api_getHome_400 表了。回到 Grafana,在一行(row)里创建两个图表,分别为: 231 | 232 | - 请求量:包含了正常请求(200)和错误请求(4xx、5xx 等等)请求量的折线图。 233 | - 响应时间:正常请求的最低(lower)、平均(mean)、最高(upper)响应时间的折线图。 234 | 235 | ![](./assets/7.1.8.png) 236 | 237 | 以 “getHome 响应时间” 的图表为例,Metrics 配置截图如下: 238 | 239 | ![](./assets/7.1.9.png) 240 | 241 | ## 7.1.8 参考链接 242 | 243 | - https://www.cnblogs.com/shhnwangjian/p/6897216.html 244 | 245 | 上一节:[6.5 Sentry](https://github.com/nswbmw/node-in-debugging/blob/master/6.5%20Sentry.md) 246 | 247 | 下一节:[7.2 Telegraf + InfluxDB + Grafana(下)](https://github.com/nswbmw/node-in-debugging/blob/master/7.2%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8B).md) 248 | -------------------------------------------------------------------------------- /7.2 Telegraf + InfluxDB + Grafana(下).md: -------------------------------------------------------------------------------- 1 | 上一小节主要讲解了 Telegraf(StatsD) + InfluxDB + Grafana 的搭建和基本用法,并创建了请求量和响应时间这两种图表。本节讲解几个高级用法: 2 | 3 | 1. 如何将 Grafana(监控)跟 ELK(日志)结合起来。 4 | 2. Grafana 监控报警。 5 | 3. 脚本一键生成图表。 6 | 7 | ## 7.2.1 Grafana + ELK 8 | 9 | 在观察 Grafana 监控时,我们发现某个 api 接口的响应时间突然有一个尖刺,这个时候想查一查到底是什么原因导致的。在前面介绍过 koa-await-breakpoint + ELK 的用法,是否可以结合 Grafana 使用呢?答案是可以的。 10 | 11 | 因为涉及的代码量大,所以笔者写了一个 demo 托管到了 GitHub 上,有两个 repo,分别为: 12 | 13 | - [grafana-to-elk](https://github.com/nswbmw/grafana-to-elk):包含 web server 和模拟请求的 client,分别将统计信息发送到 StatsD 和将日志发送到 ELK。 14 | - [grafana-to-elk-extension](https://github.com/nswbmw/grafana-to-elk-extension):Chrome 扩展,作用是: 15 | - 格式化从 Grafana 跳转到 ELK 的时间范围。 16 | - 添加 requestId 的链接。 17 | - 高亮显示重要的字段。 18 | 19 | 首先 clone 到本地: 20 | 21 | ```sh 22 | $ git clone https://github.com/nswbmw/grafana-to-elk.git 23 | $ git clone https://github.com/nswbmw/grafana-to-elk-extension.git 24 | ``` 25 | 26 | 测试步骤如下: 27 | 28 | 1. 按照 7.2 节启动 Telegraf(StatsD)+ InfluxDB + Grafana。 29 | 30 | 2. 按照 6.3 节启动 ELK。 31 | 32 | 3. 到 grafana-to-elk 目录下运行: 33 | ```sh 34 | $ npm i 35 | $ node server 36 | ``` 37 | 打开另外一个终端运行: 38 | ```sh 39 | $ node client 40 | ``` 41 | 此时,ELK 应该有日志了。 42 | 43 | 4. 加载 Chrome 扩展。打开 Chrome 扩展程序页 -> 加载已解压的扩展程序... -> 加载 grafana-to-elk-extension(非测试环境下需要修改 manifest.json 的 matches 字段)。 44 | 45 | 5. 回到 Grafana 的 “getHome 响应时间” 图表,进入编辑页的 General tab,如下填写: 46 | ![](./assets/7.2.1.png) 47 | 在保存后,图表的左上角会出现一个类似分享的按钮,鼠标悬浮到上面出现 “Go to ELK”,单击它跳转到 ELK。 48 | 49 | 6. ELK 显示如下: 50 | ![](./assets/7.2.2.png)grafana-to-elk-extension 插件会自动处理并跳转到对应 Grafana 中的时间段并且查询出了我们关心的结果。单击第 1 个 requestId,将会跳转并显示该请求所有的日志,如下所示: 51 | ![](./assets/7.2.3.png) 52 | 错误请求的日志如下: 53 | ![](./assets/7.2.4.png) 54 | 55 | ## 7.2.2 监控报警 56 | 57 | Grafana 有内置的监控报警,设置步骤如下: 58 | 59 | 1. 进入 Alerting -> Notifications 页,单击 New Notification 添加新的报警组,如下所示: 60 | ![](./assets/7.2.5.png) 61 | 2. 回到 “getHome 响应时间” 图表,进入编辑页的 Alert tab,单击 Create Alert 创建报警规则,如下所示: 62 | ![](./assets/7.2.6.png) 63 | **报警规则为**:每 60s 检查一次过去 1min 的 mean(B 在 Metrics 里面代表了别名为 mean 的折线图)折线图的平均值是否大于 50,如果是则触发报警。 64 | **注意**:如需发邮件,则需要设置 Grafana 的 [SMTP settings](http://docs.grafana.org/installation/configuration/#smtp)。 65 | 66 | 我们还可以给 “getHome 请求量” 设置错误报警监控,如下所示: 67 | 68 | ![](./assets/7.2.7.png) 69 | 70 | 每 60s 检查一次过去 1min 内是否有 400 报错,如果有则触发报警,其中 B 代表了别名为 400 的折线图。 71 | 72 | **小提示**:报警信息可以发送到 Email、Slack、DingTalk 或者 Webhook 等等。报警的内容可以包含图表的截图,需要配置 [external image uploader](http://docs.grafana.org/installation/configuration/#external-image-storage)。 73 | 74 | **小提示**:Grafana 配置文件在 /etc/grafana/grafana.ini,如需修改步骤如下: 75 | 76 | ```sh 77 | $ docker exec -it docker-statsd-influxdb-grafana bash # 进入 docker 容器 78 | $ apt update 79 | $ apt install vim 80 | $ vim /etc/grafana/grafana.ini 81 | ``` 82 | 83 | ## 7.2.3 脚本一键生成图表 84 | 85 | 我们只创建了一个接口的两种(请求量和响应时间)图表,每个图表要设置 link、alert 等等就很麻烦了。如果我们的 api 有几百个接口,岂不成了灾难了。 86 | 87 | Grafana 虽然有 Template 的功能,但我们接下来讲一个奇技淫巧。 88 | 89 | 我们在保存图表的时候从 Chrome DevTools 的 Network 看到发起了一个 Ajax 请求,如下所示: 90 | 91 | ![](./assets/7.2.8.png) 92 | 93 | dashboard 就是包含了当前仪表盘页所有图表的完整 JSON,其中: 94 | 95 | - dashboard:包含一到多行 row。 96 | - rows:一行 row 包含一到多个 panel。 97 | - panels:一个 panel 是一个具体的图表。 98 | 99 | 在拿到这个 JSON 后,我们就可以不断地尝试修改它,然后用 axios 带上浏览器拿到的 Cookie 发送到图中的 URL,模拟浏览器的保存操作,这里就不再展开讲解了。 100 | 101 | ## 7.2.4 参考链接 102 | 103 | - http://docs.grafana.org/alerting/notifications/ 104 | 105 | 上一节:[7.1 Telegraf + InfluxDB + Grafana(上)](https://github.com/nswbmw/node-in-debugging/blob/master/7.1%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8A).md) 106 | 107 | 下一节:[8.1 node-clinic](https://github.com/nswbmw/node-in-debugging/blob/master/8.1%20node-clinic.md) 108 | -------------------------------------------------------------------------------- /8.1 node-clinic.md: -------------------------------------------------------------------------------- 1 | ## 8.1.1 使用 node-clinic 2 | 3 | [node-clinic](https://github.com/nearform/node-clinic)(简称 clinic) 是一个开箱即用的 Node.js 应用诊断工具。 4 | 5 | 首先,安装 Node.js@9+ 6 | 7 | ```sh 8 | $ nvm install 9 9 | ``` 10 | 11 | 全局安装 clinic: 12 | 13 | ```sh 14 | $ npm i clinic -g 15 | ``` 16 | 17 | 创建测试代码: 18 | 19 | **app.js** 20 | 21 | ```js 22 | const Paloma = require('paloma') 23 | const app = new Paloma() 24 | 25 | function sleep (ms) { 26 | const future = Date.now() + ms 27 | while (Date.now() < future); 28 | } 29 | 30 | app.use(() => { 31 | sleep(50) 32 | }) 33 | 34 | app.listen(3000) 35 | ``` 36 | 37 | 使用 clinic doctor 启动并诊断 Node.js 应用: 38 | 39 | ```sh 40 | $ clinic doctor -- node app.js 41 | ``` 42 | 43 | 使用 ab 压测: 44 | 45 | ```sh 46 | $ ab -c 10 -n 200 "http://localhost:3000/" 47 | ``` 48 | 49 | CTRL+C 终止测试程序,终端打印出: 50 | 51 | ``` 52 | Warning: Trace event is an experimental feature and could change at any time. 53 | ^Canalysing data 54 | generated HTML file is 51485.clinic-doctor.html 55 | ``` 56 | 57 | 用浏览器打开 51485.clinic-doctor.html,如下所示: 58 | 59 | ![](./assets/8.1.1.png) 60 | 61 | **可以看出**:Event Loop 被阻塞,CPU Usage 也居高不下,一定是有 CPU 密集计算,与我们的测试代码吻合。 62 | 63 | clinic 也给出了猜测和解决方案,我们尝试使用 clinic flame 生成火焰图: 64 | 65 | ```sh 66 | $ clinic flame -- node app.js 67 | ``` 68 | 69 | 也可以用以下命令代替: 70 | 71 | ```sh 72 | $ clinic flame --collect-only -- node app.js # 只收集数据 73 | $ clinic flame --visualize-only PID.flamegraph # 将数据生成火焰图 74 | ``` 75 | 76 | 使用同样的 ab 命令压测后,生成的火焰图如下: 77 | 78 | ![](./assets/8.1.2.png) 79 | 80 | **可以看出**:app.js 第 4 行的 sleep 函数占用了大量的 CPU 计算。 81 | 82 | ## 8.1.2 参考链接 83 | 84 | - https://www.nearform.com/blog/introducing-node-clinic-a-performance-toolkit-for-node-js-developers/ 85 | 86 | 上一节:[7.2 Telegraf + InfluxDB + Grafana(下)](https://github.com/nswbmw/node-in-debugging/blob/master/7.2%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8B).md) 87 | 88 | 下一节:[8.2 alinode](https://github.com/nswbmw/node-in-debugging/blob/master/8.2%20alinode.md) 89 | -------------------------------------------------------------------------------- /8.2 alinode.md: -------------------------------------------------------------------------------- 1 | ## 8.2.1 什么是 alinode? 2 | 3 | > Node.js 性能平台(原 alinode)是面向中大型 Node.js 应用提供性能监控、安全提醒、故障排查、性能优化等服务的整体性解决方案。alinode 团队凭借对 Node.js 内核的深入理解,提供了完善的工具链和服务,协助客户主动、快速地发现和定位线上问题。 4 | 5 | ## 8.2.2 创建 alinode 应用 6 | 7 | 访问官网 ,如未开通,则使用阿里云账号登录并免费开通即可。 8 | 9 | 登录后进入[控制台](https://node.console.aliyun.com/),单击 “创建新应用”,创建一个名为 test_alinode 的应用。 10 | 11 | 进入设置页面,如下所示: 12 | 13 | ![](./assets/8.2.1.png) 14 | 15 | App ID 和 App Secret 后面会用到。 16 | 17 | ## 8.2.3 安装 alinode 18 | 19 | alinode 的整套服务由 alinode 运行时、agenthub(原 agentx + commdx 命令集)和服务平台组成,所以在自己的服务器上部署时需要安装 alinode 运行时和 agenthub。 20 | 21 | 我们使用交互式一键安装 alinode 和 agenthub: 22 | 23 | ```sh 24 | $ uname -a # 阿里云 ECS Ubuntu@16.04 25 | Linux nswbmw 4.4.0-105-generic #128-Ubuntu SMP Thu Dec 14 12:42:11 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 26 | $ wget https://raw.githubusercontent.com/aliyun-node/alinode-all-in-one/master/alinode_all.sh 27 | $ bash -i alinode_all.sh # App ID 和 App Secret 填写上面生成的 28 | ... 29 | $ node -p 'process.alinode' # 查看 alinode 版本 30 | ``` 31 | 32 | **注意**:如果遇到 wget 报错 `wget: unable to resolve host address 'raw.githubusercontent.com'`,需要修改 DNS 配置,在 /etc/resolv.conf 最上面添加 `nameserver 8.8.8.8`。 33 | 34 | 生成一个 yourconfig.json 配置文件,内容如下: 35 | 36 | ```json 37 | { 38 | "server": "agentserver.node.aliyun.com:8080", 39 | "appid": "xxx", 40 | "secret": "xxx", 41 | "logdir": "/tmp/", 42 | "reconnectDelay": 10, 43 | "heartbeatInterval": 60, 44 | "reportInterval": 60, 45 | "error_log": [], 46 | "packages": [] 47 | } 48 | ``` 49 | 50 | 使用该配置启动 agenthub: 51 | 52 | ```sh 53 | $ nohup agenthub yourconfig.json & 54 | ``` 55 | 56 | agenthub 将以常驻进程的方式运行。 57 | 58 | 下面通过两个例子使用 alinode 分别调试内存泄露和 CPU 性能瓶颈的问题。 59 | 60 | ## 8.2.4 使用 alinode 诊断内存泄露 61 | 62 | 我们以一段内存泄露代码为例,演示如何使用 alinode 调试内存泄漏的问题。代码如下: 63 | 64 | **server.js** 65 | 66 | ```js 67 | const Paloma = require('paloma') 68 | const session = require('koa-generic-session') 69 | const app = new Paloma() 70 | 71 | app.keys = ['some secret'] 72 | app.use(session()) 73 | 74 | class User { 75 | constructor () { 76 | this.name = new Array(1e6).join('*') 77 | } 78 | } 79 | 80 | app.use((ctx) => { 81 | ctx.session.user = new User() 82 | ctx.status = 204 83 | }) 84 | 85 | app.listen(3000) 86 | ``` 87 | 88 | 这段代码内存泄露的原因是:koa-generic-session 默认将 session 信息存储到了内存中。 89 | 90 | **client.js** 91 | 92 | ```js 93 | const axios = require('axios') 94 | 95 | setInterval(() => { 96 | axios.get('http://localhost:3000') 97 | }, 1000) 98 | ``` 99 | 100 | 打开两个终端,分别运行 : 101 | 102 | ```sh 103 | $ ENABLE_NODE_LOG=YES node server # 开启 alinode 的 log 功能,使得 agenthub 可以监控内核级的性能数据 104 | $ node client # 1s 发起一次请求 105 | ``` 106 | 107 | 过一会儿就可以在 alinode 控制台看到数据了,如下所示: 108 | 109 | ![](./assets/8.2.2.png) 110 | 111 | 可以看出,alinode 监控了: 112 | 113 | - 异常日志 114 | - 慢 HTTP 日志 115 | - 模块依赖 116 | - 系统监控数据(包含非常详尽的图表数据,有 Memory、CPU、Load、QPS、GC、Apdex、Apdex detail、node 进程数、磁盘) 117 | 118 | 119 | 单击 “堆快照” 生成一个 heapsnapshot 文件,单击左侧的 “文件”,查看刚才生成的堆快照: 120 | 121 | ![](./assets/8.2.3.png) 122 | 123 | 在转储后单击 “分析”,选择 “对象簇视图” 的树状列表,展开后如下所示: 124 | 125 | ![](./assets/8.2.4.png) 126 | 127 | **可以看出**:MemoryStore 的 sessions 对象中存储了 97 个 session,并且每个 session.user 上有一个 name 字段是长字符串。 128 | 129 | ## 8.2.5 使用 alinode 诊断 CPU 性能瓶颈 130 | 131 | 测试代码如下: 132 | 133 | **server.js** 134 | 135 | ```js 136 | const crypto = require('crypto') 137 | const Paloma = require('paloma') 138 | const app = new Paloma() 139 | 140 | app.route({ method: 'GET', path: '/encrypt', controller: function encryptRouter (ctx) { 141 | const password = ctx.query.password || 'test' 142 | const salt = crypto.randomBytes(128).toString('base64') 143 | const encryptedPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') 144 | 145 | ctx.body = encryptedPassword 146 | }}) 147 | 148 | app.listen(3000) 149 | ``` 150 | 151 | **client.js** 152 | 153 | ```js 154 | const axios = require('axios') 155 | 156 | setInterval(() => { 157 | const tps = Math.floor(Math.random() * 10) 158 | for (let i = 0; i < tps; i++) { 159 | axios.get('http://localhost:3000/encrypt?password=123456') 160 | } 161 | console.log(`Sent ${tps} requests`) 162 | }, 1000) 163 | ``` 164 | 165 | 打开两个终端,分别运行: 166 | 167 | ```sh 168 | $ ENABLE_NODE_LOG=YES node server 169 | $ node client 170 | ``` 171 | 172 | 回到 alinode 控制台,单击 “CPU Profile”,然后到 “文件” 查看刚才生成的 cpuprofile 文件,转储后单击 “分析”,可以看到生成的火焰图。展开后如下所示: 173 | 174 | ![](./assets/8.2.5.png) 175 | 176 | **可以看出**:server.js 的第 5 行,即 encryptRouter 占用 CPU 较多,而 encryptRouter 里的 exports.pbkdf2Sync 占用了 encryptRouter 绝大部分 CPU 时间。 177 | 178 | 回到 “文件”,选择 “devtools 分析”,如下所示: 179 | 180 | ![](./assets/8.2.6.png) 181 | 182 | **可以看出**:alinode 已经帮我们把可疑的 CPU 性能瓶颈的元凶标红显示了。 183 | 184 | **小提示**:不管是生成的 heapsnapshot 还是 cpuprofile,都可以选择 “下载” 后使用 Chrome DevTools 分析。 185 | 186 | 我们在上面只演示了 “堆快照” 和 “CPU Profile” 的使用,alinode 支持抓取以下 5 种数据: 187 | 188 | - 堆快照 189 | - 堆时间线 190 | - CPU Profile 191 | - GC Trace 192 | - Heap Profile 193 | 194 | 本节就不一一演示了。 195 | 196 | alinode 如此强大,而且免费使用,可以说是开发 Node.js 应用必不可少的好伙伴了。 197 | 198 | ## 8.2.6 参考链接 199 | 200 | - https://www.aliyun.com/product/nodejs 201 | - https://github.com/aliyun-node/agenthub 202 | - https://cnodejs.org/topic/561f289b4928c5872abc18ee 203 | 204 | 上一节:[8.1 node-clinic](https://github.com/nswbmw/node-in-debugging/blob/master/8.1%20node-clinic.md) 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | node-in-debugging 3 |
4 | 《Node.js 调试指南》 5 |

6 |

7 | 当当 | 京东 | 淘宝 | 亚马逊 | china-pub 8 |

9 | 10 | ## 开发环境 11 | 12 | - MacOS|Linux(Ubuntu@16.04 64位) 13 | - Node.js@8.9.4 14 | 15 | ## 目录 16 | 17 | - CPU 篇 18 | - [perf + FlameGraph](https://github.com/nswbmw/node-in-debugging/blob/master/1.1%20perf%20%2B%20FlameGraph.md) 19 | - [v8-profiler](https://github.com/nswbmw/node-in-debugging/blob/master/1.2%20v8-profiler.md) 20 | - [Tick Processor](https://github.com/nswbmw/node-in-debugging/blob/master/1.3%20Tick%20Processor.md) 21 | - 内存篇 22 | - [gcore + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/2.1%20gcore%20%2B%20llnode.md) 23 | - [heapdump](https://github.com/nswbmw/node-in-debugging/blob/master/2.2%20heapdump.md) 24 | - [memwatch-next](https://github.com/nswbmw/node-in-debugging/blob/master/2.3%20memwatch-next.md) 25 | - [cpu-memory-monitor](https://github.com/nswbmw/node-in-debugging/blob/master/2.4%20cpu-memory-monitor.md) 26 | - 代码篇 27 | - [Promise](https://github.com/nswbmw/node-in-debugging/blob/master/3.1%20Promise.md) 28 | - [Async + Await](https://github.com/nswbmw/node-in-debugging/blob/master/3.2%20Async%20%2B%20Await.md) 29 | - [Error Stack](https://github.com/nswbmw/node-in-debugging/blob/master/3.3%20Error%20Stack.md) 30 | - [Node@8](https://github.com/nswbmw/node-in-debugging/blob/master/3.4%20Node%408.md) 31 | - [Rust Addons](https://github.com/nswbmw/node-in-debugging/blob/master/3.5%20Rust%20Addons.md) 32 | - [Event Loop](https://github.com/nswbmw/node-in-debugging/blob/master/3.6%20Event%20Loop.md) 33 | - [uncaughtException + llnode](https://github.com/nswbmw/node-in-debugging/blob/master/3.7%20uncaughtException%20%2B%20llnode.md) 34 | - 工具篇 35 | - [Source Map](https://github.com/nswbmw/node-in-debugging/blob/master/4.1%20Source%20Map.md) 36 | - [Chrome DevTools](https://github.com/nswbmw/node-in-debugging/blob/master/4.2%20Chrome%20DevTools.md) 37 | - [Visual Studio Code](https://github.com/nswbmw/node-in-debugging/blob/master/4.3%20Visual%20Studio%20Code.md) 38 | - [debug + repl2 + power-assert](https://github.com/nswbmw/node-in-debugging/blob/master/4.4%20debug%20%2B%20repl2%20%2B%20power-assert.md) 39 | - [supervisor-hot-reload](https://github.com/nswbmw/node-in-debugging/blob/master/4.5%20supervisor-hot-reload.md) 40 | - APM 篇 41 | - [NewRelic](https://github.com/nswbmw/node-in-debugging/blob/master/5.1%20NewRelic.md) 42 | - [Elastic APM](https://github.com/nswbmw/node-in-debugging/blob/master/5.2%20Elastic%20APM.md) 43 | - 日志篇 44 | - [koa-await-breakpoint](https://github.com/nswbmw/node-in-debugging/blob/master/6.1%20koa-await-breakpoint.md) 45 | - [async_hooks](https://github.com/nswbmw/node-in-debugging/blob/master/6.2%20async_hooks.md) 46 | - [ELK](https://github.com/nswbmw/node-in-debugging/blob/master/6.3%20ELK.md) 47 | - [OpenTracing + Jaeger](https://github.com/nswbmw/node-in-debugging/blob/master/6.4%20OpenTracing%20%2B%20Jaeger.md) 48 | - [Sentry](https://github.com/nswbmw/node-in-debugging/blob/master/6.5%20Sentry.md) 49 | - 监控篇 50 | - [Telegraf + InfluxDB + Grafana(上)](https://github.com/nswbmw/node-in-debugging/blob/master/7.1%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8A).md) 51 | - [Telegraf + InfluxDB + Grafana(下)](https://github.com/nswbmw/node-in-debugging/blob/master/7.2%20Telegraf%20%2B%20InfluxDB%20%2B%20Grafana(%E4%B8%8B).md) 52 | - 应用篇 53 | - [node-clinic](https://github.com/nswbmw/node-in-debugging/blob/master/8.1%20node-clinic.md) 54 | - [alinode](https://github.com/nswbmw/node-in-debugging/blob/master/8.2%20alinode.md) 55 | -------------------------------------------------------------------------------- /assets/1.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.1.1.png -------------------------------------------------------------------------------- /assets/1.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.1.2.png -------------------------------------------------------------------------------- /assets/1.1.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.1.3.jpg -------------------------------------------------------------------------------- /assets/1.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.2.1.png -------------------------------------------------------------------------------- /assets/1.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.2.2.png -------------------------------------------------------------------------------- /assets/1.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.2.3.png -------------------------------------------------------------------------------- /assets/1.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/1.3.1.png -------------------------------------------------------------------------------- /assets/2.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.1.1.png -------------------------------------------------------------------------------- /assets/2.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.2.1.png -------------------------------------------------------------------------------- /assets/2.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.2.2.png -------------------------------------------------------------------------------- /assets/2.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.2.3.png -------------------------------------------------------------------------------- /assets/2.2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.2.4.png -------------------------------------------------------------------------------- /assets/2.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/2.3.1.png -------------------------------------------------------------------------------- /assets/3.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.1.1.png -------------------------------------------------------------------------------- /assets/3.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.1.2.png -------------------------------------------------------------------------------- /assets/3.1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.1.3.png -------------------------------------------------------------------------------- /assets/3.1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.1.4.png -------------------------------------------------------------------------------- /assets/3.4.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.4.1.jpg -------------------------------------------------------------------------------- /assets/3.4.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.4.2.jpg -------------------------------------------------------------------------------- /assets/3.4.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.4.3.jpg -------------------------------------------------------------------------------- /assets/3.4.4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.4.4.jpg -------------------------------------------------------------------------------- /assets/3.4.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.4.5.jpg -------------------------------------------------------------------------------- /assets/3.6.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.6.1.png -------------------------------------------------------------------------------- /assets/3.7.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/3.7.1.png -------------------------------------------------------------------------------- /assets/4.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.2.1.png -------------------------------------------------------------------------------- /assets/4.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.2.2.png -------------------------------------------------------------------------------- /assets/4.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.2.3.png -------------------------------------------------------------------------------- /assets/4.2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.2.4.png -------------------------------------------------------------------------------- /assets/4.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.3.1.png -------------------------------------------------------------------------------- /assets/4.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.3.2.png -------------------------------------------------------------------------------- /assets/4.3.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.3.3.png -------------------------------------------------------------------------------- /assets/4.3.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.3.4.png -------------------------------------------------------------------------------- /assets/4.3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.3.5.png -------------------------------------------------------------------------------- /assets/4.4.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.4.1.jpg -------------------------------------------------------------------------------- /assets/4.4.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/4.4.2.jpg -------------------------------------------------------------------------------- /assets/5.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.1.1.png -------------------------------------------------------------------------------- /assets/5.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.1.2.png -------------------------------------------------------------------------------- /assets/5.1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.1.3.png -------------------------------------------------------------------------------- /assets/5.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.1.png -------------------------------------------------------------------------------- /assets/5.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.2.png -------------------------------------------------------------------------------- /assets/5.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.3.png -------------------------------------------------------------------------------- /assets/5.2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.4.png -------------------------------------------------------------------------------- /assets/5.2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.5.png -------------------------------------------------------------------------------- /assets/5.2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/5.2.6.png -------------------------------------------------------------------------------- /assets/6.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.3.1.png -------------------------------------------------------------------------------- /assets/6.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.3.2.png -------------------------------------------------------------------------------- /assets/6.3.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.3.3.png -------------------------------------------------------------------------------- /assets/6.3.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.3.4.png -------------------------------------------------------------------------------- /assets/6.4.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.1.jpg -------------------------------------------------------------------------------- /assets/6.4.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.2.jpg -------------------------------------------------------------------------------- /assets/6.4.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.3.jpg -------------------------------------------------------------------------------- /assets/6.4.4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.4.jpg -------------------------------------------------------------------------------- /assets/6.4.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.5.jpg -------------------------------------------------------------------------------- /assets/6.4.6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.4.6.jpg -------------------------------------------------------------------------------- /assets/6.5.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.1.jpg -------------------------------------------------------------------------------- /assets/6.5.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.2.jpg -------------------------------------------------------------------------------- /assets/6.5.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.3.png -------------------------------------------------------------------------------- /assets/6.5.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.4.png -------------------------------------------------------------------------------- /assets/6.5.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.5.png -------------------------------------------------------------------------------- /assets/6.5.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.6.png -------------------------------------------------------------------------------- /assets/6.5.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/6.5.7.png -------------------------------------------------------------------------------- /assets/7.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.1.png -------------------------------------------------------------------------------- /assets/7.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.2.png -------------------------------------------------------------------------------- /assets/7.1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.3.png -------------------------------------------------------------------------------- /assets/7.1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.4.png -------------------------------------------------------------------------------- /assets/7.1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.5.png -------------------------------------------------------------------------------- /assets/7.1.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.6.png -------------------------------------------------------------------------------- /assets/7.1.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.7.png -------------------------------------------------------------------------------- /assets/7.1.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.8.png -------------------------------------------------------------------------------- /assets/7.1.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.1.9.png -------------------------------------------------------------------------------- /assets/7.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.1.png -------------------------------------------------------------------------------- /assets/7.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.2.png -------------------------------------------------------------------------------- /assets/7.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.3.png -------------------------------------------------------------------------------- /assets/7.2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.4.png -------------------------------------------------------------------------------- /assets/7.2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.5.png -------------------------------------------------------------------------------- /assets/7.2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.6.png -------------------------------------------------------------------------------- /assets/7.2.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.7.png -------------------------------------------------------------------------------- /assets/7.2.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/7.2.8.png -------------------------------------------------------------------------------- /assets/8.1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.1.1.png -------------------------------------------------------------------------------- /assets/8.1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.1.2.png -------------------------------------------------------------------------------- /assets/8.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.1.png -------------------------------------------------------------------------------- /assets/8.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.2.png -------------------------------------------------------------------------------- /assets/8.2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.3.png -------------------------------------------------------------------------------- /assets/8.2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.4.png -------------------------------------------------------------------------------- /assets/8.2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.5.png -------------------------------------------------------------------------------- /assets/8.2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/8.2.6.png -------------------------------------------------------------------------------- /assets/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/alipay.png -------------------------------------------------------------------------------- /assets/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/book.jpg -------------------------------------------------------------------------------- /assets/wechat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nswbmw/node-in-debugging/403f5c6431c97459c4bf263d0e96974da05dc4e2/assets/wechat.jpeg --------------------------------------------------------------------------------