Production best practices: performance and reliability

Overview

本文讨论了部署到生产环境的Express应用程序的性能和可靠性最佳实践.

这个主题显然属于"发展"世界,涵盖了传统的开发和运营. 因此,信息分为两个部分:

Things to do in your code

您可以在代码中执行以下操作来提高应用程序的性能:

Use gzip compression

Gzip压缩可以大大减小响应主体的大小,从而提高Web应用程序的速度. 在Express应用程序中使用压缩中间件进行gzip压缩. 例如:

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

对于生产中流量较高的网站,进行压缩的最佳方法是在反向代理级别实现它(请参阅使用反向代理 ). 在这种情况下,您无需使用压缩中间件. 有关在Nginx中启用gzip压缩的详细信息,请参见Nginx文档中的ngx_http_gzip_module模块 .

Don’t use synchronous functions

同步函数和方法会阻塞执行过程,直到它们返回为止. 对同步功能的单个调用可能在几微秒或几毫秒内返回,但是在高流量的网站中,这些调用加起来并降低了应用程序的性能. 避免在生产中使用它们.

尽管Node和许多模块提供其功能的同步和异步版本,但始终在生产中使用异步版本. 可以证明同步功能合理的唯一时间是在首次启动时.

如果您使用的是Node.js 4.0+或io.js 2.1.0+,则只要应用程序使用同步API,就可以使用--trace-sync-io命令行标志来打印警告和堆栈跟踪. 当然,您不想在生产中使用它,而是要确保您的代码已准备好用于生产. 有关更多信息,请参见节点命令行选项文档 .

Do logging correctly

通常,从应用程序进行日志记录有两个原因:调试和日志记录应用程序活动(本质上是其他所有事情). 在开发中,通常使用console.log()console.error()将日志消息打印到终端. 但是,当目标是终端或文件时, 这些功能是同步的,因此除非您将输出通过管道传输到另一个程序,否则它们不适合生产.

For debugging

如果您出于调试目的而记录日志,那么请使用特殊的调试模块(例如debug console.log() ,而不要使用console.log() . 此模块使您可以使用DEBUG环境变量来控制将哪些调试消息发送到console.error() (如果有). 为了使您的应用程序完全异步,您仍然需要将console.error()传递给另一个程序. 但是,那么,您真的不会在生产中进行调试,是吗?

For app activity

如果要记录应用程序活动(例如,跟踪流量或API调用),请使用WinstonBunyan之类的日志记录库,而不要使用console.log() . 有关这两个库的详细比较,请参阅StrongLoop博客文章比较Winston和Bunyan Node.js日志记录 .

Handle exceptions properly

节点应用程序遇到未捕获的异常时崩溃. 不处理异常并采取适当的措施将使您的Express应用程序崩溃并脱机. 如果您遵循下面的确保您的应用程序自动重新启动中的建议,则您的应用程序将从崩溃中恢复. 幸运的是,Express应用程序的启动时间通常很短. 不过,您首先要避免崩溃,而要做到这一点,您需要正确处理异常.

为确保您处理所有异常,请使用以下技术:

在深入探讨这些主题之前,您应该对Node / Express错误处理有基本的了解:使用错误优先的回调,以及在中间件中传播错误. Node使用"错误优先回调"约定从异步函数返回错误,其中回调函数的第一个参数是错误对象,后继参数中包含结果数据. 为了指示没有错误,请传递null作为第一个参数. 回调函数必须相应地遵循错误优先回调约定,以有意义地处理错误. 在Express中,最佳实践是使用next()函数通过中间件链传播错误.

有关错误处理基础的更多信息,请参见:

What not to do

应该做的一件事是听的uncaughtException事件,当异常气泡一路回到事件循环发出. 为uncaughtException添加事件侦听器将更改遇到异常的进程的默认行为. 尽管有例外,该过程将继续运行. 这听起来像是防止应用程序崩溃的好方法,但是在未捕获的异常之后继续运行应用程序是一种危险的做法,因此不建议这样做,因为进程的状态变得不可靠且不可预测.

此外,使用uncaughtException被正式确认为raw . 因此,监听uncaughtException只是一个坏主意. 这就是为什么我们建议使用多个流程和管理程序之类的原因:崩溃和重新启动通常是从错误中恢复的最可靠方法.

我们也不建议使用domains . 它通常不能解决问题,并且是已弃用的模块.

Use try-catch

Try-catch是一种JavaScript语言构造,可用于捕获同步代码中的异常. 例如,使用try-catch处理JSON解析错误,如下所示.

使用的工具,如JSHintJSLint的帮你找到像隐含例外未定义变量引用错误 .

这是一个使用try-catch处理潜在的进程崩溃异常的示例. 该中间件函数接受名为" params"的查询字段参数,该参数是JSON对象.

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平台主要是异步的(特别是在生产环境中),所以try-catch不会捕获很多异常.

Use promises

Promise将处理使用then()异步代码块中的任何异常(显式和隐式then() . 只需在承诺链的末尾添加.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
})

