关于Koa2.0框架中间件数量过多后存在的栈溢出隐患
发布于 8 年前 作者 hyj1991 6074 次浏览 来自 问答

今天突然想起来,Koa2.0这种Promise.resolve嵌套的方式类似递归,不知道在中间件数量比较庞大的时候会不会造成栈溢出,虽然比较罕见,测试代码如下:

'use strict';
const compose = require('koa-compose');
let middlewares = [];

for (let i = 0; i < 10000; i++) {
	middlewares.push(function (ctx, next) {
    	return next();
	});
}
compose(middlewares)({}).then(data=>console.log(data)).catch(err=>console.error(err));

结果

//在compose的return dispatch(i+1)处打印了 console.log(i+1)
4826
4827
4828
4829
[RangeError: Maximum call stack size exceeded]

在运行到5000左右的时候居然真的栈溢出了… 好吧,虽然一个项目使用到5k的中间件相当罕见,但是说明还是有这个隐患点的…尤其是如果在中间件里面又进行了一些递归操作的话

18 回复

在koa2.0下,如果编写的中间件使用co.wrap或者koa-convert,实测满足栈溢出的中间件数量下限下降很快,co.wrap大概1200个就会栈溢出,而使用koa-convert,在800个左右的中间件就会栈溢出… 这样的话,考虑到自己编写的业务逻辑里面的额函数调用栈,中间件数量还要进一步降低 有没有人遇到过这样的问题呢,还是我测验的时候逻辑有问题 按理来说,一个不错的web框架不应该有这种问题才是…

我没见过中间件嵌套特别多的,koa的benchmark最多才做到50个。。。。。兄弟,你这是要闹哪样?

中间件有事没事都过一遍这也不是一种好的设计

@i5ting 我也觉得不大可能有这么多中间件,但是这样一来,总感觉人为的有了一些限制 而且就像三楼的朋友说的那样,中间件数量一多,所有的request都要过一遍,性能上也会降低;我在想有没有办法实现按需加载中间件的机制 这两天做view层的npm组件化,有点忙,今天才想起来上下CNODE社区,所以这么久才回复,哈哈

@yakczh 确实,所以我空闲的时候也在想,能不能实现中间件的按需加载机制

@hyj1991 新版的compose又有大大的性能提升,本社区的fundon同学使用es6的迭代器写的,很快会合并.

@i5ting 期待源码,学习下 koa1.0和koa2.0的compose我都看过了,其实相比较而言,1.x的compose就像黑魔法,第一次看到是一种惊艳的感觉 2.x的compose就平淡了很多,当然相对来说,基于Promise的compose泛用性也好一些 期待新的compose

这是 V8层面的stackoverflow的检查,尝试调整系统 stack 大小,MacOSX 默认8192, ulimit -s 16384。

我现在用的compose 是 3.1.0的 新版的compose是哪个版本?

@i5ting es6 的迭代其方案 有问题,高兴早了

@fundon 能大致说下实现思路吗,了解下,哈哈

<div><p><!-- react-text: 7690 -->test<!-- /react-text --></p></div>

@fundon 感谢您的分享,代码我研究了下,您提供的例子可以拆解成:

let arrTmp = [];
Promise.resolve(co.wrap(function * (context, next) {
    	arrTmp.push(1);
    	yield wait(1);
    	yield Promise.resolve().then(() => co.wrap(function * (context, next) {
        	arrTmp.push(2)
        	yield wait(1)
        	yield Promise.resolve().then(()=>co.wrap(function * (context, next) {
            	arrTmp.push(3)
            	yield wait(1)
            	yield Promise.resolve().then(()=>undefined);
            	yield wait(1)
            	arrTmp.push(4)
        	})({}));
        	yield wait(1)
        	arrTmp.push(5)
    	})({}));
    	yield wait(1);
    	arrTmp.push(6);
	})({})
).then(function () {
	console.log(2, arrTmp)
	console.log(2, arrTmp.toString() === [1, 2, 3, 4, 5, 6].toString())
}).catch(err=>console.error(err.stack));

原理大致弄明白了,但是有几个疑惑:

1. 这个conpose的逻辑其实和ES6的迭代器关系并不大吧

因为结束条件和done:false无关,并且SYMBOL—ITERATOR 其实只是一个key,命名成别的也没关系, 而且结束控制是由:

value: fn && fn()

这个决定的,当中间件没有时,value为undefined,此时最后一个中间件的next输出:

yield Promise.resolve().then(()=>undefined);

那么就等于结束了

2.想不通为什么这样实现可以杜绝栈溢出的问题

我测试了下,确实这样实现的中间件不会有栈溢出错误,只会当中间件数量庞大时,内存溢出,但是这个目前来看是无关紧要的 但是始终想不明白,为什么把

Promise.resolve(co.wrap(function * (){
	yiled Promise.resolve(co.wrap(function * (){
		yiled Promise.resolve(co.wrap(function * (){
			yiled ...
		}))
	}()))
}()))

转换为:

Promise.resolve(co.wrap(function * () {
	yield Promise.resolve().then(() => co.wrap(function * () {
    		yield Promise.resolve().then(()=>co.wrap(function * () {
        		yield...
    		})({}));
	})({}));
})({}))

之后,就不会出现栈溢出了呢,求解惑

最后真的非常感谢您的分享,哈哈

@i5ting @fundon 我写了两个递归的测试函数,

function promise1(i) {
	console.log(i);
	i++;
	return Promise.resolve(promise1(i))
}

function promise2(i) {
	console.log(i);
	i++;
	return Promise.resolve().then(()=>promise2(i))
}

//会栈溢出
promise1(1);
//不会栈溢出
promise2(1);

感觉就是koa2.x官方的compose和@fundon 兄弟写的最大区别所在,第一个栈溢出我觉得是可以理解的,但是第二个函数写法为什么不会造成栈溢出呢,百思不得解哇,是因为then里面的本质上是注册的回调函数嘛,所以这个函数的栈信息不会记录到parent函数的调用栈里面吗? 有没有这方面的文章,想详细了解下

@hyj1991 then其实做了setImmediate在执行的,1是执行递归执行

@robbenmu 感谢,我后来仔细想了下,确实是这样的

回到顶部