Node 20 下用 undici 连内网服务,连接池被我撑爆了
上周把一个老服务迁到 Node 20,它要往内网三个下游节点发请求,QPS 峰值大概 800,每个请求体 2KB 左右。压测到一半,下游报「too many connections」,本机 ss -s 一看 TIME_WAIT 两万多。当时用的是原生 fetch,没配任何 agent。
先是怀疑下游限流,抓包发现连接确实是我这边发起的,而且每个请求一条新连接。Node 20 的 fetch 底层是 undici,默认全局 dispatcher 其实有连接池,但它按 origin 分,而且默认 connections 是 null,也就是不限上限,高并发下会一路涨。我这个场景又都是短请求,连接还没复用就被下一波新请求挤满了。
做法很简单,给每个下游建独立的 Pool,明确连接数,再传给 fetch:
const { Pool, setGlobalDispatcher } = require('undici');
const pools = {
a: new Pool('http://10.0.1.11:8080', { connections: 32, pipelining: 1 }),
b: new Pool('http://10.0.1.12:8080', { connections: 32, pipelining: 1 }),
c: new Pool('http://10.0.1.13:8080', { connections: 32, pipelining: 1 }),
};
async function call(node, path, body) {
const res = await fetch(new URL(path, pools[node].origin), {
method: 'POST',
body: JSON.stringify(body),
headers: { 'content-type': 'application/json' },
dispatcher: pools[node],
});
if (!res.ok) throw new Error(`${node} ${res.status}`);
return res.json();
}
关键是 dispatcher 这个参数,不传就落到全局 dispatcher 上,各下游混在一起,一个慢节点会把连接全占住。给每个下游单独 Pool 之后,慢的那个最多占 32 条,不会拖垮另外两个。
为什么是 32:下游单机 keep-alive 上限是 64,三台机器各 32 加起来正好压线,留点余量给健康检查。代价是并发超过 32 的请求会在 Pool 里排队,所以我把上游的接口超时从 3s 调到了 5s,排队时间得算进去。pipelining 保持 1,之前试过 10,下游有个老网关对 pipeline 支持不完整,偶发响应错位,这个不确定是不是普遍问题,但我不敢赌。
失效的地方:Pool 是按 origin 建的,路径不同不影响,但如果下游走域名且 DNS 轮询到多台,undici 只认 origin,所有请求会打到解析出的那一台上,这种场景得自己在前面加负载均衡。另外我用 AbortSignal.timeout 做超时,被中断的请求连接不会立刻回池,压测时看到 connections 短暂高于 32,这是正常的,别按这个数字去调小配置。
还有个坑是进程退出时 Pool 不会自动关,得在 SIGTERM 里手动 pool.close(),不然滚动发布时会挂住几十秒。
你们在内网多下游的场景里,是每个下游一个 Pool,还是共用全局 dispatcher 加 per-request 超时?我这边共用那版在慢节点上出过事,想看看别人怎么处理的。
0 条评论
还没有评论,写下第一条。