Node.js 性能优化

微信公众号:[前端一锅煮]
一点技术、一点思考。
问题或建议,请公众号留言。

Node.js 做为后台服务性能是很是关键的一点,而影响 Node.js 的性能不只仅要考虑其自己的因素,还应该考虑所在服务器的一些因素。好比网络 I/O 、磁盘 I/O 以及其余内存、句柄等一些问题。下面将详细地分析影响其性能的因素缘由,以及部分优化解决方案。前端

CPU 密集型计算

CPU 负责了程序的运行和业务逻辑的处理,而 CPU 密集型表示的主要是 CPU 承载了比较复杂的运算。数据库

在 Node.js 中因为主线程是单线程的,不管是主线程逻辑,仍是回调处理逻辑,最终都是在主线程处理,那么若是该线程一直在处理复杂的计算,其余请求就没法再次进来,也就是单个用户就能够阻塞全部用户的请求。这样就会由于某些用户的复杂运算,而影响到整个系统的请求处理,并且这种复杂运算占用的 CPU 时间越久,就会致使请求堆积,而进一步致使系统处于崩溃状态没法恢复。所以保持主线程的通畅是很是关键的。数组

在 Node.js 中有如下几种状况,会影响到主线程的运行,应主动避免:缓存

  1. 大的数据循环,好比没有利用好数据流,一次性处理很是大的数组;
  2. 字符串处理转化,好比加解密、字符串序列化等;
  3. 图片、视频的计算处理,好比对图片进行裁剪、缩放或者切割等。

对此咱们考虑如下优化方向:服务器

  • 将 CPU 密集型计算使用其余进程来处理;
  • 增长缓存,对于相同响应的返回数据,增长缓存处理,避免没必要要的重复计算。

本地磁盘 I/O

I/O(Input/Output)意思是输入输出,其实就是数据传递的一个过程,做为后台服务须要更多地与外部进行数据交互,那么就免不了 I/O 操做。微信

I/O 分为如下 5 种模型,在介绍分类以前,咱们先了解 I/O 在系统层面会有 2 个阶段(以读为例子):网络

  • 第一个阶段是读取文件,将文件放入操做系统内核缓冲区;
  • 第二阶段是将内核缓冲区拷贝到应用程序地址空间。
  1. 阻塞 I/O

例如读取一个文件,咱们必需要等待文件读取完成后,也就是完成上面所说的两个阶段,才能执行其余逻辑,而当前是没法释放 CPU 的,所以没法去处理其余逻辑。多线程

  1. 非阻塞 I/O

非阻塞的意思是,咱们发起了一个读取文件的指令,系统会返回正在处理中,而后这时候若是要释放进程中的 CPU 去处理其余逻辑,你就必须间隔一段时间,而后不停地去询问操做系统,使用轮询的判断方法看是否读取完成了。并发

  1. 多路复用 I/O

这一模型主要是为了解决轮询调度的问题,咱们能够将这些 I/O Socket 处理的结果统一交给一个独立线程来处理,当 I/O Socket 处理完成后,就主动告诉业务,处理完成了,这样不须要每一个业务都来进行轮询查询了。异步

它包括目前常见的三种类型:select 、poll 和 epoll。首先 select 是比较旧的,它和 poll 的区别在于 poll 使用的是链表来保存 I/O Socket 数据,而 select 是数组,所以 select 会有上限 1024,而 poll 则没有。select、poll 与 epoll 的区别在于,前二者不会告诉你是哪一个 I/O Socket 完成了,而 epoll 会通知具体哪一个 I/O Socket 完成了哪一个阶段的操做,这样就不须要去遍历查询了。

固然这里有一个重点是这三者只会告知文件读取进入了操做系统内核缓冲区,也就是上面咱们所说的第一阶段,可是第二阶段从内核拷贝到应用程序地址空间仍是同步等待的。

  1. 信号驱动 I/O

这种模式和多路复用的区别在于不须要有其余线程来处理,而是在完成了读取进入操做系统内核缓冲区后,立马通知,也就是第一阶段能够由系统层面来处理,不须要独立线程来管理,可是第二阶段仍是和多路复用同样。

  1. 异步 I/O

和信号驱动不一样的是,异步 I/O 是两个阶段都完成了之后,才会通知,并非第一阶段完成。

咱们常说 Node.js 是一个异步 I/O 这个是没有错的。具体来讲 Node.js 是其 libv 库自行实现的一种相似异步 I/O 的模型,对于 Node.js 应用来讲是一个异步 I/O,所以无须处理两个过程,而在 libv 内部实现,则是多线程的一个 epoll 模型。

在通常状况下磁盘 I/O 不会影响到主线程性能,由于磁盘 I/O 是异步其余线程处理。可是由于服务器磁盘性能是必定的,若是在高并发状况下,磁盘 I/O 压力较大,从而致使磁盘 I/O 的服务性能降低,就会从侧面影响机器性能,致使 Node.js 服务性能受影响。

网络 I/O

在后台服务中常见的网络 I/O 有以下几种类型:

  1. 缓存型,如 MemCache、Redis;
  2. 数据存储型,如 MySQL、MongoDB;
  3. 服务型,如内网 API 服务或者第三方 API。

