尽早考虑与生产相关的问题可以帮助你节省很多时间,并减少你将要承受的痛苦。在这里会学到Express对不同执行环境的支持,扩展网站的方法以及如何监控网站的健康状况。
执行环境
Express支持执行环境的概念,他是一种生产、开发或测试模式中运行应用程序的方法。实际上你可以按照自己的想法创建很多种不同的环境,然而需要记住的是,开发、生产和测试是标准环境。Express、Connect以及第三方中间件可能会基于这些环境做出决定。因此建议坚持使用标准的开发、生产和测试环境。(development/production/test)
使用app.set('env','production')可以指定执行环境,但是并不建议这么做,因为不过灵活,如果不修改代码则应用程序会一直运行在那个环境中。用环境变量NODE_ENV指定执行环境更好。通过app.get('env')得到运行在哪种模式下。
如果没有指定,开发模式就是默认模式。如果要放到生产模式下:
export NODE_ENV=production
node index.js
在某些系统上,有一个简便需要,仅为一次命令执行期间设定环境:
NODE_ENV=production node index.js
这会在生产模式下运行服务器,但当服务器终止时,环境变量NODE_ENV还是原来的值。
这是简单改变执行环境起不到太大的作用,比如在生产模式下会输出更多的警告道控制台(比如告诉你被废弃的模块将来会被移除),生产模式下,视图缓存会默认启用。
事实上,执行环境大体是一个可以利用的工具,可以轻松的决定应用程序在不同的环境下应该做何表现。尽量缩小开发、测试和生产环境之间的差别。你应该保守的使用这个功能。
,当然有些差异是不可避免的,比如如果你的程序是高度数据库驱动的,可能不想在开发期间干扰生产数据库,另外比如更加详细的日志,开发时记录的很多东西都没必要在生产环境中记录。
网站扩展
扩展通常意味着向上扩展
或向外扩展
。向上扩展是指让服务器变得更强:更快的CPU,更好的架构,更多内核,更多内存等等。向外扩展只是意味着更多的服务器。随着云计算的流行和虚拟化的普及,服务器和计算能力的相关性变的越来越小,并且对于网站的扩展需求而言,向外扩展是成本收益率更高的办法
。
在开发网站时,你应该总是考虑向外扩展的可能性,Node对于向外扩展支持的很好,在搭建一个设计好的要向外扩展的网站时,最终重要的是持久化
。除非所有的服务器都能访问到那个文件系统,否则不应该用本地本件系统做持久化。不过只读数据是个例外,比如日志和备份。
用应用集群扩展
Node本身支持应用集群,他是一种简单的、单服务器形式的向外扩展。使用应用集群,你可以为系统上每个内核创建一个独立的服务器。
应用集群的好处:
- 实现给定服务器性能的最大化
- 在并行条件下测试程序的低开销方式
给网站添加集群支持,规范开发的话,我们创建第二个程序文件,用之前一直在用的非集群程序文件在集群中运行程序,修改非集群文件如下:
function startServer() {
http.createServer(app).listen(app.get('port'), function () {
console.log('Express started in ' + app.get('env') +
' mode on http://localhost:' + app.get('port') +
'; press Ctrl-C to terminate.');
});
}
if (require.main === module) {
// application run directly; start app server
startServer();
} else {
// application imported as a module via "require": export function to create server
module.exports = startServer;
}
修改之后文件既可以直接运行,也可以通过require语句作为一个模块引入。
直接运行脚本时,require.main === module为true,如果他是false,表示你的脚本是另外一个脚本用require加载进来的。
集群文件DEMO如下:
// 主线程、工作线程都执行-start
var cluster = require('cluster');
function startWorker() {
var worker = cluster.fork();
console.log('CLUSTER: Worker %d started', worker.id);
}
// 主线程、工作线程都执行-end
if(cluster.isMaster){
// 主线程都执行-start
require('os').cpus().forEach(function(){
startWorker();
});
// log any workers that disconnect; if a worker disconnects, it
// should then exit, so we'll wait for the exit event to spawn
// a new worker to replace it
cluster.on('disconnect', function(worker){
console.log('CLUSTER: Worker %d disconnected from the cluster.',
worker.id);
});
// when a worker dies (exits), create a worker to replace it
cluster.on('exit', function(worker, code, signal){
console.log('CLUSTER: Worker %d died with exit code %d (%s)',
worker.id, code, signal);
startWorker();
});
// 主线程都执行-end
} else {
// 工作线程都执行-start
// start our app on worker; see index.js
require('./index.js')();
// 工作线程都执行-end
}
这个JavaScript文件,或者在主线程的上下文中(node cluster.js),或者在工作线程的上下文中(Node集群系统执行时)。属性cluster.isMaster和cluster.isWorker决定了运行在哪个上下文中。
在运行这个脚本时,是在主线程模式下执行的,并且我们用cluster.fork为系统中的每个CPU启动一个工作线程,同时监听工作线程的exit事件,重新繁衍死掉的工作线程。
假定你是多核系统,就能看到一些工作线程启动了,如果想看到不同工作线程处理不同请求的证据,可以在路由前添加如下中间件:
app.use(function (req, res, next) {
var cluster = require('cluster');
if (cluster.isWorker) console.log('worker %d received request', cluster.worker.id);
next();
});
用多台服务器扩展
用集群向外扩展可以实现单台服务器的性能最大化,但当你需要多台服务器时会怎样?要实现这种并行,你需要一台代理服务器(为了跟一般用户访问外部网络的代理区分开,经常被称作反向或正向代理)。
在代理领域的两个后起之秀分别是Nginx和HAProxy。
如果你配置了一台代理服务器, 请确保告知Express你使用了代理,并且他应该得到信任:
app.enable('trust proxy');
这样可以确保req.ip、req.protocol和req.secure能反映客户端和代理服务器之间连接的细节,而不是客户端和你的应用之间。
网站监控
网站监控是你采取的最重要的QA措施之一。唯一能让你的老板和客户信服你工作很优秀的办法,就是总能比他们早知道故障发生了。
压力测试
压力测试(负载测试)是为了让你相信服务器可以正常的应对成百上千的并发请求。
Node模块loadtest支持做简单的压力测试。
未捕获异常
首先看一个例子:
app.get('/fail',function(req,res){
throw new Error('Error');
})
在Express执行路由处理器时,他把他们封装在一个try/catch中,所以上述不是一个真正的未捕获异常。这不会引起太多问题,Express会在服务端记录异常,并且访问者会得到一个丑陋的栈输出(想提供一个好的错误页面,可以添加500处理器),但是服务器是稳定的。
提供一个定制的错误页面总归是一个好的做法,当错误出现时,他不仅在用户面前显得更专业,还可以让你采取行动。比如可以在错误处理器中发送一封邮件给开发团队,让他们知道网站出错了。可惜这只能用在Express处理捕获的异常上
。如下是如下错误就糟糕了:
app.get('epic-fail',function(req,res){
process.nextTick(function(){
throw new Error('boom');
})
})
此时情况非常糟糕,整个服务器都垮掉了。不仅没向用户显示一个友好的错误信息,而且现在你的服务器还宕机了,不能在处理请求。这是因为setTimeout是异步执行的,抛出异常的函数被推迟到Node空闲时才执行。问题是:当Node空闲时可以执行这个函数时,已经没有其所服务的请求的上下文了,所以他已经没有资源了,只能毫不客气的关掉整个服务器。
process.nextTick更调用没有参数的setTimeout非常想,但是它效率更高。
我们可以采取行动处理未捕获的异常,但如果Node不能确定程序的稳定性,你也不能。也就是说,如果出现了未捕获异常,唯一能做的也只是关闭服务器。此时最好的做法就是尽可能正常的关闭服务器,并且有个故障转移机制。最容易的故障转移机制是使用集群。
遇到为处理异常时,我们怎么才能极可能正常的关闭服务器呢?Node有两种机制解决这个问题:uncaughtException事件和域。
使用域是较新的方式,也是推荐的方式。一个域基本上是一个执行上下文,它会捕获其中发生的错误。有了域,在错误处理上可以更灵活,不再是只有一个全局的未捕获异常处理器,你可以有很多域,可以在处理易出错的代码时创建一个新域
。每个请求都在域中是一种好的做法,这样可以追踪那个请求中所有的未捕获错误并做出相应的响应,在所有其他路由和中间件前面添加一个中间件:
// use domains for better error handling
app.use(function(req, res, next){
// 为请求创建域
var domain = require('domain').create();
// 错误处理器,未捕获错误就会调用这个函数
domain.on('error', function(err){
console.error('DOMAIN ERROR CAUGHT\n', 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(error){
// 如果Express错处路由失效,退回去用普通Node API响应
// plain Node response
console.error('Express error mechanism failed.\n', error.stack);
res.statusCode = 500;
res.setHeader('content-type', 'text/plain');
res.end('Server error.');
}
} catch(error){
// 全部失败,记录错误,客户端得不到响应,最终超时
console.error('Unable to send 500 response.\n', error.stack);
}
});
// 一旦设置好未处理异常处理器,就把请求和响应对象添加到域中(允许哪些对象上的所有方法抛出的错误都由域处理)
domain.add(req);
domain.add(res);
// 执行该域中剩余的请求链
domain.run(next);
});