tcp의 통신 과정과 통신에서 사용되는 소켓의 상태에 대해 알아본다.
서버와 클라이언트 사이의 통신은 3-way handshake
-> 데이터 요청 및 반환 -> 4-way handshake
의 순서로 이루어진다.
위의 사진에서 연결을 먼저 끊는 쪽이 active close
를, 요청을 받은 쪽이 passive close
를 한다고 한다. 오가는 신호를 토대로 소켓의 상태를 보면 다음과 같다.
여기서 주의해서 봐야 할 상태는 TIME_WAIT
이다. TIME_WAIT 상태의 소켓은 Receiver의 소켓이 단번에 ACK를 받지 못해 LAST_ACK 상태로 남는 것을 방지하기 위해서 존재한다. 이는 통신 불량 혹은 일시적인 하드웨어 오류로 간간히 발생한다. 충분한 timeout 시간이 있다면 Receiver는 추가적인 시도를 통해 ACK를 받아 소켓을 닫을 수 있다. 하지만 TIME_WAIT 상태가 지속될 때 커널은 TIME_WAIT 소켓이 사용하고 있는 포트를 사용할 수 없기 때문에 성능 저하가 일어난다.
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 소켓이 만드는 문젯점을 완화할 수 있다.
TIME_WAIT 소켓이 클라리언트 사이드에서 생성되었을 때의 해결책은 TIME_WAIT 소켓을 재사용할 수 있도록 변경하는 것이다. 커널 파라미터 중 net.ipv4.tcp_tw_reuse
값과 net.ipv4.tcp_timestamps
값을 1로 설정하면 커널이 새로운 소켓을 만들 때 TIME_WAIT 상태의 소켓을 사용하게 된다.
TIME_WAIT 소켓이 클라이언트 사이드에서 생성되었을 때의 두 번째 해결책은 Connection Pool
을 이용하는 방법이다. 소켓을 미리 열어둔 후 끊지 않으면 같은 서버에 대한 TIME_WAIT 소켓이 많이 생길 일이 없어진다. 또한 이 방법은 소켓을 미리 열어두기 때문에 반복적으로 TCP 연결을 맺고 끊을 필요가 없어 속도를 향상시킬 수 있다.
TIME_WAIT 소켓이 서버 사이드에서 생성되었을 때는 웹 서버의 keep_alive
기능을 사용해서 문제를 완화시킬 수 있다. keep_alive timeout을 설정해두면 해당 시간동안은 같은 클라이언트로부터 요청이 왔을 때 TIME_WAIT 상태의 소켓을 사용하게된다. 대부분의 어플리케이션에서 TCP Keepalive를 설정할 수 있는 옵션을 제공한다. 필요한 커널 파라미터는 다음과 같다.
TCP Keepalive는 커널 레벨에서 두 종단 간의 연결을 유지하는 기능이며 이를 통해서 불필요한 TCP Handshake를 줄일 수 있어 성능 향상이 이루어진다. 하지만 이 외에도 좀비 커넥션을 방지하는 기능 또한 있다.
HTTP Keepalive는 TCP Keepalive와는 다르며 요청 주기를 기준으로 최대한 연결을 유지하는 것이 목적이다.
로드 밸런서를 사용하는 환경에서 TCP 통신을 한다면 TCP Keepalive를 반드시 켜놓아야 한다.