【问题标题】:Express.js - abort request on timeoutExpress.js - 超时中止请求
【发布时间】:2021-10-12 19:24:23
【问题描述】:

我正在探索如何中止耗时过长的客户端请求,从而消耗服务器资源。 阅读了一些资料(见下文)后,我尝试了如下解决方案(建议here):

const express = require('express');
const server = express();
server
    .use((req, res, next) => {
        req.setTimeout(5000, () => {
            console.log('req timeout!');
            res.status(400).send('Request timeout');
        });
        res.setTimeout(5000, () => {
            console.log('res timeout!');
            res.status(400).send('Request timeout');
        });
        next();
    })
    .use(...) // more stuff here, of course
    .listen(3000);

但是,它似乎不起作用:回调永远不会被调用,请求也不会被中断。 然而,根据recent posts,它应该。

除了全局设置超时(即server.setTimeout(...)),这不适合我的用例, 我看到很多人建议使用connect-timeout 中间件。 但是,我在其文档中读到

While the library will emit a ‘timeout’ event when requests exceed the given timeout, node will continue processing the slow request until it terminates.
Slow requests will continue to use CPU and memory, even if you are returning a HTTP response in the timeout callback.
For better control over CPU/memory, you may need to find the events that are taking a long time (3rd party HTTP requests, disk I/O, database calls)
and find a way to cancel them, and/or close the attached sockets.

我不清楚如何“找到需要很长时间的事件”和“取消它们的方法”, 所以我想知道是否有人可以分享他们的建议。 这甚至是正确的方法,还是有更现代的“标准”方法?

规格:

  • 节点 12.22
  • Ubuntu 18.04
  • Linux 5.4.0-87-generic

来源:

编辑: 我已经看到一些答案和 cmets 提供了一种在 responses 或“请求处理程序”上设置超时的方法:换句话说,中间件花费的时间被测量,如果花费太长时间则中止。但是,我正在寻找一种使 requests 超时的方法,例如在客户端通过慢速连接发送大文件的情况下。这可能甚至在快速路由器中的第一个处理程序之前发生,这就是为什么我怀疑服务器级别必须有某种设置。

【问题讨论】:

  • 这取决于究竟是什么需要很长时间。如果你阻塞了主线程,那么你就没有什么可做的了。但是如果它的一些异步调用包装在一个承诺中,你可以把它包装在 bluebir 的超时承诺中:bluebirdjs.com/docs/api/timeout.html
  • 如果您无法控制那个耗时的任务,我认为唯一正确的方法是process.exit,它将终止整个实例和所有子进程(异步)。它会引导你到cluster,当他们中的任何一个因任何原因死亡时,它会负责重生新工人。

标签: javascript node.js express


【解决方案1】:

我认为,在拒绝长请求之前,最好先衡量请求,找到长请求并优化它们。如果可能的话。

如何衡量请求?

最简单的方法是测量从请求开始到请求结束的时间。你会得到Request Time Taken = time in nodejs event loop + time in your nodejs code + wait for setTimeout time + wait for remote http/db/etc services

如果您的代码中没有很多 setTimeout,那么 Request Time Taken 是一个很好的指标。 (但在高负载情况下,它变得不可靠,受time in event loop影响很大)

因此您可以尝试此措施并记录解决方案 http://www.sheshbabu.com/posts/measuring-response-times-of-express-route-handlers/

如何中止长请求

这一切都取决于您的请求处理程序

Handler 做重计算

如果计算量大,会阻塞主线程,请注意您可以不重写处理程序。

如果你设置了 req.setTimeout(5000, ...) - 它在 res.send() 之后触发,此时主循环将被解锁

 function (req, res) {
  for (let i = 0; i < 1000000000; i++) {
    //blocking main thread loop
  }
  res.send("halted " + timeTakenMs(req));
}

因此,您可以通过在某处注入 setTimeout(, 0) 来使代码异步; 或将计算移至worker thread

Handler 有很多远程请求

我用 Promisfied setTimeout 模拟远程请求

async function (req, res) {
    let delayMs = 500;
    await delay(delayMs); // maybe axios http call
    await delay(delayMs); // maybe slow db query
    await delay(delayMs);
    await delay(delayMs);
    res.send("long delayed" + timeTakenMs(req));
  }

