注* 发表于2012年
有一些人问过我:
那么,为什么会这样呢?简单的回答是,我们的代码时不时地阻塞了node.js的事件循环(Event Loop)。你可能已经知道了node.js —— 像浏览器中的JavaScript一样 —— 是单线程的。是由一个事件循环驱动的。一次只会处理一件事件。并行处理在这里是不存在的。所以node.js很善于处理I/O密集型的工作。因为在处理一个请求时,大部分时间是花在I/O等待上面了。(从磁盘上读取数据,从网络收发数据),但是它并不善长处理CPU密集型的工作。
当没有太多的计算量且不要求马上返回结果时,这种协同可以很好的工作,比如:
JavaScript在很多方面的设计都有缺陷,但是它有一个地方的设计是对的:无论何时requestHandler被调用(假设一个HTTP请求传进来),它会立即进行一次异步调用。假设db.getUser是一个异步操作,跟看上去的那样,它只需要很少的一些计算量,然后立即进行下一个异步I/O操作。
一年以前,Ted Dziuba 发表过 NodeJS就是癌症[2011] ,其中有一个重要的观点是
他证明了用node.js写斐波那契数列的性能的确很差。这一点没问题,但是我们不需要在服务器端用node.js来进行这种程度的数学运算。但是node.js经常在服务器端进行的CPU密集和阻塞运算,是下面一种情形:
看起来没什么问题,对吧?它接收POST过来的请求,然后解析成JSON字符串,这种方法是有效的,直到有人将一个15mb的JSON文件抛过来。
我在自己的笔记本上测试过。执行JSON.parse()解析一个15Mb的JSON文件大概需要1.5秒。同样,当我使用格式化的解析 JSON.stringify(json, null, 2) 大约需要3秒钟。
你可以会想: 1.5秒,3秒仍然很快,但你意识到这个过程中事件循环是被完全阻塞的吗?这个时侯你的node服务器不能做任何事情。当然1.5Mb看起来确实比较大,但20个200Kb的文件就很合理了。你的服务器同样会挂起。
让我们假设之前1毫秒可以处理一个请求,即1/0.001 = 1000 请求/秒 (假设你不做任何I/O操作),这个看上去不错。现在Event Loop被阻塞之后呢?
当然这个问题在许多其它技术中也会存在,基本原理是一样的,平均每个请求花的时间越长,服务器能处理的并发就越少。
注* 在主流框架中都会限制POST数据的大小,如Express:
Express -> body_parser -> raw-body 中:
有一处限制 limit 默认为 100Kb
但在受大量的POST数据攻击时,100Kb也是不够的。
OurJS博客采用限制发贴间隔,并可以起多个相互独立的服务器实例来避免单个服务的阻塞,只需配置redis(存放Session)即可,此功能尚在测试。只因流量尚未达到必须起两个以上服务的程度。
有一些人问过我:
我们的node.js服务器会偶尔挂一段时间(几秒钟),为什么会这样?
那么,为什么会这样呢?简单的回答是,我们的代码时不时地阻塞了node.js的事件循环(Event Loop)。你可能已经知道了node.js —— 像浏览器中的JavaScript一样 —— 是单线程的。是由一个事件循环驱动的。一次只会处理一件事件。并行处理在这里是不存在的。所以node.js很善于处理I/O密集型的工作。因为在处理一个请求时,大部分时间是花在I/O等待上面了。(从磁盘上读取数据,从网络收发数据),但是它并不善长处理CPU密集型的工作。
当没有太多的计算量且不要求马上返回结果时,这种协同可以很好的工作,比如:
function requestHandler(req, res) {
db.getUser(req.params.uid, function(err, user) {
res.end(user.username);
});
}JavaScript在很多方面的设计都有缺陷,但是它有一个地方的设计是对的:无论何时requestHandler被调用(假设一个HTTP请求传进来),它会立即进行一次异步调用。假设db.getUser是一个异步操作,跟看上去的那样,它只需要很少的一些计算量,然后立即进行下一个异步I/O操作。
一年以前,Ted Dziuba 发表过 NodeJS就是癌症[2011] ,其中有一个重要的观点是
让我们先从看看一些场景。拿函数调用来说,当执行此函数时当前线程会等到该功能结束,然后再继续下一个操作。通常情况下,我们认为I/O是“阻塞”的,比如你在调用socket.read(),程序会等待该调用完成后继续,因为你需要它的返回值。
这里有一个有趣的现象:每个函数的调用,在CPU里也是阻塞的。比如此功能,它计算的第n个Fibonacci数(斐波那契数列),将被阻塞CPU当前的执行线程。
function fibonacci(n) {
if (n < 2)
return 1;
else
return fibonacci(n-2) + fibonacci(n-1);
}
他证明了用node.js写斐波那契数列的性能的确很差。这一点没问题,但是我们不需要在服务器端用node.js来进行这种程度的数学运算。但是node.js经常在服务器端进行的CPU密集和阻塞运算,是下面一种情形:
function requestHandler(req, res) {
var body = req.rawBody; // Contains the POST body
try {
var json = JSON.parse(body);
res.end(json.user.username);
}
catch(e) {
res.end("FAIL");
}
}看起来没什么问题,对吧?它接收POST过来的请求,然后解析成JSON字符串,这种方法是有效的,直到有人将一个15mb的JSON文件抛过来。
我在自己的笔记本上测试过。执行JSON.parse()解析一个15Mb的JSON文件大概需要1.5秒。同样,当我使用格式化的解析 JSON.stringify(json, null, 2) 大约需要3秒钟。
你可以会想: 1.5秒,3秒仍然很快,但你意识到这个过程中事件循环是被完全阻塞的吗?这个时侯你的node服务器不能做任何事情。当然1.5Mb看起来确实比较大,但20个200Kb的文件就很合理了。你的服务器同样会挂起。
让我们假设之前1毫秒可以处理一个请求,即1/0.001 = 1000 请求/秒 (假设你不做任何I/O操作),这个看上去不错。现在Event Loop被阻塞之后呢?
- 5ms/请求 = 最多 200 请求/秒
- 50ms/请求 = 最多 20 请求/秒
- 500ms/请求 = 最多 2 请求/秒
当然这个问题在许多其它技术中也会存在,基本原理是一样的,平均每个请求花的时间越长,服务器能处理的并发就越少。
注* 在主流框架中都会限制POST数据的大小,如Express:
Express -> body_parser -> raw-body 中:
有一处限制 limit 默认为 100Kb
function onData(chunk) {
received += chunk.length
decoder
? buffer += decoder.write(chunk)
: buffer.push(chunk)
if (limit !== null && received > limit) {
var err = makeError('request entity too large', 'entity.too.large')
//……
}
}但在受大量的POST数据攻击时,100Kb也是不够的。
OurJS博客采用限制发贴间隔,并可以起多个相互独立的服务器实例来避免单个服务的阻塞,只需配置redis(存放Session)即可,此功能尚在测试。只因流量尚未达到必须起两个以上服务的程度。
回复 (0)
微信扫码 立即评论
