Skip to content

작업이 오래걸리는 요청을 어떻게 응답할까?

HKLeeeee edited this page Nov 17, 2023 · 1 revision

AlgoITNi는 코드 실행 요청을 보내는 기능을 어떻게 구현했을까? 코드 실행 요청 기능은 클라이언트가 코드 실행 요청을 보내면 서버에서 그 코드를 실행해 결과를 알려주는 형태이다.


먼저 생각해보기

실행을 요청한 코드가 실행에 오래걸리는 코드이거나,

사용자가 많아서 내가 보낸 요청이 처리되는데 오래걸릴 수도 있다.

이럴 땐 어떻게 클라이언트에 응답을 줄 수 있을까? 생각해 본 방식은 두 가지가 있다.

  1. 하염없이 기다리기

    long polling에 가깝다.

    일단 polling은 주기적으로 요청하여 서버의 상태와 클라이언트 상태를 동기화 하기위해 사용하기 때문에 엄밀히 말하면 Polling은 아니라고 생각한다.

    하지만 요청을 보내면 응답할 내용이 생길 때까지 서버는 대기를 하다가 지정한 시간 내에 응답이 발생하면 클라이언트에게 응답을 보내고 지정한 시간내 응답할 내용이 생기지 않으면 timeout 처리를 하는 것이다.

    status Code는 408을 활용할 수 있을 것이다.

  2. 주기적으로 요청보내기

    서버가 코드 실행 요청을 보내면 클라이언트에 202 응답을 내려준다.

    202는 해당 요청이 accept 되었음을 알려준다.

    클라이언트가 202코드를 응답받으면 다시 결과를 가져오는 요청을 보낸다.

    이 요청에 200 응답을 받을 때 까지 주기적으로 요청을 보낼 수 있을 것이다.

    이 방식은 short Polling과 유사하다고 느꼈다.



다른 서비스 둘러보기

프로그래머스는 어떻게 하고 있을까?

프로그래머스에서 코드 실행 버튼을 누르면 events 요청이 발생하고 202 응답 받는 것을 확인할 수 있었다.

202 응답을 받은 이후에 네트워크 요청이나 응답이 없는데 코드 실행 결과는 계속 실행되고 결과까지 나왔다.

대체 어떻게~

image

한참을 코드를 들여다보는데 도저히 결과를 다시 요청하거나 받는 코드를 찾을 수 없었다.

그러다 문득 떠오른 생각. 혹시 문제 화면 들어올 때 웹소켓??

네트워크 도구를 켠 채로 나갔다 다시 들어오니 웹 소켓 연결을 찾을 수 있었다.

문제를 선택해 처음 문제풀이 화면에 들어올 때 웹소켓에 연결된다.

image

구름은 어떻게 할까?

실행을 눌렀을 때 run 이벤트가 발생하고 socket.io로 서버와 웹소켓을 연결을 통해 데이터를 주고 받는다.

여기도 역시 웹소켓을 사용한다.

image

우리의 서비스에는 어떻게 적용해볼까?

내가 생각한 방식은 결국 polling 계열인데, socket 개념이 등장하기 전에 나온 개념이다.

그렇다보니 두 사이트 모두 소켓을 사용하여 서버에서 실행완료 시 (클라이언트의 요청이 없이도) 응답을 보낸다.

우리 프로젝트의 코드 실행 기능의 흐름은 다음과 같다.

  • 클라이언트가 코드 실행 요청을 보내면 api 서버로 요청이 보내진다.
  • api 서버에서 코드 보안 검사에 통과하면 redis 메시지 큐에 job으로 등록한다.
  • job으로 등록되면 running 서버에서 consume 하여 job을 꺼내수행한다.
  • 결과를 다시 redis에 저장한다.

우리 프로젝트는 서버가 나눠져있다.

이 구조에서 발생하는 문제는 api 서버가 요청한 코드 실행이 완료되었는지 알 수가 없다는 것이다.

api 서버에 실행 완료 여부를 탐색하는 로직이 추가로 필요하다.

  1. pub/sub 구조를 도입해 running 서버에서 코드 실행이 완료되면 publish 하고 그걸 다시 api 서버가 받아 응답하거나
  2. 주기적으로 redis를 탐색해 코드실행 결과가 있는지 탐색해야한다.

