diff --git a/README.md b/README.md index 7093f9e..83c5e2c 100644 --- a/README.md +++ b/README.md @@ -149,23 +149,26 @@ Hi, 欢迎来到 ElemeFE, 如标题所示本教程的目的是教你如何通过 [阅读更多](sections/os.md) -## 错误处理/调试/优化 +## [错误处理/调试/优化](sections/error.md) -* `[Doc]` Errors (异常) -* `[Doc]` Domain (域) -* `[Doc]` Debugger (调试器) -* `[Doc]` C/C++ 插件 -* `[Doc]` V8 -* `[Point]` 内存快照 -* `[Point]` CPU剖析 +* [`[Doc]` Errors (异常)](sections/error.md#errors) +* [`[Doc]` Domain (域)](sections/error.md#domain) +* [`[Doc]` Debugger (调试器)](sections/error.md#debugger) +* [`[Doc]` C/C++ 插件](sections/error.md#c-c++-addon) +* [`[Doc]` V8](sections/error.md#v8) +* [`[Point]` 内存快照](sections/error.md#内存快照) +* [`[Point]` CPU剖析](sections/error.md#cpu-剖析) ### 常见问题 -* 怎么处理未预料的出错?用 try/catch ,domains 还是其它什么? -* domain 的原理是? 为什么要弃用 domain? +* 怎么处理未预料的出错? 用 try/catch ,domains 还是其它什么? [[more]](sections/error.md#q-handle-error) +* 什么是 `uncaughtException` 事件? 一般在什么情况下使用该事件? [[more]](sections/error.md#uncaughtException) +* domain 的原理是? 为什么要弃用 domain? [[more]](sections/error.md#domain) * 为什么要在 cb 的第一参数传 error? 为什么有的 cb 第一个参数不是 error, 例如 http.createServer? +* 为什么有些异常没法根据报错信息定位到代码调用? 如何准确的定位一个异常? [[more]](sections/error.md#错误栈丢失) +* 内存泄漏通常由哪些原因导致? 如何分析以及定位内存泄漏? [[more]](sections/error.md#内存快照) -`更多整理中` +[阅读更多](sections/error.md) ## 测试 diff --git a/assets/node-js-survey-debug.png b/assets/node-js-survey-debug.png new file mode 100644 index 0000000..cc16367 Binary files /dev/null and b/assets/node-js-survey-debug.png differ diff --git a/sections/error.md b/sections/error.md new file mode 100644 index 0000000..9d2438d --- /dev/null +++ b/sections/error.md @@ -0,0 +1,279 @@ +# 错误处理/调试/优化 + +* `[Doc]` Errors (异常) +* `[Doc]` Domain (域) +* `[Doc]` Debugger (调试器) +* `[Doc]` C/C++ 插件 +* `[Doc]` V8 +* `[Point]` 内存快照 +* `[Point]` CPU剖析 + + +## Errors + +在 Node.js 中的错误主要有一下四种类型: + +|错误|名称|触发| +|---|---|---| +|Standard JavaScript errors|标准 JavaScript 错误|由错误代码触发| +|System errors|系统错误|由操作系统触发| +|User-specified errors|用户自定义错误|通过 throw 抛出| +|Assertion errors|断言错误|由 `assert` 模块触发| + +其中标准的 JavaScript 错误常见有: + +* [EvalError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError): 调用 eval() 出现错误时抛出该错误 +* [SyntaxError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError): 代码不符合 JavaScript 语法规范时抛出该错误 +* [RangeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError): 数组越界时抛出该错误 +* [ReferenceError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError): 引用未定义的变量时抛出该错误 +* [TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError): 参数类型错误时抛出该错误 +* [URIError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError): 误用全局的 URI 处理函数时抛出该错误 + +而常见的系统错误列表可以通过 Node.js 的 os 对象常看列表: + +```javascript +const os = require('os'); + +console.log(os.constants.errno); +``` + +目前搜索 Node.js 面试题, 发现很多题目已经跟不上 Node.js 的发展了.比较老的 [NodeJS 错误处理最佳实践](https://cnodejs.org/topic/55714dfac4e7fbea6e9a2e5d), 译自 Joyent 的官方博客, 其中有这样的描述: + +> 实际上, `try/catch` 唯一常用的是在 `JSON.parse` 和类似验证用户输入的地方 + +然而实际上现在在 Node.js 中你已经可以轻松的使用 try/catch 去捕获异步的异常了. 并且在 Node.js v7.6 之后使用了升级引擎的新版 v8, 旧版中 try/catch 代码不能优化的问题也解决了. 所以我们现在再来看 + +> 怎么处理未预料的出错? 用 try/catch , domains 还是其它什么? + +在 Node.js 中错误处理主要有一下几种方法: + +* callback(err, data) 回调约定 +* throw / try / catch +* EventEmitter 的 error 事件 + +callback(err, data) 这种形式的错误处理起来繁琐, 并不具备强制性, 目前已经处于仅需要了解, 不推荐使用的情况. 而 domain 模块则是半只脚踏进棺材了. + +1) 感谢 [co](https://github.com/visionmedia/co) 的先河, 现在的你已经简单的使用 try/catch 保护关键的位置, 以 koa 为例, 可以通过中间件的形式来进行错误处理, 详见 [Koa error hangding](https://github.com/koajs/koa/wiki/Error-Handling). 之后的 async/await 均属于这种模式. + +2) 通过 EventEmitter 的错误监听形式为各大关键的对象加上错误监听的回调. 例如监听 http server, tcp server 等对象的 `error` 事件以及 process 对象提供的 `uncaughtException` 和 `unhandledRejection` 等等. + +3) 使用 Promise 来封装异步, 并通过 Promise 的错误处理来 handle 错误. + +4) 如果上述办法不能起到良好的作用, 那么你需要学习如何优雅的 [Let It Crash](http://wiki.c2.com/?LetItCrash) + +> 为什么要在 cb 的第一参数传 error? 为什么有的 cb 第一个参数不是 error, 例如 http.createServer? + +TODO + + +### 错误栈丢失 + +```javascript +function test() { + throw new Error('test error'); +} + +function main() { + test(); +} + +main(); +``` + +可以收获报错: + +```javascript +/data/node-interview/error.js:2 + throw new Error('test error'); + ^ + +Error: test error + at test (/data/node-interview/error.js:2:9) + at main (/data/node-interview/error.js:6:3) + at Object. (/data/node-interview/error.js:9:1) + at Module._compile (module.js:570:32) + at Object.Module._extensions..js (module.js:579:10) + at Module.load (module.js:487:32) + at tryModuleLoad (module.js:446:12) + at Function.Module._load (module.js:438:3) + at Module.runMain (module.js:604:10) + at run (bootstrap_node.js:394:7) +``` + +可以发现报错的行数, test 函数, main 函数的调用关系都在 stack 中清晰的体现. + +当你使用 setImmediate 等定时器来设置异步的时候: + +```javascript +function test() { + throw new Error('test error'); +} + +function main() { + setImmediate(() => test()); +} + +main(); + +``` + +我们发现 + +```javascript +/data/node-interview/error.js:2 + throw new Error('test error'); + ^ + +Error: test error + at test (/data/node-interview/error.js:2:9) + at Immediate.setImmediate (/data/node-interview/error.js:6:22) + at runCallback (timers.js:637:20) + at tryOnImmediate (timers.js:610:5) + at processImmediate [as _immediateCallback] (timers.js:582:5) +``` + +错误栈中仅输出到 test 函数内调用的地方位置, 再往上 main 的调用信息就丢失了. 也就是说如果你的函数调用深度比较深的情况下, 你使用异步调用某个函数出错了的情况下追溯这个异步的调用是一个很困难的事情, 因为其之上的栈都已经丢失了. 如果你用过 [async](https://github.com/caolan/async) 之类的模块, 你还可能发现, 报错的 stack 会非常的长而且曲折, 光看 stack 很难去定位问题. + +这项目不大/作者清楚的情况下不是问题, 但是当项目大起来, 开发人员多起来之后, 这样追溯错误会变得异常痛苦. 关于这个问题, 在上文中提到 [错误处理的最佳实践](https://cnodejs.org/topic/55714dfac4e7fbea6e9a2e5d) 中, 关于 `编写新函数的具体建议` 那一带的内容有描述到. 通过使用 [verror](https://www.npmjs.com/package/verror) 这样的方式, 让 Error 一层层封装, 并在每一层将错误的信息一层层的包上, 最后拿到的 Error 直接可以从 message 中获取用于定位问题的关键信息. + +以昨天的数据为准(2017-3-13)各位只要对比一下看看 npm 上上个月 [verror](https://www.npmjs.com/package/verror) 的下载量 `1100w` 比 [express](https://www.npmjs.com/package/express) 的 `1070w` 还高. 应该就能感受到这种写法有多流行了. + +### 防御性编程 + +错误并不可怕, 可怕的是你不去准备应对错误————[防御性编程的介绍和技巧](http://blog.jobbole.com/101651/) + +### let it crash + +[Let It Crash](http://wiki.c2.com/?LetItCrash) + +### uncaughtException + +当异常没有被捕获一路冒泡到 Event Loop 时就会触发该事件 process 对象上的 `uncaughtException` 事件. 默认情况下, Node.js 对于此类异常会直接将其堆栈跟踪信息输出给 `stderr` 并结束进程, 而为 `uncaughtException` 事件添加监听可以覆盖该默认行为, 不会直接结束进程. + +```javascript +process.on('uncaughtException', (err) => { + console.log(`Caught exception: ${err}`); +}); + +setTimeout(() => { + console.log('This will still run.'); +}, 500); + +// Intentionally cause an exception, but don't catch it. +nonexistentFunc(); +console.log('This will not run.'); +``` + +#### 合理使用 uncaughtException + +`uncaughtException` 的初衷是可以让你拿到错误之后可以做一些回收处理之后再 process.exit. 官方的同志们还曾经讨论过要移除该事件 (详见 [issues](https://github.com/nodejs/node-v0.x-archive/issues/2582)) + +所以你需要明白 `uncaughtException` 其实已经是非常规手段了, 应尽量避免使用它来处理错误. 因为通过该事件捕获到错误后, 并不代表 `你可以愉快的继续运行 (On Error Resume Next)`. 程序内部存在未处理的异常, 这意味着应用程序处于一种未知的状态. 如果不能适当的恢复其状态, 那么很有可能会触发不可预见的问题. (使用 domain 会很夸张的加剧这个现象, 并产生新人不能理解的各类幽灵问题) + +如果在 `.on` 指定的监听回调中报错不会被捕获, Node.js 的进程会直接终端并返回一个非零的退出码, 最后输出相应的堆栈信息. 否则, 会出现无限递归. 除此之外, 内存崩溃/底层报错等情况也不会被捕获, **目前猜测**是 v8/C++ 那边撂担子不干了, Node.js 完全插不上话导致的 (TODO 整理到这里才想起来这个念头尚未验证, 如果有空的朋友帮忙验证下). + +所以官方建议的使用 `uncaughtException` 的正确姿势是在结束进程前使用同步的方式清理已使用的资源 (文件描述符、句柄等) 然后 process.exit. + +在 uncaughtException 事件之后执行普通的恢复操作并不安全. 官方建议是另外在专门准备一个 monitor 进程来做健康检查并通过 monitor 来管理恢复情况, 并在必要的时候重启 (所以官方是含蓄的提醒各位用 pm2 之类的工具). + + +### unhandledRejection + +当 Promise 被 reject 且没有绑定监听处理时, 就会触发该事件. 该事件对排查和追踪没有处理 reject 行为的 Promise 很有用. + +该事件的回调函数接收以下参数: + +* `reason` `[](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)` | `` 该 Promise 被 reject 的对象 (通常为 Error 对象) +* `p` 被 reject 的 Promise 本身 + +例如 + +```javascript +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + // application specific logging, throwing an error, or other logic here +}); + +somePromise.then((res) => { + return reportToUser(JSON.pasre(res)); // note the typo (`pasre`) +}); // no `.catch` or `.then` +``` + +一下代码也会触发 `unhandledRejection` 事件: + +```javascript +function SomeResource() { + // Initially set the loaded status to a rejected promise + this.loaded = Promise.reject(new Error('Resource not yet loaded!')); +} + +var resource = new SomeResource(); +// no .catch or .then on resource.loaded for at least a turn +``` + +> In this example case, it is possible to track the rejection as a developer error as would typically be the case for other 'unhandledRejection' events. To address such failures, a non-operational `.catch(() => { })` handler may be attached to resource.loaded, which would prevent the 'unhandledRejection' event from being emitted. Alternatively, the 'rejectionHandled' event may be used. + + +## Domain + +Node.js 早期, try/catch 无法捕获异步的错误, 而错误优先的 callback 仅仅是一种约定并没有强制性并且写起来十分繁琐. 所以为了能够很好的捕获异常, Node.js 从 v0.8 开始引入 domain 这个模块. + +domain 本身是一个 EventEmitter 对象, 其中文意思是 "域" 的意思, 捕获异步异常的基本思路是创建一个域, cb 函数会在定义时会继承上一层的域, 报错通过当前域的 `.emit('error', err)` 方法触发错误事件将错误传递上去, 从而使得异步错误可以被强制捕获. (更多内容详见 [Node.js 异步异常的处理与domain模块解析](https://cnodejs.org/topic/516b64596d38277306407936)) + +但是 domain 的引入也带来了更多新的问题. 比如依赖的模块无法继承你定义的 domain, 导致你写的 domain 无法 cover 依赖模块报错. 而且, 很多人 (特别是新人) 由于不了解 Node.js 的内存/异步流程等问题, 在使用 domain 处理报错的时候, 没有做到完善的处理并盲目的让代码继续走下去, 这很可能导致**项目完全无法维护** (可能出现的问题真是不胜枚举, 各种梦魇...) + +该模块目前的情况: [deprecate domains](https://github.com/nodejs/node/issues/66) + + +## Debugger + +![node-js-survey-debug](../assets/node-js-survey-debug.png) + +类似 gdb 的命令行下 debug 工具 (上图中的 build-in debugger), 同时也支持远程 debug (类似 [node-inspector](https://github.com/node-inspector/node-inspector), 目前处于试验状态). 当然, 目前有不少同学觉得 [vscode](https://code.visualstudio.com/) 对 debug 工具集成的比较好. + +关于这个 build-in debugger 使用推荐看[官方文档](https://nodejs.org/dist/latest-v6.x/docs/api/debugger.html). 如果要深入一点, 你可能对本文感兴趣: [动态修改 NodeJS 程序中的变量值](http://code.oneapm.com/nodejs/2015/06/27/intereference/) + + +## C/C++ Addon + +在 Node.js 中开发 addon 最痛苦的地方莫过于升级 V8 导致的 C/C++ 代码不能兼容的问题, 这个问题在很早就出现了. 为了解决这个问题前人开了一个叫 [nan](https://github.com/nodejs/nan) 的项目. + +要学习 addon 开发, 除了[官方文档](https://nodejs.org/docs/latest/api/addons.html)也推荐阅读这个: https://github.com/nodejs/node-addon-examples + + +## V8 + +这里并不是介绍 V8, 而是介绍 Node.js 中的 V8 这个模块. 该模块用于开放 Node.js 内建的 V8 引擎的事件和接口. 这些接口由 V8 底层决定, 所以无法保证绝对的稳定性. + +|接口|描述| +|---|---| +|v8.getHeapStatistics()|获取 heap 信息| +|v8.getHeapSpaceStatistics()|获取 heap space 信息| +|v8.setFlagsFromString(string)|动态设置 V8 options| + +### v8.setFlagsFromString(string) + +该方法用于添加额外的 V8 命令行标志. 该方法需谨慎使用, 在 VM 启动后修改配置可能会发生不可预测的行为、崩溃和数据丢失; 或者什么反应都没有. + +通过 `node --v8-options` 命令可以查询当前 Node.js 环境中有哪些可用的 V8 options. 此外, 还可以参考非官方维护的一个 [V8 options 列表](https://github.com/thlorenz/v8-flags/blob/master/flags-0.11.md). + +用法: + +```javascript +// Print GC events to stdout for one minute. +const v8 = require('v8'); +v8.setFlagsFromString('--trace_gc'); +setTimeout(function() { v8.setFlagsFromString('--notrace_gc'); }, 60e3); +``` + +## 内存快照 + +内存快照常用与解决内存泄漏的问题. 快照工具推荐使用 [heapdump](https://github.com/bnoordhuis/node-heapdump) 用来保存内存快照, 使用 [devtool](https://github.com/Jam3/devtool) 来查看内存快照. 使用 heapdump 保存内存快照时, 只会有 Node.js 环境中的对象, 不会受到干扰(如果使用 [node-inspector](https://github.com/node-inspector/node-inspector) 的话, 快照中会有前端的变量干扰). + +使用以及内存泄漏的常见原因详见: [如何分析 Node.js 中的内存泄漏](https://zhuanlan.zhihu.com/p/25736931?group_id=825001468703674368). + +## CPU 剖析 + +整理中 + + diff --git a/sections/io.md b/sections/io.md index 49946cf..0c021b7 100644 --- a/sections/io.md +++ b/sections/io.md @@ -242,6 +242,14 @@ function Console(stdout, stderr) { Node.js 封装了标准 POSIX 文件 I/O 操作的集合. 通过 require('fs') 可以加载该模块. 该模块中的所有方法都有异步执行和同步执行两个版本. 你可以通过 fs.open 获得一个文件的文件描述符. +### 编码 + +// TODO + +UTF8, GBK, es6 中对编码的支持, 如何计算一个汉字的长度 + +BOM + ### stdio stdio (standard input output) 标准的输入输出流, 即输入流 (stdin), 输出流 (stdout), 错误流 (stderr) 三者. 在 Node.js 中分别对应 `process.stdin` (Readable), `process.stdout` (Writable) 以及 `process.stderr` (Writable) 三个 stream. diff --git a/sections/network.md b/sections/network.md index e8c05c1..e2a749b 100644 --- a/sections/network.md +++ b/sections/network.md @@ -95,7 +95,7 @@ TIME-WAIT|主动方收到 FIN, 返回收到对方 FIN 的 ACK, 等待对方是 `TIME_WAIT` 是连接的某一方 (可能是服务端也可能是客户端) 主动断开连接时, 四次挥手等待被断开的一方是否收到最后一次挥手 (ACK) 的状态. 如果在等待时间中, 再次收到第三次挥手 (FIN) 表示对方没收到最后一次挥手, 这时要再 ACK 一次. 这个等待的作用是避免出现连接混用的情况 (`prevent potential overlap with new connections` see [TCP Connection Termination](http://www.tcpipguide.com/free/t_TCPConnectionTermination.htm) for more). -出现大量的 `TIME_WAIT` 比较常见的情况是, 并发量大, 服务器在短时间断开了大量连接. 对应 HTTP server 的情况可能是没开启 `keepAlive`. 如果有开 `keepAlive`, 一般是等待客户端自己主动断开, 那么`TIME_WAIT` 就只存在客户端, 而服务端则是 `CLOSE_WAIT` 的状态, 如果服务端出现大量 `CLOSE_WAIT`, 意味着当前服务端建立的链接大面积的被断开, 可能是目标服务集群重启之类. +出现大量的 `TIME_WAIT` 比较常见的情况是, 并发量大, 服务器在短时间断开了大量连接. 对应 HTTP server 的情况可能是没开启 `keepAlive`. 如果有开 `keepAlive`, 一般是等待客户端自己主动断开, 那么`TIME_WAIT` 就只存在客户端, 而服务端则是 `CLOSE_WAIT` 的状态, 如果服务端出现大量 `CLOSE_WAIT`, 意味着当前服务端建立的连接大面积的被断开, 可能是目标服务集群重启之类. ## UDP @@ -218,7 +218,7 @@ Node.js 中的 `http.Agent` 用于池化 HTTP 客户端请求的 socket (pooling hang up 有挂断的意思, socket hang up 也可以理解为 socket 被挂断. 在 Node.js 中当你要 response 一个请求的时候, 发现该这个 socket 已经被 "挂断", 就会就会报 socket hang up 错误. -[Node.js 中源码的情况:](https://github.com/nodejs/node/blob/v6.x/lib/_http_client.js#L286): +[Node.js 中源码的情况:](https://github.com/nodejs/node/blob/v6.x/lib/_http_client.js#L286) ```javascript function socketCloseListener() { @@ -287,18 +287,21 @@ DNS 服务主要基于 UDP, 这里简单介绍 Node.js 实现的接口中的两 由于 .lookup 是同步的, 所以如果由于什么不可控的原因导致 `getaddrinfo` 缓慢或者阻塞是会影响整个 Node 进程的, 参见[文档](https://nodejs.org/dist/latest-v6.x/docs/api/dns.html#dns_dns_lookup). +> hosts 文件是什么? 什么叫 DNS 本地解析? + +TODO ## ZLIB 在网络传输过程中, 如果网速稳定的情况下, 对数据进行压缩, 压缩比率越大, 那么传输的效率就越高等同于速度越快了. zlib 模块提供了 Gzip/Gunzip, Deflate/Inflate 和 DeflateRaw/InflateRaw 等压缩方法的类, 这些类接收相同的参数, 都属于可读写的 Stream 实例. -整理中 +TODO ## RPC RPC (Remote Procedure Call Protocol) 基于 TCP/IP 来实现调用远程服务器的方法, 与 http 同属应用层. 常用于构建集群, 以及微服务 (推荐一本[《Node.js 微服务》](https://www.amazon.cn/%E5%9B%BE%E4%B9%A6/dp/B01MXY8ARP)虽然我还没看完) -常见的 RPC 几大代表: +常见的 RPC 方式: * [Thrift](http://thrift.apache.org/) * HTTP @@ -318,4 +321,4 @@ RPC (Remote Procedure Call Protocol) 基于 TCP/IP 来实现调用远程服务 使用消息队列 (Message Queue) 来进行 RPC 调用 (RPC over mq) 在业内有不少例子, 比较适合业务解耦/广播/限流等场景. -整理中 +TODO