Overview
Kubernetes 클러스터에서 발생하는 네트워크 문제를 해결하기 위해서는 내부 네트워크 구조를 정확히 이해해야 한다. 특히 온프레미스와 AWS 클러스터처럼 서로 다른 환경에서 동일한 문제가 발생할 때, tcpdump를 통한 패킷 분석은 필수적이다.
이 글에서는 kube-proxy IPVS 모드를 사용하는 환경에서 Kubernetes 네트워크의 내부 동작 원리와 실제 패킷 플로우를 tcpdump로 추적하는 방법을 상세히 다룬다. 또한 직접 따라할 수 있는 실습 예제를 통해 이론을 실전에 적용하는 방법을 알아본다.

Kubernetes 네트워크 계층 구조
Kubernetes 네트워크는 크게 세 가지 계층으로 구성된다.
Node Network (물리 네트워크)
노드 간 통신을 담당하는 실제 물리적 네트워크 계층이다. 온프레미스의 경우 일반적으로 10.0.0.0/8 또는 172.16.0.0/12 대역을 사용하고, AWS에서는 VPC CIDR 대역(예: 10.100.0.0/16)을 사용한다. 각 노드는 고유한 IP를 할당받아 서로 통신한다.
Pod Network (컨테이너 네트워크)
Pod에 할당되는 가상 네트워크 대역이다. CNI 플러그인(Calico, Flannel, AWS VPC CNI 등)에 의해 관리되며, 일반적으로 192.168.0.0/16 또는 10.244.0.0/16 같은 대역을 사용한다. 각 Pod는 이 대역에서 고유한 IP를 할당받는다.
Service Network (가상 네트워크)
Service 리소스에 할당되는 완전한 가상 IP 대역이다. 기본적으로 10.96.0.0/12 대역을 사용하며, 실제 네트워크 인터페이스에는 존재하지 않고 kube-proxy가 iptables/IPVS 규칙으로 관리한다. ClusterIP는 이 대역에서 할당된다.
IPVS vs iptables 모드
iptables 모드의 한계
iptables 모드는 모든 Service와 Endpoint를 iptables 규칙으로 관리한다. Service가 10개이고 각각 3개의 Pod가 있다면 30개 이상의 iptables 규칙이 생성된다. 문제는 iptables 규칙 처리가 선형 탐색 방식이라는 점이다. 규칙이 1000개면 평균적으로 500개의 규칙을 검사해야 하므로 레이턴시가 증가한다. 대규모 클러스터에서는 수만 개의 규칙이 생성되어 심각한 성능 저하가 발생할 수 있다.
IPVS 모드의 장점
IPVS는 Linux 커널의 L4 로드밸런싱 기술로, 해시 테이블을 사용해 O(1) 시간 복잡도로 규칙을 조회한다. 10개든 10000개든 조회 속도가 거의 동일하다. 또한 다양한 로드밸런싱 알고리즘을 지원한다. Round Robin(rr)은 순차적으로 분산하고, Least Connection(lc)은 연결 수가 적은 백엔드로 전송하며, Source Hashing(sh)은 클라이언트 IP 기반 세션 유지가 가능하다.
IPVS 모드 확인 방법
현재 클러스터가 IPVS 모드인지 확인하려면 kube-proxy의 ConfigMap을 조회한다. `kubectl get cm kube-proxy -n kube-system -o yaml` 명령으로 확인할 수 있으며, mode 필드가 ipvs로 설정되어 있으면 IPVS 모드다. 또는 노드에 접속해서 ipvsadm -Ln 명령으로 IPVS 규칙을 직접 확인할 수 있다.
k get cm -n kube-system kube-proxy -o yaml | k neat |grep mode -A6
mode: ipvs
nftables:
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0s
nodePortAddresses: []
IPVS 내부 동작 원리
가상 서버 생성
Service가 생성되면 kube-proxy는 해당 ClusterIP를 가상 서버로 등록한다. 예를 들어 nginx-service의 ClusterIP가 10.96.100.50이고 포트가 80이면, IPVS는 "10.96.100.50:80"을 가상 서버로 생성한다.
Real Server 등록
Service의 Endpoint로 등록된 각 Pod IP를 Real Server로 추가한다. nginx Pod가 3개이고 IP가 `192.168.1.10, 192.168.1.11, 192.168.1.12` 라면, 이 세 개의 IP가 모두 Real Server로 등록되어 로드밸런싱 대상이 된다.
패킷 전달 방식
클라이언트가 ClusterIP로 요청을 보내면 IPVS가 이를 가로채서 선택된 Real Server IP로 DNAT를 수행한다. 응답 패킷이 돌아올 때는 반대로 SNAT를 수행해 클라이언트가 ClusterIP로부터 응답을 받은 것처럼 보이게 한다.
kube-ipvs0 인터페이스
IPVS 모드에서는 `kube-ipvs0` 이라는 더미 인터페이스가 생성되고, 모든 ClusterIP가 이 인터페이스에 바인딩된다. `ip addr show kube-ipvs0` 명령으로 확인하면 수많은 ClusterIP가 할당되어 있는 것을 볼 수 있다. 이는 로컬 라우팅을 위한 것으로, 커널이 해당 IP를 로컬 주소로 인식하게 만든다.
ip addr show kube-ipvs0
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 86:54:48:7a:c7:9f brd ff:ff:ff:ff:ff:ff
inet 10.233.48.215/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.10.10.55/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.233.49.249/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.10.10.56/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.233.8.105/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.10.10.62/32 scope global kube-ipvs0
10.233.48.215를 확인해본다.
k get svc --all-namespaces |grep 10.233.48.215
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ingress-nginx ingress-nginx-controller LoadBalancer 10.233.48.215 10.10.10.55 80:30860/TCP,443:32200/TCP 106d
실습 환경 구축 및 네트워크 정보 파악
실습 환경 구축
Step 1: 테스트용 네임스페이스 생성
먼저 격리된 테스트 환경을 만들기 위해 별도의 네임스페이스를 생성한다.
kubectl create namespace network-test
Step 2: 클러스터 네트워크 정보 확인
실습을 시작하기 전에 현재 클러스터의 네트워크 설정을 파악한다.
Pod CIDR 확인
kubectl cluster-info dump | grep -m 1 cluster-cidr
또는
kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' | tr ' ' '\n'
출력 예시
10.233.64.0/24
10.233.65.0/24
10.233.66.0/24
10.233.67.0/24
이 정보를 통해 Pod IP가 10.233.x.x 대역에서 할당될 것임을 알 수 있다.
Service CIDR 확인
kubectl cluster-info dump | grep -m 1 service-cluster-ip-range
출력 예시
"--service-cluster-ip-range=10.233.0.0/18"
Service ClusterIP는 10.233.0.0/18 대역에서 할당된다.
CNI 플러그인 확인
kubectl get pods -n kube-system -o wide | grep -E "calico|flannel|aws-node|cilium|weave"
또는
kubectl get daemonsets -n kube-system
- 사용 중인 CNI 플러그인을 확인할 수 있다.
Step 3: 백엔드 애플리케이션 배포
간단한 nginx Deployment를 생성한다. 다음 명령으로 한 줄로 생성할 수 있다.
kubectl create deployment nginx --image=nginx:latest --replicas=3 -n network-test
배포가 완료되면 Pod 상태를 확인한다.
kubectl get pods -n network-test -o wide
출력 예시는 다음과 같다.
kubectl get pods -n network-test -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-54c98b4f84-ftvlg 1/1 Running 0 33s 10.233.66.111 k8s-compute-02 <none> <none>
nginx-54c98b4f84-tgmgh 1/1 Running 0 33s 10.233.65.212 k8s-compute-01 <none> <none>
nginx-54c98b4f84-vzgjc 1/1 Running 0 33s 10.233.67.86 k8s-compute-03 <none> <none>
- 각 Pod의 IP 주소와 실행 중인 노드를 메모해둔다.
Step 4: Service 생성
이제 이 Pod들을 노출하는 ClusterIP Service를 생성한다.
kubectl expose deployment nginx --port=80 --target-port=80 --name=nginx-service -n network-test
Service 정보를 확인한다.
kubectl get svc nginx-service -n network-test
출력 예시
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service ClusterIP 10.233.9.232 <none> 80/TCP 16s
- ClusterIP 주소를 메모한다. 이 예시에서는 10.233.9.232 이다.
Step 5: Endpoint 확인 (Kubernetes 1.21+)
Endpoint 정보를 확인하면 deprecated 경고가 나타날 수 있다.
kubectl get endpoints nginx-service -n network-test
# 출력 예시
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME ENDPOINTS AGE
nginx-service 10.233.65.212:80,10.233.66.111:80,10.233.67.86:80 20s
- 이는 정상이다. Kubernetes 1.21 이상에서는 EndpointSlice API 사용이 권장된다.
최신 방식: EndpointSlice 사용
kubectl get endpointslices -n network-test
```
출력 예시:
```
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
nginx-service-abc12 IPv4 80 10.233.65.212,10.233.66.111,10.233.67.86 2m
더 상세한 정보를 보려면
kubectl describe endpointslices -n network-test
# 출력 예시
Name: nginx-service-9jrhc
Namespace: network-test
Labels: app=nginx
endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io
kubernetes.io/service-name=nginx-service
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2025-11-12T02:38:28Z
AddressType: IPv4
Ports:
Name Port Protocol
---- ---- --------
<unset> 80 TCP
Endpoints:
- Addresses: 10.233.67.86
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/nginx-54c98b4f84-vzgjc
NodeName: k8s-compute-03
Zone: <unset>
- Addresses: 10.233.66.111
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/nginx-54c98b4f84-ftvlg
NodeName: k8s-compute-02
Zone: <unset>
- Addresses: 10.233.65.212
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/nginx-54c98b4f84-tgmgh
NodeName: k8s-compute-01
Zone: <unset>
Events: <none>
- EndpointSlice는 더 확장 가능하고 효율적인 방식으로, 각 백엔드의 상태와 노드 정보를 명확히 보여준다.
Step 6: 클라이언트 Pod 생성
네트워크 디버깅 도구가 포함된 클라이언트 Pod를 생성한다.
kubectl run client-pod --image=nicolaka/netshoot -n network-test --command -- sleep infinity
Pod가 실행될 때까지 기다린다.
kubectl wait --for=condition=ready pod/client-pod --timeout=60s -n network-test
클라이언트 Pod의 정보를 확인한다.
kubectl get pod client-pod -o wide -n network-test
# 출력 예시
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
client-pod 1/1 Running 0 43s 10.233.66.94 k8s-compute-02 <none> <none>
- 클라이언트 Pod의 IP도 메모한다.
Step 7: 환경 정보 자동 수집 스크립트
매번 IP를 확인하는 것이 번거로우므로 스크립트를 만들어 사용한다.
cat << 'EOF' > network-test-info.sh
#!/bin/bash
NAMESPACE="network-test"
echo "======================================"
echo " Kubernetes Network Test Environment"
echo "======================================"
echo ""
# Service IP
SERVICE_IP=$(kubectl get svc nginx-service -n $NAMESPACE -o jsonpath='{.spec.clusterIP}')
echo "Service ClusterIP: $SERVICE_IP"
echo ""
# Client Pod IP
CLIENT_IP=$(kubectl get pod client-pod -n $NAMESPACE -o jsonpath='{.status.podIP}' 2>/dev/null)
CLIENT_NODE=$(kubectl get pod client-pod -n $NAMESPACE -o jsonpath='{.spec.nodeName}' 2>/dev/null)
echo "Client Pod:"
echo " IP: $CLIENT_IP"
echo " Node: $CLIENT_NODE"
echo ""
# Backend Pod IPs
echo "Backend Pods:"
kubectl get pods -n $NAMESPACE -l app=nginx -o custom-columns=NAME:.metadata.name,IP:.status.podIP,NODE:.spec.nodeName --no-headers | while read name ip node; do
echo " $name"
echo " IP: $ip"
echo " Node: $node"
done
echo ""
# Endpoints
echo "Endpoints:"
ENDPOINTS=$(kubectl get endpoints nginx-service -n $NAMESPACE -o jsonpath='{.subsets[0].addresses[*].ip}' 2>/dev/null)
echo " $ENDPOINTS"
echo ""
# 첫 번째 백엔드 IP
BACKEND_IP=$(echo $ENDPOINTS | awk '{print $1}')
echo "======================================"
echo " Useful Commands"
echo "======================================"
echo ""
echo "# Export variables:"
echo "export SERVICE_IP=$SERVICE_IP"
echo "export CLIENT_IP=$CLIENT_IP"
echo "export BACKEND_IP=$BACKEND_IP"
echo ""
echo "# IPVS rules on $CLIENT_NODE:"
echo "ssh $CLIENT_NODE 'sudo ipvsadm -Ln | grep -A 5 $SERVICE_IP'"
echo ""
echo "# tcpdump on client pod:"
echo "kubectl exec -it client-pod -n $NAMESPACE -- tcpdump -i any -nn \"host $SERVICE_IP or host $BACKEND_IP\" -w /tmp/client.pcap"
echo ""
echo "# tcpdump on node:"
echo "ssh $CLIENT_NODE 'sudo tcpdump -i any -nn \"host $SERVICE_IP or host $BACKEND_IP\" -w /tmp/node.pcap'"
echo ""
EOF
chmod +x network-test-info.sh
스크립트를 실행해 환경 정보를 확인한다.
./network-test-info.sh
======================================
Kubernetes Network Test Environment
======================================
Service ClusterIP: 10.233.9.232
Client Pod:
IP: 10.233.66.94
Node: k8s-compute-02
Backend Pods:
nginx-54c98b4f84-ftvlg
IP: 10.233.66.111
Node: k8s-compute-02
nginx-54c98b4f84-tgmgh
IP: 10.233.65.212
Node: k8s-compute-01
nginx-54c98b4f84-vzgjc
IP: 10.233.67.86
Node: k8s-compute-03
Endpoints:
10.233.65.212 10.233.66.111 10.233.67.86
======================================
Useful Commands
======================================
# Export variables:
export SERVICE_IP=10.233.9.232
export CLIENT_IP=10.233.66.94
export BACKEND_IP=10.233.65.212
# IPVS rules on k8s-compute-02:
ssh k8s-compute-02 'sudo ipvsadm -Ln | grep -A 5 10.233.9.232'
# tcpdump on client pod:
kubectl exec -it client-pod -n network-test -- tcpdump -i any -nn "host 10.233.9.232 or host 10.233.65.212" -w /tmp/client.pcap
# tcpdump on node:
ssh k8s-compute-02 'sudo tcpdump -i any -nn "host 10.233.9.232 or host 10.233.65.212" -w /tmp/node.pcap'
이제 환경변수를 설정한다.
export SERVICE_IP=10.233.9.232
export CLIENT_IP=10.233.66.94
export BACKEND_IP=10.233.65.212
Step 8: 기본 연결 테스트
클라이언트 Pod에서 Service로 요청을 보내 정상 동작하는지 확인한다.
kubectl exec -it client-pod -n network-test -- curl -s http://nginx-service
nginx의 기본 HTML 응답이 출력되면 정상이다. 여러 번 실행해보자.
kubectl exec -it client-pod -n network-test -- bash -c "for i in {1..10}; do curl -s http://nginx-service | grep title; done"
```
# 출력 예시
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
- 모두 정상적으로 응답하면 테스트 환경 구축이 완료됐다.
IPVS 규칙 상세 분석
Step 1: 노드 접속 및 IPVS 규칙 확인
클라이언트 Pod가 실행 중인 노드에 SSH로 접속한다.
# 클라이언트 노드 확인
kubectl get pod client-pod -n network-test -o jsonpath='{.spec.nodeName}'
## 예시
k8s-compute-02
# 노드에 접속
ssh k8s-compute-02
IPVS 규칙을 확인한다.
sudo ipvsadm -Ln
출력에서 `nginx-service` 의 ClusterIP를 찾는다.
sudo ipvsadm -Ln | grep -A 5 "10.233.9.232"
# 출력 예시
TCP 10.233.9.232:80 rr
-> 10.233.65.212:80 Masq 1 0 0
-> 10.233.66.111:80 Masq 1 0 0
-> 10.233.67.86:80 Masq 1 0 0
TCP 10.233.10.4:80 rr
-> 10.233.67.94:8081 Masq 1 0 0
여기서 중요한 정보는 다음과 같다:
- TCP 10.233.9.232:80: 가상 서버 주소 (Service ClusterIP)
- rr: Round Robin 스케줄러 알고리즘
- → 10.233.65.212:80: Real Server (백엔드 Pod IP)
- Masq: Masquerade 모드 (NAT 사용)
- 숫자들: Weight, ActiveConn, InActConn 값
Step 2: kube-ipvs0 인터페이스 확인
ClusterIP가 실제로 어디에 바인딩되어 있는지 확인한다.
ip addr show kube-ipvs0
수많은 ClusterIP가 나열되는데, grep으로 우리의 Service를 찾는다.
ip addr show kube-ipvs0 | grep 10.233.0.50
```
# 출력 예시
inet 10.233.9.232/32 scope global kube-ipvs0
- 이는 커널이 이 IP를 로컬 주소로 인식하도록 만들어, IPVS가 처리할 수 있게 한다.
Step 3: 라우팅 테이블 확인
Pod IP로 가는 경로를 확인한다.
ip route get 10.233.65.212
# 출력 예시 (Calico)
10.233.65.212 dev cali123abc via 10.233.65.1 src 10.100.1.10
# 출력 예시 (Flannel)
10.233.65.212 dev cni0 src 10.233.64.1
# 출력 예시 (Cilium)
10.233.65.212 dev cilium_host src 10.233.66.207 uid 1000
cache mtu 1450
- 같은 노드의 Pod는 로컬 인터페이스를 통해, 다른 노드의 Pod는 터널이나 라우팅을 통해 전달된다.
Cilium 사용 시 확인사항
kubectl get pods -n kube-system | grep kube-proxy
kube-proxy-6lw4l 1/1 Running 1 (70d ago) 103d
kube-proxy-g25g7 1/1 Running 2 (70d ago) 103d
kube-proxy-h5fl7 1/1 Running 1 (70d ago) 99d
kube-proxy-xb9rt 1/1 Running 1 (70d ago) 103d
kubectl -n kube-system exec -it ds/cilium -- cilium status | grep KubeProxyReplacement
Defaulted container "cilium-agent" out of: cilium-agent, config (init), mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init), install-cni-binaries (init)
KubeProxyReplacement: False
- `KubeProxyReplacement: False` 인걸 확인해야 한다.
전체 라우팅 테이블을 확인한다.
ip route show | grep 10.233
```
# 출력 예시
10.233.64.0/24 via 10.233.66.207 dev cilium_host proto kernel src 10.233.66.207 mtu 1450
10.233.65.0/24 via 10.233.66.207 dev cilium_host proto kernel src 10.233.66.207 mtu 1450
10.233.66.0/24 via 10.233.66.207 dev cilium_host proto kernel src 10.233.66.207
10.233.66.207 dev cilium_host proto kernel scope link
10.233.67.0/24 via 10.233.66.207 dev cilium_host proto kernel src 10.233.66.207 mtu 1450
- 각 노드의 Pod CIDR에 대한 라우팅 정보를 볼 수 있다.
패킷 플로우 실습 및 분석
준비: 백엔드 Pod에 고유 응답 설정
어느 백엔드 Pod가 실제로 응답했는지 확인하기 위해 각 Pod에 고유한 응답을 설정한다.
# Pod 목록 가져오기
PODS=$(kubectl get pods -n network-test -l app=nginx -o jsonpath='{.items[*].metadata.name}')
# 각 Pod에 고유 응답 설정
i=1
for pod in $PODS; do
kubectl exec $pod -n network-test -- sh -c 'echo "Backend POD-'$i': $(hostname)" > /usr/share/nginx/html/index.html'
((i++))
done
테스트해본다.
kubectl exec -it client-pod -n network-test -- bash -c "for i in {1..12}; do curl -s http://nginx-service; done"
```
# 출력 예시
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-3: nginx-54c98b4f84-vzgjc
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-3: nginx-54c98b4f84-vzgjc
Backend POD-1: nginx-54c98b4f84-ftvlg
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-2: nginx-54c98b4f84-tgmgh
Backend POD-1: nginx-54c98b4f84-ftvlg
Backend POD-3: nginx-54c98b4f84-vzgjc
- Round Robin으로 순서대로 분산되는 것을 확인할 수 있다.
패킷 캡처 준비
이제 실제 패킷 플로우를 캡처해보자. 터미널 4개를 준비한다.
- 터미널 1: 클라이언트 Pod 내부 패킷 캡처
- 터미널 2: 노드에서 패킷 캡처
- 터미널 3: 백엔드 Pod 내부 패킷 캡처
- 터미널 4: 트래픽 생성용
터미널에서 패킷 캡처 하기전에 아까 만들었던 `network-test-info.sh` 돌려서 확인 후 export 설정해줘야 한다.
export SERVICE_IP=10.233.9.232
export CLIENT_IP=10.233.66.94
export BACKEND_IP=10.233.65.212
터미널 1: 클라이언트 Pod 패킷 캡처
kubectl exec -it client-pod -n network-test -- tcpdump -i any -nn -v "host $SERVICE_IP or host $BACKEND_IP" -w /tmp/client.pcap
- 이 명령은 ClusterIP와 특정 백엔드 Pod IP를 모두 캡처한다.
터미널 2: 노드 패킷 캡처
클라이언트 Pod가 있는 노드에 SSH로 접속한 상태에서 실행한다.
sudo tcpdump -i any -nn -v "host $SERVICE_IP or host $BACKEND_IP" -w /tmp/node.pcap
- IPVS의 DNAT/SNAT 전후를 모두 볼 수 있다.
터미널 3: 백엔드 Pod 패킷 캡처
백엔드 Pod 중 하나를 선택한다. 첫 번째 백엔드 Pod를 선택하자.
# Pod 이름 가져오기
BACKEND_POD=$(kubectl get pods -n network-test -l app=nginx -o jsonpath='{.items[0].metadata.name}')
# Pod에 접속
kubectl exec -it $BACKEND_POD -n network-test -- bash
nginx 컨테이너에는 tcpdump가 없으므로 설치한다.
apt-get update && apt-get install -y tcpdump
설치 후 캡처를 시작한다.
tcpdump -i any -nn -v port 80 -w /tmp/backend.pcap
터미널 4: 트래픽 생성
세 개의 캡처가 모두 실행 중인 상태에서, 네 번째 터미널에서 트래픽을 생성한다.
kubectl exec -it client-pod -n network-test -- bash
Pod 내부에서 다음 명령을 실행한다.
for i in {1..20}; do
echo "Request $i:"
curl -s http://nginx-service
sleep 0.5
done
# 예시
Request 1:
Backend POD-3: nginx-54c98b4f84-vzgjc
Request 2:
Backend POD-2: nginx-54c98b4f84-tgmgh
Request 3:
Backend POD-1: nginx-54c98b4f84-ftvlg
Request 4:
Backend POD-1: nginx-54c98b4f84-ftvlg
- 20개의 요청이 전송되는 동안 각 터미널에서 패킷이 캡처된다.
캡처 파일 분석
로컬로 파일 복사
캡처한 파일들을 로컬 컴퓨터로 복사해 Wireshark로 분석한다.
# 클라이언트 Pod에서
kubectl cp network-test/client-pod:/tmp/client.pcap ./client.pcap
# 노드에서 (scp 사용)
scp k8s-compute-02:/tmp/node.pcap ./node.pcap
# 백엔드 Pod에서
kubectl cp network-test/$BACKEND_POD:/tmp/backend.pcap ./backend.pcap
tcpdump로 빠르게 확인
Wireshark 없이도 tcpdump로 내용을 확인할 수 있다.
클라이언트 관점
tcpdump -r client.pcap -nn -v | head -n 50
...
# 예시
10.233.66.94.43856 > 10.233.9.232.80: Flags [S] # SYN: 연결 시작
10.233.9.232.80 > 10.233.66.94.43856: Flags [S.] # SYN-ACK: 서버 응답
10.233.66.94.43856 > 10.233.9.232.80: Flags [.] # ACK: 3-way handshake 완료
10.233.66.94.43856 > 10.233.9.232.80: Flags [P.] # HTTP GET 요청 전송
GET / HTTP/1.1
Host: nginx-service
User-Agent: curl/8.14.1
10.233.9.232.80 > 10.233.66.94.43856: Flags [.] # ACK: 요청 수신 확인
10.233.9.232.80 > 10.233.66.94.43856: Flags [P.] # HTTP 200 OK 응답
HTTP/1.1 200 OK
Server: nginx/1.29.3
Content-Length: 38
10.233.66.94.43856 > 10.233.9.232.80: Flags [F.] # FIN: 연결 종료
10.233.9.232.80 > 10.233.66.94.43856: Flags [F.] # FIN: 서버도 종료
핵심 포인트
- 클라이언트 Pod IP: 10.233.66.94
- Service ClusterIP: 10.233.9.232 (Port 80)
- 클라이언트 임시 포트: 43856, 43866, ... (매 요청마다 새로운 포트)
가장 중요한 관점
- 클라이언트는 오직 Service ClusterIP(10.233.9.232)와만 통신
- 실제 백엔드 Pod IP는 전혀 보이지 않음
- 클라이언트는 어느 백엔드가 응답했는지 알 수 없음
- 이것이 Service 추상화의 핵심
노드 관점
tcpdump -r node.pcap -nn -v | head -n 100
- 핵심: 같은 패킷이 IP 변환 전/후로 두 번씩 나타남!
요청 패킷 (SYN)
# DNAT 전 - Service IP로 들어옴
12:15:36.180603 In 10.233.66.94.34376 > 10.233.9.232.80: Flags [S]
# DNAT 후 - 실제 Pod IP로 변환되어 나감
12:15:36.180621 Out 10.233.66.94.34376 > 10.233.65.212.80: Flags [S]
IPVS가 목적지 IP를 변환
- 10.233.9.232 (Service ClusterIP) → 10.233.65.212 (실제 백엔드 Pod)
응답 패킷 (SYN-ACK)
# SNAT 전 - 실제 Pod에서 응답 도착
12:15:36.180869 P 10.233.65.212.80 > 10.233.66.94.34376: Flags [S.]
# SNAT 후 - Service IP로 변환되어 클라이언트로
12:15:36.180901 Out 10.233.9.232.80 > 10.233.66.94.34376: Flags [S.]
IPVS가 소스 IP를 변환
- 10.233.65.212 (실제 백엔드 Pod) → 10.233.9.232 (Service ClusterIP)
HTTP 요청 패킷
# DNAT 전 - Service로
12:15:36.180971 In 10.233.66.94.34376 > 10.233.9.232.80: Flags [P.]
GET / HTTP/1.1
Host: nginx-service
# DNAT 후 - 실제 Pod로
12:15:36.180975 Out 10.233.66.94.34376 > 10.233.65.212.80: Flags [P.]
GET / HTTP/1.1
Host: nginx-service
- 같은 HTTP 요청이지만 목적지 IP만 다름!
HTTP 응답 패킷
# SNAT 전 - 실제 Pod에서
12:15:36.181526 P 10.233.65.212.80 > 10.233.66.94.34376: Flags [P.]
HTTP/1.1 200 OK
Server: nginx/1.29.3
# SNAT 후 - Service IP로 위장
12:15:36.181534 Out 10.233.9.232.80 > 10.233.66.94.34376: Flags [P.]
HTTP/1.1 200 OK
Server: nginx/1.29.3
백엔드 관점 (Service IP 모름)
tcpdump -r backend.pcap -nn -v | head -n 50
- 핵심: Service IP(10.233.9.232)가 전혀 나타나지 않음!
백엔드가 보는 패킷
# 요청 수신 - 클라이언트 실제 IP로부터
12:09:49.898326 In 10.233.66.94.43874 > 10.233.66.111.80: Flags [S]
# 응답 전송 - 클라이언트 실제 IP로
12:09:49.898345 Out 10.233.66.111.80 > 10.233.66.94.43874: Flags [S.]
# HTTP 요청 수신
12:09:49.898428 In 10.233.66.94.43874 > 10.233.66.111.80: Flags [P.]
GET / HTTP/1.1
Host: nginx-service
User-Agent: curl/8.14.1
# HTTP 응답 전송
12:09:49.898686 Out 10.233.66.111.80 > 10.233.66.94.43874: Flags [P.]
HTTP/1.1 200 OK
Server: nginx/1.29.3
Content-Length: 38
백엔드 입장에서
- 클라이언트 IP: 10.233.66.94
- 백엔드 자신 IP: 10.233.66.111
- Service IP (10.233.9.232)는 전혀 모름
- IPVS의 존재도 전혀 모름
세 관점 비교 정리
| 관점 | 소스 IP | 목적지 IP | 특징 |
| 클라이언트 | 10.233.66.94 | 10.233.9.232 (Service) | Service만 알고 있음 |
| 노드 (DNAT 전) | 10.233.66.94 | 10.233.9.232 (Service) | IPVS가 가로챔 |
| 노드 (DNAT 후) | 10.233.66.94 | 10.233.65.212 (Pod) | 실제 Pod IP로 변환 |
| 백엔드 | 10.233.66.94 | 10.233.66.111 (자신) | 직접 통신처럼 보임 |
응답 패킷 (역방향)
| 관점 | 소스 IP | 목적지 IP | 특징 |
| 백엔드 | 10.233.66.111 (자신) | 10.233.66.94 | 클라이언트로 직접 응답 |
| 노드 (SNAT 전) | 10.233.65.212 (Pod) | 10.233.66.94 | IPVS가 가로챔 |
| 노드 (SNAT 후) | 10.233.9.232 (Service) | 10.233.66.94 | Service IP로 위장 |
| 클라이언트 | 10.233.9.232 (Service) | 10.233.66.94 | Service로부터 받은 것처럼 |
IPVS
노드에서 일어나는 일
- 들어오는 패킷 감지
- 목적지 = Service IP? → IPVS 개입!
- 해시 테이블 조회
- 10.233.9.232:80 → 가상 서버 발견
- Real Server 선택
- Round Robin: 10.233.65.212, 10.233.66.111, 10.233.67.86 중 선택
- DNAT 수행
- 목적지 IP 변환: 10.233.9.232 → 10.233.65.212
- 커넥션 트래킹 기록
- 나중에 응답 패킷 처리 위해 저장
- 응답 패킷 감지
- 소스 = 선택된 Real Server? → 커넥션 테이블 조회
- SNAT 수행
- 소스 IP 변환: 10.233.65.212 → 10.233.9.232
결과
- 클라이언트는 Service와만 통신한다고 생각
- 백엔드는 클라이언트와 직접 통신한다고 생각
- 둘 다 IPVS의 존재를 모름 = 완벽한 추상화!
실시간 통계 확인
패킷 캡처 대신 실시간으로 IPVS 통계를 모니터링할 수도 있다.
터미널 1: 통계 모니터링
# Export variables:
export SERVICE_IP=10.233.9.232
export CLIENT_IP=10.233.66.94
export BACKEND_IP=10.233.65.212
- SERVICE_IP 사용한다.
노드에서 다음 명령을 실행한다.
# 노드에 접속
ssh k8s-compute-02
# 실시간 통계 확인
watch -n 1 "sudo ipvsadm -Ln --stats | grep -A 5 '10.233.9.232'"
- 이 명령은 1초마다 통계를 갱신해서 보여준다.
터미널 2: 트래픽 생성
kubectl exec -it client-pod -n network-test -- bash -c "for i in {1..5}; do echo Request $i; curl -s http://nginx-service; sleep 1; done"
...
# 출력 예시
Request
Backend POD-1: nginx-54c98b4f84-ftvlg
Request
Backend POD-3: nginx-54c98b4f84-vzgjc
Request
Backend POD-3: nginx-54c98b4f84-vzgjc
Request
Backend POD-3: nginx-54c98b4f84-vzgjc
Request
Backend POD-3: nginx-54c98b4f84-vzgjc
# 첫 번째 터미널에서 Conns, InPkts, OutPkts 값이 증가하는 것을 실시간으로 볼 수 있다.
## 출력 예시
TCP 10.233.0.50:80 rr
-> 10.233.65.212:80 Masq 15 450 445
-> 10.233.66.111:80 Masq 16 480 475
-> 10.233.67.86:80 Masq 14 420 415
- 세 개의 Real Server에 트래픽이 거의 균등하게 분산되는 것을 확인할 수 있다.
상세 통계 확인
초당 전송률도 확인할 수 있다.
sudo ipvsadm -Ln --rate | grep -A 5 "10.233.9.232"
# 출력 예시
TCP 10.233.9.232:80 rr
-> 10.233.65.212:80 5 150 145 12K 120K
-> 10.233.66.111:80 5 148 143 12K 118K
-> 10.233.67.86:80 5 152 147 13K 122K
- CPS(초당 연결), InPPS(초당 입력 패킷), OutPPS(초당 출력 패킷), InBPS(초당 입력 바이트), OutBPS(초당 출력 바이트)가 표시된다.
주의: Cilium CNI를 사용하는 환경에서는 IPVS 통계가 0으로 표시될 수 있다. 이는 Cilium eBPF가 Socket-level에서 먼저 패킷을 처리하기 때문이다. 이런 환경에서의 모니터링 방법은 Cilium 네트워크 환경 분석 가이드를 참고하자.
트러블슈팅 시 유용한 명령어 모음
실제 문제 상황에서 빠르게 사용할 수 있는 명령어들을 정리한다.
연결 문제 진단
# Service에서 Pod로의 연결 확인
kubectl exec -it client-pod -n network-test -- curl -v --connect-timeout 5 http://nginx-service
# DNS 해석 확인
kubectl exec -it client-pod -n network-test -- nslookup nginx-service
# Service IP로 직접 연결 테스트
kubectl exec -it client-pod -n network-test -- curl -v --connect-timeout 5 http://$SERVICE_IP
IPVS 상태 점검
# IPVS 규칙 전체 확인
sudo ipvsadm -Ln
# 특정 Service의 연결 상태 확인
sudo ipvsadm -Lnc | grep $SERVICE_IP
# IPVS 커널 모듈 로드 확인
lsmod | grep ip_vs
# conntrack 테이블 확인 (NAT 추적)
sudo conntrack -L -d $SERVICE_IP 2>/dev/null | head -20
실시간 패킷 분석
# SYN 패킷만 캡처 (연결 시도 추적)
sudo tcpdump -i any -nn "tcp[tcpflags] & tcp-syn != 0" and host $SERVICE_IP
# RST 패킷 캡처 (연결 거부 감지)
sudo tcpdump -i any -nn "tcp[tcpflags] & tcp-rst != 0" and host $SERVICE_IP
# 재전송 패킷 확인 (네트워크 지연/손실 감지)
sudo tcpdump -i any -nn "tcp[tcpflags] & tcp-syn != 0" and host $SERVICE_IP -v | grep -i retrans
Endpoint 상태 확인
# EndpointSlice 상태 확인 (Ready 여부)
kubectl get endpointslices -n network-test -o wide
# 특정 Service의 백엔드 Pod 상태
kubectl get pods -n network-test -l app=nginx -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,IP:.status.podIP,NODE:.spec.nodeName,READY:.status.conditions[?(@.type=="Ready")].status
자주 발생하는 문제와 해결 방법
문제 1: Service 연결 시 간헐적 타임아웃
증상: curl 요청이 때때로 타임아웃 발생
원인 확인
# 백엔드 Pod 상태 확인
kubectl get pods -n network-test -l app=nginx
# IPVS에서 특정 Real Server 연결 실패 확인
sudo ipvsadm -Ln --stats | grep -A 5 $SERVICE_IP
해결: InActConn이 비정상적으로 높은 Real Server가 있다면 해당 Pod의 상태를 점검한다.
문제 2: 새로 생성된 Pod로 트래픽이 가지 않음
증상: Pod가 Running 상태이지만 트래픽을 받지 못함
원인 확인
# EndpointSlice에 Pod가 등록되었는지 확인
kubectl describe endpointslices -n network-test | grep -A 2 "Addresses"
# IPVS Real Server 목록 확인
sudo ipvsadm -Ln | grep -A 10 $SERVICE_IP
해결: Pod의 Readiness Probe가 통과했는지 확인하고, kube-proxy가 정상 동작 중인지 점검한다.
문제 3: 클라이언트 IP가 보존되지 않음
증상: 백엔드에서 클라이언트 원본 IP 대신 노드 IP가 보임
원인: SNAT(Masquerade)가 적용되어 소스 IP가 변환됨
해결
# externalTrafficPolicy를 Local로 설정 (NodePort/LoadBalancer 타입)
kubectl patch svc nginx-service -n network-test -p '{"spec":{"externalTrafficPolicy":"Local"}}'
- 주의: Local 모드는 해당 노드에 Pod가 없으면 트래픽이 드랍될 수 있다.
실습 환경 정리
실습이 완료되면 생성한 리소스를 정리한다.
# 테스트 네임스페이스 전체 삭제 (내부 리소스 모두 삭제됨)
kubectl delete namespace network-test
# 로컬 파일 정리
rm -f client.pcap node.pcap backend.pcap network-test-info.sh
# 노드에서 캡처 파일 정리 (SSH 접속 후)
ssh k8s-compute-02 'sudo rm -f /tmp/*.pcap'
삭제 확인
kubectl get all -n network-test
# 출력: No resources found in network-test namespace.
마무리
이 글에서는 Kubernetes IPVS 모드의 네트워크 동작 원리와 tcpdump를 활용한 패킷 분석 방법을 다뤘다.
핵심 내용 요약
IPVS의 역할
- Service ClusterIP를 가상 서버로 등록하고 Pod IP를 Real Server로 관리
- 해시 테이블 기반 O(1) 조회로 대규모 클러스터에서도 일정한 성능 유지
- DNAT/SNAT를 통해 클라이언트와 백엔드 모두에게 투명한 로드밸런싱 제공
패킷 플로우의 핵심
- 클라이언트는 Service IP와만 통신하며 실제 백엔드를 알지 못함
- 노드에서 IPVS가 패킷을 가로채 목적지 IP를 실제 Pod IP로 변환(DNAT)
- 응답 패킷은 반대로 소스 IP가 Service IP로 변환(SNAT)
- 백엔드는 클라이언트와 직접 통신하는 것처럼 보임
분석 포인트
- 노드 레벨에서 패킷을 캡처하면 DNAT/SNAT 전후를 모두 확인 가능
- ipvsadm으로 실시간 통계와 연결 상태 모니터링
- 세 관점(클라이언트, 노드, 백엔드)에서 동시에 분석하면 문제 원인 파악이 용이
이러한 이해를 바탕으로 온프레미스와 클라우드 환경 모두에서 발생하는 Kubernetes 네트워크 문제를 체계적으로 분석하고 해결할 수 있다.
Reference
- Kubernetes Documentation - Service
- Kubernetes Documentation - Virtual IPs and Service Proxies
- IPVS-Based In-Cluster Load Balancing Deep Dive
- LVS (Linux Virtual Server) Documentation
- tcpdump Manual
- Calico Documentation - About Kubernetes Services
- Cilium Documentation - Kubernetes Without kube-proxy
Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist
'Container Orchestration > Kubernetes' 카테고리의 다른 글
| Kubernetes 클러스터로의 외부 트래픽 흐름 완벽 가이드 (0) | 2026.06.04 |
|---|---|
| Cilium CNI 환경에서의 Kubernetes 네트워크 문제 해결 (0) | 2026.05.28 |
| ingress-nginx → nginx-gateway-fabric 마이그레이션 실전 기록 (온프레미스 K8s, 11개 인스턴스) (0) | 2026.05.14 |
| EKS 프로덕션 배포 가이드: 502 에러 제로 달성기 (0) | 2026.05.12 |
| Kubernetes 스토리지 타입 완벽 가이드: iSCSI, NFS... (0) | 2026.04.28 |