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

在这篇文章中,咱们将介绍关于开发 Node.js web 应用程序的一些最佳实践,重点关注效率和性能,以便用更少的资源得到最佳结果。html

提升 web 应用程序吞吐量的一种方法是对其进行扩展,屡次实例化其以平衡在多个实例之间的传入链接,接来下咱们要介绍的是如何在多个内核上或多台机器上对 Node.js 应用程序进行水平扩展。web

在强制性规则中,有一些好的实践能够用来解决这些问题,像拆分 API 和工做进程、采用优先级队列、管理像 cron 进程这样的周期性做业,在向上扩展到 N 个进程 / 机器时,这不须要运行 N 次。redis

水平扩展 Node.js 应用程序

水平扩展是复制应用程序实例以管理大量传入链接。 此操做能够在单个多内核机器上执行,也能够在不一样机器上执行。算法

垂直扩展是提升单机性能,它不涉及代码方面的特定工做。数据库

 在同一台机器上的多进程编程

提升应用程序吞吐量的一种经常使用方法是为机器的每一个内核生成一个进程。 经过这种方式,Node.js 中请求的已经有效的“并发”管理(请参见“事件驱动,非阻塞 I / O”)能够相乘和并行化。后端

产生大于内核的数量的大量进程可能并很差,由于在较低级别,操做系统可能会平衡这些进程之间的 CPU 时间。api

扩展单机有不一样的策略,但常见的概念是,在同一端口上运行多个进程,并使用某种内部负载平衡来分配全部进程 / 核上的传入链接。缓存

下面所描述的策略是标准的 Node.js 集群模式以及自动的,更高级别的 PM2 集群功能。服务器

 原生集群模式

原生 Node.js 群集模块是在单机上扩展 Node 应用程序的基本方法(请参阅 https://Node.js.org/api/clust...)。 你的进程的一个实例(称为“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://www.jianshu.com/p/43d...

相关文章
相关标签/搜索