/인프라/쿠버네티스 MTU 문제 진단법 — ping은 되는데 큰 응답만 멈출 때
인프라쿠버네티스 MTUVXLAN 오버헤드

쿠버네티스 MTU 문제 진단법 — ping은 되는데 큰 응답만 멈출 때

ping은 정상인데 큰 응답에서만 curl이 멈추고 TLS handshake가 무응답이라면 MTU 문제입니다. DF 비트 ping 이분 탐색, VXLAN 1450·GCP 1410 계산표, Flannel·Calico·Cilium 설정 위치, PMTUD 블랙홀과 MSS clamping 우회책까지 정리했습니다.

쿠버네티스 MTU 문제 진단법 — ping은 되는데 큰 응답만 멈출 때

ping은 100% 응답하는데 curl이 40KB에서 멈춘다

이번 편의 증상은 앞선 편들과 결이 다릅니다. 연결이 아예 안 되는 게 아니라, 연결은 완벽하게 되는데 특정 크기부터만 멈춥니다.

전형적인 재현 로그는 이렇습니다.

Bash
# 1) ping 완전 정상
$ kubectl exec -it client-pod -- ping -c 4 10.244.2.15
PING 10.244.2.15 (10.244.2.15) 56(84) bytes of data.
64 bytes from 10.244.2.15: icmp_seq=1 ttl=62 time=0.412 ms
64 bytes from 10.244.2.15: icmp_seq=2 ttl=62 time=0.388 ms
--- 10.244.2.15 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss

# 2) 작은 응답 정상 (약 200B)
$ kubectl exec -it client-pod -- curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
    http://api-svc/health
200 187

