【问题标题】:Prevent memory/connection leaks when piping fetched response to client在管道获取对客户端的响应时防止内存/连接泄漏
【发布时间】:2021-10-21 02:02:27
【问题描述】:

上下文:

我有一些代码大致如下:

const express = require("express");
const app = express();
const fetch = require('node-fetch');

app.get("/file/:path", async function(request, response) {
  const path = request.params.path;
  
  const reqController = new AbortController();
  const reqTimeout = setTimeout(() => reqController.abort(), 10000);

  const r = await fetch(`https://cdn.example.com/${encodeURIComponent(path)}`, {
    timeout:10000,
    signal: reqController.signal,
  }).catch(e => false).finally(() => clearTimeout(reqTimeout));

  if(r === false || !r.ok) {
    response.send("error");
  } else {
    r.body.pipe(response);
  } 
});

...

所以您可以看到,我只是使用node-fetch 获取响应,然后将其传送到客户端响应流。

请注意,在实际代码中,我对错误的处理比.catch(e => false) 更好。很少有错误(几个月没有错误),所以我想我会省略这些细节。

问题:

此代码泄漏 TCP 连接或内存的可能方式有哪些?我如何“覆盖”这些案例?

额外信息:

我原以为默认情况下node-fetch/express 会有默认的流式超时(基于自收到最后一个块以来的时间),以防止泄漏,但情况似乎并非如此。

我试过添加这样的代码:

r.body.on('error', (error) => { 
  response.connection.destroy();
});
response.on('error', (error) => {
  r.body.cancel();
});

r.body.pipe(response);

我对 node-fetch 和 Node.js 的流的内部没有很好的理解,所以这只是对可能涵盖某种异常故障模式的猜测,但事实证明这并没有解决问题。

请注意,我使用的是node-fetchtimeout 选项(它是浏览器fetch 中不可用的特殊扩展名)。但是我刚刚编辑了第一个代码块,以表明我也在真实/未简化的代码中使用了中止信号。我最初添加了中止信号,因为我认为timeout 选项可能存在错误,但我仍然看到连接泄漏。现在仔细研究一下,我发现timeout 选项重置了重定向的超时时间,所以这可能是连接泄漏的一个有趣原因(继续无限重定向?但我猜无论如何都有重定向限制),但是唉,这不是我在这里遇到的问题,因为我也收到了中止信号。我正在使用 node-fetch v2.6.1 和 Node.js v14.7.0

在应用程序/服务器重启几分钟后,知道这就是我在 netstat -ancat /proc/net/sockstat 上看到的内容,这也可能会有所帮助:

{
  LISTEN: 37,
  TIME_WAIT: 4644,
  ESTABLISHED: 268,
  CLOSE_WAIT: 1,
  SYN_SENT: 1,
  SYN_RECV: 4,
  FIN_WAIT1: 3
}
sockets: used 801
TCP: inuse 94 orphan 4 tw 4642 alloc 312 mem 5305
UDP: inuse 2 mem 1
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

几个小时后,您可以看到/proc/net/sockstat 中的 TCP mem 值大幅攀升:

{
  LISTEN: 37,
  TIME_WAIT: 4151,
  CLOSE_WAIT: 37,
  ESTABLISHED: 337,
  LAST_ACK: 1,
  SYN_SENT: 1,
  SYN_RECV: 4
}
sockets: used 897
TCP: inuse 171 orphan 0 tw 4151 alloc 413 mem 63487
UDP: inuse 2 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

几天后,mem 增长到大约 600k,接近我在/proc/sys/net/ipv4/tcp_mem 中看到的最大/临界阈值设置。 tcp inuse 值达到 5k 左右。服务器每秒处理大约十几个请求,因此微小/罕见的泄漏可能会在几天内变得很严重。

我尝试过每隔几分钟手动触发一次垃圾收集,以防这是由于奇怪/错误的 GC 调度(只是一个疯狂的猜测),但这没有帮助。

【问题讨论】:

  • 您似乎将await fetch()fetch().catch() 混合在一起。在异步函数中,您使用 try {} catch (err){} 来处理像 fetch() 这样的函数失败的情况。您可能有失败,但您没有记录它们。
  • @O.Jones 如果.catch(e => ...) 方法在功能上与这里的try{}catch(e){} 不同,我会感到非常惊讶!我应该注意到在实际代码中我正在记录错误(事实上,我已经设置了一个监控服务,以便在出现错误时向我发送电子邮件——它们真的很少见)。我现在将编辑问题以指定这一点。
  • 有了你的额外信息Streams 应该可以无缝地处理源和目标之间的同步;但是,如果您希望在连接仍在进行的情况下在一段时间后结束连接,那么您可以使用 node-fetch GH 页面上提到的 Request cancel with AbortSignal。结束响应也将是对response.end() 的调用。
  • @JavadM.Amiri 我正在使用node-fetch 的超时选项(如我的代码示例所示),但实际上我在实际代码中也设置了一个中止信号(我最初添加它是因为我认为timeout 选项可能存在错误)。我会将此信息添加到我的帖子中。

标签: javascript node.js stream node-fetch node-streams


【解决方案1】:

阅读这篇文章以了解如何中止提取请求:https://davidwalsh.name/cancel-fetch

您可以设置超时来中止您的 fetch 调用:

fetch(`https://cdn.example.com/${encodeURIComponent(path)}`, { signal }).then(response => {
    console.log(`Request is complete!`);
}).catch(e => {
    if(e.name === "AbortError") {
        // We know it's been canceled!
        console.warn(`Fetch aborted: ${e.message}`);
    }
    else{
      console.warn(`Some other fetch error: ${e.message}`);
    }
});

// Wait 2 seconds to abort the request
setTimeout(() => controller.abort(), 2000);

【讨论】:

  • node-fetch 带有一个 timeout 选项(如原始代码所示)我已设置为 10000 毫秒。在实际代码中,我实际上有一个中止信号 node-fetch 超时(因为我最初认为timeout 选项可能存在错误),但我仍然看到 tcp 连接泄漏所以我得出结论,这里有一个更深层次的问题。我会将这些详细信息添加到我的问题帖子中。
  • 检查是否有任何 TCP 连接泄漏,我建议使用 Wireshark,因为它可以让您更全面地了解网络活动。一旦你知道这背后的真正问题是什么,你就可以在代码中处理它。 wireshark.org/download.html
猜你喜欢
  • 2019-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-21
  • 2015-02-05
相关资源
最近更新 更多