<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hs_lol_s2.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 05 Jan 2025 18:59:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. hs_lol_s2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hs_lol_s2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[NAT Gateway Timeout 해결일지]]></title>
            <link>https://velog.io/@hs_lol_s2/NAT-Gateway-Timeout-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@hs_lol_s2/NAT-Gateway-Timeout-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Sun, 05 Jan 2025 18:59:15 GMT</pubDate>
            <description><![CDATA[<h3 id="발단">발단</h3>
<p>저희 팀에서는 스케줄링을 통해 매일 외부 API에서 특정 데이터를 가져와서 DB에 저장하는 로직이 존재하는데요. 해당 스케줄러에서 어느날 다음과 에러가 발생했습니다 ㅠㅠ</p>
<pre><code>org.springframework.web.client.ResourceAccessException: I/O error on POST request for &quot;http://helloworld:8080/hello&quot;: Connection reset</code></pre><p>_<strong>connection reset</strong>_은 처음 겪는 에러여서 발생한 원인은 바로 파악하지 못했습니다.</p>
<p>다만 로그를 보니 스케줄러가 최초 실행 및 재시도 시간으로부터 정확히 2분뒤에 에러가 발생했기에, &quot;어디선가 타임아웃을 발생시켰구나!!&quot;라고 추측할 수 있었습니다.</p>
<h3 id="추측">추측</h3>
<ol>
<li>RestClient의 timeout</li>
</ol>
<p>가장 먼저 떠올랐는데요. 그렇지만 제 기억 속에서도, 실제 코드에서도 타임아웃을 2분으로 설정한 건 없었습니다. </p>
<p>&quot;혹시 내가 설정을 잘못해서 default option이 적용됐나?&quot;라는 의심도 했지만, 로컬 환경에서 테스트 해보니 RestClient의 경우 명확히 ConnectionTimeout 혹은 ReadTimeout이라는 에러를 반환했습니다.</p>
<ol start="2">
<li>대상 서버의 Load Balancer</li>
</ol>
<p>외부 API라고 간략히 이야기했지만, 해당 API는 사내 레거시 프로젝트의 API입니다. 신규 프로젝트는 NaverCloud를, 레거시 프로젝트는 AWS를 이용하는데 해당 서버 앞단에 ALB가 존재하는 형태라 ALB의 타임아웃을 의심했습니다.</p>
<p>그렇지만 ..</p>
<ul>
<li><strong>아무리 찾아봐도 ALB에 2분 타임아웃 설정은 없었음.</strong></li>
<li><strong>로컬에서 2분 넘게 걸리는 요청을 해봐도 정상적으로 동작</strong><ul>
<li>심지어 사내 IP라서 잘되나 싶어서.. 개인 IP로도 해봤는데 잘됨 ㅜㅜ</li>
</ul>
</li>
</ul>
<p>이젠 원인이 안떠올라서 무지성 구글링을 했었는데요. 우연히 2분이라는 키워드에 걸려서 네이버클라우드 docs에서 다음과 같은 글을 발견했습니다!!</p>
<p><img src="https://velog.velcdn.com/images/carthagosu/post/049998b0-5872-4f21-a966-5c5dcd044804/image.png" alt=""></p>
<p>문제가 발생한 프로젝트는 k8s 환경으로 구성되어 있고 외부 통신은 모두 NAT Gateway를 통해 전달되는 상황이였고, 2분 타임아웃도 명확히 일치했습니다.</p>
<p>그리고 설정이 거의 동일할거라고 추측한 AWS의 NAT Gateway 문서에는 클라이언트와 연결을 끊어도 서버에 FIN 패킷을 전달하지 않는다고 적혀있었습니다.</p>
<p>실제로 서버 로그를 보니, 클라이언트와 연결이 끊긴 시점에도 서버는 커넥션이 끊기지않고 로직이 계속 돌아가고 있었습니다.
<img src="https://velog.velcdn.com/images/carthagosu/post/3f73caff-a106-4e12-bb8a-22dd041c9933/image.png" alt=""></p>
<p>이젠 이유도 알았고, Solution에 적힌대로 <strong>TCP keepalive</strong>를 활성화하는 방식으로 해결만 하면 되는 상황!</p>
<h3 id="tcp-keepalive">TCP Keepalive</h3>
<p>하나 짚고 넘어가야 할 건 HTTP keepalive와 TCP keepalive가 다르다는 것입니다.</p>
<ul>
<li>HTTP keepalive<ul>
<li>HTTP 커넥션을 일정 시간동안 &#39;유지&#39;</li>
</ul>
</li>
<li>TCP keepalive<ul>
<li>커넥션을 맺은 후 일정 시간 후에 keepalive 패킷을 전달</li>
<li>응답을 받지 못한다면 일정 횟수 retry 계속 실패한다면 connection close</li>
</ul>
</li>
</ul>
<p>즉 TCP keepalive 패킷 전송 시간을 timeout 시간보다 짧게 설정하여, timeout 발생전에 패킷을 전송한다면 유휴 커넥션으로 판단하지않아 timeout 시간을 연장할 수 있습니다.</p>
<p>구체적으로 TCP keepalive 관련된 설정은 다음과 같습니다.(liunx)</p>
<ul>
<li><p><strong>tcp_keepalive_time</strong></p>
<ul>
<li>연결 후 처음으로 TCP Keep-Alive 패킷을 보내기까지의 시간<blockquote>
<p>cat /proc/sys/net/ipv4/tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_time=7200</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>tcp_keepalive_intvl</strong></p>
<ul>
<li>keepalive 패킷 전송 간격<blockquote>
<p>cat /proc/sys/net/ipv4/tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_intvl</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>tcp_keepalive_probes</strong></p>
<ul>
<li>연결이 끊어질 때 까지 재전송 시도 횟수(retry)</li>
</ul>
<blockquote>
<p>cat /proc/sys/net/ipv4/tcp_keepalive_probes
sysctl net.ipv4.tcp_keepalive_probes</p>
</blockquote>
</li>
</ul>
<p>실제로 keepalive time과 interval을 10초로 설정하니 다음과 같이 10초 간격으로 keepalive 패킷을 전송하는 것을 볼 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/carthagosu/post/afdc83f8-956d-46df-aa7d-9ed90300eddf/image.png" alt=""></p>
<h3 id="적용">적용</h3>
<ol>
<li><strong>Spring RestClient의 SoKeepAlive 설정 활성화</strong>(기본값 false)</li>
</ol>
<pre><code>var connectionManager = new PoolingHttpClientConnectionManager();
var socketConfig = SocketConfig.custom()
        .setSoKeepAlive(true)
        .build();    
connectionManager.setDefaultSocketConfig(socketConfig);</code></pre><ol start="2">
<li><strong>net.ipv4.tcp_keepalive_time 2분 이하로 설정</strong></li>
</ol>
<ul>
<li>tcp_keepalive_intvl은 기본값이 75초라 건드리지 않았습니다.</li>
</ul>
<p>다만 k8s 버전 마다 다르지만, 대부분의 경우 해당 설정이 기본적으로 변경 불가능해 다음과 같은 에러를 만날 수 있습니다. </p>
<pre><code>forbidden sysctl: &quot;net.ipv4.tcp_keepalive_time&quot; not whitelisted</code></pre><p>따라서 해당 설정을 사용할 수 있도록 하는 작업이 필요합니다.</p>
<blockquote>
<p>// kubelet-config.yml</p>
<p>allowedUnsafeSysctls:
-&quot;net.ipv4.tcp_keepalive_time&quot;</p>
</blockquote>
<p>해당 설정을 추가하니 문제 없이 변경할 수 있었고 자고 일어나서 확인해보니 스케줄러도 타임아웃 없이 정상적으로 동작할 수 있었습니다 야호!</p>
<p>사실 리소스가 크게 드는 작업은 아니였지만, 사내에 k8s 환경으로 검증 서버가 갖추어져 있지 않아서 운영에서 벌벌 떨면서 했었습니다 ㅋㅋ ㅠㅠ</p>
<p>그렇지만 잘 해결했고, 그 과정에서 네트워크단에 대해 좀 더 배울 수 있어서 되게 재밌는 경험이였습니다.</p>
<p>Referecne
<a href="https://dev-alxndr.github.io/posts/NAT-Gateway-Connection-Rest-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D/">https://dev-alxndr.github.io/posts/NAT-Gateway-Connection-Rest-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D/</a>
<a href="https://github.com/aws/karpenter-provider-aws/issues/1279#issuecomment-1039968290">https://github.com/aws/karpenter-provider-aws/issues/1279#issuecomment-1039968290</a>
<a href="https://togomi.tistory.com/74">https://togomi.tistory.com/74</a></p>
]]></description>
        </item>
    </channel>
</rss>