[译]Express在生产环境下的最佳实践 - 性能和可靠性

前言

这将是一个分为两部分,内容是关于在生产环境下,跑Express应用的最佳实践。第一部分会关注安全性,第二部分则会关注性能和可靠性。当你读这篇文章时,会假设你已经对Node.js和web开发有所了解,而且对生产环境有了概念。html

关于第一部分,请参阅Express在生产环境下的最佳实践 - 安全性node

概览

正如第一部分所说,生产环境是供你的最终用户们所使用的,而开发环境则是供你开发和测试代码所用。故对于和两个环境的要求,是很是不一样的。例如,在开发环境下,你没必要考虑伸缩性和可靠性还有性能的问题,但这些在生产环境下都很是重要。linux

接下来,咱们会将此文分为两大部分:web

  • 须要对代码作的事,即开发部分。redis

  • 须要对环境作的事,即运维部分,shell

须要对代码作的事

为了提高你应用的性能,你能够经过:数据库

  • 使用gzip压缩express

  • 禁止使用同步方法npm

  • 使用中间件来提供静态文件json

  • 适当地打印日志

  • 合理地处理异常

使用gzip压缩

Gzip压缩能够显著地减小你web应用的响应体大小,从而提高你的web应用的响应速度。在Express中,你可使用compression中间件来启用gzip

var compression = require('compression');
var express = require('express');
var app = express();
app.use(compression());

对于在生产环境中,流量十分大的网站,最好是在反向代理层处理压缩。若是这样作,那么就不就须要使用compression了,而是须要参阅Nginxngx_http_gzip_module模块的文档。

禁止使用同步方法

同步方法会在它返回以前都一直阻塞线程。一次单独的调用可能影响不大,但在流量很是巨大的生产环境中,它是不可接受的,可能会致使严重的性能问题。

虽然大多数的Node.js和其第三方库都同时提供了一个方法的同步和异步版本,但在生产环境下,请老是使用它的异步版本。惟一可能例外的场景多是,若是这个方法只在应用初始化时调用一次,那么使用它的同步版本也是能够接受的。

若是你使用的是Node.js 4.0+ 或 io.js 2.1.0+ ,你能够在启动应用时附上--trace-sync-io参数来检查你的应用中哪里使用了同步API。更多关于这个参数的信息,你能够参阅io.js 2.1.0的更新日志

使用中间件来提供静态文件

在开发环境下,你可使用res.sendFile()来提供静态文件。但在生产环境下,这是不被容许的,由于这个方法会在每次请求时都会对文件系统进行读取。res.sendFile()并非经过系统方法sendfile实现的。

对应的,你可使用serve-static中间件来为你的Express应用提供静态文件。

更好的选择则是在反向代理层上提供静态文件。

适当地打印日志

总得来讲,为你的应用打印日志的目的有两个:调试和操做记录。在开发环境下,咱们一般使用console.log()console.err()来作这些事。可是,当这些方法的输出目标是终端或文件时,它们是同步的,因此它们并不适用于生产环境,除非你将输出导流至另外一个程序中。

为了调试

若是你正在为了调试而打印日志。那么你可使用一些专用于调试的库如debug,用于代替console.log()。这个库能够经过设置DEBUG环境变量来控制具体哪些信息会被打印。虽然这些方法也是同步的,但你必定不会在生产环境下进行调试吧?

为了操做记录

若是你正在为了记录应用的活动而打印日志。那么你可使用一些日志库如winstonBunyan,来替代console.log()。更多关于这两个库的详情,能够参阅这里

合理地处理异常

Node.js在遇到未处理的异常时就会退出。若是没有合理地捕获并处理异常,这会使你的应用崩溃和离线。若是你使用了一个自动重启的工具,那么你的应用则会在崩溃后马上重启,并且幸运的是,Express应用的重启时间一般都很快。可是无论怎样,你都想要尽可能避免这种崩溃。

为了保证你合理处理异常,请听从如下指示:

  • 使用try-catch

  • 使用promise

不该该作的事

你不该该监听全局事件uncaughtException。监听该事件将会致使进程遇到未处理异常时的行为被改变:进程将会忽略此异常并继续运行。这听上去很好,可是若是你的应用中存在未处理异常,继续运行它是很是危险的,由于应用的状态开始变得不可预测。

因此,监听uncaughtException并非一个好主意,它已被官方地列为了避免推荐的作法,而且之后可能会移除这个接口。咱们更推荐的是,使用多进程和自动重启。

咱们一样不推荐使用domains。它一般也并不能解决问题,而且已经是一个被标识为弃用的模块。

使用try-catch

Try-catch是一个JavaScript语言自带的捕获同步代码的结构。使用try-catch,你能够捕获例如JSON解析错误这样的异常。

使用JSHintJSLint这样的工具则可让你远离引用错误或未定义变量这种隐式的异常。

一个使用try-catch来避免进程退出的例子:

