간헐 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 라운드 등)을 테스트에서 경량화.
관련 노트
참고
- https://nodejs.org/api/dns.html#dnslookuphostname-options-callback (dns.lookup vs dns.resolve, 스레드풀)
- https://docs.libuv.org/en/v1.x/threadpool.html (UV_THREADPOOL_SIZE)