微信公众号:【前端一锅煮】一点技术,一点思考。有问题或建议,欢迎留言公众号。Node.js作为后台服务的性能是非常关键的,Node.js的性能不仅要考虑自身的因素,还要考虑其所在服务器的一些因素。比如网络I/O、磁盘I/O,以及其他内存、句柄等问题。下面将详细分析影响其性能的因素和一些优化方案。CPU密集型计算CPU负责程序的运行和业务逻辑的处理,而CPU密集型是指CPU承载更复杂的计算。在Node.js中,由于主线程是单线程的,无论是主线程逻辑还是回调处理逻辑最终都是在主线程上处理的,所以如果线程一直在处理复杂的计算,其他请求就无法再进来了。即单个用户可以屏蔽所有用户的请求。这会因为部分用户的复杂计算而影响整个系统的请求处理,而这种复杂计算占用的CPU时间越长,就会造成请求的堆积,进而导致系统崩溃且无法恢复.因此,保持主线程畅通非常重要。在Node.js中,有以下几种情况会影响主线程的运行,应该积极避免:大数据循环,比如没有很好地利用数据流,一次处理非常大的数组;字符串处理转换,如加解密、字符串序列化等;图片和视频的计算和处理,例如裁剪、缩放或剪切图片。对此,我们考虑以下优化方向:使用其他进程处理CPU密集型计算;增加缓存,对同一响应的返回数据增加缓存处理,避免不必要的重复计算。本地磁盘I/OI/O(Input/Output)的意思是输入和输出,其实就是一个数据传输的过程。作为后台服务,需要与外界进行更多的数据交互,因此I/O操作在所难免。I/O分为以下五种模式。在介绍分类之前,我们先了解I/O在系统层面会有两个阶段(以读取为例):第一阶段是读取文件,将文件放入操作系统内核缓冲区;第二阶段是将内核缓冲区复制到应用程序地址空间。对于阻塞I/O,比如读取一个文件,我们必须等待文件被读取完毕,也就是完成上面提到的两个阶段,才能执行其他逻辑。目前无法释放CPU,所以我们无法处理其他逻辑。.Non-blockingI/O非阻塞是指当我们发起读取文件的命令时,系统会返回它正在处理,然后如果此时要释放进程中的CPU去处理其他逻辑,你必须等待一段时间。然后不断询问操作系统,用轮询的方式看是否读取完成。多路复用I/O模型主要是为了解决round-robin调度的问题。我们可以将这些I/OSocket处理的结果交给一个独立的线程去处理。当I/OSocket处理完成后,主动告诉业务处理完成,这样就不需要每个业务都进行轮询查询了。它包括三种常见的类型:select、poll和epoll。首先,select比较老。它和poll的区别是poll使用链表保存I/OSocket数据,而select是数组,所以select有1024个上限,而poll没有。select、poll和epoll的区别在于前两者不会告诉你哪个I/OSocket完成了,而epoll会通知你哪个I/OSocket完成了哪个阶段的操作,所以你不需要遍历查询。当然,这里很重要的一点是,这三个只会通知读取的文件已经进入操作系统内核缓冲区,也就是我们上面提到的第一阶段,但是第二阶段仍然是同步等待从内核拷贝到应用程序地址空间。信号驱动I/O模式与多路复用的区别在于不需要其他线程来处理,而是在读取完成进入操作系统内核缓冲区后,会立即得到通知,即第一个stage可以在系统处理层面进行处理,不需要独立的线程来管理,但是第二stage还是和多路复用一样。异步I/O和信号驱动的区别在于,异步I/O是在两个阶段都完成后才会通知,而不是第一阶段。我们常说Node.js是异步I/O,这并没有错。具体来说,Node.js是一种类似于异步I/O的模型,由其libv库实现。对于Node.js应用来说,它是一个异步I/O,所以不需要处理两个进程,它是在libv内部实现的。它是多线程的epoll模型。一般来说,磁盘I/O不会影响主线程的性能,因为磁盘I/O是由其他线程异步处理的。但是由于服务器的磁盘性能是一定的,如果在高并发情况下磁盘I/O压力大,导致磁盘I/O服务性能下降,从侧面影响机器性能,造成Node.js服务性能受到影响。影响。网络I/O后台服务中常见的网络I/O有几种类型:缓存类,如MemCache、Redis;数据存储类型,如MySQL、MongoDB;服务类型,如内网API服务或第三方API。网络I/O的开销是最高的,涉及到两个最重要的点:依赖于其他服务的性能;取决于服务器之间的延迟。对此,我们可以从以下几个方面考虑优化策略:减少与网络I/O的交互,比如缓存获取的内容;使用更高性能的网络I/O替代其他性能更差、成本更高的网络I/O类型,例如数据库读写的I/O成本明显高于缓存类型,因此缓存类型网络I/O可以用来代替存储类型;可以降低目标网络I/O服务的并发压力。使用异步队列模式。网络I/O一般不会影响主线程的逻辑,它请求的服务往往是瓶颈,从而影响Node.js中涉及网络服务的请求。但是网络I/O的大量积累也会带来一个副作用:服务器本身的网络模块问题和Node.js的性能会导致其他服务接口受到影响。缓存问题缓存是临时存储空间,用来存放访问频率高的数据,用空间来换取响应速度。核心是减轻用户对数据库的查询压力。但是,如果缓存应用不当,就会导致一些看不见的或难以定位的问题。主要有三点:缓存雪崩、缓存击穿、缓存穿透。CacheAvalanche大多数数据都有过期时间的概念。假设我们有一批数据通过定时服务从数据库写入缓存,然后统一设置过期时间。当这个时间节点到了,但是由于某种原因数据没有从数据库写入缓存,此时所有的数据都会去数据库查询数据,这会造成数据库查询的压力,导致数据库过于并发而瘫痪,无法正常服务。那我们应该怎么处理呢?避免为所有数据设置同一个过期时间节点,应根据数据类型和数据更新时效性设置;数据过期时间应大于数据更新节点时间,并考虑更新时间,同时增加更新失败异常告警提示;对于数据库中频率高或者查询压力大的数据,不需要设置过期时间,主动控制程序对数据的移除或者替换。缓存击穿的概念有点类似于缓存雪崩,但不是大面积的缓存过期,而是某个访问频率高的数据失效,导致所有的高并发请求都在此时穿透到数据库时刻,使数据库是并发的。压力大,响应慢,进一步导致数据库异常,影响其他业务。那我们应该怎么处理呢?对于高频数据和复杂查询数据,可以不设置过期时间,但需要程序维护数据的替换和删除;如果需要缓存过期时间,必须大于缓存更新时间,避免过期后找不到key;使用原子操作方案,当多个数据需要去数据库查询同一个数据时,通知程序正在生成缓存,同时通知其他程序可以读取最后缓存的数据,避免读取同一个数据同时。缓存穿透对于经常访问的数据,这里会出现一种情况。比如查询信息总是空数据。空数据不认为是经常访问的数据,所以一直缓存,但是空数据没有缓存,而是直接渗透到数据库中。虽然数据库查询也是空数据,但是还是要经过数据库查询。这种现象就是突破缓存,直接去数据库查询。那我们应该怎么处理呢?过滤异常请求数据,比如一些从参数中可以知道为空的数据,可以直接从程序中处理;缓存空的结果,为了提高性能,还可以缓存一些为空的查询结果,以便下次用户再次访问时,可以判断并直接从缓存中返回;由于第二种方案在空数据较多时会浪费内存空间,我们可以使用布隆过滤器将这些空数据的键名缓存到缓存中,这样就可以尽可能少地使用内存,提高效率。多进程集群模式在多进程集群模式下,由于所有的请求都必须通过master进程分发,同时接收并处理worker进程的返回。因此,在实际开发过程中,如果启用了更多的worker进程,而主进程只有一个,当单机高并发时(每秒并发请求超过20000个),会造成master进程处理瓶颈,这将影响服务。性能,这时候你会发现worker进程的CPU没有任何压力。这个非常重要。在生产环境中一般很难发现这样的问题,但是应该有这样一个概念:当并发量超过20000时,master进程就会出现性能瓶颈。内存限制Node.js的内存限制在32位服务器上是0.7G,64位服务器上是1.4G,这个限制主要是Node.js的垃圾回收线程会循环很长时间超过内存限制的时间会大于1s,会影响性能问题。现在我们一般启用多进程。如果每个进程消耗1.4G,总和可能会超过服务器内存的上限,导致服务器崩溃。其次,如果内存没有超过服务器的上限,但是当达到某个上限,也就是我们上面说的0.7G和1.4G,就会导致服务器重启,从而导致界面请求失败。HandlelimitHandle可以简单理解为一个ID索引,通过它可以访问其他资源,比如文件句柄、网络I/O操作句柄等,一般的服务器句柄都有上限。当Node.js没有很好地控制句柄时,比如无限打开的文件没有关闭,就会出现句柄泄露问题,从而导致服务器异常,从而影响Node.js的服务。
