背景
最近,线上有一个重要服务发生了宕机,导致上课中的教师大面积的断线。我们通过错误日志,查到一个 socket hang up
报错。但是无法根据错误定位到异常的代码。通过查询访问日志,也无法定位到宕机前发生错误的接口。我们期望能在代码里对异常进行捕获,这样不会导致服务挂掉,还希望获取异常的上下文,以便定位错误。
events.js:183
throw er; // Unhandled 'error' event
^
Error: socket hang up
at TLSSocket.onHangUp (_tls_wrap.js:1137:19)
at Object.onceWrapper (events.js:313:30)
at emitNone (events.js:111:20)
at TLSSocket.emit (events.js:208:7)
at endReadableNT (_stream_readable.js:1064:12)
at _combinedTickCallback (internal/process/next_tick.js:138:11)
at process._tickCallback (internal/process/next_tick.js:180:9)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! lesson@1.0.0 start: `node ./app/bin/www;`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the lesson@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2018-09-30T08_22_32_703Z-debug.log
使用uncaughtException捕获异步异常
首先,我们想到了 process上的 uncaughtException 事件。只要给 process 的uncaughtException事件注册了回调,服务就不会异常退出。看起来十分美好,起码我们的服务不会挂掉了。但是,带来了其他问题。
uncaughtException 事件
uncaughtException,是NodeJS进程(Process)中的一个事件。如果进程中发生了一个异常且异常没有被任何try...catch进行捕获就会触发这个事件。
下面以同步异常为例,介绍一下NodeJS对异常的默认处理。
process.on('uncaughtException', (error) => {
console.log('call uncaughtException handle');
});
// 执行一个不存在的异常
nonexistentFunc();
首先,我们注册process的uncaughtException事件,然后执行了一个不存在的函数。当执行这段程序时,程序会因为函数nonexistentFunc
不存在,而抛出异常。由于没有进行try...catch...处理,异常将会冒泡直到事件循环为止。NodeJS对于异常的默认处理,类似代码如下:
function _MyFatalException(err){
if(!process.emit('uncaughtException',err)){
console.error(err.stack);
process.emit('exit',1);
}
}
NodeJS对于异常的默认处理的顺序是,首先触发 uncaughtException
事件,若事件没有被监听到,则打印堆栈的错误信息,最后调用进程的退出事件。若事件监听到,则处理uncaughtException
的注册回调。
uncaughtException 事件发生的条件
- 当程序发生了异常
- 且异常未被 try...catch捕获
缺点与问题
- 无法获取异常的上下文。
- 无法给出友好的异常处理。例如,当接口发生
uncaughtException
时,无法获取到 response对象(已经丢失了上下文),告知调用方服务当前出现异常。 - 会导致内存泄露。uncaughtException事件发生后,会丢失当前环境的堆栈,可能导致Node不能正常进行内存回收,从而导致内存泄露。
由于使用 uncaughtException
捕获异常,会导致内存泄露。建议的使用方式为,当 uncaughtException
事件发生时,记录error,然后结束 Node 进程进行重启服务。(😭然后我们在项目中并没有这样做。。。业务不允许重启。。)
使用domain模块捕获异步异常
注意:domain模块将被弃用。
domain 模块,简化了异常的处理方式,可以处理try...catch..无法捕获的异常。且不会丢失上下文,也不会导致程序退出。
我们老项目使用的是 express 框架,因此可以使用中间件的方式,处理请求中的异步异常。代码如下:
const domain = require('domain');
app.use((req, res, next) => {
const req_domain = domain.create();
req_domain.on('error', (err) => {
console.log(err); // 打印错误日志
res.send(500, err.stack);
});
req_domain.run(next);
});
在中间件中,首先创建一个 domain
对象,然后注册 error
事件的回调,最后在创建域的上下文中执行next函数。
什么时候触发domain的error事件:
进程抛出了异常,没有被任何的try catch捕获到,这时候将会触发整个process的processFatal,此时如果在domain包裹之中,将会在domain上触发error事件,反之,将会在process上触发uncaughtException事件。
如果想要了解更多关于 domain 模块,可以阅读这篇文章——《Node.js 异步异常的处理与domain模块解析》
优化方案
domain虽然很好用,但不是万能的。有时,也无法捕获部分异常。为了保证服务不会挂掉。我可以使用 uncaughtException
事件进行兜底。
基础版
domain + uncaughtException
domain
来捕获大部分异常和合理处理异常退出,uncaughtException
来避免服务挂掉。
升级版
domain + uncaughtException + Cluster
我们可以使用Cluster模块,开启多个工作线程。当遇到domain无法捕获的异常,在 uncaughtException
捕获到时,为了避免内存泄露,我们可以结束当前work进程。具体实现如下:
const cluster = require('cluster');
process.on('uncaughtException', (err) => {
console.log(err); // 将异常写入日志
server.close(); // 关闭服务连接
// 通知 master 进程停止服务
if (cluster.worker) {
cluster.worker.disconnect();
}
});
总结
尽管domain模块即将废弃,但是我们仍然可以使用它,发现和定位线上异常。
网友评论