在这种情况下,您可以注入一些帮助程序来中止您的请求链 blockLongRequest - 如果请求时间过长则抛出错误;

async function (req, res) {
    let delayMs = 500;
    await delay(delayMs); // mayby axios call
    blockLongRequest(req);
    await delay(delayMs); // maybe db slow query
    blockLongRequest(req);
    await delay(delayMs);
    blockLongRequest(req);
    await delay(delayMs);
    res.send("long delayed" + timeTakenMs(req));
  })

单个远程请求

function (req, res) {
    let delayMs = 1000;
    await delay(delayMs);
    //blockLongRequest(req); 
    res.send("delayed " + timeTakenMks(req));
}

我们不使用 blockLongRequest,因为它最好提供答案而不是错误。 错误可能会触发客户端重试,你的慢请求会加倍。

完整示例

(对不起 TypeScript,yarn ts-node sever.ts

import express, { Request, Response, NextFunction } from "express";

declare global {
  namespace Express {
    export interface Request {
      start?: bigint;
    }
  }
}

const server = express();
server.use((req, res, next) => {
  req["start"] = process.hrtime.bigint();
  next();
});
server.use((err: any, req: Request, res: Response, next: NextFunction) => {
  console.error("Error captured:", err.stack);
  res.status(500).send(err.message);
});

server.get("/", function (req, res) {
  res.send("pong " + timeTakenMks(req));
});
server.get("/halt", function (req, res) {
  for (let i = 0; i < 1000000000; i++) {
    //halting loop
  }
  res.send("halted " + timeTakenMks(req));
});
server.get(
  "/delay",
  expressAsyncHandler(async function (req, res) {
    let delayMs = 1000;
    await delay(delayMs);
    blockLongRequest(req); //actually no need for it
    res.send("delayed " + timeTakenMks(req));
  })
);
server.get(
  "/long_delay",
  expressAsyncHandler(async function (req, res) {
    let delayMs = 500;
    await delay(delayMs); // mayby axios call
    blockLongRequest(req);
    await delay(delayMs); // maybe db slow query
    blockLongRequest(req);
    await delay(delayMs);
    blockLongRequest(req);
    await delay(delayMs);
    res.send("long delayed" + timeTakenMks(req));
  })
);

server.listen(3000, () => {
  console.log("Ready on 3000");
});


function delay(delayTs: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, delayTs);
  });
}
function timeTakenMks(req: Request) {
  if (!req.start) {
    return 0;
  }
  const now = process.hrtime.bigint();
  const taken = now - req.start;

  return taken / BigInt(1000);
}
function blockLongRequest(req: Request) {
  const timeTaken = timeTakenMks(req);
  if (timeTaken > 1000000) {
    throw Error("slow request - aborting after " + timeTaken + " mks");
  }
}

function expressAsyncHandler(
  fn: express.RequestHandler
): express.RequestHandler {
  return function asyncUtilWrap(...args) {
    const fnReturn = fn(...args);
    const next = args[args.length - 1] as any;
    return Promise.resolve(fnReturn).catch(next);
  };
}

我希望,这种方法可以帮助您找到可接受的解决方案

【讨论】:

  • 对不起,但我认为这里有一个误解:如果它们花费太长时间,您建议一种中止 响应(或“请求处理程序”)的方法。该问题正在寻找一种中止请求的方法:为了清楚起见,我将编辑该问题。
  • 考虑您在编辑中的示例:“在慢速连接上发送文件” - 慢速连接不是问题:node.js 是异步的,它将逐块处理大文件(不匹配CPU 时间)。也许同时处理许多大文件和存储它们的内存消耗存在问题(您可以限制正文大小,将正文流保存到硬盘驱动器)。也许您在处理许多连接时遇到问题(您的限制是多少?)。因此,如果您用遇到的问题描述完整的特定任务,有人可以给您更准确的答案。
猜你喜欢
  • 2011-11-05
  • 1970-01-01
  • 1970-01-01
  • 2012-09-29
  • 2020-01-03
  • 2020-05-14
  • 2011-02-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多