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

news/2025/6/19 17:15:18

前言

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

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

概览

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

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

  • 需要对代码做的事,即开发部分。

  • 需要对环境做的事,即运维部分,

需要对代码做的事

为了提升你应用的性能,你可以通过:

  • 使用gzip压缩

  • 禁止使用同步方法

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

  • 适当地打印日志

  • 合理地处理异常

使用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环境变量来控制具体哪些信息会被打印。虽然这些方法也是同步的,但你一定不会在生产环境下进行调试吧?

为了操作记录

如果你正在为了记录应用的活动而打印日志。那么你可以使用一些日志库如winston或Bunyan,来替代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 operationsetImmediate(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 stuffqueryDb().then(function (data) {// handle datareturn 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.confenv 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=infinityStandardInput=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-pm或cluser-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/


https://dhexx.cn/news/show-4081244.html

相关文章

android 发送彩信监听,在Android中发送短信和彩信,监听短信并显示

发送短信&#xff1a;String body"this is sms demo";Intent mmsintent new Intent(Intent.ACTION_SENDTO, Uri.fromParts("smsto", number, null));mmsintent.putExtra(Messaging.KEY_ACTION_SENDTO_MESSAGE_BODY, body);mmsintent.putExtra(Messaging.K…

自定义缩放转场动画的实现

现在的APP有很多酷炫的动画,看了心痒痒地想加点动画到自己的APP上,增加一些交互效果.现在很常见的动画就是转场动画,特地做了个Demo,实现了4个转场动画,首先先来介绍一下转场动画的实现. 1. 转场动画 其实iOS对转场动画的支持非常好,基本上只要实现几个协议就行了,把四个协议弄…

android 仿qq侧滑抽屉,IOS中Swift仿QQ最新版抽屉侧滑和弹框视图

导读简单用Swift写了一个抽屉效果,可以直接使用并且简单;很多软件都运了抽屉效果&#xff0c;比如qq的左抽屉&#xff0c;英雄联盟,滴滴打车&#xff0c;和uber等等都运用了抽屉;效果iOS抽屉式结构实现分析主要是在控制器的View上添加了两个View,一个左侧leftView和一个mainVie…

GoogleDoc - 温故而知新Activity生命周期方法

3.创建Activity一般人所不知道的地方1&#xff09;Activity里的各个生命周期的方法一般执行什么代码 》》onCreate() method shows some code that performs some fundamental setup for the activity, such as declaring the user interface (defined in an XML layout file…

xamarin android tv,详解xamarin Android 实现ListView万能适配器

详解xamarin Android 实现ListView万能适配器早些时候接触xamarin Android 的列表&#xff0c;写了很多ListView的Adapter&#xff0c;建一个ListView就写一个Adapter,每一个Adapter里面还有去写一个ViewHolder的类来优化&#xff0c;自从看了hongyang博客的listview万能适配器…

T420测试下可用:解决VMWARE 虚拟机安装64位系统“此主机支持 Intel VT-x,但 Intel VT-x 处于禁用状...

Q:解决VMWARE 虚拟机安装64位系统“此主机支持 Intel VT-x&#xff0c;但 Intel VT-x 处于禁用状A:解决方法:是否支持虚拟化技术除了看CPU支持外&#xff0c;您还需要进入到BIOS当中查看是否有开启虚拟化的相关选项&#xff0c;该位置在 Security --Virtualization --Intel Vi…

html5拖动鼠标直线,html5的鼠标拖拽

鼠标拖拽Title.one {width:200px;height:200px;border:1px solid blue;margin:10px;}.two {width:50px;height:50px;border:1px solid red;margin:10px;}window.onload function() {//two为拖拽对象&#xff0c;one为目标对象var one document.getElementById("one"…

Xcode 设置文件生成时的模板

1. 目的 设置 Xcode 生成的文件的格式&#xff0c;如姓名、公司等。 2. 步骤 2.1. 找到文件 step 1. 右键Xcode图标 step 2. 显示包内容 step 3. 找到目录 /Contents/Developer/Library/Xcode/Templates/File Templates/Source/Objective-C File.xctemplate/Empty File/___FILE…

linux基于索引节点共享的命令是?两种共享有哪些区别?',Linux 第5章课后习题答案...

Linux思考题51.fork()与clone()二者之间得区别就是什么&#xff1f;答:fork创建一个进程时,子进程只就是完全复制父进程得资源,复制出来得子进程有自己得task_struct结构与pid,但却复制父进程其它所有得资源。通过fork创建子进程,需要将上面描述得每种资源都复制一个副本。fork…

html结算页面代码,结账页面.html

&#xfeff;结账页面$axure.utils.getTransparentGifPath function() { return resources/images/transparent.gif; };$axure.utils.getOtherPath function() { return resources/Other.html; };$axure.utils.getReloadPath function() { return resources/reload.html; };…