簡體   English   中英

Express.js - 超時時中止請求

[英]Express.js - abort request on timeout

我正在探索中止耗時太長的客戶端請求的方法,從而消耗服務器資源。 閱讀了一些來源(見下文)后,我嘗試了如下解決方案(如建議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);

但是,它似乎不起作用:從不調用回調,並且不會中斷請求。 然而,根據最近的帖子,它應該。

除了全局設置超時(即server.setTimeout(...) ),這不適合我的用例,我已經看到很多人建議使用連接超時中間件。 但是,我在其文檔中讀到

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-通用

資料來源:

在拒絕長請求之前,我認為最好先衡量請求,找出長請求,然后優化它們。 如果可能的話。

如何衡量請求?

最簡單的方法是測量從開始到請求結束的時間。 您將獲得Request Time Taken = time in nodejs event loop time in your nodejs code + time in your nodejs code wait for setTimeout time + wait for remote http/db/etc services wait for setTimeout time + wait for remote http/db/etc services

如果代碼中沒有很多 setTimeout,那么Request Time Taken是一個很好的指標。 (但在高負載情況下,它變得不可靠, time in event looptime 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) 來使代碼異步; 或將計算移至工作線程

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 因為最好提供答案而不是錯誤。 錯誤可能會觸發客戶端重試,並且您的慢速請求會加倍。

完整示例

(抱歉yarn ts-node sever.tsyarn 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);
  };
}

我希望這種方法可以幫助您找到可接受的解決方案

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM