상반기 채용 과정, 그리고 회사 입사 후에도 일이 너무 많아서 블로그 포스팅을 아예 작성하지 못했다..😰 노션에 대충 작성해놨던 글들을 다시 정리해보려고 한다.
스프링에서는 스레드 풀을 통해 다수의 요청이 처리가 되는데, 장고는 어떻게 처리해주는지 궁금해서 포스팅해보았다.
장고는 Gunicorn
, 즉 스프링에서 요청을 처리해주는 Tomcat
과 동일한 역할의 웹 서버가 존재한다. WSGI
라고 하며, 파이썬 어플리케이션이 웹서버와 통신하기 위한 인터페이스로 웹서버의 요청을 해석해서 어플리케이션에게 전달한다.
장고는 매 요청마다 프로세스를 생성하는 방식이 아니고, master - slave
워커 프로세스 구조로 돌아가며, 동시에 돌아가는 프로세스와 스레드 개수를 조정할 수 있다.
참고로 WSGI는 동기식 코드만 지원하고, 비동기식 이벤트를 처리해야 한다면 ASGI를 사용해야 한다.
공식 문서에서 나와있듯이, Gunicorn은 초당 수백 또는 수천 건의 요청을 처리하는 데 4~12
개의 작업자 프로세스만 필요하다. 요청을 처리할 때 모든 로드 밸런싱을 제공하기 위해 운영 체제에 의존하며, 일반적으로 처음 시작할 작업자 수로 (2 x 코어 개수) + 1
을 권장하고 있다.
회사에서는 Devops 팀이 따로 있었는데, 아래와 같은 프로세스로 워커 개수를 설정하고 있었다.
1) datadog metric을 참고해서 각 Pod가 초당 몇개의 요청을 처리할 수 있어야 하는지, 그리고 평균 duration
을 계산한다.
현재 분당 400,000개의 요청을 처리 중이라면, pod은 133대이므로
400,000/133/60 = 50
, pod 1대가 초당 50개의 요청을 처리 중이다.
avg duration은 150ms라고 가정해보자.
2) percentile
에 따른 필요한 총 thread 수를 구한다.
p50
- pod 1대가 1초 동안 150ms 요청 50개를 처리하려면 최소한 thread가 7.5개 필요p90
- pod 1대가 1초 동안 400ms 요청 50개를 처리하려면 최소한 thread가 20개 필요p99
- pod 1대가 1초 동안 1500ms 요청 50개를 처리하려면 최소한 thread가 75개 필요추가로 I/O bound요청이 많은지, CPU bound 요청이 많은지 고려하고 I/O bound가 많은 경우 thread를 더 늘리고, worker 개수는 웬만하면 core당 1개로 설정하고 있다.
Q. 아니 근데 파이썬에서는 GIL로 인해 멀티 스레딩의 효과가 없는거 아닌가요?
GIL
은 일종의 뮤텍스로써, 한 프로세스 내에서, 파이썬 인터프리터가 한 시점에 하나의 쓰레드에 의해서만 실행될 수 있게 한다. 왜냐? 파이썬에서는 모든 것이 객체이기 때문에 가비지 컬렉션이 동작했을 때 여러 스레드가 동시에 접근해 객체들의 참조 횟수에 있어서 동시성 이슈가 발생하는 것을 막기 위함이기 때문이다.
원래 멀티 코어라면 멀티 쓰레딩 시에 여러 개의 쓰레드가 여러 코어에서 병렬(Parallel) 실행될 수 있는데, Python에서는 그러한 병렬 실행이 불가능하다는 것뿐이다.
따라서 CPU Bound
, 즉 CPU가 코드에 접근해서 연산을 하고 있는 시간이 많다면 오히려 컨텍스트 스위칭 비용만 높아지므로 성능이 느려지겠지만, I/O Bound
면 충분한 효과를 볼 수 있다. 외부 연산(I/O, Sleep 등)을 하느라 CPU가 아무것도 하지 않고 기다리기만 할 때는 다른 쓰레드로의 문맥 전환을 시도하기 때문이다.
코어 개수 설정 값
# XX-api(cpu bound > i/o bound) - name: DJANGO_SETTINGS_MODULE value: "XX.settings.production" - name: GUNICORN_CMD_ARGS value: "--workers 7 --threads 15 --max-requests-jitter 100 --max-requests 100000 --graceful-timeout 90 --keep-alive 0 --limit-request-field_size 32768 --limit-request-line 0"
# webview-api(cpu bound < i/o bound) - name: DJANGO_SETTINGS_MODULE value: "XX.settings.production" - name: GUNICORN_CMD_ARGS value: "--workers=12 --threads=20 --max-requests-jitter=100 --max-requests 100000 --graceful-timeout 90 --keep-alive 0 --limit-request-field_size 32768 --limit-request-line 0"
장고는 스프링과 달리 DB 커넥션 풀이 없다. (정확히는 기본적으로 제공해주지 않는것이고, 따로 플러그인을 깔아야 한다)
요청 당 커넥션을 무한정으로 들고 있는 방식으로 동작하기 때문에, 따로 타임 아웃이 설정되지 않으면 절대 요청이 끊어지지 않고 계속 재사용 하는 방식이므로 타임 아웃 설정이 매우 중요하다.
https://docs.djangoproject.com/en/4.1/ref/databases/#persistent-connections
그래서 실무에서도 초기에 워커수 x 스레드수 x pod 수
해서 엄청나게 커넥션이 늘어나서 장애가 발생했었고, 이후 pod 하나당 들고 있는 커넥션 수가 하향되었다고 한다.
참고 자료
https://ttu.github.io/servers-handling-requests/
https://blog.hwahae.co.kr/all/tech/5567
https://docs.gunicorn.org/en/stable/faq.html#does-gunicorn-suffer-from-the-thundering-herd-problem