MTU Mismatch 발생 시 증상

1,988 단어·4 분·원문(.md)

MTU는 Maximum Transmission Unit로 네트워크 인터페이스 단편화없이 한 번에 전송할 수 있는 최대 패킷 크기 byte 단위를 의미한다.

기본 규격은 표준 이더넷 환경에서는 보통 1500바이트를 사용하고 하지만 aws, gcp같은 최신 클라우드 내부망에서는 성능 향상을 위해 9000바이트를 기본으로 사용하는 경우가 많다.

**MTU Mismatch(불일치)**라는 문제는 데이터를 주고 받는 출발지 source와 목적지 destination 혹은 그 사이에 있는 라우터, vpn, 방화벽 등 네트워크 장비들 간에 설정된 mtu 크기가 다를때 발생하는 현상이다.;

DF Bit (Don't Fragment): 현대의 tcp/ip 통신은 성능 저하를 막기 위한 패킷 헤더에 이 패킷을 쪼개지 마시오 df=1 이라는 플래그를 달고 전송한다. 만약 패킷 크기가 9000정도라고 쳤을때 중간 라우터 MTU 1500 보다 큰데 DF 비트가 켜져있다면 라우터는 패킷을 쪼개지못하고 그냥 drop되는 문제가 있다.

Problem #

MTU Mismatch가 무서운 이유는 서비스가 완전히 죽는것이 아니고 반쯤 죽어있는 상태를 만들기 때문인데

네트워크가 연결된 것처럼 속이기 때문에 디버깅이 까다롭다.

  • 작은 패킷은 성공하는게 문제인데, ping, 3wayhandshake, ssh접속, 헬스체크등과 같은 작은 크기의 수십수백바이트 데이터는 문제없이 잘 통과한다. 모니터링 시스템상으로는 정상인거임
  • 큰 패킷은 증발 connection hang, 상요자가 큰 이미지를 업로드 혹은 db에 수천줄의 select쿼리결과를 가져오거나 json이 큰 응답을 내려받는다거나 순간 트래픽이 멈춘다. 중간 장비가 패킷을 버리면서 에러메시지 icmp fragmentation needed 조차 제대로 반환하지 않아, 클라이언트와 서버는 서로 데이터가 오기만을 기다린다.
  • 스레드 풀 고갈도 있는데, 애플리케이션 입장에서는 커넥션이 끊어진게 아니라서 무한정 대기상태가 되는거고 이런 요청이 무한정 쌓이면 결국 was의 워커 스레드가 모두 고갈되어 서비스전체가 마비된다.

Example #

정상적인 상황은 aws vpc 내부 ec2 인스턴스간 통신에서 둘 다 mtu 9000를 지원해 대용량 파일 전송이나 대규모 db쿼리도 병목없이 초고속으로 처리가 된다.

장애가 발생하는상황은 (디버깅도 어려운) 예를들어 회사 하이브리드 클라우드를 구축해 aws vpc(mtu 9000)와 온프레미스 데이터센터(mtu 1500)을 ipsec vpn으로 연결했다 했을때, vpn 터널링 과정에서 암호화 헤더가 추가되면서 실제 가용 mtu는 1436바이트로 쪼그라 들었다고 하자.

  • 개발자가 ec2 서버에서 ssh로 접속(1436 바이트 이하)로 해보고 접속이 잘된것을 확인
  • ec2에서 온프레미스로 db select 1을 해봐서 잘됐다고 치자.
  • 운영환경에서 유저가 로그인해 select * from payment_history를 갈겼다. 결과는 5mb이고 이때부터 브라우저 로딩바가 멈추고 서버 로그가 멈춘다. 네트워크 팀은 방화벽도 열려있고 핑도 잘나간다고 하고 dba는 쿼리도 실행이 끝났다고한다. 즉 그 사이에 백엔드 개발자만 미쳐가는것이다.

Solution #

인프라 레벨에서 막힌 구간 path mtu를 찾아 장비 설정을 통일하는것이 근본 해결책이긴하다.

sre라면 터미널로 문제를 증명하고 코드 레벨에서 서비스가 죽지 않도록 방어해야하기도 한다.

터미널 ping df bit 확인후 병목 구간 증명 #

단순 ping이 아니라 패킷 사이즈를 강제로 키우고 쪼개지 못하게 DF 설정을 만들어서 어디서 패킷이 버려지는지 추적한다.

# -M do: Don't Fragment 비트 설정
# -s 1472: ICMP 헤더(8) + IP 헤더(20)를 더해 정확히 총 1500 바이트의 패킷 생성
$ ping -c 4 -M do -s 1472 10.100.1.50

PING 10.100.1.50 (10.100.1.50) 1472(1500) bytes of data.
From 10.0.5.1 icmp_seq=1 Frag needed and DF set (mtu = 1436)
From 10.0.5.1 icmp_seq=2 Frag needed and DF set (mtu = 1436)

--- 10.100.1.50 ping statistics ---
4 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3045ms

터미널 결과를 보면 한 번으로 네트워크 팀에게 명확한 근거 제시가 간으한데 중간 라우터 10.0.5.1에 mtu가 1436으로 제한되어 있고 큰 패킷이 버려지고 있다. vpn 장비의 mss clamping 설정을 켜주거나 mtu를 맞춰달라 해야한다.

애플리케이션 레벨 방어로직 #

네트워크 장비가 고쳐지기 전에 무한대기 hang에 빠지면 서버 전체가 죽을수도 있다.

mtu 이슈로 패킷이 오지 않을 때 철저한 read timeout과 방어적 http 클라이언트 설정을 적용해야한다.

import okhttp3.OkHttpClient
import okhttp3.Request
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit

@Configuration
class ResilientHttpClientconfig {
    private val log = LoggerFactory.getLogger(this::class.java)

    @Bean
    fun customOkClient(): OkHttpClient {
        
        return OkHttpClient.Builder()
            // 1. Connection Timeout: TCP 핸드쉐이크(작은 패킷)는 보통 성공하므로 짧게 유지 (예: 3초)
            .connectionTimeout(3, TimeUnit.SECONDS)
            // 2. Read Timeout: MUT 블랙홀로 인해 대용량 응답 패킷이 중간에 증발할경우 스레드가 무한정대기하지 않고 5초뒤에 즉시 예외를 발생시켜 스레드 반환
            .readTimeout(5, TimeUnit.SECONDS)
            // 3. Write Timeout로 서버로 큰데이터를 업로드할 때 막히는것도 방지
            .writeTimeout(5, TimeUnit.SECONDS)
            // 4. Conenction Pool 유지 관리 hang 걸린 죽은 커넥션 정리
            .connectionPool(okhttp3.ConnectionPool(50, 5, TimeUnit.MINUTES))
            // 5. 네트워크 이슈 발생시 즉시 재시도 차단함. mtu이슈는 재시도해도 똑같이 실패해서. 끄는것이 유리함
            .retryOnConnectionFailure(false)
            .build()
    }
}

class PaymentClient(private val httpClient: OkHttpClient) {
    private val log = LoggerFactory.getLogger(this::class.java)

    fun fetchLargePaymentHistory(userId: String): String {
        val request = Request.Builder(
            .url("http://on-premise-db-api.internal/payments/$userId")
            .build()
        )

        return try {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) throw RuntimeException("API Error")
                response.body?.string() ?: ""
            }
        } catch (e: SocketTimeoutException) {
            // MTU Mismatch로 인한 블랙홀 발생시 여기로 떨어짐
            // 전체 서비스 마비를 막고 고객에게 일시적 장애를 빠르게 응답하거나 fallback하기
            log.error("MTU 의심/네트워크 지연, 대용량 응답 수신중 timeout 발생함")
            throw CustomNetworkException()
        }
    }
}

이런식으로 네트워크 인프라 장애 mtu mismatch는 언제든 발생할 수 있다.

위와같이 애플리케이션 코드가 항상 최악의 네트워크 상황을 가정하고 타임아웃 방어벽을 처두는것이

진정한 sre 서버 및 서버 엔지니어 기본 소양이다.

SRE/question/q_34.md