1번 시나리오로 간다면 다음과 같이 구현해야한다.

  • 웹소켓이 연결이 된다.
  • 코드 실행 요청이 생겼을 때, 보안 검사에 통과하면 202 실패하면 forbidden 403을 응답하여 일단 http 통신을 완료한다.
  • 그리고 api 서버는 메시지 큐에, running 서버는 그걸 받아 수행하고 publish 한다.
  • api 서버에서는 subscribe로 데이터를 받고 본인이 요청한 job id 목록에 있다면 웹소켓으로 응답을 준다
  • 웹소켓을 닫는다.

(우리 서비스에서 웹소켓 연결/해제 시점은 코드 실행 요청이 있을 때가 더 좋아보인다. 방에 참여한 사람들 중 한 명이 실행 요청을 보내면 그 사람만 웹 소켓연결을 하면 모든 사람이 코드 실행 결과를 공유할 수 있다.)


2번 시나리오로 간다면 주기적으로 redis를 찾는 과정을 잘 설계하는 것이 필요하다.

이때 레디스의 부하는 무시하기로 했다. 레디스 벤치마크 테스트를 해 본 결과 백만 건의 요청도 거뜬했다.


2-1. 하염없이 기다리기

코드 실행 요청이 생겼을 때 보안 검사에 실패하면 403 응답을 주고 통과하면 메시지큐, running 서버를 거쳐 redis에 실행 결과가 쌓인다.

api 서버는 메시지 큐에 job을 push하고 주기적으로 계속 redis를 탐색해서 실행 결과를 찾고, 찾으면 그때 200 상태코드와 함께 실행 결과 응답을 보낸다.

서버는 계속 탐색하고 있어 서버 부하가 크다.


2-2. 먼저 202 응답을 주고, 클라이언트 측에서 주기적으로 요청을 보내기

계속 탐색하는 역할을 클라이언트 측으로 위임한 것이다.

2-1의 경우는 탐색 주기를 적절히 설정하는 것이 어렵고 탐색 주기와 탐색 시도 횟수에 따라 크게 성능이 달라진다.

탐색 역할을 클라이언트 측에 맡김으로써 서버 측에 부하를 줄이고 http 통신을 통한 약간에 딜레이로 시간을 벌 수 있어 두 파라미터에 대한 의존성을 약간 줄일 수 있다는 장점이 있다.


제일 처음 생각했던 것에 이제 웹소켓 옵션이 하나 더해졌다.

2-3. 웹소켓 이용

먼저 202 응답을 준다. 그리고 1번 방식과 거의 동일하다.

하지만 http 요청 핸들러의 수명주기가 끝난 상태라

job id와 웹소켓 id 정보를 어디엔가 저장하고 서버에서 주기적으로 레디스를 탐색하다가 데이터를 찾으면 웹소켓 아이디 정보를 이용해서 실행결과를 보내준다.

서비스가 나눠지니까 이렇게 복잡하다. 이쯤되니 마이크로서비스의 장점을 못 느끼겠다. 하지만 또 잘 생각해보면 api 서버에서 코드를 실행하다가 죽어버리면 이제 큰일이 난다. 로그인 조차도 못하고 모든 서비스가 중지된다. 하지만 api 서버와 running 서버가 분리되어 있어 running 서버에서 코드를 실행하다 문제가 생겨 서버가 다운되더라도 api 서버는 멀쩡하기 때문에 서비스에서 코드 실행 기능이외에 다른 기능들을 이용할 수 있다.


우리들의 고민

  1. 소켓 연결이 이렇게 많아도 되나? 이미 지금 클라이언트에 실시간 통신이 많다.

    → webRTC를 mesh방식으로 구현하여 영상, 화면 stream을 P2P로 실시간으로 받아오기에 실시간 연결이 많음

    4인기준으로 3개의 P2P연결 + 시그널링 서버와의 소켓연결 + 채팅 서버와의 소켓연결이 필요한상태

    여기서 api서버와도 소켓연결이 되는 경우 클라이언트의 부담이 너무 많아지는것은 아닌가?

  2. **short polling 방식으로 구현한다고 해도 클라이언트에 부하가 생기는 것이 아닌가?**

멘토님의 조언

선택한 방법

성능 테스트 결과

AlgoITNi

📕
📕 노션 보러가기
📕 주요 기능

👻 멤버
👻 그라운드 룰
👻 컨벤션
👻 기술 정리
👻 제품 백로그 보러가기

회의록

📄 week1
📄 week2
📄 week3
📄 week4
📄 week5
📄 week6

회고

🎞️ week1
🎞️ week2
🎞️ week3
🎞️ week4
🎞️ week5
🎞️ week6

Clone this wiki locally