# 3) 큰 응답 무한 대기 (약 80KB)
$ kubectl exec -it client-pod -- curl -v --max-time 10 http://api-svc/api/list
* Connected to api-svc (10.96.31.7) port 80 (#0)
> GET /api/list HTTP/1.1
> Host: api-svc
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Transfer-Encoding: chunked
<
* Operation timed out after 10001 milliseconds with 8192 bytes received

TLS를 쓰면 더 헷갈립니다. 인증서 체인이 큰 경우 Client Hello는 나가는데 Server Hello가 오지 않고 그대로 멈춥니다.

Bash
$ kubectl exec -it client-pod -- openssl s_client -connect internal-api:443 -servername internal-api
CONNECTED(00000003)
write to 0x... [0x...] (318 bytes => 318 (0x13E))
# ... 여기서 아무것도 오지 않고 정지

이 상태에서 개발팀은 "서버가 느리다"고 하고, 인프라팀은 "앱에서 응답을 못 만드는 것"이라고 합니다. 양쪽 다 틀렸습니다. HTTP 헤더는 넘어왔고 바디 일부(8192B)도 받았다는 사실이 이미 답을 가리키고 있습니다. 연결과 초기 교환은 성공했는데 큰 세그먼트 하나가 링크를 못 넘어간 겁니다.

앞선 편들이 다룬 "전부 아니면 전무" 실패(연결 거부, 이름 해석 실패, 백엔드 부재)와 달리 이번 계열은 크기 경계에서만 발생합니다. 그래서 재현이 불규칙해 보이고, 오진 기간이 며칠 단위로 길어집니다.

504 Gateway Time-out 계열과의 결정적 차이

혼동하기 쉬운 게 504입니다. 구분은 명확합니다.

구분504 타임아웃 계열이번 편(MTU 계열)
실패 원인 계층프록시·백엔드 타임아웃 값 불일치패킷이 링크를 물리적으로 못 넘어감
타임아웃 값 증설효과 있음 (정렬하면 해결)효과 없음 (무한정 기다려도 못 옴)
응답 크기 의존성없음 (느린 쿼리면 작은 응답도 504)있음 (작은 응답은 항상 성공)
에러 형태명시적 504 응답응답 없이 hang → 클라이언트 타임아웃

타임아웃 값을 아무리 늘려도 개선이 0이면 이번 편 쪽입니다.

증상 지문 판정표: MTU가 아닌 것부터 쳐낸다

MTU를 의심하기 전에 배제해야 할 원인들이 있습니다.

증상 지문MTU 의심도대체 원인과 1차 확인 명령
① ping OK + 작은 요청 OK + 큰 응답만 hang매우 높음사실상 MTU 확정선. ping -M do -s 1472 <IP>
② TLS Client Hello 이후 무응답(인증서 체인 큼)매우 높음Server Hello가 MTU 초과. openssl s_client -connect ...
③ 특정 노드 쌍에서만 발생, 동일 노드 Pod끼리는 정상높음노드 간 캡슐화 경로만 문제. ip link show | grep -E 'vxlan|flannel|cilium'
④ VPN·WireGuard 경유 시에만 발생높음암호화 오버헤드 중첩. wg show, ip link show wg0
⑤ 간헐적이고 요청 크기와 무관낮음conntrack table full → dmesg | grep nf_conntrack / NetworkPolicy는 크기 무관 전면 차단
⑥ 일정 시간 경과 후 끊김(크기 아닌 시간 기준)낮음keepalive·idle timeout, ingress proxy buffer. kubectl describe ingress

⑤의 conntrack 대조군은 이렇게 확인됩니다. 이 로그가 보이면 MTU 라인에서 빠지세요.

Bash
$ dmesg -T | grep -i conntrack
[Mon Aug 24 09:12:31 2026] nf_conntrack: nf_conntrack: table full, dropping packet
$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 262144
net.netfilter.nf_conntrack_max = 262144

배제 대상 주제들은 각각 별도 글에서 다룹니다. 크기와 무관하게 연결 자체가 거부되면 kubectl get endpoints <none>·Service connection refused 5분 진단을, 헬스체크 계열이 함께 흔들리면 K8s Liveness/Readiness probe failed·connection refused 원인별 해결을 참고하세요.

30초 판별 명령 시퀀스: 통과 MTU 이분 탐색

핵심은 노드에서와 Pod 안에서 각각 실행하는 것입니다. 노드는 물리 NIC 경로를, Pod는 veth → cni0 → 터널 인터페이스를 거치는 완전히 다른 경로를 탑니다. 노드에서만 확인하고 "정상"이라 판단하는 게 가장 흔한 오진입니다.

1단계 — DF 비트 고정 ping 이분 탐색 (약 10초)

Bash
# Pod 안에서 실행. -M do = Don't Fragment 고정
$ kubectl exec -it client-pod -- ping -M do -s 1472 -c 1 10.244.2.15
PING 10.244.2.15 (10.244.2.15) 1472(1500) bytes of data.
ping: local error: message too long, mtu=1450

$ kubectl exec -it client-pod -- ping -M do -s 1422 -c 1 10.244.2.15
1430 bytes from 10.244.2.15: icmp_seq=1 ttl=62 time=0.51 ms

예상 정상 결과: 통과 가능한 최대 -s 값 + 28(IP 20 + ICMP 8) = 실제 경로 MTU. 위 예시는 1422 + 28 = 1450. 인터페이스는 1500이라고 주장하는데 실제로는 1450만 넘어가는 상태입니다.

경로 중간 장비가 응답하는 경우는 메시지가 다릅니다.

Bash
From 10.0.1.1 icmp_seq=1 Frag needed and DF set (mtu = 1450)

이 메시지가 보이면 오히려 다행입니다. PMTUD가 살아있다는 뜻이고, 5번 섹션의 블랙홀 시나리오는 아닙니다.

2단계 — 인터페이스 MTU 불일치 지점 찾기 (약 10초)

Bash
# 노드에서
$ ip link show | grep -E '^[0-9]+:|mtu' | grep -E 'eth0|cni0|flannel|vxlan|cilium|tunl'
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
7: vethb31a4f2@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...

여기가 범인입니다. flannel.1은 1450인데 cni0veth는 1500입니다. Pod는 1500짜리 프레임을 만들어 보내고, VXLAN 캡슐화 시점에 1500 + 50 = 1550이 되어 물리 NIC 1500을 초과합니다.

정상적으로 정렬된 클러스터라면 이렇게 보여야 합니다.

CODE
2: eth0: ... mtu 1500
4: flannel.1: ... mtu 1450
5: cni0: ... mtu 1450
7: vethb31a4f2@if3: ... mtu 1450

Pod 안에서도 반드시 확인하세요.

Bash
$ kubectl exec -it client-pod -- ip link show eth0
3: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP

Pod 내부가 1500으로 남아 있으면 CNI 설정을 고쳤더라도 기존 Pod에는 반영되지 않은 상태입니다.

3단계 — 경로상 축소 지점 특정

Bash
$ kubectl exec -it client-pod -- tracepath 10.244.2.15
 1?: [LOCALHOST]      pmtu 1500
 1:  10.244.1.1       0.132ms
 2:  10.0.1.1         0.418ms pmtu 1450
 3:  10.244.2.15      0.522ms reached
     Resume: pmtu 1450 hops 3 back 3

pmtu 값이 떨어지는 홉이 축소 지점입니다.

4단계 — hang 시작 경계 확정

응답 크기를 파라미터로 조절할 수 있는 엔드포인트가 있다면 경계를 직접 찍습니다.

Bash
$ for n in 500 1000 1400 1500 2000; do
    echo -n "size=$n -> "
    kubectl exec -it client-pod -- curl -s --max-time 5 \
      -o /dev/null -w '%{http_code}\n' "http://api-svc/echo?bytes=$n" || echo TIMEOUT
  done
size=500  -> 200
size=1000 -> 200
size=1400 -> 200
size=1500 -> TIMEOUT
size=2000 -> TIMEOUT

1400은 되고 1500부터 죽는다 — 이 경계가 나오면 진단 종료입니다. 이제 값 계산으로 넘어갑니다.

5단계 — 3방향 매트릭스로 구간 좁히기

경로테스트 명령실패 시 의미
같은 노드 Pod ↔ Pod동일 노드 Pod IP로 ping -M do -s 1472캡슐화 미경유인데 실패 → 브리지/veth MTU 설정 오류
Pod ↔ 노드 IPPod에서 노드 IP로 동일 명령veth ↔ 호스트 NIC 불일치
노드 ↔ 노드노드에서 다른 노드 IP로 동일 명령언더레이(클라우드 VPC·물리 스위치) MTU 문제

같은 노드 Pod끼리는 되는데 다른 노드 Pod와만 안 된다 → 터널 인터페이스 오버헤드 계산 문제로 확정입니다. 가장 흔한 패턴입니다.

CNI별 오버헤드 계산표와 조치

산식은 단순합니다.

CODE
Pod MTU = 기저 링크 MTU − 캡슐화 오버헤드 (중첩 시 오버헤드 합산)

캡슐화 오버헤드

캡슐화 방식오버헤드비고
Flannel VXLAN50 B기본값
Flannel host-gw0 B캡슐화 없음, L2 인접 필요
Calico IPIP20 B기본 모드
Calico VXLAN50 BIPv4 기준
Cilium VXLAN50 B
Cilium Geneve50 B 이상옵션 헤더 따라 증가
WireGuard 암호화60~80 B위 값에 추가 합산

클라우드 기저 MTU × CNI 교차 계산표

기저 MTUFlannel VXLANCalico IPIPCalico VXLANCilium VXLANCilium VXLAN + WireGuard
AWS 9001 (점보)89518981895189518871 (−80 기준)
AWS/온프렘 150014501480145014501370
GCP 146014101440141014101330
VPN 경유 140013501380135013501270

GCP에서 사고가 잦은 이유가 여기 있습니다. 기저가 1460인데 Flannel 기본값이 1450으로 잡혀 있으면 10바이트가 초과합니다. 정답은 1410입니다.

하이브리드 클러스터(AWS 점보 9001 노드 + 온프렘 1500 노드)에서는 가장 작은 기저 MTU를 기준으로 잡아야 합니다. 점보 프레임 기본화와 온프렘 1500 혼재가 늘면서 최근 이 계열 사고가 증가하는 흐름입니다.

Flannel 설정

Bash
$ kubectl edit configmap kube-flannel-cfg -n kube-flannel
JSON
{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "vxlan",
    "MTU": 1410
  }
}