// Accepts a JSON in the query field named "params"
// for specifying the parameters
app.get('/search', function (req, res) {
  // Simulating async operation
  setImmediate(function () {
    var jsonStr = req.query.params;
    try {
      var jsonObj = JSON.parse(jsonStr);
      res.send('Success');
    } catch (e) {
      res.status(400).send('Invalid JSON string');
    }
  })
});

可是,try-catch只能捕获同步代码的异常。可是Node.js世界主要是异步的,因此,对于大多数的异常它都无能为力。

使用promise

Promise能够经过then()处理异步代码里的一切异常(显式和隐式)。记得在promise链的最后加上.catch(next)。例子:

app.get('/', function (req, res, next) {
  // do some sync stuff
  queryDb()
    .then(function (data) {
      // handle data
      return makeCsv(data)
    })
    .then(function (csv) {
      // handle csv
    })
    .catch(next)
})
 
app.use(function (err, req, res, next) {
  // handle error
})

如今全部的同步代码和异步代码的异常都传递到了异常处理中间件中。

可是,仍有两点须要提醒:

全部你的异步代码都必须返回一个promise(除了emitter)。若是你正在使用的库没有返回一个promise,那么就使用一些工具方法(如Bluebird.promisifyAll())来转换它。Event emitter(如stream)仍会形成未处理的异常。因此你必须合理地监听它们的error事件。例子:

