ENA 지표 하나가 계속 오르는데, 정작 그 트래픽이 어디서 오는지가 안 보였다.
발단: 지표 하나가 계속 올랐다
어느 날 CloudWatch의 인스턴스 네트워크 지표를 보다가, 한 인스턴스에서 ENA 지표 linklocal_allowance_exceeded가 시간이 갈수록 꾸준히 오르는 걸 발견했다. “초과해서 버려진 패킷 수”가 계속 쌓인다는 건, 어딘가에서 조용히 패킷이 손실되고 있다는 뜻이다. 이 글은 그 지표의 정체를 추적하고, 결국 바꿀 수 없는 한도를 우회한 과정의 기록이다.
linklocal_allowance_exceeded가 뭔가
ENA 드라이버가 노출하는 네트워크 지표 중 하나로, 정의는 이렇다.
로컬 프록시 서비스로 가는 트래픽의 PPS(초당 패킷 수)가 ENI의 최대값을 초과해 손실된 패킷의 수.
여기서 “로컬 프록시 서비스”가 핵심이다. AWS는 인스턴스 안에서 세 가지를 링크-로컬 주소를 통해 프록시로 제공한다.
- Amazon Provided DNS (
VPC CIDR + 2, 예:10.0.0.2) - IMDS (
169.254.169.254) - Amazon Time Sync (
169.254.169.123)
그리고 이 프록시로 가는 트래픽에는 ENI당 1024 PPS라는 고정 한도가 걸려 있다. 이 한도를 넘는 순간부터 패킷이 버려지고, 그 카운터가 linklocal_allowance_exceeded다.
첫 번째 가설과 반증
처음엔 단순하게 생각했다. 링크-로컬 트래픽이라고 하니 169.254.0.0/16, fe80::/10 대역을 의심했다. IMDS를 너무 자주 때리는 프로세스나, 비정상적인 link-local 통신이 있겠거니 했다.
그래서 그 대역을 tcpdump로 떠봤다.
# 링크-로컬 대역 트래픽 캡처sudo tcpdump -ni any 'net 169.254.0.0/16' -c 2000그런데 막상 떠보니 이 대역의 트래픽은 1024 PPS를 넘길 만큼 많지 않았다. 가설이 깨졌다. 범인은 link-local 대역 자체가 아니었다.
진짜 원인: 평범한 DNS 질의였다
막혀서 AWS 서포트에 문의했고, 돌아온 답이 관점을 바꿔놨다. 핵심은 /etc/resolv.conf였다.
# Generated by NetworkManagersearch ap-northeast-2.compute.internalnameserver 10.0.0.2 # <--- VPC CIDR + 2이 nameserver 10.0.0.2가 바로 로컬 프록시로 가는 경로다. 즉 인스턴스에서 나가는 모든 DNS 질의 — 169.254 대역이 아니라 example.com 같은 평범한 외부 도메인 조회까지 — 가 이 프록시를 거쳐 Route 53 Resolver로 전달된다. 그리고 그 트래픽이 전부 1024 PPS 한도에 합산된다.
정리하면 이렇다.
linklocal_allowance_exceeded는 “link-local 대역 트래픽”이 아니라 “로컬 프록시로 향하는 트래픽” 카운터다.- DNS는 그 프록시의 대표 사용자다.
- DNS 질의가 많은 워크로드는 이 한도를 쉽게 친다.
그제야 그림이 맞춰졌다. 이 서비스는 들어온 요청을 외부 API로 넘기는 구조다. 트래픽이 몰리면 대량의 API 호출이 한꺼번에 일어나고, 그 각각이 매번 도메인을 조회한다. 즉 피크 순간 DNS 질의도 함께 폭증해 1024 PPS를 가볍게 넘겼고, 프록시로 가던 패킷이 그만큼 버려지고 있었다.
확인도 결국 tcpdump였다. 이 영향만 따로 보여주는 전용 메트릭이 없어서, DNS 질의량을 직접 떠서 간접적으로 셀 수밖에 없었다.
# 포트 53(DNS) 질의가 실제로 얼마나 나가는지 확인sudo tcpdump -ni any 'port 53' -c 2000캡처를 떠보니 트래픽이 몰리는 구간마다 외부 도메인 조회가 폭증하고 있었다. 이게 진짜 범인이었다.
왜 한도를 못 올리나
가장 먼저 떠오른 해결책은 “한도를 올리자”였다. 하지만 이 1024 PPS 제한은 인스턴스 유형과 무관하게 고정이다. 더 큰 인스턴스로 바꿔도, 요청을 넣어도 상향되지 않는다. AWS가 인프라 보호를 위해 박아둔 값이라 손댈 수 없다.
그러면 방향은 하나다. 프록시로 나가는 DNS 질의 수 자체를 줄이는 것. 같은 도메인을 매번 AWS DNS에 묻지 않도록, 인스턴스 안에 캐시를 두면 된다.
해결: 로컬 캐시 리졸버
도구는 두 개를 놓고 비교했다.
| Unbound | dnsmasq | |
|---|---|---|
| 처리 성능 | 스레드 기반, 대용량 안정 | 단일 프로세스 중심 경량 |
| 캐시 관리 | prefetch 등 고급 기능 | 제한적 |
| 리졸버 | 완전 재귀(루트 힌트) | 단순 포워딩 |
| DNSSEC / DoT·DoH | 네이티브 지원 | 미지원/제한적 |
대량의 API 호출이 한꺼번에 몰리는 환경이라 캐시 적중률과 안정적인 처리량이 중요했다. Unbound로 정했다. (DNSSEC 검증은 AWS DNS 쪽에서 이미 수행하므로 여기선 끈다.)
구조는 단순하다.
/etc/resolv.conf의nameserver를127.0.0.1(Unbound)로 바꾼다.- Unbound는
.존(전체)을 VPC DNS(10.0.0.2)로 forward 한다. - 캐시에 적중한 질의는 AWS DNS로 나가지 않는다 → 프록시로 가는 PPS가 줄고, 지표 증가가 멈춘다.
요청: example.com 어디야? 1st 앱 → Unbound(miss) → AWS DNS # 최초 1회만 밖으로 이후 앱 → Unbound(hit) # TTL 동안은 로컬에서 응답같은 외부 API 도메인을 반복해서 묻던 서비스라, 캐시 한 겹이 곧바로 효과를 냈다.
설치 · 설정
sudo dnf install unbound -ysudo tee /etc/unbound/unbound.conf <<'EOF'server: interface: 127.0.0.1 # 로컬 캐시 전용 — 외부에 노출하지 않음 access-control: 127.0.0.0/8 allow access-control: 0.0.0.0/0 refuse # 로컬 외 질의는 거부 cache-min-ttl: 60 cache-max-ttl: 300 msg-cache-size: 64m rrset-cache-size: 128m # rrset은 msg의 2배 권장 prefetch: yes # 인기 도메인을 TTL 만료 전 미리 갱신 serve-expired: yes # 만료분도 우선 응답 후 백그라운드 갱신 serve-expired-ttl: 86400 serve-expired-client-timeout: 1800 # 먼저 resolve 시도 후 stale 응답 (RFC 8767) serve-expired-reply-ttl: 30 module-config: "iterator" # 캐싱 포워더 — DNSSEC 검증은 AWS DNS가 하므로 validator 불필요forward-zone: name: "." forward-addr: 10.0.0.2 # VPC DNS (VPC CIDR + 2) — 환경값으로 치환EOF
sudo unbound-checkconfsudo systemctl enable --now unbound여기서 prefetch와 serve-expired가 PPS를 줄이는 데 특히 효과적이다. 인기 도메인은 TTL이 만료되기 전에 미리 갱신해 두고, 혹시 만료되더라도 일단 캐시로 응답한 뒤 뒤에서 조용히 갱신하기 때문에, 밖으로 나가는 질의가 트래픽 피크와 무관하게 평탄해진다.
마지막으로 시스템 리졸버를 로컬로 돌린다.
sudo sed -i '1s|.*|nameserver 127.0.0.1|' /etc/resolv.conf함정: resolv.conf는 다시 덮어쓰인다
여기서 한 번 더 걸렸다. /etc/resolv.conf를 sed로 바꿔도, NetworkManager나 DHCP가 재기동되면 원래의 10.0.0.2로 원복된다. 재부팅 한 번에 설정이 조용히 풀려서, 지표가 다시 오르기 시작하는 식이다.
그래서 변경을 영속화해야 한다. NetworkManager가 resolv.conf를 관리하지 못하도록 빼거나(dns=none), 파일을 고정하는 방식으로 막는다. 그리고 적용 후에는 외부 도메인 질의가 실제로 127.0.0.1을 타는지 반드시 확인한다.
# 캐시가 실제로 쌓이는지 / 적중하는지 확인sudo unbound-control dump_cache | head
# 무중단으로 설정 다시 적용sudo unbound-control reload마치며
이 일에서 남은 교훈은 세 가지다.
- 지표 이름을 곧이곧대로 믿지 말 것.
linklocal_allowance_exceeded는 link-local 대역이 아니라 “로컬 프록시로 가는 트래픽”의 카운터였고, 그 대표 사용자는 평범한 DNS였다. - 못 바꾸는 한도는 우회한다. 1024 PPS는 고정값이다. 한도를 올리는 대신, 한도를 치는 양을 줄이는 게 정답이었다.
- 전용 메트릭이 없으면 패킷을 떠라. 영향 트래픽 전용 지표가 없을 땐
tcpdump가 가장 빠른 진실이었다.
DNS 캐시는 흔히 응답 속도를 위해 단다고들 하지만, 이번엔 네트워크 한도를 지키기 위한 방어선이었다. 외부 API를 대량으로 호출하는 워크로드라면, ENA 지표를 한 번쯤 들여다볼 만하다.
본문의 서브넷·CIDR(
10.0.0.x)·도메인은 모두 예시값으로 치환했다. 실제 적용 시VPC CIDR + 2에 해당하는 환경의 VPC DNS 주소로 바꿔서 쓰면 된다.