Tcp Socket Status

tcp의 통신 과정과 통신에서 사용되는 소켓의 상태에 대해 알아본다.

TCP 통신 과정

서버와 클라이언트 사이의 통신은 3-way handshake -> 데이터 요청 및 반환 -> 4-way handshake의 순서로 이루어진다.

3-way handshake

  • 클라이언트는 서버에게 통신을 시작하겠다는 SYN을 보낸다.
  • 서버는 SYN에 대한 응답으로 SYN-ACK를 보낸다.
  • 마지막으로 클라이언트는 서버로부터 받은 신호에 대한 응답으로 ACK를 보낸다.

4-way handshake

  • 연결을 먼저 끊으려는 쪽에서 FIN(종료 의사 신호)를 보낸다. (이 때 서버와 클라이언트 모두 연결을 끊는 쪽이 될 수 있다. 여기서는 편의상 서버가 FIN을 보냈다고 하자)
  • 클라이언트는 FIN에 대한 응답으로 ACK를 보낸 후, 자신의 통신이 끝날 때까지 기다렸다가 다시 FIN을 보낸다.
  • 서버는 클라이언트가 보낸 마지막 FIN에 대한 ACK를 전송하고 잠시 기다렸다가 소켓을 정리한다.
  • ACK를 받은 클라이언트 또한 소켓을 정리한다.

4-way handshake 소켓 상태

위의 사진에서 연결을 먼저 끊는 쪽이 active close를, 요청을 받은 쪽이 passive close를 한다고 한다. 오가는 신호를 토대로 소켓의 상태를 보면 다음과 같다.

  • FIN_WAIT_1: Initiator는 Receiver에게 FIN 신호를 보낸다. FIN 신호에 대한 ACK가 오기 전까지 이 상태를 유지한다.
  • CLOSE_WAIT: Receiver는 받은 FIN 신호에 대한 ACK를 보낸다. 만약 Receiver가 소켓일 닫기 전에 전송할 데이터가 남아있다면 데이터 전송이 끝날 때까지 이 상태를 유지한다.
  • FIN_WAIT_2: Initiator는 ACK를 받고 다음 FIN이 도착할 때까지 이 상태를 유지한다.
  • LAST_ACT: Receiver는 데이터 전송이 끝난 후 Initiator에게 마지막 FIN을 전송한다. FIN에 대한 ACK를 받기 전까지 이 상태를 유지한다.
  • TIME_WAIT: Initiator는 마지막 FIN에 대한 ACK를 전송한 후 타임아웃 시간동안 기다린다. TIME_WAIT 소켓의 기본 타임아웃은 1분이다.
  • Receiver Close: Initiator에게 마지막 ACK를 받으면 소켓을 정리한다.
  • Initiator Close: 타임아웃이 끝나면 소켓을 정리한다.

여기서 주의해서 봐야 할 상태는 TIME_WAIT이다. TIME_WAIT 상태의 소켓은 Receiver의 소켓이 단번에 ACK를 받지 못해 LAST_ACK 상태로 남는 것을 방지하기 위해서 존재한다. 이는 통신 불량 혹은 일시적인 하드웨어 오류로 간간히 발생한다. 충분한 timeout 시간이 있다면 Receiver는 추가적인 시도를 통해 ACK를 받아 소켓을 닫을 수 있다. 하지만 TIME_WAIT 상태가 지속될 때 커널은 TIME_WAIT 소켓이 사용하고 있는 포트를 사용할 수 없기 때문에 성능 저하가 일어난다.

TIME_WAIT Socket

TIME_WAIT 소켓은 먼저 연결을 끊는 쪽에서 생성된다. 통신이 많이 일어나게 되면 서버든 클라이언트든 상관없이 TIME_WAIT 소켓이 모든 포트를 점유하게 될 가능성이 있다. 이 때문에 사용할 로컬 포트가 없어지면 머신은 외부와 통신이 불가능해진다.

현재 TIME_WAIT 소켓이 몇 개가 있는지는 netstat 명령어로 확인할 수 있다.

$ netstat -napo | grep -i time_wait
tcp        0      0 172.31.17.125:60436     172.217.31.174:80       TIME_WAIT   -                timewait (57.91/0/0)

아래의 방법들을 통해서 TIME_WAIT 소켓이 만드는 문젯점을 완화할 수 있다.

tcp_tw_reuse

TIME_WAIT 소켓이 클라리언트 사이드에서 생성되었을 때의 해결책은 TIME_WAIT 소켓을 재사용할 수 있도록 변경하는 것이다. 커널 파라미터 중 net.ipv4.tcp_tw_reuse 값과 net.ipv4.tcp_timestamps 값을 1로 설정하면 커널이 새로운 소켓을 만들 때 TIME_WAIT 상태의 소켓을 사용하게 된다.

Connection Pool

TIME_WAIT 소켓이 클라이언트 사이드에서 생성되었을 때의 두 번째 해결책은 Connection Pool을 이용하는 방법이다. 소켓을 미리 열어둔 후 끊지 않으면 같은 서버에 대한 TIME_WAIT 소켓이 많이 생길 일이 없어진다. 또한 이 방법은 소켓을 미리 열어두기 때문에 반복적으로 TCP 연결을 맺고 끊을 필요가 없어 속도를 향상시킬 수 있다.

TCP Keepalive

TIME_WAIT 소켓이 서버 사이드에서 생성되었을 때는 웹 서버의 keep_alive 기능을 사용해서 문제를 완화시킬 수 있다. keep_alive timeout을 설정해두면 해당 시간동안은 같은 클라이언트로부터 요청이 왔을 때 TIME_WAIT 상태의 소켓을 사용하게된다. 대부분의 어플리케이션에서 TCP Keepalive를 설정할 수 있는 옵션을 제공한다. 필요한 커널 파라미터는 다음과 같다.

  • net.ipv4.tcp_keepalive_time: keepalive 소켓의 유지 시간을 의미한다.
  • net.ipv4.tcp_keepalive_probes: keepalive 패킷을 보낼 최대 전송 횟수를 정의한다.
  • net.ipv4.tcp_keepalive_intvl: keepalive 재전송 패킷을 보내는 주기를 의미한다.

TCP Keepalive는 커널 레벨에서 두 종단 간의 연결을 유지하는 기능이며 이를 통해서 불필요한 TCP Handshake를 줄일 수 있어 성능 향상이 이루어진다. 하지만 이 외에도 좀비 커넥션을 방지하는 기능 또한 있다.

HTTP Keepalive는 TCP Keepalive와는 다르며 요청 주기를 기준으로 최대한 연결을 유지하는 것이 목적이다.

로드 밸런서를 사용하는 환경에서 TCP 통신을 한다면 TCP Keepalive를 반드시 켜놓아야 한다.

comments powered by Disqus