재기동 범위: DaemonSet 재시작 → 그 다음 워크로드 Pod 롤아웃까지 필요합니다.

Bash
$ kubectl rollout restart daemonset kube-flannel-ds -n kube-flannel
$ kubectl rollout restart deployment -n <앱 네임스페이스> --all

CNI DaemonSet만 재시작하면 flannel.1은 바뀌지만 기존 Pod의 veth는 그대로 1500입니다. veth는 Pod 샌드박스 생성 시점에 만들어지므로 Pod 재생성이 있어야 반영됩니다. 이 지점을 놓쳐서 "설정 바꿨는데 안 낫는다"고 결론 내리는 경우가 많습니다.

Calico 설정

Bash
$ kubectl patch configmap calico-config -n kube-system \
  --type merge -p '{"data":{"veth_mtu":"1410"}}'

# IPIP 모드 터널 MTU
$ kubectl set env daemonset/calico-node -n kube-system FELIX_IPINIPMTU=1440
# VXLAN 모드
$ kubectl set env daemonset/calico-node -n kube-system FELIX_VXLANMTU=1410
$ kubectl rollout restart daemonset calico-node -n kube-system

재기동 범위: calico-node 재시작 후 워크로드 Pod 롤아웃 필수.

Cilium 설정

Bash
$ helm upgrade cilium cilium/cilium \
  --namespace kube-system --reuse-values \
  --set MTU=1410

