Skip to content

필사 모드: ping은 되는데 큰 요청만 멈춘다 — MTU, MSS, PMTUD 블랙홀 해부

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 작은 요청은 되고 큰 응답만 멈춥니다

증상이 유난히 특이한 장애가 있습니다.

  • ping은 100퍼센트 성공하고 왕복 시간도 정상입니다.
  • 헬스 체크 엔드포인트는 잘 응답합니다.
  • 그런데 목록 조회 API처럼 응답이 조금만 커지면 요청이 그대로 멈춥니다. 오류도 아니고 그냥 멈춥니다.
  • ssh는 접속되는데 큰 디렉터리에서 ls -la를 치면 화면이 얼어붙습니다.
  • git clone은 진행률이 어느 지점에서 멈춘 채 끝나지 않습니다.
  • 어떤 사이트는 TLS 핸드셰이크 단계에서 멈춥니다.

이 조합을 보면 다른 것을 볼 필요가 없습니다. 경로 MTU 문제입니다. 그리고 대부분 VPN, 터널, 오버레이 네트워크를 새로 붙였거나 경로가 바뀐 직후에 나타납니다.

왜 크기에 따라 결과가 갈리는지, 그리고 어떻게 몇 분 안에 확정할 수 있는지를 정리합니다.

MTU와 MSS — 40바이트의 산수

MTU는 한 인터페이스가 한 번에 내보낼 수 있는 IP 패킷의 최대 크기입니다. 이더넷 기본값은 1500바이트입니다.

TCP는 이 값에서 헤더를 빼고 자기가 담을 수 있는 데이터 크기를 계산합니다. IPv4 헤더 20바이트, TCP 헤더 20바이트를 빼면 1460바이트입니다. 이 값이 MSS이고, 핸드셰이크의 SYN 패킷에 옵션으로 실려 상대에게 전달됩니다.

ip link show ens5
2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 06:1f:2a:3b:4c:5d brd ff:ff:ff:ff:ff:ff
sudo tcpdump -nn -i any 'tcp[tcpflags] & tcp-syn != 0' -c 2
14:22:03.104512 IP 10.0.1.20.51244 > 10.20.0.5.443: Flags [S], seq 2841029371, win 62727, options [mss 1460,sackOK,TS val 33012 ecr 0,nop,wscale 7], length 0
14:22:03.106880 IP 10.20.0.5.443 > 10.0.1.20.51244: Flags [S.], seq 991044820, ack 2841029372, win 65160, options [mss 1460,sackOK,TS val 88103 ecr 33012,nop,wscale 7], length 0

여기서 결정적인 사실이 하나 나옵니다. 양쪽 모두 자기 인터페이스의 MTU만 보고 MSS를 정합니다. 경로 중간에 더 좁은 구간이 있는지는 아무도 모릅니다. 위 캡처에서 두 호스트는 1460바이트짜리 세그먼트를 주고받겠다고 합의했지만, 중간에 1420바이트짜리 터널이 있다면 그 합의는 지켜질 수 없습니다.

작은 요청이 성공하는 이유가 여기 있습니다. 헬스 체크 응답은 200바이트짜리 세그먼트 하나로 끝나므로 좁은 구간을 무사히 통과합니다. ping도 기본 페이로드가 56바이트라 항상 통과합니다. 크기가 임계값을 넘는 패킷만 사라지기 때문에 증상이 크기에 따라 갈립니다.

TLS 핸드셰이크에서 멈추는 경우도 같은 이유입니다. ClientHello는 작지만, 서버가 보내는 인증서 체인은 3천에서 5천 바이트라 곧바로 최대 크기 세그먼트가 됩니다. 그래서 연결은 되는데 핸드셰이크가 끝나지 않는 모양이 됩니다.

PMTUD 블랙홀이 만들어지는 정확한 과정

