黑人的命也是命。
支持平等司法倡议.

生产最佳实践:性能和可靠性

概述

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

此主题明显属于“DevOps”领域,涵盖了传统开发和运维。因此,信息分为两部分

在代码中要做的

以下是在代码中可以做的一些事情,以提高应用程序的性能

使用 gzip 压缩

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

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

对于生产环境中的高流量网站,实现压缩的最佳方法是在反向代理级别实现(请参阅 使用反向代理)。在这种情况下,您不需要使用压缩中间件。有关在 Nginx 中启用 gzip 压缩的详细信息,请参阅 Nginx 文档中的 Module ngx_http_gzip_module

不要使用同步函数

同步函数和方法会占用执行进程,直到它们返回。对同步函数的单个调用可能在几微秒或几毫秒内返回,但在高流量网站中,这些调用会累积并降低应用程序的性能。在生产环境中避免使用它们。

尽管 Node 和许多模块提供了同步和异步版本的函数,但在生产环境中始终使用异步版本。只有在初始启动时才可以使用同步函数。

如果您使用的是 Node.js 4.0+ 或 io.js 2.1.0+,您可以使用 --trace-sync-io 命令行标志,在您的应用程序使用同步 API 时打印警告和堆栈跟踪。当然,您不希望在生产环境中使用它,而是要确保您的代码已准备好投入生产。有关更多信息,请参阅 node 命令行选项文档

正确进行日志记录

通常,从您的应用程序中记录日志有两个原因:用于调试和用于记录应用程序活动(本质上,所有其他内容)。在开发中,使用 console.log()console.error() 将日志消息打印到终端是一种常见的做法。但是,当目标是终端或文件时,这些函数是同步的,因此它们不适合生产环境,除非您将输出管道到另一个程序。

用于调试

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

用于应用程序活动

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

正确处理异常

当 Node 应用程序遇到未捕获的异常时,它们会崩溃。不处理异常并采取适当的措施会导致您的 Express 应用程序崩溃并离线。如果您遵循下面 确保您的应用程序自动重启 中的建议,那么您的应用程序将从崩溃中恢复。幸运的是,Express 应用程序通常具有较短的启动时间。但是,您希望避免在第一时间崩溃,为此,您需要正确处理异常。

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

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

有关错误处理基础知识的更多信息,请参阅

不要做的事情

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

此外,使用 uncaughtException 被官方认定为粗糙。因此,监听 uncaughtException 只是一个坏主意。这就是我们推荐诸如多进程和监视器之类的东西的原因:崩溃和重启通常是从错误中恢复的最可靠方法。

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

使用 try-catch

Try-catch 是一个 JavaScript 语言结构,你可以使用它来捕获同步代码中的异常。例如,使用 try-catch 来处理如下所示的 JSON 解析错误。

使用诸如JSHintJSLint 之类的工具来帮助你查找隐式异常,例如未定义变量的引用错误

以下是如何使用 try-catch 来处理潜在的进程崩溃异常的示例。此中间件函数接受一个名为“params”的查询字段参数,该参数是一个 JSON 对象。

app.get('/search', (req, res) => {
  // Simulating async operation
  setImmediate(() => {
    const jsonStr = req.query.params
    try {
      const jsonObj = JSON.parse(jsonStr)
      res.send('Success')
    } catch (e) {
      res.status(400).send('Invalid JSON string')
    }
  })
})

但是,try-catch 仅适用于同步代码。由于 Node 平台主要是异步的(尤其是在生产环境中),因此 try-catch 不会捕获许多异常。

使用 promises

Promise 将处理使用 then() 的异步代码块中的任何异常(显式和隐式)。只需在 promise 链的末尾添加 .catch(next)。例如

app.get('/', (req, res, next) => {
  // do some sync stuff
  queryDb()
    .then((data) => makeCsv(data)) // handle data
    .then((csv) => { /* handle csv */ })
    .catch(next)
})

