如何创建高性能、可扩展的Node.js应用?
mhr18 2024-11-09 12:19 24 浏览 0 评论
作者|Virgafox
译者|姚佳灵
出处丨前端之巅
说明:本文根据原文作者的系列文章编辑而成,略有删改。
在这篇文章中,我们将介绍关于开发 Node.js web 应用程序的一些最佳实践,重点关注效率和性能,以便用更少的资源获得最佳结果。
提高 web 应用程序吞吐量的一种方法是对其进行扩展,多次实例化其以平衡在多个实例之间的传入连接,接来下我们要介绍的是如何在多个内核上或多台机器上对 Node.js 应用程序进行水平扩展。
在强制性规则中,有一些好的实践可以用来解决这些问题,像拆分 API 和工作进程、采用优先级队列、管理像 cron 进程这样的周期性作业,在向上扩展到 N 个进程 / 机器时,这不需要运行 N 次。
水平扩展 Node.js 应用程序
水平扩展是复制应用程序实例以管理大量传入连接。 此操作可以在单个多内核机器上执行,也可以在不同机器上执行。
垂直扩展是提高单机性能,它不涉及代码方面的特定工作。
在同一台机器上的多进程
提高应用程序吞吐量的一种常用方法是为机器的每个内核生成一个进程。 通过这种方式,Node.js 中请求的已经有效的“并发”管理(请参见“事件驱动,非阻塞 I / O”)可以相乘和并行化。
产生大于内核的数量的大量进程可能并不好,因为在较低级别,操作系统可能会平衡这些进程之间的 CPU 时间。
扩展单机有不同的策略,但常见的概念是,在同一端口上运行多个进程,并使用某种内部负载平衡来分配所有进程 / 核上的传入连接。
下面所描述的策略是标准的 Node.js 集群模式以及自动的,更高级别的 PM2 集群功能。
原生集群模式
原生 Node.js 群集模块是在单机上扩展 Node 应用程序的基本方法(请参阅 https://Node.js.org/api/cluster.html)。 你的进程的一个实例(称为“master”)是负责生成其他子进程(称为“worker”)的实例,每个进程对应一个运行你的应用程序的核。 传入连接按照循环策略分发到所有 worker 进程,从而在同一端口上公开服务。
该方法的主要缺点是必须在代码内部管理 master 进程和 worker 进程之间的差异,通常使用经典的 if-else 块,不能够轻易地修改进动态进程数。
下面的例子来自官方文档:
const cluster = require(‘cluster’); const http = require(‘http’); const numCPUs = require(‘os’).cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on(‘exit’, (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { // Workers can share any TCP connection // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); res.end(‘hello world\n’); }).listen(8000); console.log(`Worker ${process.pid} started`); }
PM2 集群模式
如果你在使用 PM2 作为你的流程管理器(我也建议你这么做),那么有一个神奇的群集功能可以让你跨所有内核扩展流程,而无需担心集群模块。 PM2 守护程序将承担“master”进程的角色,它将生成你的应用程序的 N 个进程作为 worker 进程, 并进行循环平衡。
通过这个方法,只需要按你为单内核用途一样地编写你的应用程序(我们稍后再提其中的一些注意事项),而 PM2 将关注多内核部分。
在集群模式下启动你的应用程序后,你可以使用“pm2 scale”调整动态实例数,并执行“0-second-downtime”重新加载,进程重新串联,以便始终至少有一个在线进程。
在生产中运行节点时,如果你的进程像很多其他你应该考虑的有用的东西一样崩溃了,那么 PM2 作为进程管理器将负责重新启动你的进程。
如果你需要进一步扩展,那么你也许需要部署更多的机器。
具有网络负载均衡的多台机器
跨多台机器进行扩展的主要概念类似于在多内核上进行扩展,有多台机器,每台机器运行一个或多个进程,以及用于将流量重定向到每台机器的均衡器。
一旦请求被发送到特定的节点,刚才所提到的内部均衡器发送该流量到特定的进程。
可以以不同方式部署网络平衡器。 如果使用 AWS 来配置你的基础架构,那么一个不错的选择是使用像 ELB(Elastic Load Balancer,弹性负载均衡器)这样的托管负载均衡器,因为它支持自动扩展等有用功能,并且易于设置。
但是如果你想按传统的方式来做,你可以自己部署一台机器并用 NGINX 设置一个均衡器。 指向上游的反向代理的配置对于这个任务来说非常简单。 下面是配置示例:
http { upstream myapp1 { server srv1.example.com; server srv2.example.com; server srv3.example.com; } server { listen 80; location / { proxy_pass http://myapp1; } } }
通过这种方式,负载均衡器将是你的应用程序暴露给外部世界的唯一入口点。 如果担心它成为基础架构的单点故障,可以部署多个指向相同服务器的负载均衡器。
为了在均衡器之间分配流量(每个均衡器都有自己的 IP 地址),可以向主域添加多个 DNS“A”记录,从而 DNS 解析器将在你的均衡器之间分配流量,每次都解析为不同的 IP 地址。通过这种方式,还可以在负载均衡器上实现冗余。
我们在这里看到的是如何在不同级别扩展 Node.js 应用程序,以便从你的基础架构(从单节点到多节点和多均衡器)获得尽可能高的性能,但要小心:如果想在多进程环境中使用你的应用程序,必须做好准备,否则会遇到一些问题和不期望的行为。
在向上扩展你的进程时,为了避免出现不期望的行为,现在我们来谈谈必须考虑到的一些方面。
让Node.js 应用程序做好扩展准备
从 DB 中分离应用程序实例
首先不是代码问题,而是你的基础结构。
如果希望你的应用程序能够跨不同主机进行扩展,则必须把你的数据库部署在独立的机器上,以便可以根据需要自由复制应用程序机器。
在同一台机器上部署用于开发目的的应用程序和数据库可能很便宜,但绝对不建议用于生产环境,其中的应用程序和数据库必须能够独立扩展。 这同样适用于像 Redis 这样的内存数据库。
无状态
如果生成你的应用程序的多个实例,则每个进程都有自己的内存空间。 这意味着即使在一台机器上运行,当你在全局变量中存储某些值,或者更常见的是在内存中存储会话时,如果均衡器在下一个请求期间将您重定向到另一个进程,那么你将无法在那里找到它。
这适用于会话数据和内部值,如任何类型的应用程序范围的设置。对于可在运行时更改的设置或配置,解决方案是将它们存储在外部数据库(存储或内存中)上,以使所有进程都可以访问它们。
使用 JWT 进行无状态身份验证
身份验证是开发无状态应用程序时要考虑的首要主题之一。 如果将会话存储在内存中,它们将作用于这单个进程。
为了正常工作,应该将网络负载均衡器配置为,始终将同一用户重定向到同一台机器,并将本地用户重定向到同一用户始终重定向到同一进程(粘性会话)。
解决此问题的一个简单方法是将会话的存储策略设置为任何形式的持久性,例如,将它们存储在 DB 而不是 RAM 中。 但是,如果你的应用程序检查每个请求的会话数据,那么每次 API 调用都会进行磁盘读写操作(I / O),从性能的角度来看,这绝对不是好事。
更好,更快的解决方案(如果你的身份验证框架支持)是将会话存储在像 Redis 这样的内存数据库中。 Redis 实例通常位于应用程序实例外部,例如 DB 实例,但在内存中工作会使其更快。 无论如何,在 RAM 中存储会话会在并发会话数增加时需要更多内存。
如果想采用更有效的无状态身份验证方法,可以看看 JSON Web Tokens。
JWT 背后的想法很简单:当用户登录时,服务器生成一个令牌,该令牌本质上是包含有效负载的 JSON 对象的 base64 编码,加上签名获得的哈希,该负载具有服务器拥有的密钥。 有效负载可以包含用于对用户进行身份验证和授权的数据,例如 userID 及其关联的 ACL 角色。 令牌被发送回客户端并由其用于验证每个 API 请求。
当服务器处理传入请求时,它会获取令牌的有效负载并使用其密钥重新创建签名。 如果两个签名匹配,则可以认为有效载荷有效并且不被改变,并且可以识别用户。
重要的是要记住 JWT 不提供任何形式的加密。 有效负载仅用 base64 编码,并以明文形式发送,因此如果需要隐藏内容,则必须使用 SSL。
被 jwt.io 借用的以下模式恢复了身份验证过程:
在认证过程中,服务器不需要访问存储在某处的会话数据,因此每个请求都可以由非常有效的方式由不同的进程或机器处理。 RAM 中不保存数据,也不需要执行存储 I / O,因此在向上扩展时这种方法非常有用。
S3 上的存储
使用多台机器时,无法将用户生成的资产直接保存在文件系统上,因为这些文件只能由该服务器本地的进程访问。 解决方案是,将所有内容存储在外部服务上,可以存储在像 Amazon S3 这样的专用服务上,并在你的数据库中仅保存指向该资源的绝对 URL。
然后,每个进程 / 机器都可以以相同的方式访问该资源。
使用 Node.js 的官方 AWS sdk 非常简单,可以轻松地将服务集成到你的应用程序中。 S3 非常便宜并且针对此目的进行了优化。即使你的应用程序不是多进程的,它也是一个不错的选择。
正确配置 WebSockets
如果你的应用程序使用 WebSockets 进行客户端之间或客户端与服务器之间的实时交互,则需要链接后端实例,以便在连接到不同节点的客户端之间正确传播广播消息或消息。
Socket.io 库为此提供了一个特殊的适配器,称为 socket.io-redis,它允许你使用 Redis pub-sub 功能链接服务器实例。
为了使用多节点 socket.io 环境,还需要强制协议为“websockets”,因为长轮询(long-polling)需要粘性会话才能工作。
以上这些对于单节点环境来说也是好的实例。
效率和性能的其他良好实践
接下来,我们将介绍一些可以进一步提高效率和性能的其他实践。
Web 和 worker 进程
你可能知道,Node.js 实际上是单线程的,因此该进程的单个实例一次只能执行一个操作。 在 Web 应用程序的生命周期中,执行许多不同的任务:管理 API 调用,读取 / 写入 DB,与外部网络服务通信,执行某种不可避免的 CPU 密集型工作等。
虽然你使用异步编程,但将所有这些操作委派给响应 API 调用的同一进程可能是一种非常低效的方法。
一种常见的模式是基于两种不同类型的进程之间的职责分离,这两种类型的进程组成了你的应用程序,通常是 Web 进程和 worker 进程。
Web 进程主要用于管理传入的网络呼叫,并尽快发送它们。 每当需要执行非阻塞任务时,例如发送电子邮件 / 通知、编写日志、执行触发操作,其结果是不需要响应 API 调用,web 进程将操作委派给 worker 进程。
Web 和 worker 进程之间的通信可以用不同的方式实现。 一种常见且有效的解决方案是优先级队列,如下一段所描述的 Kue 中实现的优先级队列。
这种方法的一大胜利是,可以在相同或不同的机器上独立扩展 web 和 worker 进程。
例如,如果你的应用程序是高流量应用程序,几乎没有生成的副作用,那么可以部署比 worker 进程更多的 web 进程,而如果很少有网络请求为 worker 进程生成大量作业,则可以重新分发相应的资源。
Kue
为了使 web 和 worker 进程相互通信,队列是一种灵活的方法,可以让你不必担心进程间通信。
Kue 是基于 Redis 的 Node.js 的通用队列库,允许你以完全相同的方式放入在相同或不同机器上生成的通信进程。
任何类型的进程都可以创建作业并将其放入队列,然后将 worker 进程配置为选择这些作业并执行它们。 可以为每项工作提供许多选项,如优先级、TTL、延迟等。
你生成的 worker 进程越多,执行这些作业所需的并行吞吐量就越多。
Cron
应用程序通常需要定期执行某些任务。 通常,这种操作通过操作系统级别的 cron 作业进行管理,从你的应用程序外部调用单个脚本。
在新机器上部署你的应用程序时,用此方法就需要额外的工作,如果要自动部署,这会使进程感到不自在。
实现相同结果的更自在的方法是使用 NPM 上的可用 cron 模块。 它允许你在 Node.js 代码中定义 cron 作业,使其独立于 OS 配置。
根据上面描述的 web / worker 模式,worker 进程可以创建 cron,它调用一个函数,定期将新作业放入队列。
使用队列使其更加干净,并可以利用 kue 提供的所有功能,如优先级,重试等。
当你有多个 worker 进程时会出现问题,因为 cron 函数会同时唤醒每个进程上的应用程序,并将多次执行的同一作业放入队列副本中。
为了解决这个问题,有必要确定将执行 cron 操作的单个 worker 进程。
领导者选举(Leader election)和 cron-cluster(cron 集群)
这种问题被称为“领导者选举”,对于这个特定的场景,有一个 NPM 包为我们做了一个叫做 cron-cluster 的技巧。
它暴露了为 cron 模块提供动力的相同 API,但在设置过程中,它需要一个 redis 连接,用于与其他进程通信并执行领导者选举算法。
使用 redis 作为单一事实来源,所有进程都会同意谁将执行 cron,并且只有一份作业副本将被放入队列中。 之后,所有 worker 进程都将有资格像往常一样执行作业。
缓存 API 调用
服务器端缓存是提高 API 调用的性能和反应性的常用方法,但它是一个非常广泛的主题,有很多可能的实现。
在像我们所描述的分布式环境中,使用 redis 来存储缓存的值可能是使所有节点表现相同的最佳方法。
缓存需要考虑的最困难的方面是失效。 快速而简陋的解决方案只考虑时间,因此缓存中的值在固定的 TTL 之后刷新,缺点是不得不等待下一次刷新以查看响应中的更新。
如果你有更多的时间,最好在应用程序级别实现失效,在 DB 上值更改时手动刷新 redis 缓存上的记录。
结 论
我们在本文中介绍了一些有关扩展和性能的一些主题。 文中提供的建议可以作为指导,可以根据你的项目的特定需求进行定制。
英文原文:
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3-bb06b6204197
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3-2a68f875ce79
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3-c1a3381e1382
相关推荐
- B站收藏视频失效?mybili 收藏夹备份神器完整部署指南
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:羊刀仙很多B站用户都有过类似经历:自己精心收藏的视频突然“消失”,点开一看不是“已被删除”,就是“因UP主设置不可见”。而B站并不会主动通知...
- 中间件推荐初始化配置
-
Redis推荐初始化配置bind0.0.0.0protected-modeyesport6379tcp-backlog511timeout300tcp-keepalive300...
- Redis中缓存穿透问题与解决方法
-
缓存穿透问题概述在Redis作为缓存使用时,缓存穿透是常见问题。正常查询流程是先从Redis缓存获取数据,若有则直接使用;若没有则去数据库查询,查到后存入缓存。但当请求的数据在缓存和数据库中都...
- 后端开发必看!Redis 哨兵机制如何保障系统高可用?
-
你是否曾在项目中遇到过Redis主服务器突然宕机,导致整个业务系统出现数据读取异常、响应延迟甚至服务中断的情况?面对这样的突发状况,作为互联网大厂的后端开发人员,如何快速恢复服务、保障系统的高可用...
- Redis合集-大Key处理建议
-
以下是Redis大Key问题的全流程解决方案,涵盖检测、处理、优化及预防策略,结合代码示例和最佳实践:一、大Key的定义与风险1.大Key判定标准数据类型大Key阈值风险场景S...
- 深入解析跳跃表:Redis里的"老六"数据结构,专治各种不服
-
大家好,我是你们的码农段子手,今天要给大家讲一个Redis世界里最会"跳科目三"的数据结构——跳跃表(SkipList)。这货表面上是个青铜,实际上是个王者,连红黑树见了都要喊声大哥。...
- Redis 中 AOF 持久化技术原理全解析,看完你就懂了!
-
你在使用Redis的过程中,有没有担心过数据丢失的问题?尤其是在服务器突然宕机、意外断电等情况发生时,那些还没来得及持久化的数据,是不是让你夜不能寐?别担心,Redis的AOF持久化技术就是...
- Redis合集-必备的几款运维工具
-
Redis在应用Redis时,经常会面临的运维工作,包括Redis的运行状态监控,数据迁移,主从集群、切片集群的部署和运维。接下来,从这三个方面,介绍一些工具。先来学习下监控Redis实时...
- 别再纠结线程池大小 + 线程数量了,没有固定公式的!
-
我们在百度上能很轻易地搜索到以下线程池设置大小的理论:在一台服务器上我们按照以下设置CPU密集型的程序-核心数+1I/O密集型的程序-核心数*2你不会真的按照这个理论来设置线程池的...
- 网络编程—IO多路复用详解
-
假如你想了解IO多路复用,那本文或许可以帮助你本文的最大目的就是想要把select、epoll在执行过程中干了什么叙述出来,所以具体的代码不会涉及,毕竟不同语言的接口有所区别。基础知识IO多路复用涉及...
- 5分钟学会C/C++多线程编程进程和线程
-
前言对线程有基本的理解简单的C++面向过程编程能力创造单个简单的线程。创造单个带参数的线程。如何等待线程结束。创造多个线程,并使用互斥量来防止资源抢占。会使用之后,直接跳到“汇总”,复制模板来用就行...
- 尽情阅读,技术进阶,详解mmap的原理
-
1.一句话概括mmapmmap的作用,在应用这一层,是让你把文件的某一段,当作内存一样来访问。将文件映射到物理内存,将进程虚拟空间映射到那块内存。这样,进程不仅能像访问内存一样读写文件,多个进程...
- C++11多线程知识点总结
-
一、多线程的基本概念1、进程与线程的区别和联系进程:进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程;线程:是运行中的实际的任务执行者。可以说,进程中包含了多...
- 微服务高可用的2个关键技巧,你一定用得上
-
概述上一篇文章讲了一个朋友公司使用SpringCloud架构遇到问题的一个真实案例,虽然不是什么大的技术问题,但如果对一些东西理解的不深刻,还真会犯一些错误。这篇文章我们来聊聊在微服务架构中,到底如...
- Java线程间如何共享与传递数据
-
1、背景在日常SpringBoot应用或者Java应用开发中,使用多线程编程有很多好处,比如可以同时处理多个任务,提高程序的并发性;可以充分利用计算机的多核处理器,使得程序能够更好地利用计算机的资源,...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)