现在,所有异步和异步错误都将传播到错误中间件.

但是,有两个警告:

  1. 您的所有异步代码都必须返回promise(发射器除外). 如果特定的库不返回promise,则使用诸如Bluebird.promisifyAll()之类的辅助函数来转换基础对象.
  2. 事件发射器(如流)仍可能导致未捕获的异常. 因此,请确保您正确处理了错误事件; 例如:
const wrap = fn => (...args) => fn(...args).catch(args[2])

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

wrap()函数是一个包装器,用于捕获被拒绝的promise,并以错误作为第一个参数调用next() . 有关详细信息,请参阅带有Promises,Generators和ES7的Express中的异步错误处理 .

有关使用Promise处理错误的更多信息,请参阅带有Q的Node.js中的Promises-回调的替代方法 .

Things to do in your environment / setup

您可以在系统环境中执行以下操作来提高应用程序的性能:

Set NODE_ENV to “production”

NODE_ENV环境变量指定运行应用程序的环境(通常是开发或生产环境). 可以提高性能的最简单的操作之一就是将NODE_ENV设置为" production".

将NODE_ENV设置为" production"将使Express:

测试表明 ,仅执行此操作就可以将应用程序性能提高三倍!

如果需要编写特定于环境的代码,则可以使用process.env.NODE_ENV检查NODE_ENV的值. 请注意,检查任何环境变量的值都会导致性能下降,因此应谨慎执行.

在开发中,通常会在交互式外壳程序中设置环境变量,例如通过使用export.bash_profile文件. 但是总的来说,您不应该在生产服务器上这样做; 相反,请使用操作系统的初始化系统(systemd或Upstart). 下一节将提供有关一般使用初始化系统的更多详细信息,但是设置NODE_ENV对于性能非常重要(且易于实现),因此在此处重点介绍.

对于Upstart,在作业文件中使用env关键字. 例如:

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

有关更多信息,请参见Upstart简介,菜谱和最佳实践 .

对于systemd,请在您的单元文件中使用Environment指令. 例如:

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

有关更多信息,请参见在系统单位中使用环境变量 .

Ensure your app automatically restarts

在生产中,您永远都不希望应用程序处于脱机状态. 这意味着您需要确保在应用程序崩溃以及服务器本身崩溃的情况下都重新启动. 尽管您希望这些事件均不会发生,但实际上,您必须通过以下方法解决这两种情况:

如果节点应用程序遇到未捕获的异常,则会崩溃. 您需要做的最重要的事情是确保您的应用程序经过了良好的测试并可以处理所有异常(有关详细信息,请参见正确处理异常 ). 但是,为确保故障安全,请采用适当的机制以确保当您的应用崩溃时,它会自动重启.

Use a process manager

在开发中,您仅使用node server.js或类似的东西从命令行启动了您的应用程序. 但是在生产中这样做是灾难的根源. 如果该应用崩溃,则在您重新启动它之前它将处于脱机状态. 为确保您的应用在崩溃时重启,请使用进程管理器. 流程管理器是应用程序的"容器",可促进部署,提供高可用性并允许您在运行时管理应用程序.

除了在崩溃时重新启动应用程序之外,流程管理器还可以使您:

Node最受欢迎的流程管理器如下:

有关三个流程管理器的逐项功能比较,请参阅http://strong-pm.io/compare/ . 有关这三个的更详细的介绍,请参阅Express应用程序的流程管理器 .

使用这些过程管理器中的任何一个都足以保持您的应用程序正常运行,即使它有时会崩溃.

但是,StrongLoop PM具有许多专门针对生产部署的功能. 您可以使用它和相关的StrongLoop工具执行以下操作:

如下所述,当您使用初始化系统将StrongLoop PM安装为操作系统服务时,它将在系统重新启动时自动重新启动. 因此,它将使您的应用程序进程和集群永远存活.

Use an init system

可靠性的下一层是确保服务器重新启动时您的应用程序重新启动. 系统仍然可能由于多种原因而崩溃. 为确保在服务器崩溃时您的应用程序重启,请使用操作系统内置的init系统. 今天使用的两个主要的初始化系统是systemdUpstart .

通过Express应用程序使用初始化系统有两种方法:

Systemd

Systemd是Linux系统和服务管理器. 大多数主要的Linux发行版都将systemd用作默认的init系统.

系统服务配置文件称为单元文件 ,文件名以.service结尾. 这是一个直接管理Node应用程序的示例单位文件. 替换系统和应用程序中<angle brackets>包含的值:

[Unit]
Description=<Awesome Express App>

[Service]
Type=simple
ExecStart=/usr/local/bin/node </projects/myapp/index.js>
WorkingDirectory=</projects/myapp>

User=nobody
Group=nogroup

# Environment variables:
Environment=NODE_ENV=production

# 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的更多信息,请参见systemd参考(手册页) .

StrongLoop PM as a systemd service

您可以轻松地将StrongLoop Process Manager安装为系统服务. 完成后,服务器重新启动时,它将自动重新启动StrongLoop PM,然后它将重新启动其管理的所有应用程序.

To install StrongLoop PM as a systemd service:

$ sudo sl-pm-install --systemd

然后使用以下命令启动服务:

$ sudo /usr/bin/systemctl start strong-pm

有关更多信息,请参见设置生产主机(StrongLoop文档) .

Upstart

Upstart是许多Linux发行版中可用的系统工具,用于在系统启动期间启动任务和服务,在关机期间停止它们并进行监督. 您可以将Express应用程序或流程管理器配置为服务,然后Upstart崩溃时将自动重新启动它.

Upstart服务在作业配置文件(也称为"作业")中定义,文件名以.conf结尾. 以下示例显示如何为名为" myapp"的应用程序创建一个名为" 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 NODE_ENV=production

# Run as www-data
setuid www-data
setgid www-data

# Run from inside the app dir
chdir /projects/myapp

# The process to start
exec /usr/local/bin/node /projects/myapp/index.js

# 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上受支持.

Since the job is configured to run when the system starts, your app will be started along with the operating system, and automatically restarted if the app crashes or the system goes down.

除了自动重启应用程序外,Upstart还使您可以使用以下命令:

有关Upstart的更多信息,请参阅Upstart简介,食谱和最佳实践 .

StrongLoop PM as an Upstart service

You can easily install StrongLoop Process Manager as an Upstart service. After you do, when the server restarts, it will automatically restart StrongLoop PM, which will then restart all the apps it is managing.

要将StrongLoop PM安装为Upstart 1.4服务:

$ sudo sl-pm-install

然后使用以下命令运行服务:

$ sudo /sbin/initctl start strong-pm

注意:在不支持Upstart 1.4的系统上,命令略有不同. 有关更多信息,请参见设置生产主机(StrongLoop文档) .

Run your app in a cluster

在多核系统中,您可以通过启动进程集群来多次提高Node应用程序的性能. 群集运行该应用程序的多个实例,最好在每个CPU内核上运行一个实例,从而在这些实例之间分配负载和任务.

Balancing between application instances using the cluster API

