从图片中可以看到,每秒 60 的并发请求量时,QPS 平均有 266 左右,不过还有 23 个请求超时了,响应时间还可以,99%的请求在 1817ms 毫秒内完成。就目前这几项数据来看,数据处理能力并不理想,我们还有很大的提升空间。

2. 解决方案

针对上面压测出来的数据不理想,我们这里需要采取一些措施了。

来吧-蚊子的前端博客

2.1 内存管理

我们现在写纯前端时,几乎已经很少关注内存的使用了,毕竟在前端发展的过程中,内存的垃圾回收机制相对来说比较完善,而且前端页面的生存周期比较短。如果真是要特别注意的话,也是早期在 IE 浏览器中,js 与 dom 的交互过程中可能会产生内存的泄露。而且如果真会真要是泄露的话,也只会影响当前终端的用户,其他的用户暂时不会受到影响。

而服务端则不同,所有用户都会访问当前运行的代码,只要程序有一丁点的内存泄露,在成千上万的访问量下,都会造成内存的堆积,垃圾无法回收,最终造成严重的内存泄露,并导致程序崩溃。为了预防内存泄露,我们在内存管理方面,主要三方面的内容:

  1. V8 引擎的垃圾回收机制;
  2. 造成内存泄露的原因;
  3. 如何检测内存泄露;

Node 将 JavaScript 的主要应用场景扩展到了服务器端,相应要考虑的细节也与浏览器端不同, 需要更严谨地为每一份资源作出安排。总的来说,内存在 Node 中不能随心所欲地使用,但也不是完全不擅长。

2.1.1 V8 引擎的垃圾回收机制

在 V8 中,主要将内存分为新生代和老生代两代。新生代的对象为存活时间比较短的对象,老生代中的对象为存活时间较长的或常驻内存的对象。

默认情况下,新生代的内存最大值在 64 位系统和 32 位系统上分别为 32 MB 和 16 MB。V8 对内存的最大值在 64 位系统和 32 位系统上分别为 1464 MB 和 732 MB。

为什么这样分两代呢?是为了最优的 GC 算法。新生代的 GC 算法 Scavenge 速度快,但是不合适大数据量;老生代针使用 Mark-Sweep(标记清除) & Mark-Compact(标记整理) 算法,合适大数据量,但是速度较慢。分别对新旧两代使用更适合他们的算法来优化 GC 速度。

2.1.2 内存泄露的原因

内存泄露的情况有很多,例如内存当缓存、队列、重复的事件监听等。

内存当缓存这种情况中,通常有用一个变量来缓存数据,然后没有过期时间,一直填充数据,例如下面一个简单的例子:

let cached = new Map();  server.get('*', (req, res) => {     if (cached.has(req.url)) {         return cached.get(req.url);     }     const html = app.render(req, res);     cached.set(req.url, html);     res.send(html); });

除此之外,还有闭包也是其中的一种情况。这种使用内存的不好的地方是,它没有可用的过期策略,只会让数据越来越多,最终造成内存泄露。更好的方式使用第三方的缓存机制,例如 redis、memcached 等,这些都有良好的过期和淘汰策略。

同时,也有一些队列方面的处理,例如有些日志的写入操作,当海量的数据需要写入时,就会造成队列的堆积。这时,我们设置队列的超时策略和拒绝策略,让一些操作尽快地释放掉。

再一个就是事件的重复监听。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:

setMaxListeners-蚊子的前端博客

Warning: Possible EventEmitter memory leak detected. 11 /question listeners added。Use emitter。setMaxListeners() to increase limit

2.1.3 排查的手段

内存泄露-蚊子的前端博客

我们从内存的监控图中可以看到,在用户量基本保持不变的情况下,内存是一直在缓慢上涨,说明我们产生了内存泄露,使用的内存并没有被释放掉。

这里我们可以通过node-heapdump等工具来进行判断,或者稍微简单点,使用--inspect命令实现:

node --inspect server.js

然后打开 chrome 链接chrome://inspect来查看内存的使用情况。

chrome-inspect-蚊子的前端博客

通过两次的内存抓取对比发现,handleRequestTimeout()方法一直在产生,且每个 handle 方法中有无数个回调,资源无法被释放。

通过定位查看使用的 axios 代码是:

if (config.timeout) {     timer = setTimeout(function handleRequestTimeout() {         req.abort();         reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));     } }

这里代码看起来是没任何问题的,这是在前端处理中一个很典型的超时处理解决方式。

由于 Nodejs 中,io 的链接会阻塞 timer 处理,因此这个 setTimeout 并不会按时触发,也就有了 10s 以上才返回的情况。

貌似问题解决了,巨大的流量和阻塞的 connection 导致请求堆积,服务器处理不过来,CPU 也就下不来了。