오늘 배운 것

오늘은 멘토님과의 멘토링에서 어제 Locust로 API 서버 부하테스트를 했다는 부분을 말씀드리면서 피드백을 받았다. 당시 RPS(request per second)가 30-40 정도 나오고 있어서 괜찮은지 여쭤봤더니 멘토님께서는 1000 이상은 나오는 것이 안정적이라고 피드백을 주셨다! 그래서 왜 이렇게 RPS가 낮은지를 생각하고 있었는데, 지금 서버는 단일 서버로 돌아가고 있기 때문이었다. 따라서 uvicorn을 사용해서 서버를 여러 대 띄워야 RPS를 늘리고 서버의 부하를 줄일 수 있다고 하셨다. 

 

그래서 장고에서 uvicorn을 사용하는 공식문서를 찾아보았다. uvicorn은 pip로 쉽게 설치할 수 있는 라이브러리였다. uvicorn을 사용하려면 gunicorn도 같이 필요하다고 해서 같이 설치했다. 

pip install uvicorn gunicorn

 

gunicorn은 뭐고 왜 필요하지? 라는 의문이 들었는데 공식문서를 보니 바로 해결되었다. gunicorn은 unix 운영체제를 위한 python wsgi http 서버라고 한다. 그럼 WSGI가 뭐였지? 라는 의문이 또 드는데, 문서를 찾아보니 WSGI(Web Server Gateway Interface)는 웹 서버와 웹 어플리케이션이 어떻게 통신하고, 여러 개의 웹 어플리케이션이 하나의 요청을 처리하기 위해서 체인처럼 연결될 수 있는지를 정의한 인터페이스였다. 이제 gunicorn이 뭔지는 알았다. 

 

그러면 uvicorn은 뭐고 왜 필요하지? 라는 의문이 든다. uvicorn은 파이썬 웹 서버에서 사용하기 위해 ASGI 인터페이스를 구현한 것이다. ASGI는 뭘까? 문서에는 ASGI(Asynchronous Server Gateway Interface)는 WSGI를 상속받은 또 다른 인터페이스이며, 비동기 요청을 처리하는 웹 서버와 웹 어플리케이션이 어떻게 통신하고, 비동기 요청을 처리하는 여러 개의 웹 어플리케이션들이 하나의 요청을 처리하기 위해서 체인처럼 연결될 수 있는지를 정의한 인터페이스였다. Implementation을 보니 uvicorn 말고도 daphne, granian, hypercorn 등 여러 신기한 라이브러리들이 있었다. 

 

그런데 이렇게만 봐도 그래서 이 라이브러리들을 사용하는 게 서버를 여러 대로 늘리는 것과 무슨 상관이 있는지 사실 잘 와닿지 않았다. 

 

알고보니 uvicorn은 별다른 설정이 없으면 단일 프로세스로 실행되는 ASGI 서버이다. 성능을 확장하는 방법은 여러 개의 uvicorn 프로세스(인스턴스)를 실행하여 각 프로세스가 독립적으로 요청을 처리하게 하는 것이다. 그러면 물론 당연히 하나의 서버에 실행 가능한 프로세스 수는 제한이 있겠지만, 적어도 늘어난 프로세스만큼 요청을 병렬적으로 처리하게 되어 서버의 성능이 향상되겠다고 이해했다. 

 

그러면 gunicorn은 왜 필요한지 의아할 수도 있지만, 이렇게 여러 개의 uvicorn 프로세스를 통합해서 관리하도록 도와주는 것이 gunicorn이다. gunicorn에서는 워커(worker)라는 개념이 있다. 워커는 uvicorn 서버의 독립적인 인스턴스이다. 이쯤 되면 조금 헷갈린다. 아까는 여러 개의 uvicorn 프로세스를 통합해서 관리한다면서, 지금 얘기를 들으면 여러 개의 uvicorn 서버의 독립적인 인스턴스를 관리한다는 건가? 헷갈린다. 

요점만 정리하자면 여기서 말하는 '워커'는 uvicorn 인스턴스가 맞았다. 그리고 uvicorn을 단독 실행할 때와 gunicorn과 같이 실행할 때의 차이점도 알 수 있었다. uvicorn을 단독 실행할 때도 기본적인 워커 관리 기능을 제공하지만 그 기능이 다소 제한적이다. 반면 gunicorn과 uvicorn을 같이 사용하면 워커 관리의 제어권을 gunicorn이 갖는 대신에 더 확장된 기능을 제공한다고 한다. 예를 들면 gunicorn과 같이 사용할 때는 워커를 모니터링하거나, 필요에 따라 재시작하는 등의 부가 기능이 있다고 한다. 

 

gunicorn, uvicorn을 사용해서는 다음 명령어로 서버를 띄울 수 있다고 한다. 다만 우리 프로젝트에서는 개발, 프로덕션, 테스트의 세 가지 환경을 사용하고 있었기에 관련된 값도 넣어줘야 했다. 

# 4개의 worker를 사용하는 경우
gunicorn onestep_be.asgi:application -w 4 -k uvicorn.workers.UvicornWorker

 

그리고 해당 문서를 보면 gunicorn에서 몇 개의 uvicorn 워커를 갖는 것이 적당한가? 라는 물음에 대한 답이 나와있다. 기본적으로 4개 ~ 12개 사이의 uvicorn 워커로 초당 수백 개에서 수천 개의 요청을 처리할 수 있다고 하며, 2*(core의 개수)+1개의 uvicorn 워커를 사용할 것을 권장하고 있었다. 왜 저런 식에 근거하였는지도 궁금하다. 

 

프로젝트는 현재 ECS fargate 옵션으로 서버가 실행되고 있었는데, 몇 개의 uvicorn 워커가 필요한지를 알기 위해서는 서버에서 몇 개의 cpu core를 사용하고 있는지를 알아봐야 했다. 

 

공식문서에서는 ECS fargate에서 cpu core의 수는 vCPU와 직접적으로 연관된다고 한다(왜인지는 모른다). 그리고 vCPU(virtualized CPU)의 수는 태스크 정의에서 정의된 "cpu"의 값에 따라 달라진다고 한다. ECS의 태스크 정의에서 확인하니 개발 서버의 경우 1vCPU(1024)를 사용하고 있었다. 

 

그렇다면 공식으로 계산해보면 필요한 uvicorn 워커의 개수는 3개이다. 그러나 공식문서에서는 4개-12개 사이의 값을 권장하는 듯 보여서 4개로 설정해 주었다. 

 

그리고 해당 값을 Dockerfile에 커맨드로 추가해 주었다. 

# production 환경의 경우
CMD ["sh", "-c", "python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn onestep_be.asgi:application -w 4 -k uvicorn.workers.UvicornWorker"]

 

 궁금한 점

1. 가상화된 CPU(vCPU) 개념은 무엇일까

2. 테스트 서버도 로드밸런서가 필요할까?

+ Recent posts