Node.js服务器开发异常处理实践

随着Node.js的流行和各种服务框架的发展完善,越来越多的前端开发者参与到后台服务器的开发中去,用自己熟悉的语言和思维方式来实现服务器端的功能。然而,后台开发与前端开发一个重大区别在于,后台对于服务的安全性和稳定性的要求比前端要严格得多。在前端,即使有一些隐藏bug或安全隐患,只要后台作了防范处理,最终只是用户体验上的问题,受影响的也只是少数用户。但如果后台服务出了bug或安全漏洞,则可能破坏后台数据的完整性和一致性,甚至导致整个web服务崩溃,其影响面是一个前端bug所越不能比的。因此,为了提高服务器的稳定性和安全性,对程序异常的监控和及时处理是一个必须密切关注的问题。

程序中的异常可分为参数不合法、前提条件不满足等可预期的异常和JavaScript 引擎的运行时异常等非预期的异常两类。一般情况下,我们可以在可能出错的地方使用try/catch来捕获异常,防止程序出错。但由于Node的异步特性,导致使用try/catch无法捕获回调函数中的异常。之后 Node 会触发 uncaughtException 事件,如果这个事件依然没有得到响应,整个进程就会 crash。程序员永远也无法保证代码中不出现 uncaughtException,即使自己的代码写得足够小心,但也不能保证用的第三方模块没有 bug或异常,例如数据库或redis连接出错。如果不处理这些异常,其后果可能是:

1、运行报错直接导致程序奔溃,由于是后台运行,我们不知道任何出错的原因。这显然不是我们想要的。

2、处理http请求过程中出错,将导致请求无法响应和释放,直至连接超时。这种用户体验非常差,我们要做的应该是在出错时,给用户一个友好的提示,并记录下此次异常,以便排查。

任何线上服务都不应该因为 uncaughtException 导致服务器崩溃。一个友好的的错误处理机制应该满足以下几点:

1、出现错误时能将任务中断在一个合适的位置,并将处理了一半的数据还原

2、能记录错误的摘要、调用栈及其他上下文

3、对于引发异常的用户,返回 500 页面

4、不影响整个进程的正常运行,其他用户不受影响,可以正常访问

因此,我们使用Node.js编写服务器代码时必须注意对可能出现的程序异常的处理。下面通过实践中的例子来说明Node.js中常用的几种异常处理方式。

1try catch 方式

将可能出现异常的代码块用try/catch包裹,必要时主动抛出异常:在当前的层级没有足够的信息去决定如何处理错误,因此需要使用异常来将错误沿着调用栈逆向抛出,直到某个层级有足够的信息来处理这个错误。

try {
    while (readLen < buffer.length) {
        let buff = buffer.slice(readLen)
        let rpcIndex = getRpcIndex(buff)
        let callback = callbackList[rpcIndex](dispatch)
        let packLen = lib.OnDataReceive(pipeId, buff, buff.length, callback)
        readLen += packLen
        if (packLen <= 0) {
            throw 'UNPACK_ZERO_BYTE_ERROR'
        }
    }
} catch (ex) {
    /*处理异常*/
    Util.log(ex)
    Logger.error(ex)
    Logger.add('Bad_packed_data', buffer.toString('hex'))
}

但是try catch方式无法处理异步代码块内出现的异常,因为执行catch时,异常还没有发生。

2callback方式

对于异步回调可能出错的情况,在callback中判断是否出现异常并进行处理。

fs.writeFile(SAVE_FILE_NAME, JSON.stringify(data), err => {
    if (err) {
        /*处理异常*/
        Logger.error(err)
        onFail()
    } else {
        onSucc()
    }
})

3event 方式

EventEmitter实例添加error事件监听器,统计并处理异常出错。

let socket = new events.EventEmitter()
socket.addListener('error', err => {
    /*处理异常*/
    Logger.error(err)
    this.disconnect()
})

4Promise 方式

在每个返回Promise对象的地方,除了定义.then方法处理正常逻辑外,还需要定义.catch方法处理异常情况。

RedisClient.getUserByIds(playerIds) // 返回Promise
    .then(list => {
        this.sendClientData('N_SendFBFriends', list)
    }).catch(err => {
        /*处理异常*/
        Logger.error(err)
        this.reportError('GET_FB_FRIENDS_ERROR', 'Get facebook friends error')
})

Promise同样无法处理异步代码块中抛出的异常

5domain方式

Node.js定义了一个domain模块,专门用于处理异步回调的异常。所有在业务代码中发生的无法被恢复的错误将传递到程序的边界处,在这里我们可以以一种统一的、健壮的方式去处理这些错误。使用domain我们可以很轻松的捕获异步异常:

let domain = require('domain')
app.use(function (req, res, next) {
    let reqDomain = domain.create();
reqDomain.on('error', (err) => {
        /*处理异常*/
        Logger.error(err)
    res.send(500)
    });

    reqDomain.run(next);
})

若使用的是restify框架,它已经将domain捕获异常的步骤封装好了,我们可以直接使用如下方法定义异常处理方法。

app.on('uncaughtException', function(req, res, route, err) {
    Util.log(err)
    try {
        res.send(500, Util.response(err))
    } catch(ex) {
        Logger.error(ex)
    }
}) 

通过 domain 似乎就已经解决了我们的需求: 给触发异常的用户一个 500,停止接收新请求,提供正常的服务给已经建立连接的用户,直到所有请求都已结束,退出进程。但是,domain 有个最大的问题,它不能捕获所有的异步异常!也就是说,即使用了 domain,程序依然有因为 发生uncaughtException导致crash 的可能。所幸的是我们可以监听 uncaughtException 事件。

6process方式

process方式可以捕获任何异常,不管是同步代码块中的异常还是异步代码块中的异常。当 Node 发现一个未捕获的异常时,会触发uncaughtException事件。并且如果这个事件存在回调函数,Node 就不会强制结束进程。利用这个特性,可以用来弥补 domain 的不足:

process.on('uncaughtException', function (ex) {
    /*处理异常*/
    Util.log(ex)
    Logger.error(ex)

    const Promise = require('bluebird')
    Promise.delay(1000).then(() => {
        process.exit(1);
    })
}) 

当 Node 抛出 uncaughtException 异常时就会丢失当前环境的堆栈,导致 Node 不能正常进行内存回收。也就是说,每一次 uncaughtException 都有可能导致内存泄露。因此,当我们已经尽处理了已知的可能错误,异常还是发生了,这种最好选择退出进程以便重启服务。线上环境我们的node服务通常都不是直接运行的,而是通过守护进程(如pm2、passenger等服务)来启动和维护。因此,在node中我们只需负责将进程结束,后续的重启交给守护进程就行。

上面的例子中,我们统一使用Logger.error方法来追踪记录所有的异常和错误,具体实现上,我采用了一个叫的bunyan日志组件将格式化后的数据写入日志文件中。当然你可以实现自己的错误统计方式,比如说统一写入数据库或者发送到一个专门的监控服务器上。


参考文章:

https://segmentfault.com/a/1190000009651765#articleHeader7

http://www.infoq.com/cn/articles/quit-scheme-of-node-uncaughtexception-emergence

https://jysperm.me/2016/10/nodejs-error-handling/

https://itbilu.com/nodejs/core/VJC6oetP.html

本文来自网易实践者社区,经作者蔡娟授权发布。