개요
클라이언트와 서버 간 통신 과정에서, 어떠한 이유로든 둘 중 하나가 제 기능을 할 수 없게 되는 일이 종종 발생한다. 그러나 한 쪽의 무응답으로 인해 다른 한 쪽이 영영 응답을 대기할 수는 없는 노릇이다.
이러한 난감한 상황을 해결하기 위해 존재하는 설정이 바로 타임아웃(Timeout)이다. 이 타임아웃 값을 잘 설정하면 클라이언트와 서버가 불필요하게 시간과 리소스를 낭비하는 것을 상당히 개선할 수 있다.
오늘은 웹 서비스를 제공하기 위한 TCP/HTTP 통신 과정에서 주로 발생하는 타임아웃에는 어떤 것들이 있는지 알아보고, 이들을 어떻게 잘 다룰 수 있는지를 클라이언트와 서버 각각의 시점으로 확인해보는 시간을 가지고자 한다.
오늘 알아볼 타임아웃의 주제는 아래와 같다.
- Connection Timeout
- Read Timeout
- Keep-Alive Timeout
- Idle Timeout
Connection Timeout
클라이언트가 요청을 보내기 위해서는 먼저 서버와 연결을 맺어야 한다. 그러나 이 연결 자체가 확립되지 못하는 경우 Connection Timeout이 발생한다.
Connection Timeout은 클라이언트와 서버 모두 발생이 가능한데, 그 이유는 TCP의 3-way handshaking 과정에 있다.
클라이언트와 서버의 연결은 3번의 패킷 통신을 통해 확립된다. 즉, 이 과정에서 클라이언트가 서버의 패킷을 기다리는 순간과 서버가 클라이언트의 패킷을 기다리는 순간이 모두 생긴다는 것이다.
이 때문에 두 주체 모두 Connection Timeout을 발생시킬 수 있으며, 둘의 Timeout은 조금 다른 목적을 갖는다.
클라이언트 입장에서의 Connection Timeout
클라이언트는 해당 서버가 정상 구동 상태인지, 본인이 접근 가능한지, 심지어 존재하는지조차 확신할 수 없다. 따라서, 서버가 자신과 통신할 수 있는 서버라고 판단되면 연결 시도를 종료해야 한다.
만일 연결할 서버와 처음으로 통신을 시도하는 거라면, 문제가 클라이언트 측에 있을 수도 있음을 항상 점검해야 한다. Connection Timeout은 서버 주소를 잘못 설정하거나, 클라이언트의 방화벽이 해당 연결을 차단하여 발생할 수도 있기 때문이다.
평소에는 통신에 문제가 없었으나, 일시적으로 Connection Timeout이 발생할 수도 있다. 이 경우에는 Retry를 통해 여러 번 연결 시도를 하거나, 기존에 제공받은 데이터를 캐싱해뒀다가 제공하는 방식으로 일시적 장애가 사용자에게까지 전파되지 않도록 조치할 수도 있다.
서버 입장에서의 Connection Timeout
서버 입장에서는 클라이언트의 존재 여부, 접근 가능성을 판별할 필요는 없으나 클라이언트가 자원을 점유하지 않도록 유의해야 한다. 즉, 일부 클라이언트에 의해 전체적인 처리 성능이 저하되지 않도록 해야 한다는 것이다.
예를 들어, 악의적 공격자가 서버에게 SYN 패킷을 보낸 후 SYN + ACK를 받고 ACK를 보내지 않는 여러 클라이언트를 구동했다고 가정해보자. 이 경우 Timeout이 없다면 서버는 ACK를 기다리는 연결 대기 상태 (SYN-RECEIVED)로 머물게 되고, 영영 오지 않을 클라이언트의 ACK를 기다리게 될 것이다.
따라서, 서버는 자신의 리소스가 불필요하게 낭비되는 것을 방지하기 위해 적절한 Connection Timeout 값을 설정해야 한다.
Read Timeout
클라이언트가 서버와의 연결에는 성공했으나, 서버가 요청에 대한 응답을 제공하지 못할 때에는 Read Timeout이 발생한다.
Read Timeout의 경우 클라이언트와 서버 모두 정상적으로 작동하는 상태라고 볼 수 있으나 응답 데이터의 크기가 너무 크거나, 일시적인 네트워크 지연 등에 의해 발생할 수 있다.
Read Timeout의 경우, 너무 짧게 설정되어있어 문제가 되는 경우도 많다. 크기가 큰 데이터를 주고받기 위해서는 어쩔 수 없이 많은 시간이 필요하기 때문이다.
따라서, Read Timeout 값을 제어할 때에는 해당 요청의 평균 응답 시간이 어느 정도인지를 잘 측정하여 평균 응답 시간에 여유 시간을 더한 값으로 설정해야 불필요한 타임아웃이 발생하지 않는다.
클라이언트 입장에서의 Read Timeout
클라이언트의 입장에서는 응답 데이터가 너무 커서 발생하는 문제라면 아래와 같은 조치를 취할 수 있다.
- 요청을 비동기로 처리하여 사용자와의 인터렉션은 정상적으로 유지
- 여러 개의 데이터를 받느라 응답이 느려졌다면, 페이징 적용을 고려
- 캐싱을 적용하여 반복적인 시간 소모를 막기
서버 입장에서의 Read Timeout
서버의 입장에서는 Connection Timeout 때와 마찬가지로 일부 클라이언트에게 요청을 제공하느라 자원이 점유되지 않도록 하는 것이 최우선 목표이다. 이를 위해 클라이언트와 마찬가지로 페이징을 고려하고 내부에 캐시 데이터를 저장하는 방식으로 조치할 수 있다.
또한, 서버의 경우, 데이터 자체를 최적화하는 방안도 확인해보면 좋다. 보통 크기가 큰 데이터는 이미지, 동영상과 같은 데이터인 경우가 많은데, 이 경우 압축과 포맷 변환 등을 통해 데이터 크기 자체를 줄이는 방법이 있을 수도 있다.
또한, 서버는 항상 응답에 소요되는 시간을 모니터링하고 있어야 한다. 비정상적으로 오래 걸린 응답이 있는지, 있다면 이들의 특징이 무엇인지 분석하여 Infra 단계에서의 조치를 취할 수도 있어야 하기 때문이다.
Keep-Alive Timeout
우선 이 타임아웃 값을 이해하기 위해서는 Keep-Alive가 무엇인지부터 알아야 한다. 자세히 설명하면 이 글의 주제에서 벗어나므로, 간략하게만 설명한다.
HTTP 통신을 할 때, 기본적으로는 TCP 연결 확립 → HTTP 패킷 송수신 → TCP 연결 종료 순서로 통신이 이루어진다. 그러나 TCP 연결을 확립하고 종료하는 것 또한 리소스를 요구하는 작업이기 때문에, 너무 잦은 핸드쉐이킹은 부하를 유발할 수 있다. 이를 개선하기 위해 등장한 것이 바로 Keep-Alive로, TCP 연결을 재사용해 한 번의 연결로 여러 번 HTTP 패킷을 주고받을 수 있도록 하는 기능이다.
그러나 이 Keep-Alive 기능을 사용하면, 요청-응답이 종료된 이후에도 클라이언트와 서버 간 연결이 유효하게 되므로, 자칫하면 불필요한 연결 유지로 양쪽 모두에게 불필요한 자원 낭비가 될 수 있다. 따라서, 적절한 타임아웃을 설정하는 것이 좋다.
클라이언트 입장에서의 Keep-Alive Timeout
사실 클라이언트 입장에서 Kee-Alive Timeout을 설정하는 것은 서버에 대한 배려이다.
한 손님이 한 종업원을 오래 붙들고 있는 것이 손님 입장에서는 부담이 아니지만, 종업원 입장에서는 다른 손님들이 기다리니 부담이 되는 것이기 때문이다.
따라서 적절한 Keep-Alive Timeout을 설정하고, 만약 서버의 Keep-Alive Timeout이 더 짧아 먼저 연결이 끊어졌다면 요청이 필요할 때 재연결을 하면 된다.
서버 입장에서의 Keep-Alive Timeout
서버의 입장에서는 많은 TCP Connection을 유지하는 것이 부하가 될 수 있기 때문에, 이 타임아웃 값을 잘 설정하는 것이 중요하다.
서버 입장에서는 Keep-Alive 타임아웃 값을 클라이언트와 동일하거나 더 짧은 값으로 설정하는 것이 좋은데, 그 이유는 상대적으로 자원 확보가 더 중요한 서버 측이 자원을 빠르게 회수한 뒤 클라이언트가 필요할 때 재연결할 수 있도록 하는 방향이 더 효율적이기 때문이다.
Idle Timeout
Idle(유휴)란, 별다른 요청 처리가 발생하지 않는 상태를 말한다. Idle Timeout은 유휴 상태인 요청을 종료하기 위한 타임아웃 설정값이다.
그런데, 위에서 살펴본 Keep-Alive Timeout 또한 요청이 발생하지 않는 시간이 길면 연결을 종료하는 타임아웃이라는 점은 동일한데 왜 Idle Timeout이라는 용어가 별도로 있는 것일까? 그것은 두 타임아웃이 커버하는 계층이 다르기 때문이다.
Keep-Alive Timeout의 경우 HTTP 요청의 송수신 여부를 기준으로 동작한다. 즉, HTTP 요청이 설정된 시간 동안 발생하지 않으면 연결을 종료하는 것이다. (여기서 말하는 Keep-Alive는 HTTP KeepAlive를 의미한다. TCP KeepAlive의 동작 방식은 별도이지만 해당 글에서는 생략한다.)
그러나 Idle Timeout은 어떠한 데이터도 주고받지 않는 상태를 기준으로 한다. 만약 HTTP 패킷이 아닌 데이터를 주고받았다면, Keep-Alive Timeout은 갱신되지 않지만 Idle Timeout은 갱신되는 것이다.
Idle Timeout은 HTTP 이외의 데이터도 감지할 수 있어야 하는 값이기 때문에, 서버에만 설정이 가능하다.
서버의 Idle Timeout
서버 입장에서 Idle Timeout은 완전한 Idle 상태의 연결을 감지하고 종료하는 것이다. 이 설정은 Keep-Alive Timeout과 함께 사용될 때도 많은데, Idle Timeout이 더 넓은 범위의 데이터 송수신을 감지하는 만큼 설정값을 동일하거나 더 긴 시간으로 두는 경우가 많다.
마치며
타임아웃과 관련된 설정들은 통신에 참여하는 2가지 주체가 갖는 목적을 명확히 시사한다.
클라이언트는 사용자에게 빠르게, 불편함을 느끼지 않게 서비스를 제공하기 위해 타임아웃을 사용한다. 클라이언트는 서버들의 이상을 빠르게 감지하고 알맞은 조치를 취하여 이를 달성하려고 노력한다.
서버는 여러 클라이언트가 요청을 보내는 상황에서 효율적으로 리소스를 다뤄 부하를 최소화하기 위해 타임아웃을 사용한다. 서버는 리소스가 불필요하게 낭비되는 상황을 막고, 이를 의도한 악의적 클라이언트의 공격을 방어하고자 노력한다.
이러한 특성들로 보아, 타임아웃은 서비스가 안정적이고 효율적으로 운영되기 위해 개발자가 알아야 할 아주 중요한 요소임은 틀림 없다. 또한, 이러한 타임아웃 유형을 잘 숙지하고 있는 것은 장애 대응 시 원인을 파악하는 것에도 많은 도움을 줄 것이라 생각하니 많은 사람들이 타임아웃의 중요성을 알아줬으면 한다.