网络 I/O 的成本是最高的,涉及两个最重要的点:

  • 依赖其余服务的性能;
  • 依赖服务器之间的延时。

对此,咱们能够从如下几个方面来考虑优化的策略:

  • 减小与网络 I/O 的交互,好比缓存已获取的内容;
  • 使用更高性能的网络 I/O 替代其余性能较差的、成本更高的网络 I/O 类型,好比数据库读写的 I/O 成本是明显高于缓存型的,所以可使用缓存型网络 I/O 替换存储型;
  • 下降目标网络 I/O 服务的并发压力,能够采用异步队列方式。

网络 I/O 通常不影响主线程逻辑,其请求的服务每每是瓶颈端,从而影响 Node.js 中涉及该网络服务的请求。可是网络 I/O 堆积较多也会侧面影响:服务器自己的网络模块问题以及Node.js 性能,致使其余服务接口受影响。

缓存问题

缓存是临时的一块存储空间,用于存放访问频次较高的数据,用空间换响应速度,核心是减小用户对数据库的查询压力。可是若是没有应用好缓存,将会致使一些不可见或者说很难定位的问题,主要是三点:缓存雪崩、缓存击穿和缓存穿透。

  1. 缓存雪崩

大部分数据都有一个过时时间的概念,假设咱们有一批数据是经过定时服务从数据库写入缓存中,而后咱们统一设置了过时时间。当这个时间节点到了,可是因为某种缘由数据又没有从数据库写入缓存,致使这时候全部的数据都会前往数据库查询数据,从而引发数据库查询压力,致使数据库并发过大而瘫痪没法正常服务。

那么应该如何应对呢?

  • 避免全部数据都设置同一个过时时间节点,应该按数据类型、数据更新时效性来设置;
  • 数据过时时间应大于数据更新节点时间,并考虑更新时长,同时增长更新失败异常告警提示;
  • 对于一些相对较高频次或者数据库查询压力较大的数据,可不设置过时时间,主动从程序上来控制该数据的移除或者更替。
  1. 缓存击穿

这个概念和缓存雪崩有点相似,但不是大面积的缓存过时失效,而是某个访问频次较高的数据失效了,从而致使这一刻高并发的请求所有穿透到了数据库,从而数据库并发压力较高,响应较慢,也进一步致使数据库异常,影响其余业务。

那么应该如何应对呢?

  • 高频数据、查询较为复杂的数据,能够不设置过时时间,可是须要程序去维护数据的更替删除;
  • 若是须要缓存过时时间,要大于缓存更新时间,避免过时没法找到键;
  • 使用原子操做方案,当多个数据都须要前往数据库查询同一个数据时,告知程序缓存正在生成中,而且告知其余程序能够读取上一次缓存数据,避免同时读取同一份数据。
  1. 缓存穿透

对于访问频繁的数据,这里就会出现一种状况,好比说查询信息一直是空数据,空数据按理不属于访问频繁较高的数据,因此通过了缓存,可是并无缓存该空数据,而是直接穿透进入了数据库,虽然数据库查询也是空数据,可是仍是须要通过数据库的查询,这种现象就是击穿了缓存直接前往了数据库查询。

那么应该如何应对呢?

  • 过滤非正常请求数据,好比一些从参数就能够知道为空的数据,能够直接从程序上处理;
  • 缓存空的结果,为了提高性能,能够将一些查询为空的结果也缓存起来,这样下次用户再进行访问时,能够直接从缓存中判断返回;
  • 因为第 2 种方案在空数据较多时会浪费内存空间,咱们能够将这些空数据的键名,使用布隆过滤器来缓存到缓存,这样能够尽量地减小内存占用,而且更加高效。

多进程 cluster 模式

在多进程 cluster 模式中,由于全部的请求都必须通过 master 进程进行分发,同时接收处理 worker 进程的返回。

所以在实际开发过程当中,若是启用了比较多的 worker 进程,而主进程只有一个,从而在单机高并发时(2 万以上的每秒并发请求)会致使 master 进程处理瓶颈,这样就影响到了服务性能,而且这时候你会发现 worker 进程的 CPU 并无任何压力。

这点很是重要,在生产环境下通常很难发现这类问题,不过应该有这样的一个概念:大概在 2 万以上的并发时,master 进程会存在性能瓶颈。

内存限制

在 32 位服务器上 Node.js 的内存限制是 0.7 G,而在 64 位服务器上则是 1.4 G,而这个限制主要是由于 Node.js 的垃圾回收线程在超过限制内存时,回收时长循环会大于 1s,从而会影响性能问题。

如今咱们通常会启用多个进程,若是每一个进程损耗 1.4 G,那么加起来可能超出了服务器内存上限,从而致使服务器瘫痪。其次若是内存不会超出服务器上限,而是在达到必定上限时,也就是咱们上面说的 0.7 G和 1.4 G,会致使服务器重启,从而会致使接口请求失败的问题。

句柄限制

句柄能够简单理解为一个 ID 索引,经过这个索引能够访问到其余的资源,好比说文件句柄、网络 I/O 操做句柄等等,而通常服务器句柄都有上限。当 Node.js 没有控制好句柄,好比说无限的打开文件并未关闭,就会出现句柄泄漏问题,而这样会致使服务器异常,从而影响 Node.js 服务。