当前位置: 首页 > Web前端 > JavaScript

使用Node.jsExpress的最佳实践

时间:2023-03-26 23:21:09 JavaScript

生产最佳实践:性能和可靠性本文讨论部署到生产中的Express应用程序的性能和可靠性最佳实践。这个话题显然属于“devops”的世界,它涵盖了传统的开发和运维。所以信息分为两部分:在你的代码中要做的事情(开发部分)使用gzip压缩不要使用同步函数正确记录和处理异常在你的环境/设置中要做什么(操作部分)设置NODE_ENV设置为“Production”以确保您的应用程序自动重启在集群中运行您的应用程序缓存请求结果使用负载均衡器使用反向代理使用gzip压缩Gzip压缩可以大大减少响应主体的大小,从而提高您的Web应用程序的速度。在您的Express应用程序中使用压缩进行gzip压缩。例如:varcompression=require('compression')varexpress=require('express')varapp=express()app.use(compression())对于生产中的高流量站点,实现压缩的最佳方式是它是在反向代理级别实现的。在这种情况下,您不需要使用压缩中间件。有关在Nginx中启用gzip压缩的详细信息,请参阅Nginx文档中的模块ngx_http_gzip_module。不要使用同步函数同步函数和方法会阻塞执行过程,直到它们返回。对同步函数的单个调用可能会在几微秒或几毫秒内返回,但在高流量网站中,这些调用会增加并降低应用程序的性能。避免在生产中使用它们。尽管Node和许多模块同时提供其功能的同步和异步版本,但在生产中始终使用异步版本。唯一可以证明同步功能是合理的是在初始启动时。如果您使用的是Node.js4.0+或io.js2.1.0+,您可以使用--trace-sync-io命令行标志在您的应用程序使用同步API时打印警告和堆栈跟踪。当然,您不想在生产中使用它,而是要确保您的代码已准备好生产。正确记录日志通常,从您的应用程序中记录日志有两个原因:调试和记录应用程序活动(本质上,其他一切)。使用console.log()或console.error()将日志消息打印到终端是开发中的常见做法。但是当目标是终端或文件时,这些函数是同步的,因此它们不适合生产,除非您将输出通过管道传输到另一个程序。如果您为了调试目的而记录日志,那么不要使用console.log(),而是使用像debug这样的特殊调试模块。此模块使您能够使用DEBUG环境变量来控制将哪些调试消息发送到console.error()(如果有)。为了让您的应用程序完全异步,您仍然希望通过管道将console.error()传递给另一个程序。如果您正在记录应用程序活动(例如,跟踪流量或API调用),请不要使用console.log(),而应使用像Winston或Bunyan这样的日志库。有关这两个库的详细比较,请参阅StrongLoop博客文章比较Winston和BunyanNode.js日志记录。正确处理异常节点应用程序在未捕获的异常时崩溃。不处理异常并采取适当的措施将使您的Express应用程序崩溃并离线。如果您遵循下面确保您的应用程序自动重新启动中的建议,您的应用程序将从崩溃中恢复。幸运的是,Express应用程序的启动时间通常很短。不过,您首先要避免崩溃,为此,您需要正确处理异常。为确保处理所有异常,请使用以下技术:try-catchpromises在深入研究这些主题之前,您应该对Node/Express错误处理有一个基本的了解:使用错误优先回调,以及在中间件中传播错误。Node使用“错误优先回调”约定从异步函数返回错误,其中回调函数的第一个参数是错误对象,后面是参数中的结果数据。要指示没有错误,请将null作为第一个参数传递。回调函数必须相应地遵循错误优先的回调约定,以便有意义地处理错误。然而,在Express中,最佳实践是使用next()函数通过中间件链传播错误。不该做什么你不应该做的一件事是监听uncaughtException事件,当异常冒泡回到事件循环时会发出该事件。为uncaughtException添加事件侦听器将改变进程遇到异常时的默认行为;尽管出现异常,该过程仍将继续运行。这听起来像是防止应用程序崩溃的好方法,但在出现未捕获的异常后继续运行应用程序是一种危险的做法,不推荐这样做,因为进程的状态变得不可靠且不可预测。此外,使用uncaughtException被官方认为是粗鲁的。所以监听uncaughtException只是一个坏主意。这就是为什么我们推荐多进程和主管之类的东西:崩溃和重新启动通常是从错误中恢复的最可靠方法。我们也不建议使用域。它通常不能解决问题,是一个已弃用的模块。使用try-catchTry-catch是一种JavaScript语言结构,可用于捕获同步代码中的异常。例如,使用try-catch来处理JSON解析错误,如下所示。使用JSHint或JSLint等工具来帮助您查找隐式异常,例如未定义变量的引用错误。下面是一个使用try-catch处理潜在的进程崩溃异常的例子。这个中间件函数接受一个名为“params”的查询字段参数,它是一个JSON对象。app.get('/search',(req,res)=>{//模拟异步操作setImmediate(()=>{varjsonStr=req.query.paramstry{varjsonObj=JSON.parse(jsonStr)res.send('Success')}catch(e){res.status(400).send('InvalidJSONstring')}})})但是,try-catch仅适用于同步代码。因为Node平台主要是异步的(尤其是在生产环境中),try-catch不会捕获很多异常。使用promisesPromise将处理使用then()的异步代码块中的任何异常(显式和隐式)。只需将.catch(next)添加到承诺链的末尾即可。例如:app.get('/',(req,res,next)=>{//做一些同步的事情queryDb().then((data)=>makeCsv(data))//处理数据.then((csv)=>{/*handlecsv*/}).catch(next)})app.use((err,req,res,next)=>{//handleerror})现在所有的异步和同步错误都是传播到错误中间件。但是,有两个注意事项:所有异步代码都必须返回承诺(发射器除外)。如果特定库不返回承诺,请使用Bluebird.promisifyAll()等辅助函数来转换底层对象。事件发射器(如流)仍然会导致未捕获的异常。所以一定要正确处理错误事件;例如:constwrap=fn=>(...args)=>fn(...args).catch(args[2])app.get('/',wrap(async(req,res,next)=>{constcompany=awaitgetCompanyById(req.query.id)conststream=getLogoStreamById(company.id)stream.on('error',next).pipe(res)}))wrap()函数是一个包装器,它捕获被拒绝的承诺并调用next()并将错误作为第一个参数。更多细节请参考这篇博客:AsynchronousErrorHandlinginExpresswithPromises,GeneratorsandES7.SetNODE_ENVto"production"NODE_ENV环境变量指定了应用程序运行的环境(通常是开发环境或生产环境).要提高性能,您可以做的最简单的事情之一就是将NODE_ENV设置为“production”。将NODE_ENV设置为“production”会生成Express:cache视图模板。缓存从CSS扩展生成的CSS文件。生成不太详细的错误消息。如果您需要编写特定于环境的代码,您可以使用process.env.NODE_ENV检查NODE_ENV的值。请注意,检查任何环境变量的值都会导致性能下降,因此应谨慎进行。在开发中,您通常在交互式shell中设置环境变量,例如使用导出或.bash_profile文件。但一般来说,您不应该在生产服务器上这样做;相反,请使用操作系统的初始化系统(systemd或Upstart)。下一节将提供有关使用init系统的更多详细信息,但设置NODE_ENV对于性能(以及易于操作)很重要,因此在此处突出显示。对于Upstart,在您的作业文件中使用env关键字。例如:#/etc/init/env.confenvNODE_ENV=production要使用systemd,请在单元文件中使用环境指令。例如:#/etc/systemd/system/myservice.serviceEnvironment=NODE_ENV=productionEnsureyourappautomaticallyrestarts在生产中,你永远不希望你的应用程序离线。这意味着如果应用程序崩溃并且服务器本身崩溃,您需要确保它重新启动。尽管您不希望这两种情况发生,但实际上您必须通过使用进程管理器在崩溃时重新启动您的应用程序(和节点)来解决这两种情况。使用操作系统提供的init系统在操作系统崩溃时重新启动进程管理器。也可以在没有进程管理器的情况下使用init系统。如果遇到未捕获的异常,节点应用程序将崩溃。您需要做的最重要的事情是确保您的应用程序经过良好测试并处理所有异常。但作为一种故障安全措施,包括一种机制以确保当您的应用程序崩溃时,它会自动重新启动。使用进程管理器在开发中,您只需使用nodeserver.js或类似的东西从命令行启动您的应用程序。但是在生产中这样做可能会导致灾难。如果应用程序崩溃,它将处于离线状态,直到您重新启动它。为确保您的应用程序在崩溃时重新启动,请使用进程管理器。流程管理器是应用程序的“容器”,可促进部署、提供高可用性并使您能够在运行时管理应用程序。除了在应用程序崩溃时重新启动应用程序之外,ProcessManager还可以让您:深入了解运行时性能和资源消耗。动态修改设置以提高性能。控制集群(StrongLoopPM和pm2)。以下是三个更流行的流程管理器:StrongLoopProcessManagerPM2Forever有关三个流程管理器的逐个功能比较,请参阅http://strong-pm.io/compare/。使用这些进程管理器中的任何一个都足以让您的应用程序保持运行,即使它有时会崩溃。使用init系统下一个可靠性级别是确保您的应用程序在服务器重新启动时重新启动。系统仍然可能由于各种原因而失败。为确保您的应用程序在服务器崩溃时重新启动,请使用操作系统内置的init系统。目前使用的两个主要初始化系统是systemd和Upstart。有两种方法可以在Express应用程序中使用init系统:在进程管理器中运行您的应用程序,并使用init系统将进程管理器安装为服务。进程管理器会在应用程序崩溃时重启你的应用程序,而init系统会在操作系统重启时重启进程管理器。这是推荐的方法。直接使用init系统运行您的应用程序(和Node)。它更容易一些,但是您没有获得使用流程管理器的额外优势。SystemdSystemd是一个Linux系统和服务管理器。大多数主要的Linux发行版都使用systemd作为它们的默认初始化系统。systemd服务配置文件称为单元文件,文件名以.service结尾。这是一个用于直接管理Node应用程序的示例单元文件。为您的系统和应用程序替换尖括号中的值:[Unit]Description=[Service]Type=simpleExecStart=/usr/local/bin/nodeWorkingDirectory=User=nobodyGroup=nogroup#环境变量:Environment=NODE_ENV=production#AllowmanyincomingconnectionsLimitNOFILE=infinity#AllowcoredumpsfordebuggingLimitCORE=infinityStandardInput=nullStandardOutput=syslogStandardError=BsyslogRestart=always]WedInstymuser.targetRunyour集群中的应用程序在多核系统上,您可以通过启动进程集群来多次提高Node应用程序的性能。一个集群运行一个应用程序的多个实例,理想情况下每个CPU核心一个实例,从而在这些实例之间分配负载和任务。[图片]重要说明:由于应用程序实例作为单独的进程运行,因此它们不共享相同的内存空间。也就是说,对象对于应用程序的每个实例都是本地的。因此,您无法在应用程序代码中维护状态。但是,您可以使用像Redis这样的内存数据存储来存储与会话相关的数据和状态。这个警告基本上适用于所有形式的水平扩展,无论是多进程集群还是多物理服务器。在集群应用程序中,工作进程可以单独崩溃而不影响其余进程。除了性能优势之外,故障隔离是运行应用程序进程集群的另一个原因。每当工作进程崩溃时,请始终确保记录该事件并使用cluster.fork()生成新进程。使用PM2如果您使用PM2部署应用程序,则无需修改应用程序代码即可利用集群。您应该首先确保您的应用程序是无状态的,这意味着没有本地数据存储在进程中(例如会话、websocket连接等)。使用PM2运行应用程序时,您可以启用集群模式以在具有您选择的实例数量的集群中运行它,例如匹配机器上可用的CPU数量。您可以使用pm2命令行工具在不停止应用程序的情况下手动更改集群中的进程数。要启用集群模式,请像这样启动您的应用程序:#启动4个工作进程$pm2startnpm--namemy-app-i4--start#自动检测可用CPU的数量并启动那么多工作进程$pm2startnpm--namemy-app-imax--start这也可以在PM2进程文件(ecosystem.config.js或类似文件)中完成,方法是将exec_mode设置为cluster并将instances设置为要开始配置的worker数量。运行后,应用程序可以像这样扩展:#再添加3个worker$pm2scalemy-app+3#扩展到特定数量的worker$pm2scalemy-app2缓存请求结果在生产中提高性能的另一种策略是缓存请求的结果,这样您的应用程序就不会重复操作来一遍又一遍地处理相同的请求。使用诸如Varnish或Nginx之类的缓存服务器(另请参阅Nginx缓存)可以极大地提高应用程序的速度和性能。使用负载均衡器无论应用程序如何优化,单个实例都只能处理有限的负载和流量。扩展应用程序的一种方法是运行它的多个实例并通过负载均衡器分配流量。设置负载均衡器可以提高应用程序的性能和速度,并允许它比单个实例扩展更多。负载均衡器通常是一个反向代理,用于协调进出多个应用程序实例和服务器的流量。您可以使用Nginx或HAProxy轻松地为您的应用程序设置负载均衡器。使用负载平衡,您可能必须确保与特定会话ID关联的请求连接到发起它们的进程。这称为关联会话或粘性会话,可以按照上述建议解决,使用Redis等数据存储来存储会话数据(取决于您的应用程序)。使用反向代理反向代理位于Web应用程序之前,除了将请求定向到应用程序之外,还对请求执行支持操作。它处理错误页面、压缩、缓存、服务文件、负载平衡等。将不需要应用程序状态知识的任务卸载到反向代理可以释放Express来执行专门的应用程序任务。因此,建议在生产环境中使用Nginx或HAProxy等反向代理运行Express。