Better

Ethan的博客,欢迎访问交流

Node与Express开发之生产篇

尽早考虑与生产相关的问题可以帮助你节省很多时间,并减少你将要承受的痛苦。在这里会学到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本身支持应用集群,他是一种简单的、单服务器形式的向外扩展。使用应用集群,你可以为系统上每个内核创建一个独立的服务器。

应用集群的好处:

  1. 实现给定服务器性能的最大化
  2. 在并行条件下测试程序的低开销方式

给网站添加集群支持,规范开发的话,我们创建第二个程序文件,用之前一直在用的非集群程序文件在集群中运行程序,修改非集群文件如下:

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);
});


留言