[리눅스 커널이야기] TCP 다시 살펴보기

피누·2021년 3월 25일
2
post-thumbnail

TCP TIME_WAIT이란

  • Time wait은 active close에서 발생한다.
  • Time wait 상태의 소켓은 타이머가 종료되어 커널로 다시 돌아갈 때까지는 사용할 수 없다. (기본 타임아웃은 1분이다.)
  • Time wait의 핵심은 연결이 종료된 후에도 소켓을 바로 정리하지 않고 연결 종료에 대한 흔적을 남겨 놓는 것
  • Time wait가 매우 짧다면 아래와 같은 문제가 발생할 수 있다.
    • 위 그림에서 마지막에 Server가 보낸 Fin에 대한 응답으로 클라이언트가 Ack를 보냈으나 패킷이 유실되어 Server가 받지 못한다.
    • Server는 Ack를 받지 못해 Fin을 재전송한다.
    • Client의 소켓은 이미 닫혀버렸기 때문에 정상적인 패킷으로 판단하지 않고 Ack를 보내지 않는다.
    • Server는 Ack를 받지 못했기때문에 Last Ack 상태로 남아있게 된다.

TIME_WAIT 소켓의 문제점

netstat -napo | grep -i time_wait 명령어를 통해 TIME_WAIT 소켓을 확인할 수 있다.

TIME_WAIT 소켓이 많아지면 로컬 포트 고갈에 따른 어플리케이션 타임아웃이 발생할 수 있다. 요청을 보내기 위해 소켓을 만드는 과정은 다음과 같다.

  1. 서버와 통신할 때 사용할 포트를 커널에게 요청한다.
  2. 가용 가능한 로컬 포트를 찾아서 커널이 어플리케이션에 할당한다.
  3. 어플리케이션은 목적지 ip, 목적지 포트를 커널에게 넘겨주고 커널은 소켓을 생성한다.
  4. 커널은 소켓 생성 후 소켓 접근 때 사용할 FD(File Descriptor)을 어플리케이션에게 제공한다.
  5. 이렇게 사용된 소켓을 active close하게 되면 일정시간동안 TIME_WAIT 상태로 남게 된다.

정상적인 상황이라면 5번에서 TIME_WAIT 상태의 소켓은 커널로 다시 돌아갈 때까지 사용할 수 없다. 이런 식으로 다량의 로컬 포트가 TIME_WAIT 상태로 쌓이게되면 로컬포트가 고갈되어 서버와 통신 할 수 없게 된다.

실제 트러블 슈팅 사례 - 로컬 포트 부족과 TIME_WAIT

위와 같은 문제는 아래 방법 중 하나로 해결 할 수 있다.

  • 커널 파라미터 net.ipv4.tcp_tw_reuse 옵션을 통해 TIME_WAIT 소켓을 재사용 할 수 있게 설정한다.
  • 커널 파라미터 net.ipv4.tcp_tw_recycle 옵션을 통해 타임아웃을 RTO 기반의 작은 값으로 변경한다.
  • Connection pool을 통해 매번 연결을 끊지 않고 미리 소켓을 열어놓고 요청을 처리한다.
  • keepalive를 한번 맺은 세션을 요청이 끝나더라도 유지한다.

TCP Keep alive vs HTTP Keep alive
TCP Keep alive가 60초라면 60초 간격으로 연결이 유지되었는지 패킷을 보내고, 응답을 받으면 연결을 유지하는 반면 HTTP Keep alive가 60초라면 60초 이후 연결을 끊는다.

Case Study - MQ 서버와 로드 밸런서


위와 같은 구조에서 로드밸런서의 Idle timeout때문에 간혈적으로 타임아웃이 발생할 수 있다. 이는 서버에서의 ESTABLISHED 상태의 소켓이 클라이언트에는 존재하지 않기 때문에 발생하는데, 구체적인 이유는 다음과 같다.
1. 로드 밸런서는 클라이언트와 서버 간 TCP 세션을 테이블에 저장한다.
2. 로드 밸런서는 idle timeout 기능을 통해서 일정 시간 동안 사용되지 않은 세션을 세션 테이블에서 정리한다. 이때 세션은 로드밸런서의 세션 테이블에서만 지워지고 두 종단(클라이언트와 서버)에는 이를 알리지 않는다.

위 과정에서 idle timeout이 지나 세션 테이블에서 삭제 된 이후에도 두 종단간의 커넥션이 살아있을 경우 문제가 된다. 클라이언트는 커넥션이 맺어진 서버가 아니라 로드 밸런서에 의해 확률적으로 새로운 서버로 요청이 전달 될 수 있다. 이때 서버는 커넥션을 맺지 않은 요청 패킷이므로 비정상 패킷으로 판단하고 RST 패킷을 보낸다.

클라이언트는 RST 패킷을 받았기때문에 TCP Handshake를 맺고 다시 맺고 요청을 보내야한다. 이때 소요되는 시간이 애플리케이션에서 설정한 타임아웃 임계치를 넘어가면 Time out이 발생하게 된다. 반대로 기존 커넥션이 맺어진 서버는 좀기 커넥션이 남겨진다.

사용자 요청이 적은 새벽 시간대에는 맺어져 있는 세션으로 패킷이 흐르지 않을 가능성이 커 idle timeout에 걸려 열려 있는 커넥션이 로드 밸런서에서 지워져 위와 같은 문제가 발생 할 수 있다. 이런 이유로 로드 밸런서를 사용하는 TCP 기반의 서비스 환경에서는 반드시 TCP Keepalive를 설정해야한다.

Reference

리눅스 커널이야기
http://docs.likejazz.com/time-wait/#%ED%83%80%EC%9E%84%EC%8A%A4%ED%83%AC%ED%94%84
https://www.popit.kr/%EB%A1%9C%EC%BB%AC-%ED%8F%AC%ED%8A%B8-%EB%B6%80%EC%A1%B1%EA%B3%BC-time-wait/

profile
어려운 문제를 함께 풀어가는 것을 좋아합니다.

0개의 댓글