app.get('/', wrap(async (req, res, next) =>; {
  let company = await getCompanyById(req.query.id)
  let stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

更多关于使用promise处理异常的信息,请参阅这里

须要对环境作的事

如下是一些你能够对你的系统环境作的事,用于提高你应用的性能:

  • NODE_ENV设置为“production”

  • 保证你的应用在发生错误后自动重启

  • 使用集群模式运行你的应用

  • 缓存请求结果

  • 使用负载均衡

  • 使用反向代理

NODE_ENV设置为“production”

NODE_ENV环境变量指明了应用当前的运行环境(开发或生产)。你能够作的为你的Express提高性能的最简单的事情之一,就是将NODE_ENV设置为“production”

NODE_ENV设置为“production”将使Express

  • 缓存视图模板

  • 缓存CSS文件

  • 生成更简洁的错误信息

若是你想写环境相关的代码,你能够经过process.env.NODE_ENV来获取运行时NODE_ENV的值。不过须要注意的,检查环境变量的值会形成少量的性能损失,因此不要有太多这类操做。

你可能已经习惯了SHELL中设置环境变量,例如使用export.bash_profile文件。可是你不该该在你的生产服务器上这么作。你应该使用操做系统的初始化系统(systemdsystemd)。下一个章节将会更详细的讲述初始化系统,可是因为设置NODE_ENV是如此的重要以及简单,因此咱们在这里就列出它:

当使用Upstart时,请在任务文件中使用env关键字。例子:

# /etc/init/env.conf
 env NODE_ENV=production

更多信息,请参阅这里

当使用systemd时,请在你的单元文件中使用Environment指令。例子:

# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production

更多信息,请参阅这里

若是你正在使用StrongLoop Process Manager,你也能够参阅这篇文章

保证你的应用在发生错误后自动重启

在生产环境下,你必定不但愿你的应用离线。因此你须要保证在你的应用发生错误时或你的服务器自身崩溃时,你的应用能够自动重启。虽然你可能不指望它们的发生,可是咱们须要更现实得预防它们,能够经过:

  • 使用一个进程管理员(process manager)库来重启你的应用

  • 当你的操做系统崩溃时,使用它提供的初始化系统来重启你的进程管理员。

Node.js应用在遇到未处理异常时就会退出。你的首要任务是保证你的代码的测试健全而且合理地处理了全部的异常。可是若有万一,请准备一个机制来确保它的自动重启。

使用进程管理员(process manager)

在开发环境下,你能够简单地使用node server.js这样的命令来启动你的应用。当时在生产环境下这么作将是不被容许的。若是应用崩溃了,在你手动重启它以前,它都会处于离线状态。为了保证你应用的自动重启,请使用一个进程管理员,它能够帮助你管理正在运行的应用。

除了保证你的应用的自动重启,一个进程管理员还可使你:

  • 获取当前运行环境的性能表现和资源消耗状况。

  • 自动地修改环境设置

  • 管理集群(StrongLoop PMpm2

Node.js世界里比较流行的进程管理员有:

  • StrongLoop Process Manager

  • PM2

  • Forever

更多的它们之间的比较,你能够参阅这里。关于它们三者的简介,你能够参阅这篇文章

使用一个初始化系统

接下来要保证的就是,在你的服务器重启时,你的应用也会相应的重启。尽管咱们认为咱们的服务器是十分稳定的,但它们仍有挂掉的可能。因此为了保证在你的服务器时重启时你的应用也会重启,请使用你操做系统内建的初始化系统。现在比较主流的是systemdUpstart

如下是经过你的Express应用来使用初始化系统的两种方法:

  • 将你的应用运行于一个进程管理员中,而后将进程管理员设置为系统的一个服务。这个是比较推荐的作法。

  • 直接经过初始化系统运行你的应用。这个方法更为简单,但你却享受不到进程管理员带来的福利。

Systemd

Systems是一个linux系统的服务管理员。大多数的linux发行版都将它做为默认的初始化系统。

一个systems服务的配置文件也被称为一个单元文件,有一个.service后缀。如下是一个直接管理Node.js应用的例子:

[Unit]
Description=Awesome Express App
 
[Service]
Type=simple
ExecStart=<strong>/usr/local/bin/node /projects/myapp/index.js</strong>
WorkingDirectory=<strong>/projects/myapp</strong>
 
User=nobody
Group=nogroup
 
# Environment variables:
Environment=<strong>NODE_ENV=production</strong>
 
# Allow many incoming connections
LimitNOFILE=infinity
 
# Allow core dumps for debugging
LimitCORE=infinity
 
StandardInput=null
StandardOutput=syslog
StandardError=syslog
Restart=always
 
[Install]
WantedBy=multi-user.target

更多关于systemd的信息,请参阅这里

Upstart

Upstart是一个大多数linux发行版均可用的系统工具,用于在系统启动时启动任务和服务,在系统关闭时中止它们,而且监控它们。你能够先将你的Express应用或进程管理员配置为一个服务,而后Upstart会自动地在系统重启后重启它们。

一个Upstart服务被定义在一个任务配置文件中,有一个.conf后缀。下面的例子展现了如何建立一个名为“myapp”的任务,且应用的入口是/projects/myapp/index.js

/etc/init/下建立一个名为myapp.conf的文件:

# When to start the process
start on runlevel [2345]
 
# When to stop the process
stop on runlevel [016]
 
# Increase file descriptor limit to be able to handle more requests
limit nofile 50000 50000
 
# Use production mode
env <strong>NODE_ENV=production</strong>
 
# Run as www-data
setuid www-data
setgid www-data
 
# Run from inside the app dir
chdir <strong>/projects/myapp</strong>
 
# The process to start
exec <strong>/usr/local/bin/node /projects/myapp/index.js</strong>
 
# Restart the process if it is down
respawn
 
# Limit restart attempt to 10 times within 10 seconds
respawn limit 10 10

注意:这个脚本要求Upstart 1.4 或更新的版本,支持于Ubuntu 12.04-14.10。

除了自动重启你的应用,Upstart还为你提供了如下命令:

  • start myapp – 手动启动应用

  • restart myapp – 手动重启应用

  • stop myapp – 手动退出应用

更多关于Upstart的信息,请参阅这里

使用集群模式运行你的应用

在多核的系统里,你能够经过启动一个进程集群来成倍了提高你应用的性能。一个集群运行了你的应用的多个实例,理想状况下,一个CPU核对应一个实例。这样,即可以在多个实例件进行负载均衡。

值得注意的是,因为应用实例跑在不一样的进程里,因此它们并不分享同一块内存空间。由于,应用里的全部对象都是本地的,你不能够在应用代码里维护状态。不过,你可使用如redis这样的内存数据库来存储session这样的数据和状态。

在集群中,一个工做进程的崩溃不会影响到其余的工做进程。因此除了性能因素以外,单独工做进程崩溃的相互不影响也是另外一个使用集群的好处。一个工做进程崩溃后,请确保记录下日志,而后从新经过cluster.fork()建立一个新的工做进程。

使用Node.jscluster模块

Node.js提供了cluster模块来支持集群。它使得一个主进程能够建立出多个工做进程。可是,比起直接使用这个模块,许多的库已经为你封装了它,并提供了更多自动化的功能:如node-pmcluser-service

缓存请求结果

另外一个提高你应用性能的途径是缓存请求的结果,这样一来,对于同一个请求,你的应用就没必要作多余的重复动做。

使用一个如VarnishNginx这样的缓存服务器能够极大地提高你应用的响应速度。

使用负载均衡

不论一个应用优化地多么好,一个单独的实例老是有它的负载上限的。一个很好的解决办法就是将你的应用跑上多个实例,而后在它们以前加上一个负载均衡器。

一个负载均衡器一般是一个反向代理,它接受负载,并将其均匀得分配给各个实例或服务器。你能够经过NginxHAProxy十分方便地架设一个负载均衡器。

使用了负载均衡后,你能够保证每一个请求都根据它的来源被设置了独特session id。固然,你也可使用如Redis这样的内存数据库来存储session。更多详情,能够参阅这里

负载均衡是一个至关复杂的话题,更加细致的讨论已超过了本文的范畴。

使用反向代理

一个反向代理被设置与web应用以前,用于支持各种对于请求的操做,如将请求发送给应用,自动处理错误页,压缩,缓存,提供静态文件,负载均衡,等等。

在生产环境中,这里推荐将Express应用跑在NginxHAProxy以后。

最后

原文连接:https://strongloop.com/strongblog/best-practices-for-express-in-production-part-two-performance-and-reliability/

相关文章
相关标签/搜索