Linux Network Internal
이번 글에서 다룰 내용은 커널 파라미터로 있는 tcp_keepalive와 nginx에서 사용하는 keepalive의 차이점과 특징 등에 대해 살펴보겠습니다.
TCP 관련된 커널 파라미터 중에 keepalive와 관련된 3개의 값이 있습니다. 그 값들의 의미는 각각 아래와 같습니다.
net.ipv4.tcp_keepalive_time
TCP 세션에 대해 keepalive를 유지하는 시간 (Default : 7200)
net.ipv4.tcp_keepalive_probes
keepalive가 끊어졌다고 판단하고 세션을 정리하는 동안 보낼 ping-pong 패킷 수 (Default : 9)
net.ipv4.tcp_keepalive_intvl
첫 번째 health_check 이후 ping-pong 패킷을 보내는데 걸리는 패킷 사이의 주기 (Default : 75)
만약 두 노드 간에 TCP 세션이 있다고 하면 아무런 통신도 하지 않는 Idle 상태가 된지 7200초 후에 살아 있는지를 체크하는 ping-pong 패킷을 보냅니다. 아무런 의미도 없는 ACK를 보내게 되죠. 그 후 상대방으로부터 ACK를 받지 못하면 최대 9번까지 75초의 간격으로 ping-pong 패킷을 보내고 세션을 정리하게 됩니다.
이 값들은 setsockopt() 함수에서 명시적으로 SO_KEEPALIVE 옵션을 설정할 때에만 의미를 가지게 됩니다. 이 방식으로 bind() 된 포트들은 netstat -napo로 살펴보면 소켓에 대한 타이머가 설정되어 있음을 볼 수 있습니다. ( -o 옵션이 타이머를 볼 수 있는 옵션입니다. keepalive 타이머 외에 TIME_WAIT에 대한 타이머 등 커널에서 소켓을 관리하는데 필요로 하는 모든 타이머를 볼 수 있습니다. )
우리가 흔히 사용하는 SSH 역시도 타이머를 사용하고 있습니다. 마법의 도구 strace를 통해서 살펴보면 아래와 같이 setsockopt를 이용해서 SO_KEEPALIVE 옵션을 설정하는 것을 볼 수 있습니다.
nginx keepalive 설정은 조금 다릅니다. nginx에서 사용하는 keepalive는 기본적으로 애플리케이션 레벨에서의 keepalive입니다. 즉, tcp keepalive를 사용하지 않으며, tcp keepalive처럼 ping-pong을 통해 상대방이 살아 있는지를 확인하지도 않습니다. 클라이언트로부터 먼저 FIN을 받지 않는 이상 먼저 끊지 않고 keepalive time 동안 세션을 그대로 EST 상태로 유지하고 있습니다.
하지만 nginx에서도 tcp keepalive 기능을 사용할 수 있는데요, listen 지시자 옆에 so_keepalive옵션을 넣으면 됩니다. 이렇게 되면 tcp keepalive 기능도 함께 사용할 수는 있지만, 어차피 nginx keepalive 시간에 종속적인 keepalive 기능이기 때문에 특별히 켜서 사용할 필요는 없습니다.
간단하게 tcp keepalive와 nginx keepalive를 살펴봤는데요, 두 설정 모두 TCP 세션을 유지하기 위해 사용한다는 공통점이 있지만, 큰 차이점이 있습니다.
첫 번째로는 tcp keepalive의 경우 keepalive 유지/관리에 대한 작업을 커널이 직접 한다는 것입니다.
TCP 스택에서 직접 해당 소켓에 대해 살아 있는지를 확인합니다.
두 번째로는 tcp keepalive의 경우 ping-pong 패킷에 대한 응답이 온다면 끊지 않고 계속해서 세션을 유지하지만, nginx keepalive의 경우는 keepalive에 설정된 시간이 지나면 클라이언트와의 연결을 능동적으로 끊는다는 것입니다.
즉 전자의 경우는 계속적으로 세션이 유지되는 반면에 후자의 경우는 설정된 시간 이후에는 세션이 유지되지 않습니다.
그래서 tcp keepalive와 nginx keepalive는 비슷해 보이지만, 서로 연관성이 없습니다. tcp keepalive 관련된 커널 파라미터를 변경함으로써 nginx의 keepalive 설정에 영향이 있지 않을까에 대한 걱정은 하지 않으셔도 됩니다. 이 부분은 apache도 마찬가지입니다. 애플리케이션에서 자체적으로 keepalive 세션을 관리하는 경우에는 tcp keepalive 값은 무시하셔도 됩니다.
따라서 사용하는 애플리케이션에 keepalive 옵션이 있다면, SO_KEEPALIVE 옵션을 통해서 커널의 관리를 받는 것인지 아니면 자체적으로 keepalive 로직을 구현한 것인지 정확하게 알고 있어야 합니다.
웹 서버에는 웹 서버 내에서 구현되어 있는 keepalive가 있는데 왜 tcp keepalive가 필요할까요? tcp keepalive는 보통 DSR 방식의 로드 밸런서 하단에 커넥션 풀링 방식으로 서비스를 할 때 필요합니다. 웹 서버가 아닌 active mq, rabbit mq 등 TCP 기반의 서비스들입니다.
예를 들어 보겠습니다. 만약 아래와 같이 LB 밑에 두 대의 서버가 구성되어 있고 웹 서비스가 아닌 TCP 기반의 통신 서비스를 제공한다고 가정합니다. 클라이언트는 LB로 요청을 보내고 LB는 세션 테이블을 확인한 후 정책에 의해 적당한 서버를 할당해 줍니다.
커넥션 풀 방식이기 때문에 클라이언트는 LB와 계속해서 세션을 열어 두고 있겠죠. 이 상태에서 11분이 경과되었다고 가정합니다. 11분째에 클라이언트는 데이터를 전송할 일이 있어서 자신이 열어 두었던 커넥션 풀 중 하나를 이용해서 데이터를 보냅니다. 바로 여기서 문제가 시작됩니다.
LB는 이미 10분이 지났기 때문에 기존 세션에 대한 정보를 삭제하게 되고, 클라이언트로부터 들어온 데이터를 새로운 데이터로 인식해서 B 서버로 보내게 됩니다. (물론 재수가 좋으면 A 서버로 보낼 수도 있습니다.) B 서버는 세션이 연결되어 있지 않은 클라이언트로부터 패킷을 받기 때문에 RST 패킷을 보내게 되고요, 클라이언트는 영문도 모른 채 RST 패킷을 받게 되어 타임아웃이 나게 됩니다. 그리고 나서 다시 SYN을 맺게 되고 B 서버로 세션이 다시 맺어지게 됩니다.
이 과정에서 중요한 점은 첫째, 클라이언트에서는 원치 않는 타임아웃이 발생하게 되며, 둘째, A 서버는 클라이언트로부터 FIN 패킷 조차도 받을 수 없기 때문에 계속해서 EST 소켓이 쌓이는 garbage 세션이 발생하는 상황이 됩니다. 그러다가 FD를 다 사용하게 되면 장애가 발생하게 되죠.
이럴 경우에 tcp keepalive를 사용하게 된다면, A 서버는 지속적으로 클라이언트로 ping-pong 형식의 ACK를 보내게 되고 클라이언트는 계속해서 응답 ACK를 보내기 때문에 LB의 세션 테이블에서 지워지지 않고 계속해서 세션 정보가 유지됩니다.
물론, 세션 테이블의 주기인 10분 보다 tcp_keepalive_time 값이 낮아야합니다.
이를 이용해서 웹 서비스가 아닌 TCP 기반의 서비스도 LB를 통해 서비스할 수 있게 됩니다.