NAT Gateway Timeout 해결일지

hs_hello·2025년 1월 5일
post-thumbnail

발단

저희 팀에서는 스케줄링을 통해 매일 외부 API에서 특정 데이터를 가져와서 DB에 저장하는 로직이 존재하는데요. 해당 스케줄러에서 어느날 다음과 에러가 발생했습니다 ㅠㅠ

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://helloworld:8080/hello": Connection reset

connection reset은 처음 겪는 에러여서 발생한 원인은 바로 파악하지 못했습니다.

다만 로그를 보니 스케줄러가 최초 실행 및 재시도 시간으로부터 정확히 2분뒤에 에러가 발생했기에, "어디선가 타임아웃을 발생시켰구나!!"라고 추측할 수 있었습니다.

추측

  1. RestClient의 timeout

가장 먼저 떠올랐는데요. 그렇지만 제 기억 속에서도, 실제 코드에서도 타임아웃을 2분으로 설정한 건 없었습니다.

"혹시 내가 설정을 잘못해서 default option이 적용됐나?"라는 의심도 했지만, 로컬 환경에서 테스트 해보니 RestClient의 경우 명확히 ConnectionTimeout 혹은 ReadTimeout이라는 에러를 반환했습니다.

  1. 대상 서버의 Load Balancer

외부 API라고 간략히 이야기했지만, 해당 API는 사내 레거시 프로젝트의 API입니다. 신규 프로젝트는 NaverCloud를, 레거시 프로젝트는 AWS를 이용하는데 해당 서버 앞단에 ALB가 존재하는 형태라 ALB의 타임아웃을 의심했습니다.

그렇지만 ..

  • 아무리 찾아봐도 ALB에 2분 타임아웃 설정은 없었음.
  • 로컬에서 2분 넘게 걸리는 요청을 해봐도 정상적으로 동작
    - 심지어 사내 IP라서 잘되나 싶어서.. 개인 IP로도 해봤는데 잘됨 ㅜㅜ

이젠 원인이 안떠올라서 무지성 구글링을 했었는데요. 우연히 2분이라는 키워드에 걸려서 네이버클라우드 docs에서 다음과 같은 글을 발견했습니다!!

문제가 발생한 프로젝트는 k8s 환경으로 구성되어 있고 외부 통신은 모두 NAT Gateway를 통해 전달되는 상황이였고, 2분 타임아웃도 명확히 일치했습니다.

그리고 설정이 거의 동일할거라고 추측한 AWS의 NAT Gateway 문서에는 클라이언트와 연결을 끊어도 서버에 FIN 패킷을 전달하지 않는다고 적혀있었습니다.

실제로 서버 로그를 보니, 클라이언트와 연결이 끊긴 시점에도 서버는 커넥션이 끊기지않고 로직이 계속 돌아가고 있었습니다.

이젠 이유도 알았고, Solution에 적힌대로 TCP keepalive를 활성화하는 방식으로 해결만 하면 되는 상황!

TCP Keepalive

하나 짚고 넘어가야 할 건 HTTP keepalive와 TCP keepalive가 다르다는 것입니다.

  • HTTP keepalive
    • HTTP 커넥션을 일정 시간동안 '유지'
  • TCP keepalive
    • 커넥션을 맺은 후 일정 시간 후에 keepalive 패킷을 전달
    • 응답을 받지 못한다면 일정 횟수 retry 계속 실패한다면 connection close

즉 TCP keepalive 패킷 전송 시간을 timeout 시간보다 짧게 설정하여, timeout 발생전에 패킷을 전송한다면 유휴 커넥션으로 판단하지않아 timeout 시간을 연장할 수 있습니다.

구체적으로 TCP keepalive 관련된 설정은 다음과 같습니다.(liunx)

  • tcp_keepalive_time
    - 연결 후 처음으로 TCP Keep-Alive 패킷을 보내기까지의 시간

    cat /proc/sys/net/ipv4/tcp_keepalive_time
    sysctl net.ipv4.tcp_keepalive_time=7200

  • tcp_keepalive_intvl
    - keepalive 패킷 전송 간격

    cat /proc/sys/net/ipv4/tcp_keepalive_intvl
    sysctl net.ipv4.tcp_keepalive_intvl

  • tcp_keepalive_probes
    - 연결이 끊어질 때 까지 재전송 시도 횟수(retry)

    cat /proc/sys/net/ipv4/tcp_keepalive_probes
    sysctl net.ipv4.tcp_keepalive_probes

실제로 keepalive time과 interval을 10초로 설정하니 다음과 같이 10초 간격으로 keepalive 패킷을 전송하는 것을 볼 수 있었습니다.

적용

  1. Spring RestClient의 SoKeepAlive 설정 활성화(기본값 false)
var connectionManager = new PoolingHttpClientConnectionManager();
var socketConfig = SocketConfig.custom()
        .setSoKeepAlive(true)
        .build();    
connectionManager.setDefaultSocketConfig(socketConfig);
  1. net.ipv4.tcp_keepalive_time 2분 이하로 설정
  • tcp_keepalive_intvl은 기본값이 75초라 건드리지 않았습니다.

다만 k8s 버전 마다 다르지만, 대부분의 경우 해당 설정이 기본적으로 변경 불가능해 다음과 같은 에러를 만날 수 있습니다.

forbidden sysctl: "net.ipv4.tcp_keepalive_time" not whitelisted

따라서 해당 설정을 사용할 수 있도록 하는 작업이 필요합니다.

// kubelet-config.yml

allowedUnsafeSysctls:
-"net.ipv4.tcp_keepalive_time"

해당 설정을 추가하니 문제 없이 변경할 수 있었고 자고 일어나서 확인해보니 스케줄러도 타임아웃 없이 정상적으로 동작할 수 있었습니다 야호!

사실 리소스가 크게 드는 작업은 아니였지만, 사내에 k8s 환경으로 검증 서버가 갖추어져 있지 않아서 운영에서 벌벌 떨면서 했었습니다 ㅋㅋ ㅠㅠ

그렇지만 잘 해결했고, 그 과정에서 네트워크단에 대해 좀 더 배울 수 있어서 되게 재밌는 경험이였습니다.

Referecne
https://dev-alxndr.github.io/posts/NAT-Gateway-Connection-Rest-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D/
https://github.com/aws/karpenter-provider-aws/issues/1279#issuecomment-1039968290
https://togomi.tistory.com/74

0개의 댓글