WaffleStudio 리크루팅 서버 개발 후기

iwin1203·2022년 10월 15일
0

백엔드&DevOps

목록 보기
3/9

(2022년 08월에 작성한 글)

유저들의 접속이 많은 서버를 개발(유지/보수)한 것은 이번이 처음이라, 간략히 후기를 남겨두고자 한다.


나름의 ..


아키텍쳐 다이어그램. 왜 모든게 EC2 안에 있냐고 묻지 마십시오.


문제의 시작


문제는 작년에 root traversal 공격이 들어온 것으로부터 시작되었다. 이번에는 채점 시 유저 코드를 실행할 때 확실히 해당 코드로 우리 서버에 접근하지 못하게 하는 방책이 필요했다.

작년 담당자분들께서는 도커 컨테이너로 분리하는게 가장 깔끔할 것이라 했고, 나와 친구도 마침 도커를 공부하고 있던 때라 그렇게 하기로 했다.


팀 구성


백엔드 개발자 두 명, (나랑, 아주 유능한 친구 한명) 프론트 두 명으로 구성된 팀이었다.

우리는 다음과 같은 작업을 했다.

  • drf로 리팩토링
  • jwt 토큰 인증
  • 채점 환경을 컨테이너로 분리
  • db 저장 방식 및 구조 변경
  • 비동기 처리를 고려한 nginx, gunicorn, celery 설계 및 배포
  • 부하테스트
  • 문제 출제 및 테케 작성

프론트 분들도 무지 열심히 작업해주셨는데, 내가 프론트를 몰라서.. 그냥 리팩토링으로 퉁치겠다. (죄송합니다!)

  • 리팩토링

꽤 고생함


장고를 오랜만에 사용하는 것이라 nginx와 gunicorn 설정에 애를 먹었다. 이상하게 https 연결도 매끄럽게 진행되지 않았다. 컨테이너 분리도 이전에 딱히 다뤄본 적이 없는 것이라 시간이 적지 않게 들었다.

그러나 무엇보다도 끝까지 우리를 괴롭혔던 문제는 유저 권한 분리였다. 부하테스트와 나름의 코너케이스를 테스트한 후 자신있게 동아리원들에게 확인 부탁드린다고 올렸는데, 올린지 채 30분이 되지 않아 서버가 멎어버렸다. stopword를 사전에 정의해놓고, 또 컨테이너 차원에서 서버 내부 경로에 접근할 수 없도록 권한을 설정하였기에 보안 상 별다른 문제가 없을 것이라 생각했었는데, stopword를 피해 child process를 반복생성하는 방식으로 서버 메모리를 터뜨려버린 것이었다!

이에, 컨테이너 시작 시 실행시킬 수 있는 프로세스 수를 제한한 유저를 생성하고, 채점이 끝나면 유저를 삭제하는 방식으로 스크립트를 작성하였다. 그런데, 아무리 해도 프로세스가 멀쩡히 계속 생성되었다…! ulimit 커맨드를 활용하여 온갖 방법을 다 시도했다. 컨테이너 생성 / 실행 단계에서 각각 option으로 줘보고, config 파일을 만들어서 컨테이너를 실행해보기도 하고, 컨테이너 생성 후 스크립트로 ulimit을 걸기도 해봤다. 도저히 안됐다. 이게 서버가 열리기 고작 하루 전의 일인데, 아주 식은땀이 흘렀다.


급한 불을 끔


그래서 급히 아래 두 가지를 차선책으로 생각해냈다.

  • 1초 (문제 제한 시간)가 지나면 유저의 모든 프로세스(child process포함)를 강제 종료한다.
  • 컨테이너의 리소스를 제한한다.

1초가 이내에는 이론상 무제한으로 프로세스를 생성할 수 있는 구조이기 때문에, 가용 CPU 비율을 크게 제한해서 한 컨테이너가 순간 폭발하더라도 CPU가 죽지는 않도록 구성했다. 그리고 한 컨테이너에서 하나의 채점만 진행될 수 있도록 컨테이너 CPU 사용량에 기반한 나름의 로드밸런싱(ㅋㅋㅋㅋ) 코드를 짰다. 세밀한 코드는 아니지만 나름 잘 분배해주는 것을 확인했다.

또, AWS CloudWatch를 1분 단위로 두어 아주 촘촘히 서버 상황을 트래킹하고자 했다.

일단 내가 임의로 만든 stress test는 무난하게 잘 돌아가는 것 같았다.


바로 터짐


연지 두시간만에 터졌다.

편의상 celery worker를 백그라운드 옵션으로 돌렸는데, 이를 까먹고 이것저것 실험한답시고 몇번씩 재시작을 했더니, 백그라운드에서 몇십개의 worker가 투쟁하고 있는 것이었다!

다행히 이 이슈는 금방 알아차려 바로 해결할 수 있었다.


생각보다 잘 돌아감


사실 자잘하게 테스트케이스나 문제 명세 오류는 좀 있었다. 그래도 거진 10일 정도 되는 기간동안 서버가 터진다던지, db가 날아간다던지 하는 대형 사고는 없었다.

초반 2~3일은 새벽같이 일어나 서버를 리셋했다. 별다른 문제가 발생한 건 아니었지만, 아무래도 채점 과정에서 스크립트를 많이 돌리는 구조다보니 나같은 초보자는 분명 어디선가 메모리 leak를 발생시켰을 것 같았다. 그게 조금씩 쌓이다가 터지면 큰일이니 사전에 미리미리 메모리를 정리해두기 위해서였다. 그렇게 몇일을 다른 백엔드 친구와 일어나서 재시작하다가, 그 뒤로는 안심하고 꿀잠을 잤다.


잘 마무리 됨


그렇게 별 탈 없이 리크루팅은 마무리되었고, (물론 중간에 파이썬 관련 오류로 사과문 한번 크게 올림..ㅎ) 루키들을 성공적으로 뽑을 수 있었다.

10일 정도의 기간에 몇천번의 채점이 이루어졌을 만큼 인기가 있었다. 물론 같이 한 친구가 굉장히 개발 지식이 뛰어났고, 또 레거시 코드의 아이디어도 좋았던 점이 크지만, 그래도 내가 기여한 코드가 그 정도 로드를 무리없이 버텨냈다는게 너무너무 신기하고 뿌듯했다.

오프더레코드지만, 그 친구도 중요한 일이 있는 시기였고 나도 입사한지 갓 일주일 된 신입이었는데, 출근 전과 퇴근 후 계속 요거에 매달렸다는게 무슨 정신이었는지 모르겠다. ㅋㅋㅋㅋ


느낀 점 & 배운 점


왜 기업이 면접에서 ‘사용자가 있는 서비스를 출시 / 운영’ 해본 경험을 그렇게도 중시하는지 알 것 같았다. 서비스를 기획하고, 프론트와 잘 협업하고, 클린 코드를 짜고, 파이써닉한 코드를 고민하고.. 이런것과 실제 사용자들의 리퀘스트를 어떻게 처리할 것인가는 완전히 다른 차원의 태스크였다.

평소에 별 생각없던 비동기 처리와 큐잉을 공부할 수 있었고, 계속 부딪히며 도커도 나름 익힐 수 있었다.

  • 사용량이 폭증할 가능성이 있고, 각 사용자의 리퀘스트에 대한 로드가 큰 서비스를 경험해보았다.
  • gunicorn, celery 등을 통한 비동기 처리와 큐잉을 설계, 구현하였다.
  • 도커와 컨테이너에 익숙해졌다.

0개의 댓글