用 undici Agent 限制连接池并区分分层超时

举报
yd_250998817 发表于 2026/10/31 23:06:02 2026/10/31
【摘要】 在 Node.js 中用 undici 的 Agent 限制每个 origin 的连接数,并分别设置连接建立、响应头与响应体超时,把慢下游的故障归类到具体阶段。文中给出可运行的最小示例:用本地慢服务器触发 headersTimeout 与 bodyTimeout,观察连接池释放后快请求仍能完成。

问题场景

Node.js 服务调用下游时,常见两类“慢”:TCP 连接迟迟建不起来,或者连接已建立但响应头/响应体迟迟不返回。如果不加区分,只配一个总超时,排查时只能看到“请求失败”,也很难判断是网络不通、下游过载,还是下游处理慢。更麻烦的是,慢请求会长期占用 undici 连接池中的连接,后续请求排队,表现为“连接池被打满”。

undici 的 Agent 可以按 origin 维护连接池。通过 connections 限制并发连接数,通过连接建立超时、headersTimeout、bodyTimeout 把不同阶段拆开,可以让失败原因更清晰,并在超时后释放连接,避免慢下游无限期占用资源。

配置要点

  • connections:每个 origin 的连接上限。示例配合 pipelining: 1,让一条连接同一时间只跑一个请求,便于观察池占用。
  • 连接建立超时:控制 TCP 连接建立阶段。不同 undici 版本可使用 connectTimeout,或写成 connect: { timeout: ... };以所用版本文档为准。
  • headersTimeout:等待响应头的上限。它管的是“请求已发出,响应头还没来”,不是整个响应耗时。
  • bodyTimeout:响应体接收阶段的超时。流式响应、大文件下载要单独评估。

建议让 headersTimeout 明显大于连接建立超时,避免连接还没建好就被误判为响应头超时。错误码通常可区分为 UND_ERR_CONNECT_TIMEOUT、UND_ERR_HEADERS_TIMEOUT、UND_ERR_BODY_TIMEOUT,但应以实际运行结果为准。

最小可运行示例

准备一个本地 HTTP 服务:/fast 立即返回;/slow-headers 延迟 3 秒才写响应头;/slow-body 先写响应头,再延迟 3 秒结束响应体。客户端把连接数限制为 2,并设置较短的连接、响应头、响应体超时。

const http = require('node:http');
const { Agent, request } = require('undici');

const agent = new Agent({
  connections: 2,
  pipelining: 1,
  connectTimeout: 500, // 若版本不支持,可改为 connect: { timeout: 500 }
  headersTimeout: 800,
  bodyTimeout: 800,
});

const server = http.createServer((req, res) => {
  if (req.url === '/fast') {
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.end('ok');
    return;
  }

  if (req.url.startsWith('/slow-headers')) {
    const timer = setTimeout(() => {
      res.writeHead(200, { 'content-type': 'text/plain' });
      res.end('late headers');
    }, 3000);
    res.on('close', () => clearTimeout(timer));
    return;
  }

  if (req.url === '/slow-body') {
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.write('part-');
    const timer = setTimeout(() => res.end('late body'), 3000);
    res.on('close', () => clearTimeout(timer));
    return;
  }

  res.writeHead(404);
  res.end('not found');
});

async function call(base, path) {
  const start = Date.now();
  try {
    const res = await request(base + path, { dispatcher: agent });
    const text = await res.body.text();
    console.log('[ok]', path, res.statusCode, text, `${Date.now() - start}ms`);
  } catch (err) {
    console.log('[err]', path, err.name, err.code, err.message, `${Date.now() - start}ms`);
  }
}

(async () => {
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  const { port } = server.address();
  const base = `http://127.0.0.1:${port}`;

  const slow = [];
  for (let i = 0; i < 4; i++) {
    slow.push(call(base, `/slow-headers?i=${i}`));
  }

  await new Promise((resolve) => setTimeout(resolve, 1200));
  await call(base, '/fast');

  await Promise.allSettled(slow);
  await call(base, '/slow-body');

  await agent.close();
  await new Promise((resolve) => server.close(resolve));
})();

运行:

npm init -y
npm i undici
node demo.js

预期能看到前 4 个 /slow-headers 请求以 UND_ERR_HEADERS_TIMEOUT 失败(或相近错误码),随后 /fast 能完成,说明池中的连接在超时后被释放,排队请求继续执行;/slow-body 则用于观察响应体阶段超时。若本地环境或 undici 版本对 connectTimeout 字段处理不同,先改写成 connect: { timeout: 500 } 再运行。连接建立超时可在隔离测试环境里指向一个不可达地址验证,但不同网络可能直接返回不可达错误,不一定是 UND_ERR_CONNECT_TIMEOUT。

边界与限制

  1. 超时不是扩容手段。它只能让慢请求更早释放资源,不能让慢下游变快;如果所有请求都慢,吞吐仍受下游能力限制。
  2. connections 不是越大越好。过大可能放大下游压力,并增加本机文件描述符和内存开销;过小会造成排队。应结合下游承载能力和业务可接受的排队时间设置。
  3. headersTimeout 过小会把“慢但正常”的响应判失败;bodyTimeout 过小会影响流式响应和大文件下载;连接建立超时过小会在跨地域、弱网环境下误判。
  4. Agent 的连接池通常按 origin 隔离。多下游、多域名场景要分别评估,不要把一个域名的结论直接套到另一个域名。
  5. DNS、TLS 握手、代理等阶段的耗时是否计入某个超时,受 undici 版本和配置影响,需要用错误码和日志实测确认。
  6. 生产环境应复用同一个 Agent,避免每个请求新建实例;进程退出或配置变更时调用 agent.close(),并处理未完成请求。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。