app.use((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()。有关详细信息,请参阅 使用 Promise、生成器和 ES7 在 Express 中进行异步错误处理

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

在您的环境/设置中需要做的事情

以下是在您的系统环境中可以做的一些事情,以提高应用程序的性能

将 NODE_ENV 设置为“production”

NODE_ENV 环境变量指定应用程序运行的环境(通常是开发或生产)。提高性能最简单的方法之一是将 NODE_ENV 设置为“production”。

将 NODE_ENV 设置为“production”会使 Express

测试表明,仅仅这样做就可以将应用程序性能提高三倍!

如果您需要编写特定于环境的代码,可以使用 process.env.NODE_ENV 检查 NODE_ENV 的值。请注意,检查任何环境变量的值都会带来性能损失,因此应谨慎使用。

在开发中,您通常在交互式 shell 中设置环境变量,例如使用 export 或您的 .bash_profile 文件。但一般来说,您不应该在生产服务器上这样做;相反,请使用您操作系统的 init 系统(systemd 或 Upstart)。下一节将详细介绍如何使用您的 init 系统,但设置 NODE_ENV 对性能非常重要(而且很容易做到),因此在这里重点介绍。

使用 Upstart,在您的作业文件中使用 env 关键字。例如

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

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

使用 systemd,在您的单元文件中使用 Environment 指令。例如

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

有关更多信息,请参阅 在 systemd 单元中使用环境变量

确保您的应用程序自动重启

在生产环境中,您不希望您的应用程序永远处于离线状态。这意味着您需要确保应用程序在应用程序崩溃和服务器本身崩溃时都能重启。虽然您希望这两种情况都不会发生,但实际上您必须通过以下方式来应对这两种情况:

如果 Node 应用程序遇到未捕获的异常,则会崩溃。首先需要确保应用程序经过充分测试并处理所有异常(有关详细信息,请参阅 正确处理异常)。但是,作为安全措施,请设置一个机制,以确保应用程序在崩溃时能够自动重启。

使用进程管理器

在开发过程中,您只需从命令行使用 node server.js 或类似命令启动应用程序。但在生产环境中这样做会导致灾难。如果应用程序崩溃,它将处于离线状态,直到您重启它。为了确保应用程序在崩溃时重启,请使用进程管理器。进程管理器是应用程序的“容器”,它可以简化部署、提供高可用性,并使您能够在运行时管理应用程序。

除了在应用程序崩溃时重启应用程序外,进程管理器还可以使您能够

Node 最流行的进程管理器如下所示

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

使用任何这些进程管理器都可以使您的应用程序保持运行,即使它偶尔崩溃。

但是,StrongLoop PM 具有许多专门针对生产部署的功能。您可以使用它以及相关的 StrongLoop 工具来

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

使用初始化系统

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

有两种方法可以使用初始化系统与您的 Express 应用程序一起使用

Systemd

Systemd 是一个 Linux 系统和服务管理器。大多数主要的 Linux 发行版都已将 systemd 作为其默认的初始化系统。

systemd 服务配置文件称为单元文件,其文件名以 .service 结尾。以下是一个直接管理 Node 应用程序的单元文件示例。将 <尖括号> 中的值替换为您的系统和应用程序

[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 作为 systemd 服务

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

要将 StrongLoop PM 安装为 systemd 服务

$ 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 上受支持。

由于作业配置为在系统启动时运行,因此您的应用程序将与操作系统一起启动,并在应用程序崩溃或系统宕机时自动重启。

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

有关 Upstart 的更多信息,请参阅 Upstart 简介、手册和最佳实践

StrongLoop PM 作为 Upstart 服务

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

要将 StrongLoop PM 安装为 Upstart 1.4 服务

$ sudo sl-pm-install

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

$ sudo /sbin/initctl start strong-pm

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

在集群中运行您的应用程序

在多核系统中,您可以通过启动一组进程来大幅提高 Node 应用程序的性能。集群运行应用程序的多个实例,理想情况下每个 CPU 内核运行一个实例,从而将负载和任务分配到各个实例之间。

Balancing between application instances using the cluster API

重要提示:由于应用程序实例作为独立进程运行,它们不共享相同的内存空间。也就是说,对象对于应用程序的每个实例都是本地的。因此,您无法在应用程序代码中维护状态。但是,您可以使用内存数据存储(如 Redis)来存储与会话相关的数据和状态。此注意事项适用于所有形式的水平扩展,无论是使用多个进程或多个物理服务器进行集群。

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

使用 Node 的 cluster 模块

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

使用 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 文档中的 集群

使用 PM2

如果您使用 PM2 部署应用程序,那么您可以利用集群功能,而无需修改应用程序代码。您应该首先确保您的 应用程序是无状态的,这意味着没有本地数据存储在进程中(例如会话、WebSocket 连接等)。

在使用 PM2 运行应用程序时,您可以启用 **集群模式**,以便在您选择的多个实例中运行应用程序,例如与机器上可用 CPU 数量相匹配。您可以使用 `pm2` 命令行工具手动更改集群中的进程数量,而无需停止应用程序。

要启用集群模式,请像这样启动您的应用程序

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

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

运行后,应用程序可以像这样进行扩展

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

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

缓存请求结果

另一种提高生产环境中性能的策略是缓存请求的结果,这样您的应用程序就不会重复执行操作来重复服务相同的请求。

使用像 VarnishNginx(另请参阅 Nginx 缓存)这样的缓存服务器,可以极大地提高应用程序的速度和性能。

使用负载均衡器

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

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

使用负载均衡时,您可能需要确保与特定会话 ID 关联的请求连接到生成它们的进程。这被称为 *会话亲和性* 或 *粘性会话*,可以通过上述建议使用 Redis 等数据存储来解决会话数据(取决于您的应用程序)。有关讨论,请参阅 使用多个节点

使用反向代理

反向代理位于 Web 应用程序前面,除了将请求定向到应用程序外,还会对请求执行支持操作。它可以处理错误页面、压缩、缓存、提供文件以及负载均衡等其他事项。

将不需要应用程序状态知识的任务交给反向代理,可以释放 Express 来执行专门的应用程序任务。因此,建议在生产环境中将 Express 运行在反向代理(如 NginxHAProxy)之后。