Node 出现 uncaughtException 之后的优雅退出方案
Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个。由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如:
try {
console.log('进入 try/catch');
require('fs').stat('SOME_FILE_DOES_NOT_EXIST',
function readCallback(err, content) {
if (err) {
throw err; // 抛出异常
}
});
} catch (e) {
// 这里捕获不到 readCallback 函数中抛出的异常
} finally {
console.log('离开 try/catch');
}
运行结果是:
进入 try/catch
离开 try/catch test.js:7
throw err; // 抛出异常
^
Error: ENOENT, stat 'SOME_FILE_DOES_NOT_EXIST'
上面代码中由于 fs.stat
去查询一个不存在的文件的状态,导致 readCallback
抛出了一个异常。由于 fs.read
的异步特性,readCallback
函数的调用发生在 try/catch
块结束之后,所以该异常不会被 try/catch 捕获。之后 Node 会触发 uncaughtException
事件,如果这个事件依然没有得到响应,整个进程(process
)就会 crash。
程序员永远无法保证代码中不出现 uncaughtException
,即便是自己代码写的足够小心,也不能保证用的第三方模块没有 bug,例如:
var deserialize = require('deserialize');
// 假设 deserialize 是一个带有 bug 的第三方模块 // app 是一个 express 服务对象
app.get('/users', function (req, res) {
mysql.query('SELECT * FROM user WHERE id=1', function (err, user) {
var config = deserialize(user.config);
// 假如这里触发了 deserialize 的 bug
res.send(config);
});
});
如果不幸触发了 deserialize
模块的 bug,这里就会抛出一个异常,最终结果是整个服务 crash。
当这种情况发生在 Web 服务上时结果是灾难性的。uncaughtException
错误会导致当前的所有的用户连接都被中断,甚至不能返回一个正常的 HTTP 错误码,用户只能等到浏览器超时才能看到一个no data received
错误。
这是一种非常野蛮粗暴的异常处理机制,任何线上服务都不应该因为 uncaughtException
导致服务器崩溃。一个友好的错误处理机制应该满足三个条件:
- 对于引发异常的用户,返回 500 页面
- 其他用户不受影响,可以正常访问
- 不影响整个进程的正常运行
很遗憾的是,保证 uncaughtException
不影响整个进程的健康运转是不可能的。当 Node 抛出uncaughtException
异常时就会丢失当前环境的堆栈,导致 Node 不能正常进行内存回收。也就是说,每一次 uncaughtException
都有可能导致内存泄露。
既然如此,退而求其次,我们可以在满足前两个条件的情况下退出进程以便重启服务。
用 domain 来捕获异步异常
普遍的思路是,如果可以通过某种方式来捕获回调函数中的异常,那么就不会有uncaughtException
错误导致的崩溃。为了解决这个问题,Node 0.8 之后的版本新增了 domain
模块,它可以用来捕获回调函数中抛出的异常。
domain
主要的 API 有 domain.run
和 error
事件。简单的说,通过 domain.run
执行的函数中引发的异常都可以通过 domain
的 error
事件捕获,例如:
var domain = require('domain');
var d = domain.create();
d.run(function () {
setTimeout(function () {
throw new Error('async error'); // 抛出一个异步异常
}, 1000);
}); d.on('error', function (err) {
console.log('catch err:', err); // 这里可以捕获异步异常
});
通过 domain
模块,以及 JavaScript 的词法作用域特性,可以很轻易的为引发异常的用户返回 500 页面。以 express 为例:
var app = express();
var server = require('http').createServer(app);
var domain = require('domain'); app.use(function (req, res, next) {
var reqDomain = domain.create();
reqDomain.on('error', function (err) { // 下面抛出的异常在这里被捕获
res.send(500, err.stack); // 成功给用户返回了 500
}); reqDomain.run(next);
}); app.get('/', function () {
setTimeout(function () {
throw new Error('async exception'); // 抛出一个异步异常
}, 1000);
});
上面的代码将 domain 作为一个中间件来使用,保证之后 express 所有的中间件都在 domain.run
函数内部执行。这些中间件内的异常都可以通过 error
事件来捕获。
尽管借助于闭包,我们可以正常的给用户返回 500 错误,但是 domain
捕获到错误时依然会丢失堆栈信息,此时已经无法保证程序的健康运行,必须退出。Node http server 提供了 close
方法,该方法在调用时会停止 server 接收新的请求,但不会断开当前已经建立的连接。
reqDomain.on('error', function () {
try {
// 强制退出机制
var killTimer = setTimeout(function () {
process.exit(1);
}, 30000);
killTimer.unref(); // 非常重要 // 自动退出机制,停止接收新链接,等待当前已建立连接的关闭
server.close(function () {
// 此时所有连接均已关闭,此时 Node 会自动退出,不需要再调用
process.exit(1) 来结束进程
});
} catch(e) {
console.log('err', e.stack);
}
});
这个例子来自 Node 的文档。其中有几个关键点:
- Node 有个非常好的特性,所有连接都被释放后进程会自动结束,所以不需要再
server.close
方法的回调函数中退出进程 - 强制退出机制: 因为用户连接有可能因为某些原因无法释放,在这种情况下应该强制退出整个进程。
killTimer.unref()
: 如果不使用unref
方法,那么即使 server 的所有连接都关闭,Node 也会保持运行直到killTimer
的回调函数被调用。unref
可以创建一个"不保持程序运行"的计时器。- 处理异常时要小心的把异常处理逻辑用 try/catch 包住,避免处理异常时抛出新的异常
通过 domain
似乎就已经解决了我们的需求: 给触发异常的用户一个 500,停止接收新请求,提供正常的服务给已经建立连接的用户,直到所有请求都已结束,退出进程。但是,理想很丰满,现实很骨感,domain
有个最大的问题,它不能捕获所有的异步异常!。也就是说,即使用了 domain
,程序依然有因为 uncaughtException
crash 的可能。
所幸的是我们可以监听 uncaughtException
事件。
uncaughtException
事件
uncaughtException
是一个非常古老的事件。当 Node 发现一个未捕获的异常时,会触发这个事件。并且如果这个事件存在回调函数,Node 就不会强制结束进程。这个特性,可以用来弥补domain
的不足:
process.on('uncaughtException', function (err) {
console.log(err); try {
var killTimer = setTimeout(function () {
process.exit(1);
}, 30000);
killTimer.unref(); server.close();
} catch (e) {
console.log('error when exit', e.stack);
}
});
uncaughtException
事件的缺点在于无法为抛出异常的用户请求返回一个 500 错误,这是由于uncaughtException
丢失了当前环境的上下文,比如下面的例子就是它做不到的:
javascript
app.get('/', function (req, res) {
setTimeout(function () {
throw new Error('async error');
// uncaughtException, 导致 req 的引用丢失
res.send(200);
}, 1000);
}); process.on('uncaughtException', function (err) {
res.send(500); // 做不到,拿不到当前请求的 res 对象
});
最终出错的用户只能等待浏览器超时。
domain
+ uncaughtException
所以,我们可以结合两种异常捕获机制,用 domain
来捕获大部分的异常,并且提供友好的 500 页面以及优雅退出。对于剩下的异常,通过 uncaughtException
事件来避免服务器直接 crash。
代码如下:
var app = express();
var server = require('http').create(app);
var domain = require('domain'); // 使用 domain 来捕获大部分异常
app.use(function (req, res, next) {
var reqDomain = domain.create();
reqDomain.on('error', function () {
try {
var killTimer = setTimeout(function () {
process.exit(1);
}, 30000);
killTimer.unref(); server.close(); res.send(500);
} catch (e) {
console.log('error when exit', e.stack);
}
}); reqDomain.run(next);
}); // uncaughtException 避免程序崩溃
process.on('uncaughtException', function (err) {
console.log(err); try {
var killTimer = setTimeout(function () {
process.exit(1);
}, 30000);
killTimer.unref(); server.close();
} catch (e) {
console.log('error when exit', e.stack);
}
});
其他的一些问题
express
中异常的处理
使用 express
时记住一定不要在 controller 的异步回调中抛出异常,例如:
app.get('/', function (req, res, next) { // 总是接收 next 参数
mysql.query('SELECT * FROM users', function (err, results) {
// 不要这样做
if (err) throw err; // 应该将 err 传递给 errorHandler 处理
if (err) return next(err);
});
}); app.use(function (err, req, res, next) {
// 带有四个参数的 middleware 专门用来处理异常
res.render(500, err.stack);
});
和 cluster 一起使用
cluster 是 node 自带的负载均衡模块,使用 cluster 模块可以方便的建立起一套 master/slave 服务。在使用 cluster 模块时,需要注意不仅需要调用 server.close()
来关闭连接,同时还需要调用cluster.worker.disconnect()
通知 master 进程已停止服务:
var cluster = require('cluster'); process.on('uncaughtException', function (err) {
console.log(err); try {
var killTimer = setTimeout(function () {
process.exit(1);
}, 30000);
killTimer.unref(); server.close(); if (cluster.worker) {
cluster.worker.disconnect();
}
} catch (e) {
console.log('error when exit', e.stack);
}
});
不要通过 uncaughtException
来忽略错误
当 uncaughtException
事件有一个以上的 listener
时,会阻止 Node 结束进程。因此就有一个广泛流传的做法是监听 process
的 uncaughtException
事件来阻止进程退出,这种做法有内存泄露的风险,所以千万不要这么做:
javascript
process.on('uncaughtException', function (err) { // 不要这么做
console.log(err);
});
pm2 对于 uncaughtException
的额外处理
如果你在用 pm2 0.7.1 之前的版本,那么要当心。pm2 有一个 bug,如果进程抛出了uncaughtException
,无论代码中是否捕获了这个事件,进程都会被 pm2 杀死。0.7.2 之后的 pm2 解决了这个问题。
要小心 worker.disconnect()
如果你在退出进程时希望可以发消息给监控服务器,并且还使用了 cluster,那么这个时候要特别小心,比如下面的代码:
var udpLog = dgram.createSocket('udp4');
var cluster = require('cluster'); process.on('uncaughtException', function (err) {
udpLog.send('process ' + process.pid + ' down',
/* ... 一些发送 udp 消息的参数 ...*/); server.close();
cluster.worker.disconnect();
});
这份代码就不能正常的将消息发送出去。因为 udpLog.send
是一个异步方法,真正发消息的操作发生在下一个事件循环中。而在真正的发送消息之前 cluster.worker.disconnect()
就已经执行了。worker.disconnect()
会在当前进程没有任何链接之后,杀掉整个进程,这种情况有可能发生在发送 log 数据之前,导致 log 数据发不出去。
一个解决方法是在 udpLog.send
方法发送完数据后再调用 worker.disconnect
:
var udpLog = dgram.createSocket('udp4');
var cluster = require('cluster'); process.on('uncaughtException', function (err) {
udpLog.send('process ' + process.pid + ' down', /* ...
一些发送 udp 消息的参数 ...*/, function () {
cluster.worker.disconnect();
}); server.close(); // 保证 worker.disconnect 不会拖太久..
setTimeout(function () {
cluster.worker.disconnect();
}, 100).unref();
});
小节
说了这么多,结论是,目前为止(Node 0.10.25),依然没有一个完美的方案来解决任意异常的优雅退出问题。用 domain
来捕获大部分异常,并且通过 uncaughtException
避免程序 crash 是目前来说最理想的方案。回调异常的退出问题在遇到 cluster 以后会更加复杂,特别是对于连接关闭的处理要格外小心。
参考文章
感谢田永强对本文的审校。
转自:http://www.infoq.com/cn/articles/quit-scheme-of-node-uncaughtexception-emergence
Node 出现 uncaughtException 之后的优雅退出方案的更多相关文章
- 优雅的App完全退出方案(没有任何内存泄漏隐患)
在Android开发过程中,特别是界面比较多的情况下,用平常的退出方式往往是不能完全退出这个应用,网络上也好多各种退出方案.其中一种应该是被广大开发者采纳使用,也非常的清晰方便,就是在Applicat ...
- 优雅的App全然退出方案(没有不论什么内存泄漏隐患)
在Android开发过程中,特别是界面比較多的情况下,用寻常的退出方式往往是不能全然退出这个应用,网络上也好多各种退出方案.当中一种应该是被广大开发人员採纳使用,也很的清晰方便.就是在Applicat ...
- NodeJS服务器退出:完成任务,优雅退出
上一篇文章,我们通过一个简单的例子,学习了NodeJS中对客户端的请求(request)对象的解析和处理,整个文件共享的功能已经完成.但是,纵观整个过程,还有两个地方明显需要改进: 首先,不能共享完毕 ...
- android 退出方案 导致内存泄露
比较奇怪android没有给出一个统一的退出接口,网上查了很多材料也出现了一些错误,在此记录一下,遇到的,与总结的. 1.常见概念,方法 finish() 通知结束当前activity实例,finis ...
- 正确使用‘trap指令’实现Docker优雅退出
一般应用(比如mariadb)都会有一个退出命令,用户使用类似systemctl stop ****.service方法,停止其服务时,systemd会调用其配置文件注册的退出命令,该命令执行清理资源 ...
- golang channel详解和协程优雅退出
非缓冲chan,读写对称 非缓冲channel,要求一端读取,一端写入.channel大小为零,所以读写操作一定要匹配. func main() { nochan := make(chan int) ...
- golang中使用Shutdown特性对http服务进行优雅退出使用总结
golang 程序启动一个 http 服务时,若服务被意外终止或中断,会让现有请求连接突然中断,未处理完成的任务也会出现不可预知的错误,这样即会造成服务硬终止:为了解决硬终止问题我们希望服务中断或退出 ...
- Docker 容器优雅终止方案
原文链接:Docker 容器优雅终止方案 作为一名系统重启工程师(SRE),你可能经常需要重启容器,毕竟 Kubernetes 的优势就是快速弹性伸缩和故障恢复,遇到问题先重启容器再说,几秒钟即可恢复 ...
- .NET Worker Service 如何优雅退出
上一篇文章中我们了解了 .NET Worker Service 的入门知识[1],今天我们接着介绍一下如何优雅地关闭和退出 Worker Service. Worker 类 从上一篇文章中,我们已经知 ...
随机推荐
- spring使用JdbcDaoSupport中封装的JdbcTemplate进行query
1.Dept package cn.hxex.springcore.jdbc; public class Dept { private Integer deptNo; private String d ...
- android开发中系统自带语音模块的使用
android开发中系统自带语音模块的使用需求:项目中需要添加语音搜索模块,增加用户体验解决过程:在网上搜到语音搜索例子,参考网上代码,加入到了自己的项目,完成产品要求.这个问题很好解决,网上能找到很 ...
- ubuntu server获取并自动设置最快镜像的方法
一,安装方法1 add-apt-repository ppa:ossug-hychen/getfastmirrorapt-get install getfastmirror 如果添加了临时源,这样移除 ...
- [译]rabbitmq 2.2 Building from the bottom: queues
我对rabbitmq学习还不深入,这些翻译仅仅做资料保存,希望不要误导大家. You have consumers and producers under your belt, and now you ...
- 第三方登录开发-Facebook
这次这个项目要分别可以使用新浪微博,qq互联以及Facebook和Twitter授权登录 facebook目前只支持oauth2技术,个人理解其工作流程是当用户想访问当前网站,却不想注册账号,此时当前 ...
- 011--VS2013 C++ 斜角地图贴图
准备好的图片 //全局变量HDC mdc;HBITMAP fullmap;//声明位图对象,在初始化函数中完成的斜角地图会保存在这个位图中const int rows = 10, cols = 10; ...
- 60.ISE PhysDesignRules ERROR
PhysDesignRules:2100 - Issue with pin connections and/or configuration on block:<U_ila_pro_0/U0/I ...
- foj 2044 1 M possible 二进制压缩
题目链接: http://acm.fzu.edu.cn/problem.php?pid=2044 题意: 给出 一大堆数,找出2个出现次数模3 为1 的两个数字 题解: 把一个数分为几位拆开统计 ...
- bzoj 2038 莫队算法
莫队算法,具体的可以看10年莫涛的论文. 大题思路就是假设对于区间l,r我们有了一个答案,那么对于区间l,r+1,我们 可以暴力的转移一个答案,那么对于区间l1,r1和区间l2,r2,需要暴力处理 的 ...
- bzoj 1270 DP
w[i,j]代表高度j,第i颗树的时候的最大值 那么w[i,j]:=max(w[i,j+1],w[k,j+heigh])+sum[i,j]: 但是这样枚举是n^3的,我们发现转移的第二个选择w[k,j ...