K8s Networking - 13. troubleshooting
"네트워크가 안 된다"에서 시작해 범인을 좁히는 법 — 트러블슈팅 결정 트리
한 서비스가 "DB에 연결이 안 된다"고 보고됐다. 운영자가 1시간을 헤맸다 — Pod가 살아있는지, Service가 있는지, NetworkPolicy가 막는지, CNI가 꼬였는지, DNS가 풀리는지 전부 의심했다. 결국 원인은 conntrack 테이블 만료. 그런데 그 1시간의 대부분은 틀린 층을 뒤진 데 쓰였다. 이 글은 그 1시간을 5분으로 줄이는 결정 트리를 다룬다.
이 글이 푸는 것은: "네트워크가 안 된다"는 모호한 증상에서 시작해, 어느 층(DNS/kube-proxy/CNI/정책/커널)인지를 체계적으로 좁히는 절차다. 핵심은 층별로 나눠 한 층씩 검증하는 것이다.
먼저 층을 나눈다 — 어디를 의심할 것인가
지금까지 01~12장에서 본 네트워크 층을 정리하면 5개로 좁혀진다. "안 된다"는 보고가 오면, 이 5층을 위에서부터 한 층씩 검증한다:
flowchart TD
Q1{"이름이 풀리나?<br/>(DNS 층)"} -->|"아니오"| DNS["CoreDNS 문제"]
Q1 -->|"예"| Q2{"Service IP로 가나?<br/>(kube-proxy 층)"}
Q2 -->|"아니오"| KP["kube-proxy/EndpointSlice"]
Q2 -->|"예"| Q3{"Pod IP로 직접 가나?<br/>(CNI 층)"}
Q3 -->|"아니오"| CNI["CNI/오버레이"]
Q3 -->|"예"| Q4{"정책이 막나?<br/>(정책 층)"}
Q4 -->|"예"| POL["NetworkPolicy"]
Q4 -->|"아니오"| Q5{"커널 자원 고갈?"}
Q5 -->|"예"| KER["conntrack/포트/메모리"]
이 결정 트리가 "틀린 층을 뒤지는" 낭비를 막는다. 증상을 층별 질문으로 환원하면 범인이 좁혀진다. 각 층을 순서대로 보자.
1단계: DNS 풀이 확인 (CoreDNS 층)
# Kubernetes 1.36
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- nslookup <service-name>
- 실패(NXDOMAIN/timeout) → CoreDNS 문제. CoreDNS Pod 상태, Corefile, ndots 점검.
- 성공 → DNS 층 통과, 다음으로.
자주 놓치는 함정: Pod의
/etc/resolv.conf의ndots로 외부 도메인이 여러 번 실패 질의. "느린 DNS"의 범인.
2단계: Service IP로 가나 (kube-proxy 층)
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- wget -T2 -qO- <service-clusterip>
- 실패 → kube-proxy 또는 EndpointSlice. EndpointSlice에 Pod가 있는지, kube-proxy Pod 상태 점검.
- 성공 → kube-proxy 층 통과.
kubectl get endpointslice -l kubernetes.io/service-name=<svc>
확인할 것: Pod IP들이 EndpointSlice에 있는가. 없으면 셀렉터/라벨 불일치, 또는 readinessProbe 통과 못 함.
3단계: Pod IP로 직접 가나 (CNI 층)
# 대상 Pod IP로 직접
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- wget -T2 -qO- <pod-ip>
- Service로는 되는데 Pod IP로 안 됨 → CNI 문제. 노드 라우팅, CNI Pod, 오버레이 인터페이스 점검.
- 둘 다 안 됨 → 여전히 CNI 또는 정책.
4단계: NetworkPolicy가 막나 (정책 층)
정책에 막힌 패킷은 조용히 버려진다. 로그가 안 남아 잡기 어렵다. Hubble(Cilium)이나 Calico 로그로 실시간 확인.
cilium hubble observe --from-pod <src> --to-pod <dst> # Cilium
확인할 것: denied 또는 정책 드롭 플래그. Hubble 없으면 정책을 임시 제거해 보며 격리 — 정책을 빼면 되면 정책이 원인.
5단계: 커널 자원 — conntrack 함정
여기까지 다 통과했는데 간헐적으로 안 된다면 커널 자원을 의심. 특히 conntrack(연결 추적 테이블). 모든 연결이 이 테이블에 항목을 만들고, 테이블이 꽉 차면 새 연결이 드롭된다. (리눅스 커널 netfilter conntrack)
# 노드에서 conntrack 사용량 (노드 접근 시)
docker exec <node> cat /proc/sys/net/netfilter/nf_conntrack_count
docker exec <node> cat /proc/sys/net/netfilter/nf_conntrack_max
확인할 것: count가 max에 가까우면 conntrack 고갈. 새 연결이 간헐 드롭.
conntrack 함정의 증상: "어떤 연결은 되고 어떤 연결은 안 된다" — 테이블이 꽉 찰 때만 드롭하므로 간헐적. 흔히 "랜덤하게 안 된다"로 보고되지만, 커널 자원 고갈이다. 도입의 1시간 사례가 바로 이것.
tcpdump — 층을 좁혔지만 여전히 모호할 때, 노드에서 패킷 직접 보기
결정 트리 1~5단계를 전부 통과했는데도 모호하면, 노드에서 패킷을 직접 잡는다. 이것이 최후의 진단 도구다. Pod 안이 아니라 노드에서 — CNI 인터페이스를 잡아야 Pod 간 패킷이 보인다.
# 노드에서 (kind 노드 컨테이너 또는 실제 노드)
# 1. Pod가 붙은 인터페이스 식별
docker exec <node> ip link | grep -E 'calico|cilium|cni|veth'
# 2. 해당 인터페이스에서 패킷 캡처
docker exec <node> tcpdump -i <iface> -n host <dst-pod-ip> -c 20
# 3. 응답이 나가는지, 들어오기만 하는지 확인
docker exec <node> tcpdump -i any -n host <dst-pod-ip> -c 20
읽는 법:
- 요청은 가는데 응답이 안 온다 → 목적지 노드의 Pod가 안 받거나, 응답이 라우팅에서 길을 잃음.
- 요청 자체가 안 간다 → 출발 노드의 라우팅/CNI 문제.
- SYN만 나가고 SYN-ACK이 안 온다 → 목적지 Pod가 리스닝 안 함(앱 문제) 또는 정책이 조용히 드롭.
tcpdump가 "정책이 조용히 드롭"과 "앱이 안 받음"을 구분하는 유일한 수단인 경우가 많다. NetworkPolicy 드롭은 패킷이 사라지고 로그가 안 남아, tcpdump로 "패킷이 특정 노드까지만 간다"를 봐야 원인을 좁힐 수 있다.
실제 실패 사례 3선 — 결정 트리를 적용한 진단
결정 트리가 실제 사례에서 어떻게 쓰이는지 세 시나리오로 체감하자.
사례 1: "Service로는 되는데 Pod IP로 안 된다" (CNI)
한 팀이 디버그 Pod에서 kubectl exec로 Service 이름을 호출하면 응답이 오는데, 같은 대상 Pod의 IP로 직접 호출하면 timeout. 결정 트리 3단계(Pod IP 직접)에서 실패 → CNI 층. 원인: 노드 간 VXLAN 터널 인터페이스가 한 노드에서 다운. CNI Pod 상태 점검 → 해당 노드의 vxlan.calico 인터페이스 재생성 → 해결.
핵심: "Service는 되고 Pod IP는 안 된다"는 CNI 신호라는 것을 결정 트리가 즉시 알려준다. 무작정 apiserver 로그를 뒤지는 낭비를 막음.
사례 2: "간헐적으로 연결이 끊긴다" (conntrack)
서비스가 99%는 정상인데 무작위로 연결이 실패. 결정 트리 1~4단계 전부 통과(이름 풀이, Service, Pod IP, 정책 모두 OK) → 5단계(커널 자원). 노드의 nf_conntrack_count가 nf_conntrack_max에 근접. 원인: 짧은 수명 연결이 많아 conntrack 테이블 고갈. 해결: max 상향 또는 연결 풀링으로 연결 수 감소.
사례 3: "NetworkPolicy 넣었더니 모니터링이 멈췄다" (정책)
보안 정책으로 default-deny를 넣자 prometheus가 메트릭을 못 긁음. 결정 트리 4단계(정책) → NetworkPolicy가 prometheus의 접근을 막음. Hubble로 denied 플로우 확인 → prometheus→모든 Pod:메트릭 포트 허용 규칙 추가 → 해결. 이것이 06장 "정책 넣었더니 다 막힘"의 실전 사례.
진단 명령 치트시트 — 층별 한 줄 점검
층별로 가장 먼저 칠 명령을 정리:
| 층 | 첫 명령 | 보는 것 |
|---|---|---|
| DNS | nslookup <svc> (debug Pod에서) |
NXDOMAIN? timeout? SERVFAIL? |
| Service | kubectl get endpointslice -l ... |
Pod IP가 있는가? 비어있으면 셀렉터 문제 |
| CNI | kubectl exec <node> -- ip route (또는 노드 접속) |
Pod 대역 라우팅/터널 인터페이스 |
| 정책 | cilium hubble observe (Cilium) 또는 정책 임시 제거 |
denied 드롭? |
| 커널 | cat /proc/sys/net/netfilter/nf_conntrack_count (노드) |
count ≈ max? |
| tcpdump | tcpdump -i any -n host <pod-ip> -c 20 (노드) |
패킷 도달? 응답 나감? |
이 표를 벽에 붙여두면 "어디서부터"의 막막함이 사라진다. 증상 → 층 → 명령 → 증거의 루프.
"안 된다"를 분해하는 질문 — 정확한 증상 파악
네트워크 트러블슈팅의 절반은 증상을 정확히 파악하는 것이다. "안 된다"를 구체적 질문으로:
- 무엇이 안 되나? (DNS 풀이? 연결? 응답? 특정 포트?)
- 언제부터? (배포 후? 정책 변경 후? 특정 시간?)
- 얼마나 자주? (항상? 간헐? 특정 Pod만?)
- 어디서? (모든 클라이언트? 특정 노드의 Pod만?)
이 질문들에 답하면 결정 트리의 어느 층인지가 좁혀진다. "전부 안 된다"면 DNS/CNI 전체. "특정 Pod만"이면 정책/라벨. "간헐"이면 conntrack/자원. 증상의 정밀도가 진단 속도를 결정.
일반적 함정 요약 — 증상 → 의심 층
| 증상 | 먼저 의심할 층 |
|---|---|
| 이름 풀이 실패 | CoreDNS / ndots |
| Service IP로 안 됨, Pod IP로 됨 | kube-proxy/EndpointSlice |
| Pod IP로도 안 됨 | CNI/오버레이 |
| 정책 켠 후부터 안 됨 | NetworkPolicy |
| 간헐적 연결 실패 | conntrack 고갈 |
| 외부 도메인만 느림 | ndots |
| 메시 적용 후 안 됨 | 사이드카/메시 정책 |
직접 확인하기 (실습 흐름)
# Kubernetes 1.36 — 위 결정 트리를 따라가는 빠른 점검 스크립트 예
SVC=web; POD_IP=$(kubectl get pod -l app=web -o jsonpath='{.items[0].status.podIP}')
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- sh -c "
echo '== DNS =='; nslookup $SVC;
echo '== Service IP =='; wget -T2 -qO- $SVC || echo fail;
echo '== Pod IP =='; wget -T2 -qO- $POD_IP || echo fail
"
확인할 것: 각 단계의 성공/실패로 어느 층이 문제인지 즉시 분리.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "네트워크 안 되면 CNI 재설치" | 층별 검증 먼저. DNS/정책일 때가 많음 |
| "NetworkPolicy가 막으면 에러가 난다" | 조용히 드롭. 로그 안 남음(Hubble 필요) |
| "간헐적 실패는 랜덤이다" | 아니다. conntrack/자원 고갈이 흔한 원인 |
| "tcpdump는 Pod에서 한다" | 노드에서(cni 인터페이스 잡기). Pod 안에선 패킷 보기 어려움 |
| "EndpointSlice는 항상 Pod를 가리킨다" | 셀렉터 불일치/ready 조건에 따라 비어있을 수 있음 |
| "외부 도메인이 느리면 외부망 탓" | ndots로 내부 DNS가 여러 번 시도 중일 수 있음 |
요약 — 이 글의 결론
- 층별 결정 트리가 트러블슈팅의 핵심 — DNS → kube-proxy → CNI → 정책 → 커널 자원 순으로 한 층씩 검증. "틀린 층 뒤지기" 낭비를 막는다.
- DNS 풀이(CoreDNS/ndots) → Service IP(kube-proxy/EndpointSlice) → Pod IP(CNI) → 정책(NetworkPolicy) → 커널 자원(conntrack) 순서.
- 정책에 막힌 패킷은 조용히 드롭. Hubble(Cilium)/Calico 로그로 실시간 추적.
- conntrack 고갈이 "간헐적 연결 실패"의 흔한 커널 원인. 테이블 꽉 차면 새 연결 드롭.
- tcpdump(노드 단)가 CNI/라우팅 문제의 최종 확인 도구. Pod 안이 아닌 노드 인터페이스에서.
- 증상-층 매핑 표가 "무엇부터 의심할까"의 빠른 길잡이. 증상의 정밀도가 진단 속도를 결정.
생각해 볼 문제
- "Service IP로는 되는데 Pod IP로 안 된다" — 어느 층인가? 다음 확인 명령은?
- 간헐적으로 연결이 실패한다. conntrack을 의심해 확인하는 명령 흐름을 써라.
- NetworkPolicy가 막았다고 의심되는데 Hubble이 없다. 어떻게 확인할까?
- 외부 API 호출이 느리다. 결정 트리에서 어디부터? ndots 확인 방법은?
- tcpdump로 노드 인터페이스를 잡았다. 패킷이 들어오기만 하고 나가지 않는다. 의미는?
- EndpointSlice가 비어있다. 가능한 원인 3가지(셀렉터/readiness/오프라인).
참고
- Kubernetes 공식 문서 - Debugging DNS resolution - 접근 2026-07-13
- Kubernetes 공식 문서 - Debug Services - 접근 2026-07-13
- Cilium Hubble - 관측 - 접근 2026-07-13
- Calico - flow log/정책 디버그 - 접근 2026-07-13
- conntrack과 Kubernetes - 리눅스 커널 netfilter - 접근 2026-07-13
'Tech Artifacts > K8s Networking' 카테고리의 다른 글
| K8s Networking - 14. multi-cluster (0) | 2026.07.14 |
|---|---|
| K8s Networking - 12. cilium-service-mesh (0) | 2026.07.14 |
| K8s Networking - 11. istio (0) | 2026.07.14 |
| K8s Networking - 10. service-mesh concepts (0) | 2026.07.14 |
| K8s Networking - 09. gateway-api (0) | 2026.07.14 |