处理未捕获的异常
- 在Node的异步世界中,未捕获的异常是特别需要关注的问题
app.get('/fail', function(req, res){
throw new Error('Nope!');
});
- 在Express执行路由处理器时,它把它们封装在一个try/catch块中,所以这不是一个真正的未捕获异常。
- Express会在服务器端记录异常,并且访问者会得到一个丑陋的栈输出。然而服务器是稳定的,其他请求还能得到正确处理。
- 如果我们想提供一个“好的”错误页面,可以创建文件views/500.handlebars并在所有路由后面添加一个错误处理器:
app.use(function(err, req, res, next){
console.error(err.stack);
app.status(500).render('500');
});
- 提供一个定制的错误页面总归是一个好的做法;比如,可以在这个错误处理器中发送一封邮件给开发团队,让他们知道网站出错了。
//更糟的情况:
app.get('/epic-fail', function(req, res){
process.nextTick(function(){
throw new Error('Kaboom!');
});
});
-
它把你的整个服务器都搞垮了。这是因为setTimeout是异步执行的,抛出异常的函数被推迟到Node空闲时才执行。问题是,当Node得到空闲可以执行这个函数时,它已经没有其所服务的请求的上下文了,所以它已经没有资源了,只能毫不客气地关掉整个服务器,因为现在它处于不确定的状态;
-
process.nextTick
跟调用没有参数的setTimeout
非常像,但它效率更高。我们在这里用它是为了演示,一般你不会在服务器端代码里用它。 -
可以采取行动处理未捕获的异常,但如果Node不能确定程序的稳定性,也不能。
-
换句话说,如果出现了未捕获异常,唯一能做的也只是关闭服务器。在这种情况下,最好的做法就是尽可能正常地关闭服务器,并且有个故障转移机制。
-
最容易的故障转移机制是使用集群。如果你的程序是运行在集群模式下的,当一个工作线程死掉后,主线程会繁衍另一个工作线程来取代它。
关闭服务器
-
Node有两种机制解决这个问题:
uncaughtException
事件(可能会在将来的Node版本中去掉)和域(推荐)。 -
一个域基本上是一个执行上下文,它会捕获在其中发生的错误。可以有很多域,可以在处理易出错的代码时创建一个新域。
-
每个请求都在一个域中处理是一种好的做法,这样就可以追踪那个请求中所有的未捕获错误并做出相应的响应(正常地关闭服务器)。
-
添加一个中间件就可以非常轻松地满足这个要求。这个中间件应该在所有其他路由或中间件前面
app.use(function(req, res, next){
// 为这个请求创建一个域
var domain = require('domain').create();
// 处理这个域中的错误
domain.on('error', function(err) {
console.error('DOMAIN ERROR CAUGHT
', err.stack);
try {
// 在5秒内进行故障保护关机
setTimeout(function(){
console.error('Failsafe shutdown.');
process.exit(1);
}, 5000);
// 从集群中断开
var worker = require('cluster').worker;
if(worker) worker.disconnect();
// 停止接收新请求
server.close();
try {
// 尝试使用Express错误路由
next(err);
} catch(err) {
// 如果Express错误路由失效,尝试返回普通文本响应
console.error('Express error mechanism failed.
', err.stack);
res.statusCode = 500;
res.setHeader('content-type', 'text/plain');
res.end('Server error.');
}
} catch(err){
console.error('Unable to send 500 response.
', err.stack);
}
});
// 向域中添加请求和响应对象
domain.add(req);
domain.add(res);
// 执行该域中剩余的请求链
domain.run(next);
});
// 其他中间件和路由放在这里
var server = http.createServer(app).listen(app.get('port'), function(){
console.log('Listening on port %d.', app.get('port'));
});
- 我们做的第一件事是创建一个域,然后在上面附着一个错误处理器。只要这个域中出现未捕获的错误,就会调用这个函数。
- 我们在这里采取的方式是试图给任何处理中的请求以恰当的响应,然后关闭服务器。
- 根据错误的性质,可能无法响应处理中的请求,所以我们首先要确立关闭服务器的截止时间。
- 在这个例子中,我们允许服务器在5秒内响应处理中的请求(如果它可以)。你所选择的数值取决于你的程序,如果程序经常有长请求,就应该给更多的时间。
- 一旦确立了截止时间,我们会从集群中断开(如果在集群中),以防止集群给我们分配更多的请求。然后明确告诉服务器我们不再接受新的连接。
- 最后,我们试图传到错误处理路由(
next(err)
)来响应产生错误的请求。如果那会抛出错误,我们退回去用普通的Node API
响应。如果其他的全部失败了,我们会记录错误(客户端得不到响应,最终会超时)。 - 一旦设置好未处理异常处理器,我们就把请求和响应对象添加到域中(允许那些对象上的所有方法抛出的错误都由域处理)。
- 最后,我们在域的上下文中运行管道中的下一个中间件。注意,这可以有效地运行域中管道里的所有中间件,因为对
next()
的调用是链起来的。
一篇介绍的文章
用多台服务器扩展
-
用集群向外扩展可以实现单台服务器的性能最大化, 但当需要多台服务器时会怎样?这时情况会变得有点复杂。要实现这种并行,需要一台代理服务器(为了跟一般用于访问外部网络的代理区别开,经常被称为反向代理或正向代理,但我发现这种叫法既费解又没必要,所以我只称它为代理)。
-
在代理领域的两个后起之秀分别是Nginx和HAProxy。还有一些比较小的基于Node的代理服务器,比如proxy和node-http-proxy。
-
如果要求不高,或者是用于开发,这些都是很好的选择。对于生产环境而言,我推荐用Nginx或HAProxy (这两个都是免费的,尽管提供服务是收费的)。
-
如果确实配置了一台代理服务器,请确保告知Express你用了代理,并且它应该得到信任:
app.enable('trust proxy');
- 这样可以确保
req.ip
、req.protocol
和req.secure
能反映客户端和代理服务器之间连接的细节,而不是客户端和你的应用之间的。还有,req.ips
将会是一个数组,表明原始客户端IP和所有中间代理的名称或IP地址。
网站监控
- 网站监控是你可以采取的最重要的(也是最常被忽视的) QA措施之一;
第三方正常运行监控
-
在网站服务器上正常运行一个监控可能可以发现某些页面不能访问,但如果整个服务器都宕掉了,它甚至可能都发不出一个SOS。
-
一道防线应该是第三方正常运行监控。UptimeRobot有50个免费监控,并且配置简单。警报可以通过邮件、短信(文本消息)、Twitter或者iPhone应用程序发送。
-
可以监控单个页面的返回码(除200之外的所有返回码都可以视为错误),或者检查页面上有没有某个关键字。不过要记住,如果用关键字监控,它可能会影响你的分析
应用程序故障
- 正常运行监控可以非常有效地监测大规模故障。如果用关键字监控,它们甚至可以用来监测应用程序故障。
- 然而,一般在处理故障时都想表现得更优雅。给用户显示一个友好的消息“对不起,这项服务目前不正常”,并且你会收到一封邮件或一条短信告诉你有故障了。
- 当你依赖第三方组件时,比如数据库或其他Web服务器,一般会采取这种方式。
- 一种简单的故障处理方式是有错误时给你自己发邮件;如果通知需求复杂,可能要考虑找一个通知服务,比如亚马逊的简单通知服务(SNS)。
压力测试
-
压力测试是为了相信服务器可以正常地应对成百上千的并发请求; 压力测试可能非常复杂,并且很大程度上取决于你的项目。
-
先添加一个简单的测试,确保程序可以满足一秒内对主页的100次请求; 用Node模块loadtest做压力测试:
//qa/tests-stress.js:
var loadtest = require('loadtest');
var expect = require('chai').expect;
suite('Stress tests', function(){
test('Homepage should handle 100 requests in a second', function(done){
var options = {
url: 'http://localhost:3000',
concurrency: 4,
maxRequests: 100
};
loadtest.loadTest(options, function(err,result){
expect(!err);
expect(result.totalTimeSeconds < 1);
done();
});
});
});