应用场景
现在前端主流的技术方案都是服务端渲染,那就面临着一个问题,Node.js需要承担接口转发的任务,所以原来的客户端渲染的http请求的路线(brower -> server),需要调整为(Node.js -> server);看上去没有什么本质的区别,无非是原来是又浏览器发起请求,现在改为Node.js发起请求。
但是高并发呢?举个例子,假如有1000000个请求,原来从browser请求到server,server端如果没有做限流处理,那么部分请求就会一直处于pendding状态;但是1000000个pendding是分布在每个客户端上的,所以并没有什么压力;但是如果是Node.js发起的请求,则会出现很大一部分请求在Node.js端处于pendding状态,则会导致Node.js的CPU和内存占用压力剧增。
Promise timeout方案
前端http请求的代码一般会添加一个timeout的时间限制,配合Promise.race大概的代码思路是:
function timeout() {
    const ms = 2000;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject({
                code: 999,
                msg: ` 调用接口超时: >${ms}ms`,
            });
        }, ms);
    });
}
Promise.race([fetch(url, option), timeout()])
    .then(
        res => res.json(),
        err => {
            console.log('请求失败')
            return Promise.reject({
                code: 0,
                msg: '加载失败',
            });
        }
    )
上面的代码很简单,借助Promise.race,如果期望的fetch请求在2000ms内没有返回,则timeout中的promise会进入rejected状态,导致整个promise reject,相当于正常的fetch请求我们不会继续处理,直接抛弃了。
但是事实如此吗?这个被我们抛弃的Promise实际上依然处于pendding状态,直到http请求结束,promise进入resolve or reject状态,下图可见,错误日志已经打印出来了,但是http依然处于pendding的状态;可见Promise.race并不能真正的"抛弃请求"。内存占用,cpu占用依然存在。

上面的例子是发生在browser,问题不大,因为通常一个页面请求不会超过10个,对于单个用户机器来说,性能完全没有什么问题。
但是如果上述事件是发生在Node.js请求server的过程中呢?大量的请求堆积在Node.js端,导致机器cpu或者内存占满,别忘了我们是服务端渲染,这个时候,用户打开页面将会异常缓慢(肯本没有机会继续处理页面渲染了),或者长时间的等待之后502。
上面的例子是在云音乐项目中实际案例。
所以我们需要找到另外一种解决方案(能真正取消fetch请求的方案),此时[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)映入眼帘。
AbortController使用方法
使用方法很简单,直接看文档。
MDN文档
Google Developers: abortable-fetch
现在我们已经确定AbortController可以真正的取消请求,但是对性能的影响呢?下面我们做个实验:
使用AbortController取消 Node.js -> java server的请求
测试代码:
const fetch = require('node-fetch');
const AbortController = require('abort-controller');
function App() {
    const controller = new AbortController();
    const signal = controller.signal;
    Promise.race([fetchInterface(signal), timeout(controller)]).then(res => {
        // console.log('请求成功')
    }, err => {
        //console.log('请求失败')
    })
}
function timeout(controller) {
    const ms = 2000
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 是否取消请求
            controller.abort();
            reject({
                code: 999,
                msg: ` 调用接口超时: >${ms}ms`,
            });
        }, ms);
    });
}
function fetchInterface(signal) {
    return fetch('http://xx.xx.xx.xx:xxxx/fiveseconds', {
        signal
    })
        .then(function (response) {
            // handle success
        })
        .catch(function (error) {
            // handle error
        })
}
for (let index = 0; index < 10000; index++) {
    App()
}
// 全部延迟到6s之后关闭进程
setTimeout(() => {
    console.log('测试结束')
}, 6000)
Node.js 进程性能监控
监控工具
Node.js 版本 - v10.16.3
node-clinic
测试命令:clinic doctor -- node abort.js
测试方法
在Node.js端连续发起10000次(~~再多电脑散热器要揭竿而起~~)fetch请求,请求随机延迟0-5秒之后响应;
设置2s的超时期限;分别测试2s之后,取消请求与不取消请求的差别下Node.js进程的性能对比;
最后采用
setTimeout 6s的原因是全部延迟Node.js进程到6s之后结束,取消请求会导致2s后进程关闭,后面的性能数据无法继续采集
测试结果
- 
不取消请求的结果
- 相同点:2s之后
Promise.race返回的promise毫无疑问处于rejected状态 - 不同点:2s之后,原来发起的请求,依然处于pending状态,Node.js进程并未结束,需等待所有异步请求结束之后才会结束进程
 
 - 相同点:2s之后
 - 
取消请求的结果
- 相同点:2s之后
Promise.race返回的promise毫无疑问处于rejected状态 - 不同点:2s之后,原来发起的请求,处于canceled状态,Node.js进程结束
 
 - 相同点:2s之后
 - 
性能图对比:
- 不取消请求
- cpu持续占用,最高600%
 - 内存逐渐小幅度增加,最高252MB
 - active handles到结束逐渐减少

 
 - 取消请求
- 2s内cpu持续占用,最高355%,2s后极速降低为0
 - 2s内内存逐渐小幅度增加,最高248MB,2后极速降低到133MB
 - active handles 2s后极速降低到0

 
 
 - 不取消请求
 
CPU占用超过100%的原因JS为单线程,但是Node.js为多线程,Node.js会启用其他线程进行垃圾回收,性能优化等等,所以CPU占用是一个综合之后的数据
node-fetch timeout
现在在Node.js端做转发请求一般会使用node-fetch库,node-fetch本身实现了timeout的option,node-fetch timeout的实现原理如下:
if (request.timeout) {
    req.once('socket', socket => {
        reqTimeout = setTimeout(() => {
            reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'));
            finalize();
        }, request.timeout);
    });
}
node-fetch并不推荐使用timeout,而是建议使用标准的AbortController来控制超时:
结论
引入abort-controller polyfill包 3kb,还是值得的,周下载量超过200万,应该是挺多人在使用。