netstat -napo | grep -i time_wait 명령어를 통해 TIME_WAIT 소켓을 확인할 수 있다.
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초 이후 연결을 끊는다.
위와 같은 구조에서 로드밸런서의 Idle timeout때문에 간혈적으로 타임아웃이 발생할 수 있다. 이는 서버에서의 ESTABLISHED
상태의 소켓이 클라이언트에는 존재하지 않기 때문에 발생하는데, 구체적인 이유는 다음과 같다.
1. 로드 밸런서는 클라이언트와 서버 간 TCP 세션을 테이블에 저장한다.
2. 로드 밸런서는 idle timeout 기능을 통해서 일정 시간 동안 사용되지 않은 세션을 세션 테이블에서 정리한다. 이때 세션은 로드밸런서의 세션 테이블에서만 지워지고 두 종단(클라이언트와 서버)에는 이를 알리지 않는다.
위 과정에서 idle timeout이 지나 세션 테이블에서 삭제 된 이후에도 두 종단간의 커넥션이 살아있을 경우 문제가 된다. 클라이언트는 커넥션이 맺어진 서버가 아니라 로드 밸런서에 의해 확률적으로 새로운 서버로 요청이 전달 될 수 있다. 이때 서버는 커넥션을 맺지 않은 요청 패킷이므로 비정상 패킷으로 판단하고 RST 패킷을 보낸다.
클라이언트는 RST 패킷을 받았기때문에 TCP Handshake를 맺고 다시 맺고 요청을 보내야한다. 이때 소요되는 시간이 애플리케이션에서 설정한 타임아웃 임계치를 넘어가면 Time out이 발생하게 된다. 반대로 기존 커넥션이 맺어진 서버는 좀기 커넥션이 남겨진다.
사용자 요청이 적은 새벽 시간대에는 맺어져 있는 세션으로 패킷이 흐르지 않을 가능성이 커 idle timeout에 걸려 열려 있는 커넥션이 로드 밸런서에서 지워져 위와 같은 문제가 발생 할 수 있다. 이런 이유로 로드 밸런서를 사용하는 TCP 기반의 서비스 환경에서는 반드시 TCP Keepalive를 설정해야한다.
리눅스 커널이야기
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/