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

如何创建高性能、可扩展的Node.js应用程序?

时间:2023-04-02 21:14:40 HTML

在这篇文章中,我们将介绍一些开发Node.jsWeb应用程序的最佳实践,重点关注效率和性能,以用更少的资源获得最佳结果。增加Web应用程序吞吐量的一种方法是扩展它,多次实例化它以平衡多个实例之间的传入连接在一台机器上横向扩展Node.js应用程序。在强制性规则中,有一些好的实践可以用来解决这些问题,比如拆分API和worker进程,采用优先级队列,管理cron进程等周期性作业,扩展到N进程/机器,这不需要运行N次。水平扩展Node.js应用程序水平扩展是复制应用程序实例来管理大量传入连接。此操作可以在单个多核机器上执行,也可以跨不同机器执行。纵向扩展是为了提高单机的性能,不涉及代码上的具体工作。同一台机器上的多个进程提高应用程序吞吐量的常用方法是为机器的每个核心生成一个进程。通过这种方式,Node.js中已经高效的请求“并发”管理(请参阅“事件驱动的非阻塞I/O”)可以成倍增加并并行化。产生大量大于内核数量的进程可能不好,因为在低级别上,操作系统可能会在这些进程之间平衡CPU时间。扩展单个机器有不同的策略,但共同的概念是在同一个端口上运行多个进程,并使用某种内部负载平衡来在所有进程/核心之间分配传入连接。下面描述的策略是标准的Node.js集群模式以及自动的、更高级别的PM2集群功能。原生集群模式原生Node.js集群模块是在单台机器上扩展Node应用程序的基本方式(参见https://Node.js.org/api/clust…)。您的进程的一个实例(称为“master”)负责生成其他子进程(称为“worker”)的实例,每个运行您的应用程序的核心都有一个实例。传入连接以循环策略分配给所有工作进程,在同一端口上公开服务。这种方法的主要缺点是必须在代码内部管理主进程和工作进程之间的差异,通常使用经典的if-else块,不能轻易修改以包含动态数量的进程。以下示例来自官方文档:constcluster=require('cluster');consthttp=require('http');constnumCPUs=require('os').cpus().length;if(cluster.isMaster){console.log(`Master${process.pid}正在运行`);//分叉工人。for(leti=0;i{console.log(`worker${worker.process.pid}died`);});}else{//Worker可以共享任何TCP连接//在这种情况下它是一个HTTP服务器http.createServer((req,res)=>{res.writeHead(200);res.end('helloworld\n');}).listen(8000);console.log(`Worker${process.pid}started`);}PM2集群模式不用担心集群模块。PM2守护进程将承担“主”进程的角色,它将派生应用程序的N个进程作为工作进程,循环平衡。使用这种方法,您只需要像单核使用一样编写您的应用程序(稍后我们将讨论其中的一些注意事项),而PM2将专注于多核部分。以集群模式启动您的应用程序后,您可以使用“pm2scale”调整动态实例的数量,并执行“0秒停机”重新加载,其中进程重新链接,以便始终至少有一个在线过程。在生产中运行节点时,如果您的进程像您应该考虑的许多其他有用的事情一样崩溃,作为进程管理器的PM2将负责重新启动您的进程。如果您需要进一步扩展,您可能需要部署更多机器。具有网络负载平衡的多台机器跨多台机器扩展的主要概念类似于跨多核扩展,多台机器,每台机器运行一个或多个进程,并将流量重定向到每个机器均衡器。一旦将请求发送到特定节点,刚才提到的内部平衡器就会将该流量发送到特定进程。网络平衡器可以以不同的方式部署。如果使用AWS来配置您的基础设施,一个不错的选择是使用托管负载均衡器,如ELB(弹性负载均衡器),因为它支持自动缩放等有用的功能,并且易于设置。但是如果你想用传统的方式来做,你可以自己部署一台机器,用NGINX设置一个均衡器。指向上游的反向代理的配置对于此任务非常简单。这是一个示例配置:http{upstreammyapp1{serversrv1.example.com;服务器srv2.example.com;服务器srv3.example.com;}服务器{听80;位置/{proxy_passhttp://myapp1;}}}这样,负载均衡器将成为您的应用程序暴露于外界的唯一入口点。如果您担心它会成为基础设施的单点故障,您可以部署多个负载均衡器指向同一台服务器。为了在您的平衡器之间分配流量(每个都有自己的IP地址),您可以将多个DNS“A”记录添加到您的主域,以便DNS解析器将在您的平衡器之间分配流量,每次解析到不同的IP地址.这样,也可以在负载均衡器上实现冗余。我们在这里看到的是如何在不同级别扩展Node.js应用程序以从您的基础设施(从单节点到多节点和多平衡器)中获得尽可能高的性能,但要小心:如果你想使用你的应用程序在多进程环境中,你必须做好准备,否则你会遇到一些问题和意想不到的行为。现在让我们谈谈在扩大流程时必须考虑的一些方面,以避免出现不良行为。让Node.js应用程序准备好扩展从数据库中分离应用程序实例首先不是代码问题,而是您的基础设施问题。如果您希望您的应用程序跨不同主机扩展,您必须将数据库部署在单独的机器上,以便您可以根据需要自由复制应用程序机器。在同一台机器上为开发目的部署应用程序和数据库可能很便宜,但绝对不推荐用于生产环境,因为应用程序和数据库必须能够独立扩展。这同样适用于像Redis这样的内存数据库。无状态如果您生成应用程序的多个实例,每个进程都有自己的内存空间。这意味着即使在一台机器上运行,当你将一些值存储在全局变量中,或者更常见的是内存中的会话中,如果平衡器在下一个请求期间将你重定向到另一个进程,那么你将无法找到它那里。这适用于会话数据和内部值,例如任何类型的应用程序范围的设置。对于可以在运行时更改的设置或配置,解决方案是将它们存储在外部数据库(存储或内存中),以便所有进程都可以访问它们。使用JWT身份验证的无状态身份验证是开发无状态应用程序时首先要考虑的主题之一。如果会话存储在内存中,它们将作用于这个单一进程。要正常工作,网络负载均衡器应配置为始终将同一用户重定向到同一台机器,并将本地用户重定向到同一进程(粘性会话)。此问题的一个简单解决方案是将会话的存储策略设置为任何形式的持久性,例如将它们存储在DB而不是RAM中。但是,如果您的应用程序在每次请求时都检查会话数据,那么每次API调用都会进行磁盘读写操作(I/O),从性能的角度来看这绝对不是一件好事。更好更快的解决方案(如果您的身份验证框架支持它)是将会话存储在像Redis这样的内存数据库中。Redis实例通常位于应用程序实例之外,例如数据库实例,但在内存中工作使其速度更快。无论如何,随着并发会话数量的增加,将会话存储在RAM中需要更多内存。如需更有效的无状态身份验证方法,请查看JSONWebTokens。JWT背后的想法很简单:当用户登录时,服务器生成一个令牌,它本质上是一个包含有效负载的JSON对象的base64编码,加上通过使用服务器拥有的密钥对有效负载进行签名而获得的哈希值。有效负载可以包含用于对用户进行身份验证和授权的数据,例如用户ID及其关联的ACL角色。令牌被发送回客户端并由其用于对每个API请求进行身份验证。当服务器处理传入请求时,它会获取令牌的有效负载并使用其密钥重新创建签名。如果两个签名匹配,则可以认为有效负载有效且未更改,并且可以识别用户。请务必记住,JWT不提供任何形式的加密。有效负载仅经过base64编码并以明文形式发送,因此如果您需要隐藏内容,则必须使用SSL。jwt.io借用的以下模式还原了身份验证过程:在身份验证期间,服务器不需要访问存储在某处的会话数据,因此每个请求都可以由不同的进程或机器以非常有效的方式处理。RAM中没有数据,也不需要执行存储I/O,因此这种方法在扩展时很有用。在S3上使用多台机器进行存储时,无法将用户生成的资产直接保存在文件系统上,因为这些文件只能由该服务器本地的进程访问。解决方案是将所有内容存储在外部服务上,也许是像AmazonS3这样的专用服务上,并且只在数据库中保留指向该资源的绝对URL。然后每个进程/机器都可以以相同的方式访问该资源。使用官方AWSSDKforNode.js非常简单,可以轻松地将服务集成到您的应用程序中。S3非常便宜并为此目的进行了优化。即使您的应用程序不是多进程的,这也是一个不错的选择。正确配置WebSockets如果您的应用程序使用WebSockets进行客户端之间或客户端与服务器之间的实时交互,您需要链接后端实例,以便广播消息或消息在连接到不同节点的客户端之间正确传播。Socket.io库为此提供了一个特殊的适配器,称为socket.io-redis,它允许您使用Redis发布-订阅功能链接服务器实例。为了使用多节点socket.io环境,还需要强制协议为“websockets”,因为长轮询需要粘性会话才能工作。这些也是单节点环境的好例子。其他提高效率和性能的良好实践接下来,我们将介绍一些可以进一步提高效率和性能的额外实践。Web和工作进程您可能知道,Node.js实际上是单线程的,因此进程的单个实例一次只能执行一个操作。在Web应用程序的生命周期中,会执行许多不同的任务:管理API调用、读取/写入数据库、与外部Web服务通信、执行某种不可避免的CPU密集型工作等。当您进行异步编程时,委托对响应API调用的同一进程执行所有这些操作可能是一种非常低效的方法。一种常见的模式是基于构成应用程序的两种不同类型的进程之间的职责分离,通常是Web进程和工作进程。Web进程主要用于管理传入的网络调用并尽快发送它们。每当需要执行非阻塞任务时,例如发送电子邮件/通知、写入日志、执行触发操作,因此不需要响应API调用,Web进程将操作委托给工作进程。Web和工作进程之间的通信可以通过不同的方式实现。一个常见且有效的解决方案是优先级队列,例如在下一段中描述的Kue中实现的那个。这种方法的一大优势是Web和工作进程可以在相同或不同的机器上独立扩展。例如,如果你的应用是一个高流量的应用,产生的副作用很少,你可以部署比worker进程更多的web进程,而如果很少的网络请求为worker进程产生大量的工作,你可以重新分配相应的H.kue对于web和工作进程相互通信,队列是一种灵活的方式,使您不必担心进程间通信。Kue是基于Redis的Node.js通用队列库,它允许您以完全相同的方式将在相同或不同机器上产生的通信进程放在一起。任何类型的进程都可以创建作业并将它们排队,并且工作进程被配置为获取这些作业并执行它们。可以为每个作业提供许多选项,例如优先级、TTL、延迟等。您生成的工作进程越多,执行这些作业所需的并行吞吐量就越大。Cron应用程序通常需要定期执行某些任务。通常,此操作通过操作系统级别的cron作业进行管理,从您的应用程序外部调用单个脚本。在新机器上部署您的应用程序时,使用此方法需要额外的工作,如果您想自动化部署,这会让流程感到不舒服。获得相同结果的更舒适的方法是使用NPM上可用的cron模块。它允许您在Node.js代码中定义cron作业,使其独立于操作系统配置。根据上面描述的web/worker模式,一个worker进程可以创建一个cron,它调用一个定期将新作业放入队列的函数。使用队列使它更干净,并利用kue提供的所有功能,如优先级排序、重试等。当你有多个工作进程时,问题就会出现,因为cron函数会同时唤醒每个进程上的应用程序,并多次执行相同的作业放入排队的副本中。要解决这个问题,有必要确定一个将执行cron操作的工作进程。Leaderelection和cron-cluster(croncluster)这种问题被称为“leaderelection”,针对这种特定的场景,有一个npm包帮我们搞定了cron-cluster。它公开了为cron模块提供支持的相同API,但在设置期间,它需要一个redis连接来与其他进程通信并执行领导者选举算法。使用Redis作为唯一的真实来源,所有进程将就谁将执行cron达成一致,并且只有一份作业副本将放入队列中。之后,所有工作进程将有资格照常执行作业。缓存API调用服务器端缓存是提高API调用的性能和反应性的常用方法,但它是一个非常广泛的主题,有许多可能的实现。在像我们描述的分布式环境中,使用Redis存储缓存值可能是使所有节点行为相同的最佳方式。缓存最难考虑的方面是失效。quickanddirty解决方案只考虑时间,所以缓存中的值在固定的TTL后刷新,缺点是必须等待下一次刷新才能看到响应中的更新。如果你有更多的时间,最好在应用程序级别实现失效,当DB上的值发生变化时手动刷新redis缓存上的记录。#Conclusion我们在本文中讨论了一些关于缩放和性能的主题。本文提供的建议可以作为指南,可以根据您项目的具体需求进行定制。