자칫하면 서버가 숨을 쉬지 않아요!
프로젝트를 수행하면서 클라이언트에서 발생 시킨 이벤트를 기준으로 서버에서 일정 시간이 지난 후 응답을 보내줄 필요가 있었다.
이를 해결할 방법을 찾던 과정에 문득 궁금증이 생겼고, 이런 과정들도 기억해두기 위해 글로 남겼다.
서버 : Spring Boot + tomcat / 클라이언트 : Flutter (모바일 앱)
클라이언트에서 이벤트 접수 시작을 서버에 요청 ⇢ 서버는 일정 시간 동안 여러 클라이언트에서 신청을 받음 ⇢ 시간이 지나면 신청을 종료하고 접수 결과를 당첨자에게 반환
이런 형태로 진행되는 작업이었다. 동시에 하나의 이벤트만 발생한다면 서버에서 간단히 측정할 수 있겠지만 수십, 수백 개의 이벤트가 진행될 수 있게 설계해야 했는데, 그중에 가장 큰 문제는
1. 5초, 10초 등의 시간을 어떻게 측정하는가?
2. 그 시간 동안 HTTP 요청이었다면 응답을 받기 위해 연결을 유지해야 하는가?
이렇게 두 가지의 문제가 있었기 때문에 그 문제를 해결하는 과정을 정리해봤다.
1번을 어떻게 구현해야 하나 생각하다가 우선 생각한 것이 클라이언트에서 시간을 잴까, 서버에서 시간을 잴까였다.
클라이언트에서 측정하는 경우는 서버에서 리소스를 잡아먹지 않기 때문에 효과적이었지만, 클라이언트가 정상적으로 종료 요청을 보내지 않을 때 해당 이벤트가 붕 뜨는 문제가 있었다. 서버에서 측정하는 경우는 해당 시간 동안 어떤 스레드가 시간을 측정하기 위해서 묶여 있는 문제가 있었다.
두 경우 중에서 일단 정상적으로 종료되지 않는 경우를 막기 위해서 서버에서 단순히 스레드를 sleep 시키는 방법을 사용해봤다.
sleep으로 스레드를 잡아 두는 방식은 효과적이지 못할 것 같았지만, 생각해봐도 적절한 방법이 안 보여서 구현을 통해 얼마나 느려지나 체감을 해보려 했다.
우선 실제로 한 번에 이벤트 시작 요청을 5개부터 약 1,000개까지 변화를 주며 테스트 클라이언트에서 보내 모든 응답이 돌아오는 데 얼마나 걸리는지 측정해봤다.
이벤트 수행에 필요한 시간은 6초로 두고, 테스트 했을 때 다음과 같았다.
요청 수가 100개, 혹은 그 이하 정도로 적을 때 충분히 예측한 시간대로 6초에 모든 응답이 돌아왔다.
하지만 그 이상 500개, 1000개의 경우에는 요청 수가 많아질수록 10 ~ 30초까지 더욱더 응답에 많은 시간이 들었다.
왜 그랬을까? 이유를 생각해봤다.
tomcat에서는 스레드를 최대 200개까지 생성하여 풀로 관리를 하며, 동기, Blocking 방식의 서버는 요청 당 1개의 스레드가 작업을 한다.
그러면 tomcat이 수용할 수 있는 스레드를 넘기는 수의 요청이 들어오면?
이전에 사용한 스레드가 가용할 때까지 이후의 요청들은 그대로 대기 상태가 되는 것이다.
따라서 6초가 걸리는 이벤트가 1,000개가 되면 30초까지 늘어지는 경우가 생기는 것이다.
"그러면 서버에서 어떻게 재요...?" 라는 생각이 들었지만, 마땅한 방법이 떠오르지 않았다... 우선은 클라이언트에서 측정하는 방법을 두고 최대한 처리하지 못 하는 이벤트가 없도록 관리하는 방법을 고민하며 2번으로 넘어갔다.
이 경우에 이벤트 시작을 요청한 클라이언트마다 시간을 측정하면 서버에는 별도의 자원을 들이지 않고 종료 시에 응답을 즉시 줄 수 있다는 장점이 있었다.
2번은 어떻게 하면 좋을까?
연결을 유지하는 상태가 어떤 의미인지 생각해보니, 클라이언트와 서버 모두 해당 연결을 유지하기 위해 하나의 스레드가 할당된 것과 똑같은 결과였다. 때문에 이벤트를 시작할 때 해당 클라이언트에서 시작 요청 1번, 종료할 때 요청 1번, 종료 전까지 다른 클라이언트에서 신청 요청을 꾸준히 받는 형태로 바꾸었다.
클라이언트에서 시작한 요청이 끝나지 않으면 어떻게 하지? 라는 문제를 해결해야 했는데, 여기서는 Redis를 활용했다!
이벤트 시작 요청이 들어오면 Redis에 해당 요청에 대한 데이터를 생성하고, Expire를 해당 이벤트 지속 시간보다 조금 길게 두었다.
이벤트 참가 요청이 들어오면 Redis에서 해당 요청 데이터의 TTL을 기준으로 판단하여, 이벤트 진행 시간이 지난 경우라면 참가를 금지
(예를 들어, Expire 10초, 이벤트 지속 5초, TTL(Expire까지 남은 시간)이 5초보다 작다면 신청 불가능)
그리고, 서버에서 정상적으로 종료되지 않은 응답을 처리하지 않고, Redis에서 만료된 데이터를 지워주도록 구성했다.
해당 프로젝트를 수행하면서 Netty TCP 채팅 서버를 담당했지만, 서비스 서버와 같이 연결하여 작업하는 일이 많았기 때문에 tomcat의 동작이나, HTTP 요청에 대한 서버의 처리 과정을 고민해 볼 수 있었다. 막힐 때는 왜 가능한지, 어째서 불가능한지, 구현했을 때 발생하는 주변에 대한 영향을 생각하는 것도 중요하다 :)
'프로젝트' 카테고리의 다른 글
NPM으로 가져온 Slider가 이상하다? (0) | 2023.03.07 |
---|---|
Protocol Buffers를 활용해서 Netty와 Flutter 연결하기 (0) | 2023.02.17 |