$ kubectl rollout restart daemonset cilium -n kube-system
$ kubectl rollout restart deployment -n <앱 네임스페이스> --all

WireGuard 노드 간 암호화(encryption.enabled=true, encryption.type=wireguard)를 함께 쓰면 위 표대로 추가 합산해야 합니다. eBPF 기반 CNI 확산으로 캡슐화 옵션이 다양해진 만큼, 켜 둔 옵션을 전부 나열한 뒤 합산하는 습관이 필요합니다.

반영 검증

Bash
# 새로 뜬 Pod에서 확인
$ kubectl exec -it <신규-pod> -- ip link show eth0
3: eth0@if11: ... mtu 1410 ...

$ kubectl exec -it <신규-pod> -- ping -M do -s 1382 -c 1 <상대 Pod IP>
1390 bytes from ...: icmp_seq=1 ttl=62 time=0.44 ms

MTU를 못 바꾸는 상황: MSS clamping

관리형 CNI라 변경이 막혀 있거나 변경 승인이 안 나는 경우, 노드에서 TCP MSS를 경로 MTU에 맞춰 강제로 낮출 수 있습니다.

Bash
# 모든 노드에서 실행 (SYN 패킷의 MSS를 경로 MTU에 맞춤)
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# PMTUD가 아예 안 되는 환경이면 고정값으로
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --set-mss 1370

# 확인
sudo iptables -t mangle -L FORWARD -n -v | grep TCPMSS

한계는 분명합니다.

  • TCP에만 적용됩니다. SYN 패킷의 MSS 옵션을 고쳐 쓰는 방식이라 UDP는 대상이 아닙니다.
  • QUIC(HTTP/3)에는 무효입니다. UDP 기반이라 MSS 개념 자체가 없습니다. gRPC over HTTP/3, DoQ 등을 쓴다면 근본 MTU 조정이 유일한 해법입니다.
  • DNS(UDP 53)의 큰 응답, VXLAN 안의 UDP 트래픽도 보호되지 않습니다.
  • 재부팅 시 사라지므로 iptables-persistent나 부팅 스크립트로 영속화해야 합니다.

실패 분기: MTU를 낮췄는데도 여전할 때

계산표대로 맞추고 Pod까지 재생성했는데 증상이 그대로라면, PMTUD 블랙홀을 의심합니다.

원리는 이렇습니다. 경로 중간 장비가 "이 패킷 너무 크다"고 알려주는 신호가 **ICMP type 3 code 4 (Destination Unreachable / Fragmentation Needed)**입니다. 이게 방화벽에서 드롭되면 송신 측은 축소를 학습하지 못하고, 계속 큰 패킷을 보내다 조용히 사라집니다. 에러 로그도 남지 않습니다.

확인 방법

Bash
# 송신 노드에서 ICMP 수신 여부 관찰
$ sudo tcpdump -ni any 'icmp[icmptype] == 3 and icmp[icmpcode] == 4' -c 5
listening on any, link-type LINUX_SLL (Linux cooked v1)
0 packets captured

큰 패킷을 계속 보내는데 0 packets captured면 블랙홀 확정입니다. 정상이라면 아래처럼 잡혀야 합니다.

CODE
10:14:22.331 IP 10.0.1.1 > 10.0.2.31: ICMP 10.244.2.15 unreachable -
  need to frag (mtu 1450), length 556

커널이 학습한 PMTU 캐시도 확인합니다.

Bash
$ ip route get 10.244.2.15
10.244.2.15 via 10.0.1.1 dev eth0 src 10.0.2.31 mtu 1450

mtu 항목이 아예 안 붙으면 학습이 안 된 상태입니다.

