5.生产最佳实践:性能和可靠性 | 5. Production best practices: performance and reliability
产品最佳实践:性能和可靠性
概述
本文讨论部署到生产环境中的Express应用程序的性能和可靠性最佳实践。
这个话题显然属于“devops”世界,跨越传统的发展和运营。因此,信息分为两部分:
- 你的代码要做的事情(开发部分):
你的代码需要做的事情
以下是您可以在代码中执行以提高应用程序性能的一些事情:
- 使用gzip压缩
使用gzip压缩
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。
不要使用同步功能
同步函数和方法将执行过程捆绑在一起,直到它们返回。对同步功能的单个调用可能会在几微秒或几毫秒内返回,但在高流量网站中,这些调用会加起来并降低应用程序的性能。避免在生产中使用它们。
尽管节点和许多模块提供了其功能的同步和异步版本,但始终在生产中使用异步版本。唯一一次可以证明同步功能是初次启动时。
如果您使用的是Node.js 4.0+或io.js 2.1.0+,则--trace-sync-io
只要应用程序使用同步API ,就可以使用命令行标志来打印警告和堆栈跟踪。当然,你不想在生产中使用它,而是为了确保你的代码已经准备好用于生产。有关更多信息,请参阅节点命令行选项文档。
记录正确
一般来说,从您的应用程序进行日志记录有两个原因:用于调试和记录应用程序活动(实质上是其他所有内容)。开发中使用console.log()
或console.error()
打印日志消息到终端是常见的做法。但是,当目的地是终端或文件时,这些功能是同步的,所以它们不适合生产,除非将输出管道输送到另一个程序。
用于调试
如果你登录用于调试的目的,而不是再使用console.log()
,使用特殊的调试模块像调试。此模块使您能够使用DEBUG环境变量来控制发送给哪些调试消息console.err()
(如果有)。要保持你的应用程序纯粹是异步的,你仍然想要管道console.err()
到另一个程序。但是,那么你不是真的要在生产中进行调试,对吗?
对于应用活动
如果您记录应用程序活动(例如跟踪流量或API调用),而不是使用console.log()
,请使用Winston或Bunyan等日志库。有关这两个库的详细比较,请参阅StrongLoop博客文章比较Winston和Bunyan Node.js日志记录。
正确处理异常
节点应用遇到未捕获的异常时会崩溃。未处理例外情况并采取适当的措施会使您的Express应用程序崩溃并脱机。如果您按照以下建议确保您的应用在下面自动重新启动,那么您的应用将从崩溃中恢复。幸运的是,Express应用程序的启动时间通常很短。尽管如此,你首先要避免崩溃,要做到这一点,你需要正确处理异常。
为确保处理所有异常,请使用以下技术:
- 使用try-catch
在深入探讨这些主题之前,您应该对Node / Express错误处理有一个基本的了解:使用错误优先回调以及在中间件中传播错误。节点使用“错误优先回调”约定来返回来自异步函数的错误,其中回调函数的第一个参数是错误对象,随后是后续参数中的结果数据。要指示没有错误,请将null作为第一个参数。回调函数必须相应地遵循错误优先回调约定来有意义地处理错误。在Express中,最佳做法是使用next()函数通过中间件链传播错误。
什么不该做
有一件事你应该不会
做的是侦听uncaughtException
事件,发出异常时气泡回到事件循环的所有道路。添加事件侦听器uncaughtException
将改变遇到异常的进程的默认行为; 尽管例外,该过程仍将继续运行。这可能听起来像是一种防止应用程序崩溃的好方法,但在未捕获的异常之后继续运行应用程序是一种危险的做法,并且不推荐使用,因为进程的状态变得不可靠且不可预知。
此外,使用uncaughtException
被官方认定为原油。所以倾听uncaughtException
是一个坏主意。这就是为什么我们推荐像多个进程和主管一样的东西:崩溃和重启通常是从错误中恢复的最可靠的方法。
我们也不建议使用域名。它通常不能解决问题并且是不推荐的模块。
使用try-catch
Try-catch是一种JavaScript语言结构,可用于捕获同步代码中的异常。例如,使用try-catch来处理JSON解析错误,如下所示。
使用的工具,如JSHint或JSLint的帮你找到像隐含例外未定义变量引用错误。
以下是使用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不会捕获很多异常。
使用承诺
Promises将处理使用的异步代码块中的任何异常(显式和隐式)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
})
现在,异步和同步的所有错误都会传播到错误中间件。
但是,有两个警告:
- 您的所有异步代码都必须返回promise(发射器除外)。如果某个特定的库没有返回promise,请使用Bluebird.promisifyAll()等助手函数转换基础对象。
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)
}))
有关使用承诺进行错误处理的更多信息,请参阅:
在你的环境/设置中要做的事情
您可以在系统环境中执行以下操作来提高应用的性能:
- 将NODE_ENV设置为“生产”
将NODE_ENV设置为“生产”
NODE_ENV环境变量指定应用程序运行的环境(通常是开发或生产)。您可以通过将NODE_ENV设置为“生产”来提高性能,这是最简单的事情之一。
将NODE_ENV设置为“生产”使Express:
- 缓存视图模板。
测试表明,这样做可以将应用程序性能提高三倍!
如果您需要编写特定于环境的代码,则可以使用以下命令检查NODE_ENV的值process.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 server.js
或类似的东西开始您的应用程序。但在生产中这样做是一种灾难。如果应用程序崩溃,它将脱机,直到您重新启动它。要确保您的应用在崩溃时重新启动,请使用进程管理器。流程管理器是便于部署,提供高可用性并使您能够在运行时管理应用程序的应用程序的“容器”。
除了在应用程序崩溃时重新启动应用程序外,进程管理器还可以使您:
- 深入了解运行时性能和资源消耗。
最受欢迎的Node进程管理器如下所示:
有关三个进程管理器的逐个功能比较,请参阅http://strong-pm.io/compare/。有关这三者的更详细介绍,请参阅Express应用程序的流程管理器。
使用这些流程管理器中的任何一个都足以让您的应用程序保持运行,即使它不时崩溃。
但是,StrongLoop PM具有许多专门针对生产部署的功能。您可以使用它和相关的StrongLoop工具来:
- 在本地构建和打包应用,然后将其安全地部署到生产系统。
如下所述,当您使用init系统将StrongLoop PM作为操作系统服务安装时,系统将在系统重新启动时自动重新启动。因此,它将使您的应用程序进程和群集永远存活。
使用init系统
下一层可靠性是确保您的应用程序在服务器重新启动时重新启动。由于各种原因,系统仍然可能停机。要确保您的应用程序在服务器崩溃时重新启动,请使用内置于您的操作系统中的init系统。目前使用的两个主要init系统是systemd和Upstart。
There are two ways to use init systems with your Express app:
- 在进程管理器中运行您的应用程序,并使用init系统将进程管理器作为服务安装。当应用程序崩溃时,进程管理器将重新启动您的应用程序,并且当系统重新启动时,init系统将重新启动进程管理器。这是推荐的方法。
Systemd
Systemd是一个Linux系统和服务管理器。大多数主要的Linux发行版都采用systemd作为其默认的init系统。
系统服务配置文件称为单元文件
,文件名以.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安装为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
。
使用以下内容创建一个名为myapp.conf
at 的文件/etc/init/
(将粗体文本替换为系统和应用程序的值):
# 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还允许您使用这些命令:
start myapp
– Start the app
有关Upstart的更多信息,请参阅Upstart Intro,Cookbook和Best Practices。
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核心上有一个实例,从而在实例之间分配负载和任务。
重要提示:由于应用程序实例作为单独的进程运行,因此它们不共享相同的内存空间。也就是说,对象是每个应用程序实例的本地对象。因此,您无法在应用程序代码中维护状态。但是,您可以使用像Redis这样的内存数据存储来存储会话相关的数据和状态。此警告适用于基本上所有形式的水平缩放,无论是使用多个进程聚集还是多台物理服务器。
在集群应用程序中,工作进程可能会单独崩溃而不会影响其他进程。除了性能优势之外,故障隔离是运行一组应用程序进程的另一个原因。每当工作进程崩溃时,务必确保使用cluster.fork()记录事件并产生一个新进程。
使用节点的集群模块
集群可以通过节点的集群模块实现。这使主进程能够派生工作进程并在工作进程间分配传入连接。但是,不是直接使用这个模块,而是使用其中的许多工具之一自动完成工作。例如节点-pm或群集服务。
使用StrongLoop PM
如果您将应用程序部署到StrongLoop Process Manager(PM),那么您可以利用群集而不
修改应用程序代码。
当StrongLoop进程管理器(PM)运行应用程序时,它会自动在具有与系统上的CPU核心数量相等的工作人员数量的群集中运行。您可以使用slc命令行工具手动更改集群中工作进程的数量,而无需停止应用程序。
例如,假设您已将应用程序部署到prod.foo.com,并且StrongLoop PM正在侦听端口8701(默认值),然后使用slc将群集大小设置为八:
$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8
有关使用StrongLoop PM进行群集的更多信息,请参阅StrongLoop文档中的群集。
缓存请求结果
提高生产性能的另一个策略是缓存请求的结果,以便您的应用不会重复该操作来重复提供相同的请求。
使用像Varnish或Nginx这样的缓存服务器(另请参阅Nginx缓存)可以大大提高应用程序的速度和性能。
使用负载平衡器
无论应用程序的优化程度如何,单个实例只能处理有限数量的负载和流量。扩展应用程序的一种方法是运行它的多个实例并通过负载均衡器分配流量。设置负载平衡器可以提高应用程序的性能和速度,并使其能够扩展比单个实例更多的功能。
负载平衡器通常是一种逆向代理,用于协调来往于多个应用程序实例和服务器的流量。您可以使用Nginx或HAProxy轻松地为您的应用程序设置负载平衡器。
通过负载平衡,您可能必须确保与特定会话ID关联的请求连接到发起它们的进程。这被称为会话亲缘关系
或粘性会话
,并且可以通过上面的建议来解决,以使用数据存储(如Redis)作为会话数据(取决于您的应用程序)。有关讨论,请参阅使用多个节点。
使用反向代理
反向代理位于Web应用程序的前端,并对请求执行支持操作,而不是将请求发送到应用程序。它可以处理错误页面,压缩,缓存,服务文件和负载平衡等等。
将不需要应用程序状态知识的任务移交给反向代理可以释放Express以执行专门的应用程序任务。出于这个原因,建议在生产中使用Nginx或HAProxy之类的反向代理。