VPP 데이터 플레인 문제 해결
VPP 데이터 플레인을 사용하는 클러스터에서 문제가 생겼을 때 따라 해 볼 수 있는 진단 절차를 안내해요. calivppctl 설치 방법부터 로그 확인, 패킷 트레이싱, 크래시 분석까지 실제 명령과 함께 정리했어요. VPP 데이터 플레인을 구성하지 않은 클러스터라면 이 문서는 필요하지 않아요.
출처: 문서
본문
큰 그림
이 페이지는 VPP 데이터 플레인의 문제 해결 단계를 설명해요. VPP 데이터 플레인을 구성하지 않았다면 이 페이지는 필요 없어요!
VPP 데이터 플레인에서 문제를 만나면 Calico slack의 #vpp 채널로 이야기하거나 GitHub)에 새 이슈를 열어 알려 주세요.
calivppctl 설치
calivppctl은 vpp 컨테이너 이미지와 함께 배포되는 bash 헬퍼 스크립트예요. 다음 방법으로 호스트에 설치할 수 있고, VPP 데이터 플레인이 설치된 클러스터의 로그 수집과 디버깅에 도움이 돼요.
-
curl로 설치
curl https://raw.githubusercontent.com/projectcalico/vpp-dataplane/v3.32.0/test/scripts/vppdev.sh | tee /usr/bin/calivppctl chmod +x /usr/bin/calivppctl -
docker로 설치(Calico VPP가 실행 중인 클러스터)
vppcontainer=$(docker ps | grep vpp_calico-vpp | awk '{ print $1 }') docker cp ${vppcontainer}:/usr/bin/calivppctl /usr/bin/calivppctl -
kubectl로 설치(Calico VPP가 실행 중인 클러스터)
vpppod=$(kubectl -n calico-vpp-dataplane get pods -o wide | grep calico-vpp-node- | awk '{ print $1 }' | head -1) kubectl -n calico-vpp-dataplane exec -it ${vpppod} -c vpp -- cat /usr/bin/calivppctl | tee /usr/bin/calivppctl > /dev/null chmod +x /usr/bin/calivppctl
문제 해결
쿠버네티스 클러스터
먼저 쿠버네티스가 정상적으로 떠 있는지 확인해야 해요.
service kubelet status가 첫 번째 힌트를 줘요.- 문제는 kubelet 로그에 기록되는데, systemd를 사용한다면
journalctl -u kubelet -r -n200명령으로 확인할 수 있어요.
SECONDARY: 쿠버네티스는 swap이 활성화된 상태로 동작하지 않아요.
calico-vpp-node 데몬셋 시작
클러스터가 정상적으로 시작되면 다음 문제 후보는 데몬셋 구성이에요. 가장 좋은 방법은 pod부터 살펴보는 거예요. pod들이 정상 실행 중인가요? hugepages 가용량, 메모리 같은 구성 문제는 보통 여기에 기록돼요:
kubectl -n calico-vpp-dataplane describe pod/calico-vpp-node-XXXXX
SECONDARY: 이 시점에 hugepages가 부족하다면 할당한 뒤 kubelet을 재시작해야 반영돼요(예:
service kubelet restart)
VPP가 정상 동작하는지 확인
pod들이 더 이상 문제를 보고하지 않으면 pod들이 시작된 상태일 거예요. 각 노드에는 컨테이너가 두 개 있어요. VPP 프로세스를 시작하고 연결을 구성하는 vpp-manager, 그리고 pod 연결, 서비스 로드 밸런싱, BGP, 정책 등을 처리하는 에이전트죠.
먼저 VPP가 정상 실행 중인지 확인해요. 연결 구성이나 인터페이스 이름이 잘못됐다면 여기에 표시돼요. 이게 실행되면 다른 노드를 VPP 경유로 ping할 수 있어야 해요.
# Print VPP's log : basic connectivity and NIC configuration
calivppctl log -vpp myk8node1
그다음 에이전트가 보고하는 문제를 확인해요(예: 포트가 이미 사용 중이라 BGP listen에 실패하거나, 구성 일부가 빠진 경우). 오류가 없다면 pod에서 nslookup kubernetes.default가 동작해야 해요.
# Print the logs for the Calico VPP data plane agent, programming serviceIPs, BGP, ...
calivppctl log -agent myk8node1
여기까지 잘 안 풀리면 export로 export.tar.gz 번들을 만들어 #vpp 채널에서 도움을 요청할 수 있어요.
calivppctl export
VPP CLI 접근
더 깊이 디버깅하거나 패킷을 트레이싱하고 VPP 내부를 살펴보려면 다음 명령으로 vpp 셸을 열 수 있어요.
calivppctl vppctl myk8node1
인터페이스 나열과 기본 확인
기존 인터페이스와 기본 카운터를 나열하려면 이렇게 해요.
vpp# show int
vpp# show int addr
기본 인터페이스(예: dpdk를 사용하는 경우)를 더 자세히 보려면 오류와 드롭을 여기서 확인하세요.
vpp# show hardware-interfaces
오류를 확인할 만한 다른 곳들
vpp# show log # VPP startup log
vpp# show err # Prints out packet counters (not always actual errors, but includes drops)
vpp# show buffers # You should have non zero free buffers, otherwise traffic won't flow
패킷 트레이싱
내부 네트워크 구성
우선 네트워크가 어떻게 생겼는지 간단한 그림으로 보면:
컨테이너 인터페이스 이름은 tun[0-9]+ 형태예요. 어떤 인터페이스가 어떤 컨테이너에 속하는지 다음 순서로 찾을 수 있어요:
-
VPP에 접속
calivppctl vppctl NODENAME -
인터페이스 나열
vpp# show interface Name Idx State MTU (L3/IP4/IP6/MPLS) Counter Count avf-0/d8/a/0 1 up 9000/0/0/0 tx packets 2 tx bytes 216 local0 0 down 0/0/0/0 tap0 2 up 0/0/0/0 rx packets 9 [...] tun3 5 up 0/0/0/0 rx packets 5 rx bytes 431 tx packets 5 tx bytes 387 ip4 5 -
주소 11.0.166.132의 라우트 조회
vpp# show ip fib 11.0.166.132 ipv4-VRF:0, fib_index:0, flow hash:[src dst sport dport symmetric ] epoch:0 flags:none locks:[adjacency:1, default-route:1, ] 11.0.166.132/32 fib:0 index:19 locks:5 cnat refs:1 entry-flags:uRPF-exempt,interpose, src-flags:added,contributing,active, cover:-1 interpose: [@0]: [4] cnat-client:[11.0.166.132] tr:0 sess:1 path-list:[26] locks:3 flags:shared, uPRF-list:24 len:1 itfs:[5, ] path:[32] pl-index:26 ip4 weight=1 pref=0 attached-nexthop: oper-flags:resolved, cfg-flags:attached, 11.0.166.132 tun3 (p2p) [@0]: ipv4 via 0.0.0.0 tun3: mtu:9000 next:7 [...] -
이 주소는
tun3뒤에 있어요. 인터페이스에 대해 더 알고 싶다면(Linux에서의 이름, 큐, 디스크립터 등)vpp# show tun tun3 Interface: tun3 (ifindex 5) name "eth0" host-ns "/proc/17675/ns/net" [...]tap0는 호스트 연결을 제공하는 인터페이스로, Linux 쪽에서는 원래 인터페이스 이름을 사용해요(show tap tap0과show ip punt redirect를 사용하세요).
클러스터 내부 트래픽 캡처
클러스터에서 두 pod가 서로 통신하는 경우를 생각해 봐요(위의 그림 참고). 트래픽은 세 위치에서 관찰할 수 있어요:
- pod에서 나갈 때(첫 번째 pod 안의 Linux)
- VPP를 통과할 때
- 두 번째 pod에서 수신될 때(다시 Linux)
세 경우를 모두 다뤄요. 먼저 VPP 내부(트래픽이 pod에서 오는지, 호스트 밖에서 오는지에 따라), 그다음 pod 내부(보통 tcpdump 사용)예요.
VPP 안에서 트래픽 캡처
pod에서 오는 트래픽
다음 코드는 특정 노드의 컨테이너에서 오는 모든 트래픽을 캡처하고, 원하는 패킷을 grep으로 찾아 어떻게 처리됐는지 확인하게 해 줘요.
# Make sure that the trace buffer is clean in VPP
calivppctl vppctl NODENAME clear trace
# Add a trace from the virtio-input input-node
calivppctl vppctl NODENAME trace add virtio-input 500
# generate some traffic
calivppctl vppctl NODENAME show trace max 500 > somefile
# Grep for your IPs
cat somefile | grep '1.2.3.4 -> 5.6.7.8' -A40 -B40
출력에는 패킷이 VPP에서 수신부터 송신까지 거치는 전체 경로가 담겨 있어서 처음에는 꽤 복잡해 보여요.
vpp# show trace
Packet 1
00:09:46:518858: virtio-input
This packet has been received on the interface number #2 (column Idx in `show int`)
and is 688 Bytes long
virtio: hw_if_index 2 next-index 1 vring 0 len 688
hdr: flags 0x00 gso_type 0x00 hdr_len 0 gso_size 0 csum_start 0 csum_offset 0 num_buffers 1
00:09:46:518866: ip4-input
we read TCP header, addresses and ports
TCP: 20.0.0.1 -> 11.0.166.133
tos 0x00, ttl 64, length 688, checksum 0x1bc5 dscp CS0 ecn NON_ECN
fragment id 0x56fd, flags DONT_FRAGMENT
TCP: 6443 -> 34112
seq. 0xa1f93599 ack 0x818eb1c1
flags 0x18 PSH ACK, tcp header: 32 bytes
window 502, checksum 0x00b7
00:09:46:518870: ip4-lookup
fib 0 dpo-idx 5 flow hash: 0x00000000
TCP: 20.0.0.1 -> 11.0.166.133
tos 0x00, ttl 64, length 688, checksum 0x1bc5 dscp CS0 ecn NON_ECN
fragment id 0x56fd, flags DONT_FRAGMENT
TCP: 6443 -> 34112
seq. 0xa1f93599 ack 0x818eb1c1
flags 0x18 PSH ACK, tcp header: 32 bytes
window 502, checksum 0x00b7
00:09:46:518873: ip4-cnat-tx
We need to do some NATing as it's Kubernetes
found: session:[20.0.0.1;6443 -> 11.0.166.133;34112, TCP] => 11.96.0.1;443 -> 11.0.166.133;34112 lb:-1 age:4190
00:09:46:518879: ip4-rewrite
We rewrite the ip packet
mac addresses only when coming / going to a PHY, as tun interfaces are L3-only
tx_sw_if_index 6 dpo-idx 7 : ipv4 via 0.0.0.0 tun4: mtu:9000 next:8 flow hash: 0x00000000
00000000: 450002b056fd40003f0625650b6000010b00a68501bb8540a1f93599818eb1c1
00000020: 801801f620c700000101080a3f906c98fbaaba031703030277413d39
Output happens on the interface `tun4`
00:09:46:518880: tun4-output
tun4
00000000: 450002b056fd40003f0625650b6000010b00a68501bb8540a1f93599818eb1c1
00000020: 801801f620c700000101080a3f906c98fbaaba031703030277413d39b97817c1
00000040: 41392fdbe0e9d4886849851476cdb8986362ee2f789bfefd8a5c106c898d1309
00000060: 4f8f8cb89159d99e986813a48d91334930eb5eb10ca4248c
00:09:46:518881: tun4-tx
buffer 0x24cf615: current data 0, length 688, buffer-pool 1, ref-count 1, totlen-nifb 0, trace handle 0x1000000
ipv4 tcp hdr-sz 52 l2-hdr-offset 0 l3-hdr-offset 0 l4-hdr-offset 20 l4-hdr-sz 32
0x0b60: 40:00:3f:06:25:65 -> 45:00:02:b0:56:fd
Packet 2
[...]
물리 NIC에서 오는 트래픽
물리 NIC에서 오는 트래픽을 캡처하려면 trace add를 사용하되 virtio-input 대신 dpdk-input, af-packet-input, af_xdp-input, avf-input 같은 다른 소스 노드를 지정해야 해요.
show run을 실행하면 트레이싱할 X-input 노드가 무엇인지 힌트를 얻을 수 있어요.
예를 들어 아래 show run 출력을 보면 avf-input을 트레이싱 소스 노드로 쓰면 될 것 같아요:
vpp# show run
Thread 1 vpp_wk_0 (lcore 25)
Time 1.9, 10 sec internal node vector rate 1.05 loops/sec 1074819.68
vector rates in 7.5356e0, out 7.5356e0, drop 0.0000e0, punt 0.0000e0
Name State Calls Vectors Suspends Clocks Vectors/Call
avf-input polling 2233530 0 0 8.24e1 0.00
ip4-cnat-snat active 1 1 0 5.35e3 1.00
ip4-cnat-tx active 14 15 0 1.18e3 1.07
[...]
그러므로 컨테이너 트래픽과 마찬가지 방법으로 이렇게 사용하면 돼요.
# Make sure that the trace buffer is clean in VPP
calivppctl vppctl NODENAME clear trace
# Add a trace from the virtio-input input-node
calivppctl vppctl NODENAME trace add avf-input 500
# generate some traffic
calivppctl vppctl NODENAME show trace max 500 > somefile
# Grep for your IPs
cat somefile | grep '1.2.3.4 -> 5.6.7.8' -A40 -B40
Wireshark로 확인
트레이스 대신 캡처 파일을 만들어 Wireshark에서 분석할 수도 있어요. 다음과 같이 해요:
vpp# pcap dispatch trace on max 1000 file vppcapture buffer-trace dpdk-input 1000
vpp# pcap dispatch trace off
이러면 /tmp/vppcapture라는 파일이 만들어져요.
그다음 호스트에서 이렇게 실행해요:
calivppctl sh vpp NODENAME
root@server:~ mv /tmp/vppcapture /var/lib/vpp/
root@server:~ exit
이제 파일이 'NODENAME' 호스트의 /var/lib/vpp/vppcapture에 있어요. 이후 scp NODENAME:/var/lib/vpp/vppcapture .로 내려받아 Wireshark로 열면 돼요. 자세한 내용은 여기를 참고하세요.
pod에서 수신되는 트래픽
pod가 실제로 수신한 트래픽을 보려면(pod 안에 tcpdump가 설치된 경우) pod 안에서 tcpdump -ni eth0를 실행하면 돼요. pod에 tcpdump가 없다면 pod 트래픽을 캡처할 수 있는 방법이 두 가지 있어요:
호스트에 tcpdump가 있는 경우
호스트에 tcpdump가 설치되어 있다면 nsenter로 pod의 네트워크 네임스페이스에 붙어서 컨테이너 인터페이스에 호스트의 tcpdump를 사용할 수 있어요.
docker에서는 다음과 같이 동작해요:
-
확인할 컨테이너 ID를 찾기
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4c01db0b339c ubuntu:12.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp -
컨테이너 PID 얻기
docker inspect --format '{{ .State.Pid }}' 4c01db0b339c 12345 -
붙기
nsenter -t 12345 -n bash
tcpdump는 없지만 python이 있을 때
다음 코드로 python에서 AF_PACKET 소켓을 열고, 앞서처럼 실행 중인 네임스페이스에 붙여서 실행하세요.
#!/usr/bin/env python
from socket import *
from struct import unpack
IFNAME = "eth0"
N_PKT = 50
MTU=1500
sock = socket(AF_PACKET, SOCK_DGRAM, 0x0800)
sock.bind((IFNAME, 0x0800))
for _ in range(N_PKT):
data = sock.recvfrom(MTU, 0)[0]
src_addr = inet_ntop(AF_INET, data[12:16])
dst_addr = inet_ntop(AF_INET, data[16:20])
src_port, = unpack("!H", data[20:22])
dst_port, = unpack("!H", data[22:24])
data_len, = unpack("!H", data[24:26])
cksum, = unpack("!H", data[26:28])
print("%s:%d -> %s:%d len %d cs %d" % (src_addr, src_port, dst_addr, dst_port, data_len, cksum))
권한이 필요하기 때문에 보통은 호스트에서 실행하는 편이 쉬워요. 호스트에서는 echo "위의 python 코드" | nsenter -t <thePID> -n python으로 이 코드를 실행할 수 있어요.
kubelet 에이전트로 향하는 트래픽
kubelet 에이전트는 네트워크 네임스페이스 없이 호스트에서 직접 실행되기 때문에, 이곳과 통신하는 pod(예: coredns 리졸버) 트래픽은 특별한 경로를 거쳐요. kubelet으로 향하는 패킷은 VPP의 punt 메커니즘이 가로채고, Linux에서 원래 인터페이스와 같은 이름을 가진 tap 인터페이스를 통해 호스트로 전달돼요.
VPP 안의 트래픽을 디버깅하려면 trace를 사용해 트래픽이 tap0 인터페이스로 정상적으로 punt되는지 확인하세요.
호스트에서는 평소처럼 tcpdump로 트래픽을 확인할 수 있어요.
크래시와 coredump
VPP가 예기치 않게 종료되면 /var/lib/vpp/ 디렉터리에 coredump 파일 vppcore.vpp_main.<pid>를 생성해요. 이 상황을 만나면 추가 문제 해결에 도움이 되도록 coredump를 만든 vpp 이미지의 정확한 버전(이미지 해시 기준)을 기록해 두세요.
coredump를 탐색하려면 이렇게 실행해요:
docker run -it --entrypoint=bash -v /var/lib/vpp/vppcore.vpp_main.12345:/root/vppcore calicovpp/vpp:VERSION
이제 vpp 컨테이너 안에서 셸을 사용할 수 있어요.
apt update && apt install -y gdb
gdb vpp ./vppcore
더 알아보기 (Learn more)
calivppctl은 설치, 로그 확인, 트레이싱, 내보내기까지 VPP 디버깅을 한곳에서 처리해 주는 헬퍼예요. 문제를 좁힐 때는 kubelet 상태 → 데몬셋 pod → VPP 로그 → 에이전트 로그 순서로 확인하는 흐름이 좋아요. coredump가 남는다면 이미지 버전을 반드시 기록해 두고 gdb로 분석하세요.