국내 환경 ICMP 정책 체크포인트

ICMP를 통째로 막아 두는 보안 정책 때문에 발생하는 경우가 많습니다. 아래를 순서대로 확인하세요.

확인 대상체크포인트
NCP ACGInbound/Outbound 규칙에 ICMP 프로토콜 허용 항목이 존재하는지
NCP Network ACL서브넷 단위 ACL에서 ICMP가 Deny로 잡혀 있지 않은지
KT클라우드 방화벽방화벽 정책에 ICMP 허용 룰 유무, VPC 간 통신 구간 별도 확인
AWS Security GroupICMP - Destination Unreachable (type 3) 허용 여부
사내 방화벽·UTMICMP 전면 차단 정책, type 3 code 4 예외 등록 여부
DPI·IPS 장비ICMP 페이로드 검사로 인한 선택적 드롭 여부

정책 변경 승인이 어렵거나 통제 밖 경로가 섞여 있다면, MSS clamping이 사실상 유일한 실전 해법입니다. PMTUD에 의존하지 않고 연결 수립 시점에 크기를 확정해 버리기 때문입니다. 단, 앞서 말한 UDP/QUIC 한계는 그대로 남습니다.

재발 방지

  • CNI 설치 시 MTU를 명시적으로 고정합니다. 자동 감지에 맡기면 노드 교체·클라우드 변경 시 조용히 어긋납니다.
  • 노드 추가 시 ip link show로 MTU 정렬을 확인하는 절차를 노드 부트스트랩 체크리스트에 넣습니다.
  • 하이브리드 클러스터는 최소 기저 MTU 기준으로 통일합니다.
  • 합성 모니터링에 "8KB 이상 응답을 받는 요청"을 하나 넣어 두면 크기 경계 회귀를 조기에 잡을 수 있습니다.
  • WireGuard·VPN을 새로 켤 때는 반드시 오버헤드를 재계산합니다.

요약 체크리스트

  1. 증상 판정 — ping OK + 작은 요청 OK + 큰 응답만 hang인가? conntrack·NetworkPolicy 배제했는가?
  2. 3방향 매트릭스 — 같은 노드 / Pod↔노드 / 노드↔노드 중 어디서 깨지는가?
  3. 계산표 대입 — 기저 MTU − 캡슐화 오버헤드(중첩 합산) = 목표 Pod MTU
  4. 설정 반영 — CNI 설정 변경 + DaemonSet 재시작 + 워크로드 Pod 롤아웃까지
  5. PMTUD 확인 — ICMP type 3 code 4가 실제로 도달하는가? 아니면 MSS clamping

자주 묻는 질문 (FAQ)

Q. ping은 잘 되는데 왜 MTU 문제일 수 있나요? A. 기본 ping은 페이로드 56바이트(총 84바이트)라 어떤 MTU에서도 통과합니다. MTU 문제는 링크 한계를 넘는 큰 패킷에서만 드러나므로, ping -M do -s 1472처럼 DF 비트를 세우고 크기를 키워 테스트해야 확인됩니다.

Q. curl 타임아웃을 늘리면 해결되나요? A. 안 됩니다. 패킷이 링크를 넘어가지 못하는 것이므로 아무리 기다려도 도착하지 않습니다. 타임아웃 값 조정으로 개선되는 건 프록시·백엔드 타임아웃이 어긋난 504 계열이고, 이번 증상과는 원인 계층이 다릅니다.

Q. MTU를 바꿨는데도 그대로입니다. 무엇을 놓쳤나요? A. 두 가지가 대표적입니다. 첫째, CNI DaemonSet만 재시작하고 워크로드 Pod를 롤아웃하지 않아 기존 veth가 옛 MTU로 남아 있는 경우(kubectl exec -- ip link show eth0으로 확인). 둘째, ICMP type 3 code 4가 방화벽에서 드롭돼 PMTUD가 블랙홀인 경우이며, 이때는 MSS clamping이 필요합니다.

✦ ✦ ✦
편집 검토 · Editorial Review

AI 도구는 자료 조사와 초안 작성의 보조 수단으로 사용될 수 있습니다. Nodelog는 공개 전 내용과 출처를 검토하고, 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서를 함께 확인하도록 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 ·
관련 공식 문서Kubernetes 공식 문서

댓글

첫 번째 댓글을 남겨보세요.