当流管道用于缓冲时,节点回显服务器性能下降 10 倍
Node echo server degrades 10x when stream pipes are used over buffering
在节点 v8.1.4 和 v6.11.1 上
我从以下回显服务器实现开始,我将其称为 pipe.js 或 pipe。
const http = require('http');
const handler = (req, res) => req.pipe(res);
http.createServer(handler).listen(3001);
我用 wrk 和以下内容对其进行了基准测试
lua 脚本(为简洁起见缩短)将发送一个小的 body 作为有效负载。
wrk.method = "POST"
wrk.body = string.rep("a", 10)
每秒 2k 个请求和 44 毫秒的平均延迟,性能不是很好。
所以我写了另一个使用中间缓冲区的实现,直到
请求完成,然后将这些缓冲区写出。我将其称为
buffer.js 或 缓冲区.
const http = require('http');
const handler = (req, res) => {
let buffs = [];
req.on('data', (chunk) => {
buffs.push(chunk);
});
req.on('end', () => {
res.write(Buffer.concat(buffs));
res.end();
});
};
http.createServer(handler).listen(3001);
性能发生了巨大变化,buffer.js 每次服务 20k 个请求
平均延迟为 4 毫秒。
从视觉上看,下图描述了平均数
超过 5 次运行和各种延迟百分位数(p50 是
中位数)。
因此,buffer 在所有类别中都好一个数量级。我的问题是为什么?
接下来是我的调查笔记,希望它们至少具有教育意义。
响应行为
两种实现都经过精心设计,因此它们将提供相同的确切信息
return 由 curl -D - --raw
编辑的响应。如果给出 10 d 的 body,两者都会
return 完全相同的响应(当然有修改时间):
HTTP/1.1 200 OK
Date: Thu, 20 Jul 2017 18:33:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked
a
dddddddddd
0
两者都输出 128 字节(记住这一点)。
缓冲这一事实
在语义上,两种实现之间的唯一区别是
pipe.js 在请求未结束时写入数据。这可能会让一个
怀疑 buffer.js 中可能有多个 data
事件。这不是
是的。
req.on('data', (chunk) => {
console.log(`chunk length: ${chunk.length}`);
buffs.push(chunk);
});
req.on('end', () => {
console.log(`buffs length: ${buffs.length}`);
res.write(Buffer.concat(buffs));
res.end();
});
根据经验:
- 块长度始终为 10
- 缓冲区长度始终为 1
因为永远只有一个块,如果我们移除缓冲并实现一个穷人的管道会发生什么:
const http = require('http');
const handler = (req, res) => {
req.on('data', (chunk) => res.write(chunk));
req.on('end', () => res.end());
};
http.createServer(handler).listen(3001);
事实证明,这与 pipe.js 一样糟糕。我发现这个
有趣,因为进行了相同数量的 res.write
和 res.end
调用
具有相同的参数。到目前为止,我最好的猜测是性能
差异是由于在请求数据结束后才发送响应数据。
分析
我只包含了相关行:
pipe.js
[Summary]:
ticks total nonlib name
2043 11.3% 14.1% JavaScript
11656 64.7% 80.7% C++
77 0.4% 0.5% GC
3568 19.8% Shared libraries
740 4.1% Unaccounted
[C++]:
ticks total nonlib name
6374 35.4% 44.1% syscall
2589 14.4% 17.9% writev
buffer.js
[Summary]:
ticks total nonlib name
2512 9.0% 16.0% JavaScript
11989 42.7% 76.2% C++
419 1.5% 2.7% GC
12319 43.9% Shared libraries
1228 4.4% Unaccounted
[C++]:
ticks total nonlib name
8293 29.6% 52.7% writev
253 0.9% 1.6% syscall
我们看到,在这两种实现中,C++ 支配着时间;然而,功能
占主导地位的被交换。系统调用占了将近一半的时间
pipe,但 buffer 只有 1%(请原谅我的四舍五入)。下一步,哪个
系统调用是罪魁祸首?
Strace 我们来了
像 strace -c node pipe.js
这样调用 strace 会给我们一个系统调用的总结。以下是最重要的系统调用:
pipe.js
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
43.91 0.014974 2 9492 epoll_wait
25.57 0.008720 0 405693 clock_gettime
20.09 0.006851 0 61748 writev
6.11 0.002082 0 61803 106 write
buffer.js
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
42.56 0.007379 0 121374 writev
32.73 0.005674 0 617056 clock_gettime
12.26 0.002125 0 121579 epoll_ctl
11.72 0.002032 0 121492 read
0.62 0.000108 0 1217 epoll_wait
pipe (epoll_wait
) 的顶级系统调用占 44% 的时间仅为 0.6%
buffer 的时间(增加了 140 倍)。虽然有很多时间
差异,调用 epoll_wait
的次数不那么不平衡
pipe 调用 epoll_wait
的频率提高了 ~8 倍。我们可以得出一些
该语句中的有用信息,例如 pipe 调用 epoll_wait
持续且平均而言,这些呼叫比 epoll_wait
更重
缓冲区.
对于buffer,最上面的系统调用是writev
,考虑到大多数
的时间应该花在将数据写入套接字上。
从逻辑上讲,下一步是查看这些 epoll_wait
语句
使用常规 strace,显示 buffer 始终包含 epoll_wait
100 个事件(代表与 wrk
一起使用的一百个连接)和 pipe
大部分时间少于100。像这样:
pipe.js
epoll_wait(5, [.16 snip.], 1024, 0) = 16
buffer.js
epoll_wait(5, [.100 snip.], 1024, 0) = 100
图形化:
这解释了为什么 pipe 中有更多的 epoll_wait
,因为 epoll_wait
不会在一个事件循环中为所有连接提供服务。 epoll_wait
为
零事件使事件循环看起来像是空闲的!这一切都不解释
为什么 epoll_wait
占用 pipe 的更多时间,从手册页中可以看出
epoll_wait
应该 return 立即:
specifying a timeout equal to zero cause epoll_wait() to return immediately,
even if no events are available.
虽然手册页立即说明函数 returns,但我们可以确认这一点吗? strace -T
救援:
除了支持buffer调用次数较少外,我们还可以看到几乎
所有调用都不到 100ns。 Pipe 有一个更有趣的分布
嘘一边说虽然大多数调用都在 100ns 以下,但 non-negligible 数量
更长并降落到微秒级。
Strace 确实发现了另一个奇怪的地方,那就是 writev
。 return 值为
写入的字节数。
pipe.js
writev(11, [{"HTTP/1.1 200 OK\r\nDate: Thu, 20 J"..., 109},
{"\r\n", 2}, {"dddddddddd", 10}, {"\r\n", 2}], 4) = 123
buffer.js
writev(11, [{"HTTP/1.1 200 OK\r\nDate: Thu, 20 J"..., 109},
{"\r\n", 2}, {"dddddddddd", 10}, {"\r\n", 2}, {"0\r\n\r\n", 5}], 5) = 128
还记得我说过两者都输出 128 字节吗?嗯,writev
returned 123
pipe 的字节和 buffer 的 128 个字节。 pipe 的五个字节差异是
在每个 writev
.
的后续 write
调用中协调
write(44, "0\r\n\r\n", 5)
如果我没记错的话,write
系统调用正在阻塞。
结论
如果我必须做出有根据的猜测,我会说请求时管道
未完成导致 write
次调用。这些阻塞调用显着减少
吞吐量部分地通过更频繁的 epoll_wait
语句。为什么
write
被调用而不是在 buffer 中看到的单个 writev
是
超越我。有人可以解释为什么我看到的一切正在发生吗?
踢球者?在官方Node.js
指南
您可以看到指南如何从缓冲区实现开始,然后移动
管!如果管道实现在官方指南中,则不应该
这样的性能打击,对吧?
旁白:这个问题对现实世界的性能影响应该是最小的,因为这个问题非常人为设计,尤其是在功能和 body 方面,尽管这并不意味着它是一个有用的问题。假设地,答案可能看起来像 "Node.js uses write
to allow for better performance under x situations (where x is a more real world use case)"
披露:从 my blog post 复制并稍作修改的问题,希望这是回答此问题的更好途径
2017 年 7 月 31 日编辑
我最初的假设是在请求流完成后写入回显 body 可以提高性能,但 @robertklep 用他的 readable.js (或可读) 实现:
const http = require('http');
const BUFSIZ = 2048;
const handler = (req, res) => {
req.on('readable', _ => {
let chunk;
while (null !== (chunk = req.read(BUFSIZ))) {
res.write(chunk);
}
});
req.on('end', () => {
res.end();
});
};
http.createServer(handler).listen(3001);
在 end
事件之前写入数据时,Readable 的执行级别与 buffer 相同。如果有什么让我更困惑的话,因为 readable 和我最初的穷人的管道实现之间的唯一区别是 data
和 readable
事件之间的区别,然而导致 10 倍的性能提升。但我们知道 data
事件本身并不慢,因为我们在 缓冲区 代码中使用了它。
出于好奇,strace on readable reported writev
outputs all 128 bytes output like buffer
这令人费解!
线索在于延迟,延迟大约相差 10 倍。我认为因为缓冲方法将写入调用移至 req.on('end', ...)
,服务器可以优化响应。即使在任何给定请求中只读取和写入一个十字节的缓冲区,也会同时发出许多请求。
粗略估计每秒 2K 个 10 字节请求和约 50 毫秒的延迟,我认为实际传输 'data' 所花费的时间可以忽略不计。这表明服务器在任何给定时间处理大约 100 个并发请求。
1 / .05 = 20. 2000/20 = 100
现在切换到 ~5ms 延迟,并再次考虑实际数据 tx 时间为 0。
1 / .005 = 200. 20000/200 = 100.
我们仍然让服务器处理大约 100 个在任何时间点同时传入的请求。
我不知道服务器的内部结构,但是如果你的服务器像这样达到上限,它可能会引入延迟,让 'data' 事件处理程序也处理将数据写入响应.
通过缓冲并立即返回,可以更快地释放处理程序,从而大大减少读取端的延迟。我脑海中悬而未决的问题是:处理程序真的需要将近 50 毫秒的开销来编写响应吗?我不这么认为,但如果 100 个请求正在争夺资源来写入他们的数据,那可能会开始加起来。再加上 res.end() 仍然需要调用(在不同的处理程序中)这一事实,您可能会发现延迟问题。
在'end'处理中,20K 10byte 的响应几乎不能称为数据负载,因此这是一个资源管理问题,即响应完成处理程序。如果 res.write() 和 res.end() 发生在同一个处理程序上,它可能比在一个处理程序上写入 10 个字节并在另一个处理程序上结束响应更有效。无论哪种方式,我都无法想象响应完成代码会引入任何滞后。更有可能是它急需工作(即使在缓冲方法中)。
编辑
您也可以在缓冲方法中尝试 res.end(data)
,而不是先调用 res.write(data)
再调用 res.end()
,然后看看这是否会为您的分析添加任何澄清数据点。
编辑
我刚刚在我的系统上尝试了相同的测试。我在另一台物理机上使用了 Ubuntu Linux VM 作为客户端,wrk 作为测试台,像你一样的 lua 脚本和默认设置。我使用 Windows 8 桌面到 运行 nodejs,相同的脚本,除了使用端口 8080。我的 pipe() 和缓冲的性能都比你的低得多,但相互比较,缓冲区大约是 9 倍比 pipe() 快。所以,这只是一个独立的确认。
你的问题真有趣!
事实上,缓冲与管道不是这里的问题。你有一小块;它在一个事件中处理。要显示手头的问题,您可以这样编写您的处理程序:
let chunk;
req.on('data', (dt) => {
chunk=dt
});
req.on('end', () => {
res.write(chunk);
res.end();
});
或
let chunk;
req.on('data', (dt) => {
chunk=dt;
res.write(chunk);
res.end();
});
req.on('end', () => {
});
或
let chunk;
req.on('data', (dt) => {
chunk=dt
res.write(chunk);
});
req.on('end', () => {
res.end();
});
如果 write
和 end
在同一个处理程序上,延迟会减少 10 倍。
如果勾选write
function code,这条线附近有
msg.connection.cork();
process.nextTick(connectionCorkNT, msg.connection);
cork
和 uncork
连接下一个事件。这意味着您为数据使用缓存,然后在处理其他事件之前强制在下一个事件上发送数据。
总而言之,如果您在不同的处理程序上有 write
和 end
,您将有:
- 软木塞连接(+ 创建一个勾号来打开软木塞)
- 用数据创建缓冲区
- 断开与另一个事件的连接(发送数据)
- 调用结束进程(发送另一个包含最终块的数据包并关闭)
如果它们在同一个处理程序上,end
函数在处理 uncork
事件之前被调用,因此最终块将在缓存中。
- 软木连接
- 用数据创建缓冲区
- 在缓冲区上添加“结束”块
- 打开连接发送所有内容
还有end
函数runs cork
/ uncork
synchronously,会快一点
为什么这很重要?因为在 TCP 端,如果您发送一个包含数据的数据包,并希望发送更多,进程将在发送更多之前等待来自客户端的确认:
write
+ end
在不同的处理程序上:
- 0.044961s:
POST
/ => 是请求
- 0.045322s:
HTTP/1.1
=> 第一个块:header + "aaaaaaaaa"
- 0.088522s:确认数据包
- 0.088567s:继续 => 第二块(结束部分,
0\r\n\r\n
)
在发送第一个缓冲区后 ack
前约 40 毫秒。
write
+ end
在同一处理程序中:
数据在一个数据包中完整,不需要ack
。
为什么 ACK
是 40 毫秒?这是 OS 中的一项 built-in 功能,可提高整体性能。 section 4.2.3.2 of IETF RFC 1122: When to Send an ACK Segment' 中对其进行了描述。
Red Hat (Fedora/CentOS/RHEL) 使用 40ms:它是一个参数并且 can be modified。在 Debian(包括 Ubuntu)上,它似乎被硬编码为 40 毫秒,因此它不可修改(除非您使用 TCP_NO_DELAY
选项创建连接)。
我希望这些足够详细,可以让您对流程有更多的了解。这个答案已经很大了,所以我想就到这里吧。
可读
我查看了你关于 readable
的注释。大胆的猜测:如果 readable
检测到一个空输入,它会在同一刻关闭流。
编辑:我阅读了可读性代码。正如我所怀疑的:
https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L371
https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L1036
如果读取完成一个事件,end
会立即发出以供下一步处理。
所以事件处理是:
readable
事件:读取数据
readable
检测到它已完成 => 创建 end
事件
- 您写入数据,以便它创建一个事件来开瓶
end
事件已处理(开瓶完成)
- 开瓶已处理(但什么都不做,因为一切都已经完成)
如果减少缓冲区:
req.on('readable',()=> {
let chunk2;
while (null !== (chunk2 = req.read(5))) {
res.write(chunk2);
}
});
这会强制写入两次。该过程将是:
readable
事件:读取数据。你得到五个 a
s.
- 您写入了创建开瓶器事件的数据
- 您读取了数据。
readable
检测到它已完成 => 创建 end
事件
- 你写入数据,它被添加到缓冲数据中
- uncork 已处理(因为它是在
end
之前启动的);你发送数据
end
事件已处理(解开完成)=> 等待 ACK
发送最终块
- 过程会很慢(确实如此;我检查过)
在节点 v8.1.4 和 v6.11.1 上
我从以下回显服务器实现开始,我将其称为 pipe.js 或 pipe。
const http = require('http');
const handler = (req, res) => req.pipe(res);
http.createServer(handler).listen(3001);
我用 wrk 和以下内容对其进行了基准测试 lua 脚本(为简洁起见缩短)将发送一个小的 body 作为有效负载。
wrk.method = "POST"
wrk.body = string.rep("a", 10)
每秒 2k 个请求和 44 毫秒的平均延迟,性能不是很好。
所以我写了另一个使用中间缓冲区的实现,直到 请求完成,然后将这些缓冲区写出。我将其称为 buffer.js 或 缓冲区.
const http = require('http');
const handler = (req, res) => {
let buffs = [];
req.on('data', (chunk) => {
buffs.push(chunk);
});
req.on('end', () => {
res.write(Buffer.concat(buffs));
res.end();
});
};
http.createServer(handler).listen(3001);
性能发生了巨大变化,buffer.js 每次服务 20k 个请求 平均延迟为 4 毫秒。
从视觉上看,下图描述了平均数 超过 5 次运行和各种延迟百分位数(p50 是 中位数)。
因此,buffer 在所有类别中都好一个数量级。我的问题是为什么?
接下来是我的调查笔记,希望它们至少具有教育意义。
响应行为
两种实现都经过精心设计,因此它们将提供相同的确切信息
return 由 curl -D - --raw
编辑的响应。如果给出 10 d 的 body,两者都会
return 完全相同的响应(当然有修改时间):
HTTP/1.1 200 OK
Date: Thu, 20 Jul 2017 18:33:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked
a
dddddddddd
0
两者都输出 128 字节(记住这一点)。
缓冲这一事实
在语义上,两种实现之间的唯一区别是
pipe.js 在请求未结束时写入数据。这可能会让一个
怀疑 buffer.js 中可能有多个 data
事件。这不是
是的。
req.on('data', (chunk) => {
console.log(`chunk length: ${chunk.length}`);
buffs.push(chunk);
});
req.on('end', () => {
console.log(`buffs length: ${buffs.length}`);
res.write(Buffer.concat(buffs));
res.end();
});
根据经验:
- 块长度始终为 10
- 缓冲区长度始终为 1
因为永远只有一个块,如果我们移除缓冲并实现一个穷人的管道会发生什么:
const http = require('http');
const handler = (req, res) => {
req.on('data', (chunk) => res.write(chunk));
req.on('end', () => res.end());
};
http.createServer(handler).listen(3001);
事实证明,这与 pipe.js 一样糟糕。我发现这个
有趣,因为进行了相同数量的 res.write
和 res.end
调用
具有相同的参数。到目前为止,我最好的猜测是性能
差异是由于在请求数据结束后才发送响应数据。
分析
我只包含了相关行:
pipe.js
[Summary]:
ticks total nonlib name
2043 11.3% 14.1% JavaScript
11656 64.7% 80.7% C++
77 0.4% 0.5% GC
3568 19.8% Shared libraries
740 4.1% Unaccounted
[C++]:
ticks total nonlib name
6374 35.4% 44.1% syscall
2589 14.4% 17.9% writev
buffer.js
[Summary]:
ticks total nonlib name
2512 9.0% 16.0% JavaScript
11989 42.7% 76.2% C++
419 1.5% 2.7% GC
12319 43.9% Shared libraries
1228 4.4% Unaccounted
[C++]:
ticks total nonlib name
8293 29.6% 52.7% writev
253 0.9% 1.6% syscall
我们看到,在这两种实现中,C++ 支配着时间;然而,功能 占主导地位的被交换。系统调用占了将近一半的时间 pipe,但 buffer 只有 1%(请原谅我的四舍五入)。下一步,哪个 系统调用是罪魁祸首?
Strace 我们来了
像 strace -c node pipe.js
这样调用 strace 会给我们一个系统调用的总结。以下是最重要的系统调用:
pipe.js
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
43.91 0.014974 2 9492 epoll_wait
25.57 0.008720 0 405693 clock_gettime
20.09 0.006851 0 61748 writev
6.11 0.002082 0 61803 106 write
buffer.js
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
42.56 0.007379 0 121374 writev
32.73 0.005674 0 617056 clock_gettime
12.26 0.002125 0 121579 epoll_ctl
11.72 0.002032 0 121492 read
0.62 0.000108 0 1217 epoll_wait
pipe (epoll_wait
) 的顶级系统调用占 44% 的时间仅为 0.6%
buffer 的时间(增加了 140 倍)。虽然有很多时间
差异,调用 epoll_wait
的次数不那么不平衡
pipe 调用 epoll_wait
的频率提高了 ~8 倍。我们可以得出一些
该语句中的有用信息,例如 pipe 调用 epoll_wait
持续且平均而言,这些呼叫比 epoll_wait
更重
缓冲区.
对于buffer,最上面的系统调用是writev
,考虑到大多数
的时间应该花在将数据写入套接字上。
从逻辑上讲,下一步是查看这些 epoll_wait
语句
使用常规 strace,显示 buffer 始终包含 epoll_wait
100 个事件(代表与 wrk
一起使用的一百个连接)和 pipe
大部分时间少于100。像这样:
pipe.js
epoll_wait(5, [.16 snip.], 1024, 0) = 16
buffer.js
epoll_wait(5, [.100 snip.], 1024, 0) = 100
图形化:
这解释了为什么 pipe 中有更多的 epoll_wait
,因为 epoll_wait
不会在一个事件循环中为所有连接提供服务。 epoll_wait
为
零事件使事件循环看起来像是空闲的!这一切都不解释
为什么 epoll_wait
占用 pipe 的更多时间,从手册页中可以看出
epoll_wait
应该 return 立即:
specifying a timeout equal to zero cause epoll_wait() to return immediately, even if no events are available.
虽然手册页立即说明函数 returns,但我们可以确认这一点吗? strace -T
救援:
除了支持buffer调用次数较少外,我们还可以看到几乎 所有调用都不到 100ns。 Pipe 有一个更有趣的分布 嘘一边说虽然大多数调用都在 100ns 以下,但 non-negligible 数量 更长并降落到微秒级。
Strace 确实发现了另一个奇怪的地方,那就是 writev
。 return 值为
写入的字节数。
pipe.js
writev(11, [{"HTTP/1.1 200 OK\r\nDate: Thu, 20 J"..., 109},
{"\r\n", 2}, {"dddddddddd", 10}, {"\r\n", 2}], 4) = 123
buffer.js
writev(11, [{"HTTP/1.1 200 OK\r\nDate: Thu, 20 J"..., 109},
{"\r\n", 2}, {"dddddddddd", 10}, {"\r\n", 2}, {"0\r\n\r\n", 5}], 5) = 128
还记得我说过两者都输出 128 字节吗?嗯,writev
returned 123
pipe 的字节和 buffer 的 128 个字节。 pipe 的五个字节差异是
在每个 writev
.
write
调用中协调
write(44, "0\r\n\r\n", 5)
如果我没记错的话,write
系统调用正在阻塞。
结论
如果我必须做出有根据的猜测,我会说请求时管道
未完成导致 write
次调用。这些阻塞调用显着减少
吞吐量部分地通过更频繁的 epoll_wait
语句。为什么
write
被调用而不是在 buffer 中看到的单个 writev
是
超越我。有人可以解释为什么我看到的一切正在发生吗?
踢球者?在官方Node.js 指南 您可以看到指南如何从缓冲区实现开始,然后移动 管!如果管道实现在官方指南中,则不应该 这样的性能打击,对吧?
旁白:这个问题对现实世界的性能影响应该是最小的,因为这个问题非常人为设计,尤其是在功能和 body 方面,尽管这并不意味着它是一个有用的问题。假设地,答案可能看起来像 "Node.js uses write
to allow for better performance under x situations (where x is a more real world use case)"
披露:从 my blog post 复制并稍作修改的问题,希望这是回答此问题的更好途径
2017 年 7 月 31 日编辑
我最初的假设是在请求流完成后写入回显 body 可以提高性能,但 @robertklep 用他的 readable.js (或可读) 实现:
const http = require('http');
const BUFSIZ = 2048;
const handler = (req, res) => {
req.on('readable', _ => {
let chunk;
while (null !== (chunk = req.read(BUFSIZ))) {
res.write(chunk);
}
});
req.on('end', () => {
res.end();
});
};
http.createServer(handler).listen(3001);
在 end
事件之前写入数据时,Readable 的执行级别与 buffer 相同。如果有什么让我更困惑的话,因为 readable 和我最初的穷人的管道实现之间的唯一区别是 data
和 readable
事件之间的区别,然而导致 10 倍的性能提升。但我们知道 data
事件本身并不慢,因为我们在 缓冲区 代码中使用了它。
出于好奇,strace on readable reported writev
outputs all 128 bytes output like buffer
这令人费解!
线索在于延迟,延迟大约相差 10 倍。我认为因为缓冲方法将写入调用移至 req.on('end', ...)
,服务器可以优化响应。即使在任何给定请求中只读取和写入一个十字节的缓冲区,也会同时发出许多请求。
粗略估计每秒 2K 个 10 字节请求和约 50 毫秒的延迟,我认为实际传输 'data' 所花费的时间可以忽略不计。这表明服务器在任何给定时间处理大约 100 个并发请求。
1 / .05 = 20. 2000/20 = 100
现在切换到 ~5ms 延迟,并再次考虑实际数据 tx 时间为 0。
1 / .005 = 200. 20000/200 = 100.
我们仍然让服务器处理大约 100 个在任何时间点同时传入的请求。
我不知道服务器的内部结构,但是如果你的服务器像这样达到上限,它可能会引入延迟,让 'data' 事件处理程序也处理将数据写入响应.
通过缓冲并立即返回,可以更快地释放处理程序,从而大大减少读取端的延迟。我脑海中悬而未决的问题是:处理程序真的需要将近 50 毫秒的开销来编写响应吗?我不这么认为,但如果 100 个请求正在争夺资源来写入他们的数据,那可能会开始加起来。再加上 res.end() 仍然需要调用(在不同的处理程序中)这一事实,您可能会发现延迟问题。
在'end'处理中,20K 10byte 的响应几乎不能称为数据负载,因此这是一个资源管理问题,即响应完成处理程序。如果 res.write() 和 res.end() 发生在同一个处理程序上,它可能比在一个处理程序上写入 10 个字节并在另一个处理程序上结束响应更有效。无论哪种方式,我都无法想象响应完成代码会引入任何滞后。更有可能是它急需工作(即使在缓冲方法中)。
编辑
您也可以在缓冲方法中尝试 res.end(data)
,而不是先调用 res.write(data)
再调用 res.end()
,然后看看这是否会为您的分析添加任何澄清数据点。
编辑
我刚刚在我的系统上尝试了相同的测试。我在另一台物理机上使用了 Ubuntu Linux VM 作为客户端,wrk 作为测试台,像你一样的 lua 脚本和默认设置。我使用 Windows 8 桌面到 运行 nodejs,相同的脚本,除了使用端口 8080。我的 pipe() 和缓冲的性能都比你的低得多,但相互比较,缓冲区大约是 9 倍比 pipe() 快。所以,这只是一个独立的确认。
你的问题真有趣!
事实上,缓冲与管道不是这里的问题。你有一小块;它在一个事件中处理。要显示手头的问题,您可以这样编写您的处理程序:
let chunk;
req.on('data', (dt) => {
chunk=dt
});
req.on('end', () => {
res.write(chunk);
res.end();
});
或
let chunk;
req.on('data', (dt) => {
chunk=dt;
res.write(chunk);
res.end();
});
req.on('end', () => {
});
或
let chunk;
req.on('data', (dt) => {
chunk=dt
res.write(chunk);
});
req.on('end', () => {
res.end();
});
如果 write
和 end
在同一个处理程序上,延迟会减少 10 倍。
如果勾选write
function code,这条线附近有
msg.connection.cork();
process.nextTick(connectionCorkNT, msg.connection);
cork
和 uncork
连接下一个事件。这意味着您为数据使用缓存,然后在处理其他事件之前强制在下一个事件上发送数据。
总而言之,如果您在不同的处理程序上有 write
和 end
,您将有:
- 软木塞连接(+ 创建一个勾号来打开软木塞)
- 用数据创建缓冲区
- 断开与另一个事件的连接(发送数据)
- 调用结束进程(发送另一个包含最终块的数据包并关闭)
如果它们在同一个处理程序上,end
函数在处理 uncork
事件之前被调用,因此最终块将在缓存中。
- 软木连接
- 用数据创建缓冲区
- 在缓冲区上添加“结束”块
- 打开连接发送所有内容
还有end
函数runs cork
/ uncork
synchronously,会快一点
为什么这很重要?因为在 TCP 端,如果您发送一个包含数据的数据包,并希望发送更多,进程将在发送更多之前等待来自客户端的确认:
write
+ end
在不同的处理程序上:
- 0.044961s:
POST
/ => 是请求 - 0.045322s:
HTTP/1.1
=> 第一个块:header + "aaaaaaaaa" - 0.088522s:确认数据包
- 0.088567s:继续 => 第二块(结束部分,
0\r\n\r\n
)
在发送第一个缓冲区后 ack
前约 40 毫秒。
write
+ end
在同一处理程序中:
数据在一个数据包中完整,不需要ack
。
为什么 ACK
是 40 毫秒?这是 OS 中的一项 built-in 功能,可提高整体性能。 section 4.2.3.2 of IETF RFC 1122: When to Send an ACK Segment' 中对其进行了描述。
Red Hat (Fedora/CentOS/RHEL) 使用 40ms:它是一个参数并且 can be modified。在 Debian(包括 Ubuntu)上,它似乎被硬编码为 40 毫秒,因此它不可修改(除非您使用 TCP_NO_DELAY
选项创建连接)。
我希望这些足够详细,可以让您对流程有更多的了解。这个答案已经很大了,所以我想就到这里吧。
可读
我查看了你关于 readable
的注释。大胆的猜测:如果 readable
检测到一个空输入,它会在同一刻关闭流。
编辑:我阅读了可读性代码。正如我所怀疑的:
https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L371
https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L1036
如果读取完成一个事件,end
会立即发出以供下一步处理。
所以事件处理是:
readable
事件:读取数据readable
检测到它已完成 => 创建end
事件- 您写入数据,以便它创建一个事件来开瓶
end
事件已处理(开瓶完成)- 开瓶已处理(但什么都不做,因为一切都已经完成)
如果减少缓冲区:
req.on('readable',()=> {
let chunk2;
while (null !== (chunk2 = req.read(5))) {
res.write(chunk2);
}
});
这会强制写入两次。该过程将是:
readable
事件:读取数据。你得到五个a
s.- 您写入了创建开瓶器事件的数据
- 您读取了数据。
readable
检测到它已完成 => 创建end
事件 - 你写入数据,它被添加到缓冲数据中
- uncork 已处理(因为它是在
end
之前启动的);你发送数据 end
事件已处理(解开完成)=> 等待ACK
发送最终块- 过程会很慢(确实如此;我检查过)