← 中文资源

简体中文版本

Node.js — 高并发下的 getaddrinfo EAI_AGAIN

诊断 Node.js 中仅在高并发下出现的 EAI_AGAIN DNS 失败,测量解析器并发上限,并用缓存或预取消除抖动。

阅读英文原文

症状

  EAI_AGAIN 的含义是“临时失败”,这解释了它为何从不像 EAI_NOTFOUND 那样孤立出现,而总是与负载相关:请求并发一升就成片闪现,流量回落后彻底消失。同主机上 getent hosts 在故障窗口内仍然成功,说明不是域名本身的问题。突发性、与流量尖峰吻合、系统工具正常——三者合起来指向 Node 进程内或容器解析路径的并发瓶颈。

诊断思路

  分两层定位:并行 200 个 getent 测 OS 解析器,若同样退化则问题在解析器配置(如容器内单个 nameserver、默认 5 秒超时、musl 串行化);把 UV_THREADPOOL_SIZE 从默认 4 提到 16 重跑压测,失败率显著下降则证明 getaddrinfo 在与其他线程池工作(fs、crypto)排队。getaddrinfo 由 libuv 派发到线程池再调用系统解析器,两层各有各的饱和方式。

根因

  getaddrinfo 在 libuv 线程池与系统解析器上执行。高并发下查询在少量线程后面排队,解析器本身也可能限速或超时。EAI_AGAIN 是“窗口内没拿到确定答案”的信号——并不存在持久的 DNS 故障,但请求确实失败了。没有任何 DNS 缓存时,每个出站请求都做一次全新解析,把尖峰直接灌进解析链路。

修复与验证

  结构性修复是不再逐请求解析:给所有出站客户端套一层带 TTL 的查找缓存(http.Agent 的 lookup 选项接 cachedLookup,命中直接返回地址),启动时和尖峰前预热;线程池测量指向饱和就把 UV_THREADPOOL_SIZE 提到 16 作为部署默认值;容器里核对 resolv.conf 的 timeout 与 attempts,走本地缓存式 stub resolver(如 dnsmasq sidecar);Alpine 上确认 musl 串行化影响显著再考虑换 glibc 镜像。验证时重放之前制造 EAI_AGAIN 的尖峰负载:日志零 EAI_AGAIN;解析器侧查询速率保持平坦(缓存命中吸收尖峰);再做一次上游 IP 轮换演练,客户端必须在 TTL 窗口内拾取新地址,证明缓存没有钉死陈旧端点。缓存命中率导出为指标,持续观察解析压力趋势。

代码示例

Error: getaddrinfo EAI_AGAIN api.example.com
    at GetAddrInfoReqWrap.oncomplete (node:dns:109:18)
import { LookupAddress } from 'node:dns';

const cache = new Map(); // host -> { addrs, expiresAt }

async function cachedLookup(hostname, options, callback) {
  const hit = cache.get(hostname);
  if (hit && hit.expiresAt > Date.now()) {
    return callback(null, hit.addrs);
  }
  const addrs = await promisifiedLookup(hostname, { family: 4 });
  cache.set(hostname, { addrs, expiresAt: Date.now() + 30_000 });
  callback(null, addrs);
}

const agent = new http.Agent({ lookup: cachedLookup, keepAlive: true });