wsgiref
내가 알기론, Django 내장 서버인 wsgiref는 한 번에 하나의 요청만 처리할 수 있는 싱글 스레드로 동작한다고 알고 있었다.
하지만, locust를 사용해서 한 번에 수십개의 요청을 동시에 받는 상황을 재현해보니, 로그가 순서대로 찍히는 것이 아니라 뒤죽박죽 찍혀있는 것을 확인할 수 있었다.
멀티 스레드로 동작하는건가싶어 thread id를 콘솔에 출력해보았더니, 각 요청마다 다른 thread id가 찍혀있는 것을 확인할 수 있었다.
예제 코드
class ConcurrencyTestView(APIView):
permission_classes = (AllowAny, )
def do_long_task(self):
sum = 0
for i in range(0, 1_000_000):
sum += (i%2)
return sum
def get(self, request):
print(f'Request 1 {threading.currentThread().ident}')
self.do_long_task()
print(f'Request 2 {threading.currentThread().ident}')
self.do_long_task()
print(f'Request 3 {threading.currentThread().ident}')
return Response({'message': 'Complete!'})
아무리 구글링해도 잘 모르겠어서 클로드에게 물어보았더니, Django 3.1 부터 nodemon처럼 Auto reload 기능이 추가되면서 멀티 스레드 환경으로 동작하게 되었다고 한다.
멀티스레드로 동작하게 된 히스토리는 팩트체크가 필요하긴하다. 궁금해서 3.1 릴리즈 문서를 찾아보았더니 따로 Auto-reload 때문에 멀티스레드가 추가되었다고 나오진 않는데..
키워드를 얻었으니, 구글링 해보자
어렵지 않게 여러 글들을 찾을 수 있었다.
결론은 개발환경에서는 따로 설정해주지 않는 이상 멀티 스레드로 동작하도록 되어있다는것을 알게 되었다.
Why django runserver command starts 2 processes? What are they for? And how to distinguish between each in the code?
While building some standalone Django app, which will be running tasks in the background as a separate daemon Thread, I ran into a problem because it seemed as if there are two MainThreads when sta...
stackoverflow.com
--nothreading 옵션
개발 서버에서 멀티스레드로 테스트하길 원치 않는다면 --nothreading 옵션을 커맨드에 추가하면 된다.
python manage.py runserver --nothreading
해당 명령어로 서버를 다시 실행하고, locust로 테스트해보면, 아래와 같이 싱글 스레드로 첫 번째 요청이 끝날때까지 기다렸다가, 다음 요청을 수행하는 것을 확인할 수 있었다.
주의할 점
만약 CPU 작업이 아닌 DB 작업을 추가하고 테스트하면 어떨까?
일반적으로, Django에서는 request 1번에 1개의 connection을 사용한다.
DB는 postgresql을 사용한다고 하면, postgresql의 기본 max_connections는 100이다.
만약 해당 view를 개발환경(멀티스레드)에서 동시에 실행시키면 어떻게 될까?
class ConcurrencyTestView(APIView):
permission_classes = (AllowAny, )
def find_all_users(self):
User = get_user_model()
users = list(User.objects.all())
time.sleep(1)
return users
def get(self, request):
print(f'Request 1 {threading.currentThread().ident}')
self.find_all_users()
print(f'Request 2 {threading.currentThread().ident}')
self.find_all_users()
print(f'Request 3 {threading.currentThread().ident}')
return Response({'message': 'Complete!'})
request가 종료되어야 connection을 해제하므로, 테스트를 위해 time.sleep(1)으로 connection을 가지고 있는 시간을 늘렸다.
--nothreading 옵션이 아닌, 그냥 runserver로 서버를 실행시키고, 부하 테스트는 최대 유저 수 200, Ramp up은 100으로 설정해주었다.
실행하자마자, django.db.utils.OperationalError: connection to server at "localhost" (::1), port 5432 failed: FATAL: sorry, too many clients already 에러가 발생한것을 확인할 수 있었다.
따라서, 멀티 스레드 또는 멀티 프로세스 환경에서 connection관리에 주의해야한다. Django에는 connection을 CONN_MAX_AGE 설정으로 정해진 시간동안 connection을 계속 유지하는 방법이 있다.
참고
https://docs.djangoproject.com/en/dev/ref/django-admin/#cmdoption-runserver-nothreading
django-admin and manage.py | Django documentation
The web framework for perfectionists with deadlines.
docs.djangoproject.com
https://americanopeople.tistory.com/260
(Django) DB Connection을 관리하는 방법
Connection 재사용 ( Persistent connections ) Django는 데이터베이스에 쿼리를 처음 날리기 전에 Connection을 맺는다. 그리고 커넥션을 계속 열어뒀다가, 다음 요청이오면 이걸 재사용한다. Request가 날라올
americanopeople.tistory.com
'Django,Python' 카테고리의 다른 글
[Django] 마이그레이션 파일에 초기 데이터 넣기 (1) | 2024.07.23 |
---|---|
[Django] shell_plus로 개발 생산성 높이기 (0) | 2024.07.07 |
[Django] Django에서는 HTTP 요청을 어떻게 처리할까? (1) | 2024.06.21 |
[Django] 동시성 고려하기(2) - optimistic lock 적용하기 (0) | 2024.06.10 |
[Django] 동시성 고려하기(1) - F 객체 사용하기 (0) | 2024.05.12 |