Tech Artifacts/K8s Networking

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.confndots로 외부 도메인이 여러 번 실패 질의. "느린 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_countnf_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 안이 아닌 노드 인터페이스에서.
  • 증상-층 매핑 표가 "무엇부터 의심할까"의 빠른 길잡이. 증상의 정밀도가 진단 속도를 결정.

생각해 볼 문제

  1. "Service IP로는 되는데 Pod IP로 안 된다" — 어느 층인가? 다음 확인 명령은?
  2. 간헐적으로 연결이 실패한다. conntrack을 의심해 확인하는 명령 흐름을 써라.
  3. NetworkPolicy가 막았다고 의심되는데 Hubble이 없다. 어떻게 확인할까?
  4. 외부 API 호출이 느리다. 결정 트리에서 어디부터? ndots 확인 방법은?
  5. tcpdump로 노드 인터페이스를 잡았다. 패킷이 들어오기만 하고 나가지 않는다. 의미는?
  6. EndpointSlice가 비어있다. 가능한 원인 3가지(셀렉터/readiness/오프라인).

참고

은 관리자 편집기 위젯 라벨이 됨 -->