NodeJs:腾讯新闻构建高性能的 react 同构直出方案

腾讯新闻抢金达人活动 node 同构直出渲染方案的总结文章中咱们总体了解了下同构直出渲染方案在咱们项目中的使用。正如我在上篇文章结尾所说的:javascript

应用型技术的难点不是在克服技术问题,而是在于可以不断的结合自身的产品体验,发现其中存在的体验问题,不断使用更好的技术方案去优化用户的体验,为整个产品发展添砖加瓦。

咱们在根据产品的体验效果选择了 react 同构直出渲染方案,必然也要保证当前方案的可用性和可靠性。例如咱们的服务能同时支撑多少人访问,当用户量增大时是否能够依然保证用户的正常访问,如何保证 CPU、内存等正常运做,而不被一直占用没法释放等。所以,这里咱们应当下咱们项目的几项数据:html

  1. 项目一天的访问量是多少,高峰期的访问量是多少,即并发的用户量有多少;
  2. 咱们的单机服务最大能支持多少 QPS;
  3. 高并发时的服务响应时间如何,页面,接口的失败率有多少;
  4. CPU 和内存的使用状况,是否存在 CPU 使用不充分或者内存泄露等问题;

这些数据,都是咱们上线前要知道的。压力测试的重要性就提现出来了,咱们在上线前进行充分的测试,可以让咱们掌握程序和服务器的运行性能,大体申请多少台机器等等。前端

1. 初次压力测试

咱们这里使用autocannon来对项目进行压测。注意,咱们如今尚未进行任何的优化措施,就是要先暴露出问题来,而后针对性的进行优化。java

每秒钟 60 的并发,并持续 100 秒:node

autocannon -c 60 -d 100

压测后的数据:react

初始压测的数据-蚊子的前端博客

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

2. 解决方案

针对上面压测出来的数据不理想,咱们这里须要采起一些措施了。nginx

来吧-蚊子的前端博客

2.1 内存管理

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

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

  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 也就下不来了。

经过定位并查看axios 的源码

if (config.timeout) {
    // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
    // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
    // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
    // And then these socket which be hang up will devoring CPU little by little.
    // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
    req.setTimeout(config.timeout, function handleRequestTimeout() {
        req.abort();
        reject(
            createError(
                'timeout of ' + config.timeout + 'ms exceeded',
                config,
                'ECONNABORTED',
                req
            )
        );
    });
}

额,我以前使用的版本比较早,跟我本地使用的代码不同,说明是更新过了,再查看这个文件的9 月 16 日的改动历史

seTimeout-axios-蚊子的前端博客

这里咱们就须要把 axios 更新到最新的版本了。并且通过本地大量测试,发如今高负载下 CPU 和内存都在正常范围内了。

2.2 缓存

缓存真是性能优化的一把好手,服务不够,缓存来凑。不过缓存的类型有不少种,咱们应当根据项目的实际状况,合理地选择使用缓存的策略。这里咱们使用了 3 层的缓存策略。

缓存-蚊子的前端博客

在 nginx 中,可使用 proxy_cache 设置要缓存的路径和缓存的时间,同时能够启用proxy_cache_lock

当 proxy_cache_lock 被启用时,当多个客户端请求一个缓存中不存在的文件(或称之为一个 MISS),只有这些请求中的第一个被容许发送至服务器。其余请求在第一个请求获得满意结果以后在缓存中获得文件。若是不启用 proxy_cache_lock,则全部在缓存中找不到文件的请求都会直接与服务器通讯。

不过这个字段的启用也要很是慎重,当访问量过大时,会形成请求的堆积,必须等待第一个请求返回完成后,才能处理后面的请求。

proxy_cache_path /data/cached keys_zone=answer:16m levels=1:2 inactive=60m;

server {
    location / {
        proxy_cache answer;
        proxy_cache_valid 1m;
    }
}

在业务层面,咱们能够启用 redis 缓存,来缓存整个页面、页面的某个部分或者接口等等,当穿透 nginx 缓存时,能够启用 redis 缓存。使用第三方缓存的特色咱们在以前的文章也说了:多个进程之间能够共享,同时减小项目自己对缓存淘汰算法的处理。

当前面的两层缓存失效时,进入到咱们的 node 服务层。二层的缓存机制,能实现不一样的缓存策略和缓存粒度,业务须要根据自身场景, 选用适合本身业务的缓存便可。

3. 效果

这时咱们项目的性能怎样了呢?

autocanon -c 1000 -d 100

压力测试-蚊子的前端博客

从图片里能够看到,99%的请求在182ms内完成,每秒平均处理的请求有15707左右,相比咱们最开始只能处理200多个请求,性能足足提高了60倍多。

蚊子的前端博客

蚊子的公众号,欢迎一块儿交流:
v2-9250ebca58effda39b85fbb00b8dea40_hd.jpg

相关文章
相关标签/搜索