Zettelkasten

간헐 getaddrinfo ENOTFOUND는 DNS 부하가 아니라 libuv 스레드풀 starvation을 먼저 의심한다

·수정 2026.06.08·수정 2

요약

  • Node.js에서 getaddrinfo ENOTFOUND간헐적으로 나면 DNS 서버/네트워크가 아니라 libuv 스레드풀(기본 4개) 고갈이 원인일 수 있다.
  • dns.lookup은 스레드풀 위에서 돌기 때문에, bcrypt·pbkdf2·fs 같은 블로킹 작업이 슬롯을 점유하면 lookup이 큐에 밀려 드라이버 timeout을 넘기고 ENOTFOUND로 표출된다.
  • 切り分け 핵심: "코드 → 워커 수 → 순수 DNS burst → 스레드풀 starvation" 순으로 변수를 하나씩 제거한다.

본문

증상 패턴

  • 통합 테스트(또는 동시 커넥션 많은 워크로드)에서 N개 중 1~2개만 랜덤 실패, 매번 다른 테스트가 실패.
  • 에러: MongoNetworkError: getaddrinfo ENOTFOUND <host>MongoPoolClearedError (드라이버가 노드 해석 실패로 pool clear).
  • 항상 같은 호스트명으로 표출되지만, 그 호스트 자체는 평상시 정상 해석됨.

진단 절차 (변수를 하나씩 제거)

각 단계는 "이 가설이 맞다면 이렇게 변해야 한다"는 예측을 세우고 검증한다.

1. 코드 무관함 확인

  • 변경이 순수 rename/스키마 추가인데도 발생 → 코드 원인 배제.
  • 단위 테스트(DB 안 닿음)는 전부 통과 → 로직 무죄.

2. 동시성(워커 수) 변수 제거

jest --maxWorkers=2      # 절반
jest --runInBand         # 완전 직렬 (한 번에 1 suite)
  • runInBand(동시 커넥션 최소)로도 실패하면 → "테스트 동시성 때문"이 아니다.

3. 순수 DNS 부하 가설 검증 (반증 도구)

  • dns.lookup을 동시/지속적으로 때려서 실패율을 직접 측정.
const dns = require('dns');
const lookup = (h) => new Promise(r => dns.lookup(h, e => r(e ? e.code : 'OK')));
// 동시 400개 burst, 그리고 5000회 지속 → 실패 카운트
  • burst·지속 모두 0 실패면 → "DNS 서버가 양 때문에 drop"은 반증됨.

4. libuv 스레드풀 starvation 가설 검증 (결정타)

  • 스레드풀을 블로킹 작업(crypto.pbkdf2)으로 채운 채 dns.lookup 지연/실패 관찰.
  • 관찰 결과: lookup이 실패는 안 하지만 수 초까지 지연됨. 이 지연이 드라이버 connect/heartbeat timeout을 넘기면 ENOTFOUND로 표출.
  • 검증: 스레드풀 키우고 재실행.
UV_THREADPOOL_SIZE=64 jest --config ... --maxWorkers=4
  • 이걸로 깨끗이 통과하면 원인 확정.

왜 ENOTFOUND로 보이나

  • dns.lookup(getaddrinfo)은 libuv 스레드풀 사용. (반면 dns.resolve*는 c-ares로 스레드풀과 무관)
  • 스레드풀 4슬롯이 다른 블로킹 작업으로 막히면 lookup 콜백이 지연 → mongo 드라이버의 SDAM 하트비트가 노드 IP 해석을 제때 못 함 → timeout을 ENOTFOUND 계열 에러로 표면화.
  • "이름을 못 찾았다"가 아니라 "제때 못 물어봤다" 가 본질.

환경 증폭 요인

  • 로컬 DNS 리졸버가 프록시(VPN/WARP/Tailscale 등) 경유면 getaddrinfo 지연이 더 누적되어 timeout을 더 잘 넘긴다.
  • 직결 DNS 환경 팀원은 같은 코드에서 덜 겪을 수 있어, "내 환경만 그런가?" 切り分け 시 리졸버 종류도 같이 확인한다.

팀 환경 비교 사례 (2026-06-08)

팀원 nameserver 분류 비고
junha 127.0.2.2 / 127.0.2.3 로컬 프록시 (VPN/sandbox) getaddrinfo에 1홉 추가
bada 100.100.100.100 / fd7a:115c:a1e0::53 + ISP(61.41.x) Tailscale MagicDNS + ISP 직결 Tailscale 경유 시 1홉 추가

두 팀원 모두 오버레이 DNS 프록시를 경유하는 환경 → UV_THREADPOOL_SIZE 상향이 팀 전체 적용 필요.

확인 명령: scutil --dns | grep nameserver (macOS)

영구 대응

  • 테스트 실행에 UV_THREADPOOL_SIZE 상향 (예: cross-env UV_THREADPOOL_SIZE=64).
  • 또는 블로킹 작업(bcrypt 라운드 등)을 테스트에서 경량화.

관련 노트

참고