Javascript 当在缓冲区上使用流管道时,节点echo服务器性能下降10倍

Javascript 当在缓冲区上使用流管道时,节点echo服务器性能下降10倍,javascript,node.js,http,stream,output-buffering,Javascript,Node.js,Http,Stream,Output Buffering,在节点v8.1.4和v6.11.1上 我从以下echo服务器实现开始,我将其称为pipe.js或pipe 我用和以下内容对其进行了基准测试 lua脚本(为了简洁而缩短),它将发送一个小体作为有效负载 wrk.method = "POST" wrk.body = string.rep("a", 10) 在每秒2k个请求和平均延迟44ms的情况下,性能不是很好 因此,我编写了另一个实现,它使用中间缓冲区,直到 请求完成,然后写出这些缓冲区。我将把它称为 buffer.js或buffer buf

在节点v8.1.4和v6.11.1上

我从以下echo服务器实现开始,我将其称为pipe.js或pipe

我用和以下内容对其进行了基准测试 lua脚本(为了简洁而缩短),它将发送一个小体作为有效负载

wrk.method = "POST"
wrk.body   = string.rep("a", 10)
在每秒2k个请求和平均延迟44ms的情况下,性能不是很好

因此,我编写了另一个实现,它使用中间缓冲区,直到 请求完成,然后写出这些缓冲区。我将把它称为 buffer.js或buffer

buffer.js每年可处理20k个请求,性能发生了巨大变化 第二,平均延迟为4ms

从视觉上看,下图描绘了平均数量 在5次运行和不同延迟百分比(p50为 中位数)

因此,在所有类别中,缓冲区都是一个数量级。我的问题是为什么

接下来是我的调查笔记,希望它们至少有教育意义

反应行为 这两种实现都经过精心设计,因此它们将给出相同的精确结果 响应由
curl-D----raw
返回。如果给一个10个d的物体,两个都会 返回完全相同的响应(当然是修改时间):

两者都输出128字节(记住这一点)

仅仅是缓冲的事实 从语义上讲,这两种实现之间的唯一区别是 pipe.js在请求尚未结束时写入数据。这可能是一个 怀疑buffer.js中可能存在多个
数据
事件。这不是
对

根据经验:

  • 区块长度将始终为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

buffer.js

我们看到,在两个实现中,C++支配时间;但是,功能 这是交换的。Syscalls占了 管道,但缓冲区仅为1%(请原谅我的舍入)。下一步是什么 系统调用是罪魁祸首

斯特拉斯,我们来了 调用类似strace的
strace-c node pipe.js
将为我们提供系统调用的摘要。以下是最重要的系统调用:

pipe.js

buffer.js

对于管道(
epoll\u wait
)来说,占44%时间的顶级系统调用仅占0.6% 缓冲时间(增加140倍)。虽然时间很长 差异,调用
epoll\u wait
的次数与 管道调用
epoll\u wait
~8倍的频率。我们可以推导出一些公式 来自该语句的有用信息,例如管道调用
epoll\u wait
这些呼叫通常都比等待的epoll\u要重 缓冲区

对于缓冲区,最上面的系统调用是
writev
,这是考虑到大多数 将数据写入套接字应该花费多少时间

逻辑上,下一步是查看这些
epoll\u wait
语句 使用常规strace,显示缓冲区始终包含
epoll\u wait
100个事件(表示与
wrk
一起使用的100个连接)和管道 大部分时间都不到100人。像这样:

pipe.js

buffer.js

以图形方式:

这解释了为什么管道中有更多的
epoll\u wait
,就像
epoll\u wait
不为一个事件循环中的所有连接提供服务。
epoll\u等待
零事件使事件循环看起来是空闲的!这一切都无法解释
为什么
epoll\u wait
占用管道更多时间,如手册页所述
epoll\u wait
应立即返回:

指定等于零的超时将导致epoll_wait()立即返回, 即使没有可用的事件

虽然手册页上说函数立即返回,但我们可以确认这一点吗<代码>战略-T
救援:

除了支持缓冲区有更少的调用外,我们还可以看到 所有通话时间均少于100纳秒。管道的分布更有趣 这表明,虽然大多数通话时间都在100ns以下,但通话量却不可忽略 然后降落到微秒级

斯特拉斯确实发现了另一个奇怪之处,那就是
writev
。返回值为 写入的字节数

pipe.js

buffer.js

还记得我说过两个都输出128字节吗?嗯,
writev
返回123 字节用于管道,128字节用于缓冲区。管道的五个字节差异为 在后续的
write
调用中对每个
writev
进行对账

write(44, "0\r\n\r\n", 5)
如果我没弄错的话,
write
syscalls被阻塞了

结论 如果我必须做出一个有根据的猜测,我会说当请求 未完成导致
写入
调用。这些阻塞调用大大减少了开销 吞吐量部分通过更频繁的
epoll\u wait
语句实现。为什么? 调用
write
,而不是在缓冲区中看到的单个
writev
超越我。有人能解释为什么我看到的一切都在发生吗

踢球者?在 您可以看到指南如何从缓冲区实现开始,然后移动 管!如果管道实现在官方指南中,那么不应该 这么卖座,对吧

旁白:这个问题对现实世界的性能影响应该是最小的,因为这个问题是人为的
HTTP/1.1 200 OK
Date: Thu, 20 Jul 2017 18:33:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked

a
dddddddddd
0
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();
});
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);
 [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
 [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
% 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
% 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
epoll_wait(5, [.16 snip.], 1024, 0) = 16
epoll_wait(5, [.100 snip.], 1024, 0) = 100
writev(11, [{"HTTP/1.1 200 OK\r\nDate: Thu, 20 J"..., 109},
  {"\r\n", 2}, {"dddddddddd", 10}, {"\r\n", 2}], 4) = 123
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
write(44, "0\r\n\r\n", 5)
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);
1 / .05 = 20.  2000/20 = 100
1 / .005 = 200.  20000/200 = 100.
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();
});
msg.connection.cork();
process.nextTick(connectionCorkNT, msg.connection);
req.on('readable',()=> {
    let chunk2;
    while (null !== (chunk2 = req.read(5))) {
        res.write(chunk2);
    }
});