重要信息:由于应用程序实例作为单独的进程运行,因此它们不会共享相同的内存空间. 也就是说,对象是应用程序每个实例的本地对象. 因此,您不能在应用程序代码中维护状态. 但是,您可以使用像Redis这样的内存中数据存储来存储与会话相关的数据和状态. 此警告实际上适用于所有形式的水平扩展,无论是与多个进程或多个物理服务器进行群集.

在群集应用中,工作进程可以单独崩溃而不会影响其余进程. 除了性能优势外,故障隔离是运行应用程序集群的另一个原因. 每当工作进程崩溃时,请始终确保记录该事件并使用cluster.fork()生成一个新进程.

Using Node’s cluster module

Node的集群模块使集群成为可能. 这使主进程可以生成工作进程并在工作进程之间分配传入的连接. 但是,与其直接使用此模块,不如使用其中自动为您执行此操作的众多工具之一. 例如node-pmcluster-service .

Using StrongLoop PM

如果将应用程序部署到StrongLoop Process Manager(PM),则可以利用群集而无需修改应用程序代码.

当StrongLoop Process Manager(PM)运行应用程序时,它将自动在群集中运行的应用程序具有一定数量的工作程序,这些工作程序的数量等于系统上CPU内核的数量. 您可以使用slc命令行工具手动更改集群中的工作进程数,而无需停止应用程序.

例如,假设您已将应用程序部署到prod.foo.com,并且StrongLoop PM正在监听端口8701(默认端口),然后使用slc将群集大小设置为8:

$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8

有关使用StrongLoop PM进行群集的更多信息,请参见StrongLoop文档中的群集 .

Using PM2

If you deploy your application with PM2, then you can take advantage of clustering without modifying your application code. You should ensure your application is stateless first, meaning no local data is stored in the process (such as sessions, websocket connections and the like).

使用PM2运行应用程序时,可以启用集群模式以在具有多个所选实例的集群中运行它,例如匹配计算机上可用CPU的数量. 您可以使用pm2命令行工具手动更改集群中的进程数,而无需停止应用程序.

要启用集群模式,请按以下方式启动应用程序:

# Start 4 worker processes
$ pm2 start app.js -i 4
# Auto-detect number of available CPUs and start that many worker processes
$ pm2 start app.js -i max

也可以通过将exec_mode设置为cluster并将instances设置为要启动的工作程序数,来在PM2进程文件( ecosystem.config.js或类似文件)中进行配置.

运行后,可以按以下方式缩放名称为app的给定应用程序:

# Add 3 more workers
$ pm2 scale app +3
# Scale to a specific number of workers
$ pm2 scale app 2

有关使用PM2进行集群的更多信息,请参阅PM2文档中的集群模式 .

Cache request results

提高生产性能的另一种策略是缓存请求的结果,以使您的应用程序不会重复操作以重复服务相同的请求.

使用VarnishNginx之类的缓存服务器(另请参阅Nginx Caching )可以大大提高应用程序的速度和性能.

Use a load balancer

无论应用程序如何优化,单个实例只能处理有限的负载和流量. 扩展应用程序的一种方法是运行该应用程序的多个实例,并通过负载平衡器分配流量. 设置负载平衡器可以提高应用程序的性能和速度,并使它的扩展能力超过单个实例.

负载平衡器通常是反向代理,可以协调往返多个应用程序实例和服务器的流量. 您可以使用NginxHAProxy轻松为您的应用设置负载均衡器.

使用负载平衡时,您可能必须确保与特定会话ID关联的请求连接到发起它们的进程. 这被称为会话亲和性粘性会话 ,可以通过上面的建议解决,以将数据存储(例如Redis)用于会话数据(取决于您的应用程序). 有关讨论,请参见使用多个节点 .

Use a reverse proxy

反向代理位于Web应用程序的前面,除了将请求定向到应用程序之外,还对请求执行支持操作. 它可以处理错误页面,压缩,缓存,服务文件和负载平衡等.

将不需要了解应用程序状态的任务移交给反向代理可以释放Express来执行专门的应用程序任务. 因此,建议在生产环境中在诸如NginxHAProxy的反向代理后面运行Express.

by  ICOPY.SITE