TCP는 원래 이 문제를 해결할 장치를 갖고 있습니다. 경로 MTU 탐색입니다. 순서는 이렇습니다.

  1. 리눅스는 TCP 패킷에 DF(Don't Fragment) 비트를 기본으로 세웁니다.
  2. 중간 라우터가 자기 다음 홉 MTU보다 큰 DF 패킷을 받으면, 분할하지 못하므로 패킷을 버립니다.
  3. 대신 송신자에게 ICMP 타입 3 코드 4, 즉 Fragmentation Needed를 보냅니다. 이 메시지에는 다음 홉의 MTU 값이 들어 있습니다.
  4. 송신자 커널은 그 값을 경로별 캐시에 저장하고, 세그먼트를 작게 잘라 다시 보냅니다.

정상 동작하면 이렇게 보입니다.

ip route get 10.20.0.5
10.20.0.5 via 10.0.1.1 dev ens5 src 10.0.1.20 uid 1000
    cache expires 588sec mtu 1420

mtu 1420이 캐시에 잡혔다는 것은 PMTUD가 동작했다는 뜻입니다.

문제는 3단계입니다. ICMP를 전부 차단한 방화벽, ICMP를 응답하지 않는 라우터, ICMP를 원 송신자에게 되돌려 주지 못하는 비대칭 경로가 있으면 이 메시지가 사라집니다. 그러면 송신자는 아무것도 모른 채 계속 큰 패킷을 보내고, 그 패킷은 계속 버려집니다. 재전송도 같은 크기라 역시 버려집니다. 이 상태를 PMTUD 블랙홀이라고 합니다.

블랙홀의 특징은 오류가 나지 않는다는 것입니다. 연결은 ESTABLISHED로 남아 있고, 애플리케이션은 응답을 기다리다 자기 타임아웃에 걸려서야 끝납니다. 그래서 "네트워크는 정상인데 애플리케이션이 느리다"는 잘못된 결론으로 이어집니다.

여기서 널리 퍼진 오해를 정정해야 합니다. "보안을 위해 ICMP는 전부 막는다"는 정책은 그 자체로 장애 요인입니다. 막아도 되는 것은 echo request와 echo reply 정도이고, 타입 3 코드 4는 반드시 통과시켜야 합니다. IPv6에서는 Packet Too Big, 즉 ICMPv6 타입 2가 같은 역할을 하며 여기서는 선택의 여지조차 없습니다.

ICMP가 실제로 도착하는지 확인합니다.

sudo tcpdump -nn -i any 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
14:31:02.884411 IP 10.0.1.1 > 10.0.1.20: ICMP 10.20.0.5 unreachable - need to frag (mtu 1420), length 556

이 줄이 보이면 PMTUD는 살아 있습니다. 큰 패킷을 계속 보내는데도 이 줄이 한 번도 안 보이면 블랙홀입니다.

리눅스에는 블랙홀을 스스로 감지하는 기능이 있습니다. 기본은 꺼져 있습니다.

sysctl net.ipv4.tcp_mtu_probing
net.ipv4.tcp_mtu_probing = 0
# 1: 블랙홀이 의심될 때만 탐색, 2: 항상 탐색
sudo sysctl -w net.ipv4.tcp_mtu_probing=1

값을 1로 두면 재전송이 반복될 때 커널이 세그먼트 크기를 스스로 줄여 가며 통과하는 크기를 찾습니다. 근본 해결은 아니지만 원인을 못 찾은 상태에서 서비스를 살려 두는 데 효과적입니다.

터널이 가져가는 바이트

경로가 좁아지는 원인은 대부분 캡슐화입니다. 원래 패킷을 다른 헤더로 감싸므로 안쪽에 쓸 수 있는 공간이 줄어듭니다. 숫자를 외워 두면 진단이 빨라집니다.

캡슐화 방식추가 헤더 크기1500 기준 실효 MTU권장 MSS 클램프 값
PPPoE8바이트14921452
GRE24바이트14761436
VXLAN (IPv4 외부)50바이트14501410
Geneve (옵션 없음)50바이트 이상1450 이하1410 이하
WireGuard (IPv4 외부)60바이트14401400
WireGuard (IPv6 외부)80바이트14201380
IPsec ESP 터널 모드최대 약 73바이트14271387
IPsec ESP + NAT-T최대 약 81바이트14191379

몇 가지 주의할 점이 있습니다.

wg-quick이 WireGuard 인터페이스에 기본으로 1420을 설정하는 이유는 IPv6 외부 헤더까지 감안한 안전값이기 때문입니다. IPv4로만 쓴다면 1440도 가능하지만, 엔드포인트가 IPv6로 바뀌는 순간 깨지므로 1420을 그대로 두는 편이 낫습니다.

IPsec은 암호 알고리즘과 패딩 때문에 오버헤드가 고정되어 있지 않습니다. 그래서 최악값을 기준으로 잡습니다. AES-CBC는 블록 패딩 때문에 가변이고, AES-GCM은 상대적으로 작습니다.

클라우드는 반대 방향의 함정이 있습니다. AWS의 대부분 인스턴스 타입은 VPC 내부에서 MTU 9001을 씁니다. 같은 VPC 안에서는 잘 돌지만 인터넷 게이트웨이나 VPN을 지나는 순간 1500 이하로 줄어듭니다. 그래서 "내부 통신은 멀쩡한데 외부 API 호출만 큰 응답에서 멈춘다"는 형태로 나타납니다.

경로 MTU를 이분 탐색으로 찾기

가장 확실한 방법은 직접 측정하는 것입니다. DF 비트를 세우고 크기를 조절해 가며 통과 여부를 봅니다.

# -M do: DF 비트를 세운다, -s: ICMP 페이로드 크기
ping -M do -s 1472 -c 1 10.20.0.5
PING 10.20.0.5 (10.20.0.5) 1472(1500) bytes of data.
From 10.0.1.1 icmp_seq=1 Frag needed and DF set (mtu = 1420)

--- 10.20.0.5 ping statistics ---
1 packets transmitted, 0 received, +1 errors, 100% packet loss

운이 좋으면 이렇게 라우터가 정확한 MTU를 알려 줍니다. ICMP가 차단된 경로라면 그냥 무응답이 되므로 직접 좁혀야 합니다.

for s in 1472 1420 1400 1392 1380 1372; do
  printf '%5d bytes payload (MTU %5d): ' "$s" "$((s + 28))"
  if ping -M do -s "$s" -c 1 -W 1 10.20.0.5 >/dev/null 2>&1; then
    echo OK
  else
    echo FAIL
  fi
done
 1472 bytes payload (MTU  1500): FAIL
 1420 bytes payload (MTU  1448): FAIL
 1400 bytes payload (MTU  1428): FAIL
 1392 bytes payload (MTU  1420): OK
 1380 bytes payload (MTU  1408): OK
 1372 bytes payload (MTU  1400): OK

여기서 28은 ICMP 헤더 8바이트와 IPv4 헤더 20바이트입니다. 통과하는 최대 페이로드가 1392이므로 경로 MTU는 1420입니다. 그러면 TCP MSS는 1420에서 40을 뺀 1380이어야 합니다.

tracepath를 쓰면 한 번에 나오기도 합니다. 어느 홉에서 줄어드는지까지 보여줍니다.

tracepath -n 10.20.0.5
 1?: [LOCALHOST]                      pmtu 1500
 1:  10.0.1.1                                              0.412ms
 1:  10.0.1.1                                              0.318ms
 2:  10.0.1.1                                              0.286ms pmtu 1420
 2:  10.60.4.1                                             8.114ms
 3:  10.20.0.5                                             8.902ms reached
     Resume: pmtu 1420 hops 3 back 3

pmtu 1420이 2번 홉에서 나타났습니다. 그 지점이 터널 진입점입니다.

커널이 이미 학습한 값이 있는지도 확인합니다.

ip route get 10.20.0.5
ip route show cache

잘못 학습된 값을 지우려면 캐시를 비웁니다.

sudo ip route flush cache

MSS 클램핑이 실무의 정답인 이유

경로가 좁다는 것을 알았습니다. 이제 고쳐야 합니다. 후보는 세 가지입니다.

첫째, 인터페이스 MTU를 낮춥니다.

sudo ip link set dev ens5 mtu 1420

이 방법은 그 호스트가 보내는 패킷에만 효과가 있습니다. 그리고 상대가 보내는 패킷도 제한하기는 합니다. 내가 광고하는 MSS가 줄어들기 때문입니다. 문제는 적용 대상입니다. 좁은 구간이 두 라우터 사이의 터널이고 양 끝에 수백 대의 서버와 수천 대의 클라이언트가 있다면, 모든 장비의 MTU를 손댈 수는 없습니다.

둘째, ICMP를 통과시켜 PMTUD를 살립니다. 원칙적으로 가장 옳은 해법이고 반드시 병행해야 합니다. 다만 경로 중간의 방화벽이 우리 통제 밖인 경우가 많고, 인터넷 반대편 어딘가에서 ICMP가 막히는 것까지는 손댈 수 없습니다.

셋째, MSS 클램핑입니다. 터널을 통과하는 라우터가 지나가는 SYN 패킷의 MSS 옵션 값을 직접 고쳐 씁니다.

# 경로 MTU에 맞춰 자동으로 맞춘다
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# 터널 인터페이스로 나가는 트래픽만 고정값으로 제한한다
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -o wg0 -j TCPMSS --set-mss 1380

# nftables 표기
sudo nft add rule inet mangle forward tcp flags syn tcp option maxseg size set rt mtu

이 방식이 실무의 표준이 된 이유는 분명합니다.

핸드셰이크의 SYN과 SYN-ACK만 건드리면 되므로 비용이 사실상 없습니다. 그리고 MSS는 "내가 받을 수 있는 최대 크기"를 알리는 값이므로, 양방향 SYN을 모두 고치면 양쪽 모두가 작은 세그먼트만 보내게 됩니다. 무엇보다 ICMP에 전혀 의존하지 않으므로 블랙홀 환경에서도 확실히 동작합니다.

적용 후에는 캡처로 확인합니다.

sudo tcpdump -nn -i wg0 'tcp[tcpflags] & tcp-syn != 0' -c 2
15:02:11.104512 IP 10.60.1.20.51302 > 10.20.0.5.443: Flags [S], seq 118203993, win 62727, options [mss 1380,sackOK,TS val 41120 ecr 0,nop,wscale 7], length 0
15:02:11.112880 IP 10.20.0.5.443 > 10.60.1.20.51302: Flags [S.], seq 771102384, ack 118203994, win 65160, options [mss 1380,sackOK,TS val 90221 ecr 41120,nop,wscale 7], length 0

양쪽 mss가 1380으로 바뀌었습니다.

주의할 점도 있습니다. MSS 클램핑은 TCP에만 적용됩니다. UDP 기반 프로토콜은 보호받지 못합니다. QUIC 기반의 HTTP/3, 대형 응답을 UDP로 받는 DNS, WireGuard 안의 또 다른 UDP 터널이 여기 해당합니다. QUIC은 자체적으로 DPLPMTUD를 수행하고 기본 1200바이트를 안전값으로 쓰므로 대체로 견디지만, UDP를 직접 다루는 애플리케이션은 데이터그램 크기를 스스로 제한해야 합니다.

쿠버네티스 CNI와 IPv6에서 달라지는 점

쿠버네티스에서 이 문제는 오버레이 네트워크 때문에 정기적으로 재발합니다. Flannel VXLAN, Calico VXLAN, Cilium의 VXLAN이나 Geneve 모드가 모두 캡슐화를 씁니다.

증상은 특징적입니다.

  • 파드끼리 ping은 되고 작은 요청도 잘 됩니다.
  • 큰 본문을 보내는 POST 요청만 멈춥니다.
  • kubectl exec는 되는데 로그가 많은 파드의 kubectl logs가 중간에 멈춥니다.
  • 이미지 풀은 되는데 큰 레이어에서 멈춥니다.

확인은 인터페이스 MTU 비교부터 합니다.

# 노드의 물리 인터페이스
ip link show ens5 | head -1
# 오버레이 인터페이스
ip link show flannel.1 | head -1
# 파드 안
kubectl exec -it deploy/api -- ip link show eth0 | head -1
2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
3: eth0@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default

여기 오류가 있습니다. VXLAN 인터페이스인 flannel.1의 MTU가 노드와 같은 1500입니다. VXLAN은 50바이트를 가져가므로 1450이어야 합니다. 파드의 eth0도 1450이어야 합니다.

정상 상태는 이렇습니다.

2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
3: eth0@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...

CNI에 MTU를 명시하는 것이 가장 확실합니다. 자동 감지에 맡기면 노드 인터페이스가 바뀌거나 클라우드가 점보 프레임을 쓸 때 어긋납니다.

# Flannel: net-conf.json
{
  "Network": "10.244.0.0/16",
  "Backend": { "Type": "vxlan", "MTU": 1450 }
}
# Calico: 설정된 MTU 확인
kubectl get configmap -n kube-system calico-config -o jsonpath='{.data.veth_mtu}'

# Cilium: 감지된 값 확인
kubectl exec -n kube-system ds/cilium -- cilium-dbg status --verbose | grep -i mtu

AWS에서 노드가 9001을 쓰는데 CNI가 1500으로 잡히면 문제가 안 나지만, 반대로 노드가 1500인데 CNI가 9001로 잡히면 즉시 블랙홀이 됩니다. 노드 MTU를 먼저 확인하고 그 값에서 캡슐화 오버헤드를 빼는 순서를 지켜야 합니다.

IPv6는 규칙이 다릅니다. 세 가지를 기억하면 됩니다.

첫째, IPv6 라우터는 패킷을 분할하지 않습니다. IPv4에서는 DF 비트가 없으면 라우터가 알아서 잘라 보낼 수 있었지만, IPv6에서는 그런 선택지가 아예 없습니다. 분할은 오직 송신 호스트만 할 수 있습니다.

둘째, 그래서 ICMPv6 Packet Too Big, 타입 2 메시지가 필수입니다. 이것을 막으면 IPv6 통신은 IPv4보다 더 확실하게 깨집니다. ICMPv6 필터링 지침은 RFC 4890에 정리되어 있으며 타입 2는 반드시 허용 대상입니다.

셋째, IPv6의 최소 MTU는 1280바이트입니다. 어떤 경로든 최소한 이 크기는 보장되므로, 원인을 못 찾은 상태에서 임시로 서비스를 살려야 한다면 1280이 안전한 하한선입니다.

# IPv6 경로 MTU 측정
ping6 -M do -s 1232 -c 1 2001:db8::5

# IPv6 ICMP 도달 여부 확인
sudo tcpdump -nn -i any 'icmp6 and ip6[40] == 2'
15:18:44.201188 IP6 2001:db8:1::1 > 2001:db8:2::20: ICMP6, packet too big, mtu 1400, length 1240

마치며 — 크기에 따라 갈리면 MTU를 의심하십시오

진단의 출발점은 증상의 모양입니다. 연결은 되는데 크기가 커질 때만 멈춘다면, 그리고 오류 없이 그냥 멈춘다면, 다른 것을 보기 전에 경로 MTU를 재야 합니다. 명령 하나면 됩니다.

ping -M do -s 1472 -c 1 목적지주소

이것이 실패하고 작은 크기는 성공한다면 확정입니다.

기억할 것은 세 가지입니다. TCP는 자기 인터페이스만 보고 MSS를 정하므로 경로 중간의 병목을 스스로 알 수 없습니다. 그것을 알려 주는 유일한 장치가 ICMP 타입 3 코드 4이고, 그 메시지를 막는 순간 조용한 블랙홀이 만들어집니다. 그리고 우리가 경로 전체를 통제할 수 없기 때문에, 터널 진입점에서의 MSS 클램핑이 현실적인 정답이 됩니다.

터널이나 오버레이를 새로 도입할 때는 오버헤드 계산과 MSS 클램핑을 처음부터 설계에 넣는 것이 좋습니다. 나중에 발견하면 증상이 워낙 모호해서 며칠을 쓰게 됩니다.

현재 단락 (1/151)

증상이 유난히 특이한 장애가 있습니다.

작성 글자: 0원문 글자: 8,429작성 단락: 0/151