Production best practices: performance and reliability
本文探讨部署到生产的 Express 应用程序的性能和可靠性最佳实际。
这个话题显然属于“devops”世界,涵盖传统的开发和经营。因而,信息分为两局部:
在您的代码中要做的事件(开发局部)
- 应用 gzip 压缩
- 不要应用同步函数
- 正确记录
- 正确处理异样
在您的环境 / 设置中要做的事件(操作局部)
- 将 NODE_ENV 设置为“生产”
- 确保您的利用主动重启
- 在集群中运行您的应用程序
- 缓存申请后果
- 应用负载均衡器
- 应用反向代理
Use gzip compression
Gzip 压缩能够大大减小响应主体的大小,从而进步 Web 应用程序的速度。在您的 Express 应用程序中应用 compression 进行 gzip 压缩。例如:
var compression = require('compression')
var express = require('express')
var app = express()
app.use(compression())
对于生产中的高流量网站,施行压缩的最佳办法是在反向代理级别施行它。在这种状况下,您不须要应用 compression 中间件。无关在 Nginx 中启用 gzip 压缩的详细信息,请参阅 Nginx 文档中的模块 ngx_http_gzip_module。
Don’t use synchronous functions
同步函数和办法会在它们返回之前阻塞正在执行的过程。对同步函数的单个调用可能会在几微秒或几毫秒内返回,然而在高流量网站中,这些调用会累加并升高应用程序的性能。防止在生产中应用它们。
只管 Node 和许多模块提供了它们性能的同步和异步版本,但在生产中始终应用异步版本。惟一能够证实同步性能正当的工夫是在初始启动时。
如果您应用的是 Node.js 4.0+ 或 io.js 2.1.0+,您能够应用 –trace-sync-io 命令行标记在您的应用程序应用同步 API 时打印正告和堆栈跟踪。当然,您不想在生产中应用它,而是要确保您的代码已筹备好用于生产。
Do logging correctly
通常,从您的应用程序进行日志记录有两个起因:用于调试和记录应用程序流动(实质上是其余所有内容)。应用 console.log() 或 console.error() 将日志音讯打印到终端是开发中的常见做法。然而当指标是终端或文件时,这些函数是同步的,因而它们不适宜生产,除非您将输入通过管道传输到另一个程序。
如果您出于调试目标进行日志记录,那么不要应用 console.log(),而是应用像 debug 这样的非凡调试模块。此模块使您可能应用 DEBUG 环境变量来管制将哪些调试音讯发送到 console.error()(如果有)。为了放弃你的利用齐全异步,你依然心愿通过管道将 console.error() 传递给另一个程序。
如果您要记录利用流动(例如,跟踪流量或 API 调用),请不要应用 console.log(),而是应用像 Winston 或 Bunyan 这样的日志库。无关这两个库的具体比拟,请参阅 StrongLoop 博客文章 Comparing Winston and Bunyan Node.js Logging.
Handle exceptions properly
Node 应用程序在遇到未捕捉的异样时解体。不解决异样并采取适当的措施将使您的 Express 应用程序解体并下线。如果您遵循上面确保您的应用程序主动重新启动中的倡议,那么您的应用程序将从解体中复原。侥幸的是,Express 应用程序的启动工夫通常很短。尽管如此,您首先要防止解体,为此,您须要正确处理异样。
为确保您解决所有异样,请应用以下技术:
- try-catch
- promises
在深入研究这些主题之前,您应该对 Node/Express 错误处理有一个根本的理解:应用谬误优先回调,以及在中间件中流传谬误。Node 应用“谬误优先回调”约定从异步函数返回谬误,其中回调函数的第一个参数是谬误对象,后跟参数中的后果数据。要批示没有谬误,请将 null 作为第一个参数传递。回调函数必须相应地遵循谬误优先回调约定能力有意义地处理错误。而在 Express 中,最佳实际是应用 next() 函数通过中间件链流传谬误。
What not to do
您不应该做的一件事是侦听 uncaughtException 事件,当异样冒泡始终返回到事件循环时会收回该事件。为 uncaughtException 增加事件监听器将扭转遇到异样的过程的默认行为;只管有异样,该过程仍将持续运行。这听起来像是避免您的应用程序解体的好办法,但在未捕捉的异样之后持续运行应用程序是一种危险的做法,不倡议这样做,因为过程的状态变得不牢靠且不可预测。
此外,应用 uncaughtException 被官网认为是粗犷的。所以监听 uncaughtException 只是一个坏主意。这就是为什么咱们举荐多个过程和主管之类的货色:解体和重新启动通常是从谬误中复原的最牢靠办法。
咱们也不倡议应用 domains. 它通常不能解决问题,并且是不举荐应用的模块。
Use try-catch
Try-catch 是一种 JavaScript 语言构造,可用于捕捉同步代码中的异样。例如,应用 try-catch 来解决 JSON 解析谬误,如下所示。
应用诸如 JSHint 或 JSLint 之类的工具来帮忙您查找隐式异样,例如未定义变量上的援用谬误。
以下是应用 try-catch 解决潜在过程解体异样的示例。这个中间件函数承受一个名为“params”的查问字段参数,它是一个 JSON 对象。
app.get('/search', (req, res) => {
// Simulating async operation
setImmediate(() => {
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() 的异步代码块中的任何异样(显式和隐式)。只需将 .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})
当初所有异步和同步谬误都会流传到谬误中间件。
然而,有两个正告:
- 所有异步代码都必须返回承诺(发射器除外)。如果特定库不返回承诺,请应用 Bluebird.promisifyAll() 等辅助函数转换根底对象。
- 事件发射器(如流)依然会导致未捕捉的异样。因而,请确保正确处理错误事件;例如:
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() 函数是一个包装器,它捕捉被回绝的承诺并调用 next() 并将谬误作为第一个参数。
更多细节能够参考这篇博客:Asynchronous Error Handling in Express with Promises, Generators and ES7.
Set NODE_ENV to“production”
NODE_ENV 环境变量指定利用程序运行的环境(通常是开发环境或生产环境)。为了进步性能,您能够做的最简略的事件之一是将 NODE_ENV 设置为“production”。
将 NODE_ENV 设置为“production”使得 Express:
- 缓存视图模板。
- 缓存从 CSS 扩大生成的 CSS 文件。
- 生成不太具体的谬误音讯。
如果您须要编写特定于环境的代码,您能够应用 process.env.NODE_ENV 查看 NODE_ENV 的值。请留神,查看任何环境变量的值都会导致性能降落,因而应审慎进行。
在开发中,您通常在交互式 shell 中设置环境变量,例如应用 export 或 .bash_profile 文件。但一般来说,你不应该在生产服务器上这样做;相同,请应用您操作系统的初始化零碎(systemd 或 Upstart)。下一节提供了无关应用 init 零碎的更多详细信息,但设置 NODE_ENV 对性能十分重要(并且易于操作),因而在此处突出显示。
应用 Upstart,在您的作业文件中应用 env 关键字。例如:
# /etc/init/env.conf
env NODE_ENV=production
应用 systemd,在单元文件中应用 Environment 指令。例如:
# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production
Ensure your app automatically restarts
在生产中,您永远不心愿您的应用程序处于离线状态。这意味着您须要确保它在应用程序解体和服务器自身解体时重新启动。只管您心愿这两种状况都不会产生,但实际上您必须通过以下形式对这两种状况进行阐明:
- 应用过程管理器在解体时重新启动应用程序(和节点)。
- 应用操作系统提供的 init 零碎在操作系统解体时重新启动过程管理器。也能够在没有过程管理器的状况下应用 init 零碎。
如果遇到未捕捉的异样,节点应用程序就会解体。您须要做的最重要的事件是确保您的应用程序通过良好测试并解决所有异样。
但作为一种故障安全措施,应采纳一种机制来确保当您的应用程序解体时,它会主动重新启动。
Use a process manager
在开发中,您只需应用 node server.js 或相似的货色从命令行启动您的应用程序。然而在生产中这样做会导致劫难。如果应用程序解体,它将处于离线状态,直到您重新启动它。为确保您的应用程序在解体时重新启动,请应用过程管理器。流程管理器是应用程序的“容器”,可促成部署、提供高可用性并使您可能在运行时管理应用程序。
除了在应用程序解体时重新启动应用程序之外,过程管理器还能够让您:
- 深刻理解运行时性能和资源耗费。
- 动静批改设置以进步性能。
- 管制集群(StrongLoop PM 和 pm2)。
上面是三个比拟风行的过程管理器:
- StrongLoop Process Manager
- PM2
- Forever
无关三个流程管理器的一一性能比拟,请参阅 http://strong-pm.io/compare/。
应用这些过程管理器中的任何一个都足以让您的应用程序放弃失常运行,即便它不断解体。
Use an init system
下一层可靠性是确保您的应用程序在服务器重新启动时重新启动。因为各种起因,零碎仍可能呈现故障。为确保您的应用程序在服务器解体时重新启动,请应用操作系统内置的 init 零碎。目前应用的两个次要初始化零碎是 systemd 和 Upstart。
有两种办法能够在 Express 应用程序中应用 init 零碎:
- 在过程管理器中运行您的应用程序,并应用 init 零碎将过程管理器装置为服务。当应用程序解体时,过程管理器将重新启动您的应用程序,当操作系统重新启动时,init 零碎将重新启动过程管理器。这是举荐的办法。
- 间接应用 init 零碎运行您的应用程序(和 Node)。这有点简略,但您无奈取得应用过程管理器的额定劣势。
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
Run your app in a cluster
在多核零碎中,您能够通过启动一组过程来将 Node 应用程序的性能进步许多倍。一个集群运行应用程序的多个实例,现实状况下每个 CPU 内核上有一个实例,从而在实例之间调配负载和工作。
[图片]
重要提醒:因为应用程序实例作为独自的过程运行,因而它们不共享雷同的内存空间。也就是说,对象对于应用程序的每个实例都是本地的。因而,您无奈在利用程序代码中保护状态。然而,您能够应用像 Redis 这样的内存中数据存储来存储与会话相干的数据和状态。这个正告基本上实用于所有模式的程度扩大,无论是多过程集群还是多物理服务器。
在集群应用程序中,工作过程能够独自解体而不影响其余过程。除了性能劣势之外,故障隔离是运行利用过程集群的另一个起因。每当工作过程解体时,请始终确保记录该事件并应用 cluster.fork () 生成一个新过程。
Using 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 并将实例设置为要启动的工作程序数量来配置。
运行后,应用程序能够像这样缩放:
# Add 3 more workers
$ pm2 scale my-app +3
# Scale to a specific number of workers
$ pm2 scale my-app 2
Cache request results
在生产中进步性能的另一个策略是缓存申请的后果,这样您的应用程序就不会反复操作来反复解决雷同的申请。
应用 Varnish 或 Nginx 等缓存服务器(另请参阅 Nginx 缓存)能够大大提高应用程序的速度和性能。
Use a load balancer
无论应用程序如何优化,单个实例只能解决无限的负载和流量。扩大应用程序的一种办法是运行它的多个实例并通过负载均衡器调配流量。设置负载均衡器能够进步应用程序的性能和速度,并使其可能比单个实例扩大更多。
负载均衡器通常是一个 反向代理,用于协调进出多个应用程序实例和服务器的流量。您能够应用 Nginx 或 HAProxy 轻松地为您的应用程序设置负载均衡器。
通过负载平衡,您可能必须确保与特定会话 ID 关联的申请连贯到发动它们的过程。这称为亲缘会话或粘性会话,能够通过下面的倡议解决,应用诸如 Redis 之类的数据存储来存储会话数据(取决于您的应用程序)。
Use a reverse proxy
反向代理位于 Web 应用程序的后面,除了将申请定向到应用程序之外,还对申请执行反对操作。它能够处理错误页面、压缩、缓存、提供文件和负载平衡等。
将不须要应用程序状态常识的工作移交给反向代理能够开释 Express 来执行专门的应用程序工作。出于这个起因,倡议在生产中应用反向代理(如 Nginx 或 HAProxy)运行 Express。