Django 삽질기 쿼리 지연?
Web/Django

Django 삽질기 쿼리 지연?

뉴비뉴 2020. 6. 15.

안녕하세요.

 

Django Views 에서 쿼리문이 응답에 끼치는 영향에 대한 질문입니다!

 

3만개의 uuid를 가지고 있는 리스트타입의 build_uuids 라는 변수가 있습니다.

Ex) build_uuids = [_, _, _, _, . . .]

 

A테이블에 sample_uuids 라는 컬럼에는 5만개의 is_build = False 인 상태입니다.

A.objects.update(is_build=False) # 1초

print(‘#1’)

A.objects.filter(sample_uuids__in=build_uuids).update(is_build=True) # 2초

print(‘#2’)

조건문을 최소화 하기 위해서 위와 같은 쿼리문을 작성하였고, Django Shell 에서 쿼리를 돌렸을 때

짧은 시간안에 업데이트가 되는 것을 확인했습니다. (장고 쉘에서는 request, response가 없음)

 

하지만 이 쿼리를 views 의 엔드포인트에 넣고 요청을 하면 응답이 2~3분이라는 시간이 걸려 응답이 오는 상황입니다.

 

Profiling 을 통해보니 middleware들이 처리하는 속도가 전체적으로 시간이 증가한 것을 확인해봤습니다.

 

비동기/스레드를 이용하여 쿼리를 돌리면 빠른 응답이 나오나 실서버에 적용하기는 어려운 상황입니다.

 

시도

1. 쿼리문을 제거하면 빠른 속도로 응답이 옵니다.

2. 쿼리문만 쓰레드로 돌렸을 시에 빠른 응답이 옵니다.

3. Runserver 가 아닌 gunicorn 으로 돌려도 응답속도에 차이는 없었습니다.

 

질문

1. 본문의 쿼리문은 동기로 돌아가지 않나요? #1이 찍혔을 때 sleep을 주어 장고쉘을 켜서 확인해봤는데 모든 값이 false로 되어있어 동기로 돌아간다 판단했는데
해당 쿼리문이 응답에 영향을 끼치는게 있을까요?

2. 쿼리문은 금방 도는데 response가 엄청 느리게 오는 상황인데 내부적으로 어떤 처리를 하는건가요? 미들웨어를 돌고 응답이 나가는데 쿼리문의 데이터와 이 응답이 연관이 있을까요?

3. 미들웨어들이 전체적으로 느려진 이유가 뭘까요?

 

감사합니다 : )

 

라는 글로 페이스북 Django 그룹에 글을 게시했고, 아직 해답이 오고 있지 않음.

 

쿼리는 몇초 안에 끝나는데 응답이 2분이나 걸리는 이유가뭘까?

내가 생각하는 가설

 

#1 가설

30,000개의 uuid 텍스트를 한번에 처리를 못해서 쪼개서 던지는 것 같다. DB도 패킷수신 크기가 한번에 받을 수 있는게 정해져 있다.

메모리에서 텍스트를 들고 잇다 던져야 하니 작업이 안끝남 보통 DB 패킷수신 크키는 500, 1000개가 기본이다.

 

#2 가설

그냥 느리다. 30,000개를 주에 한번씩 돌려야되는 상황을 개선할 수 있다면 개선할 수 있는게 좋다.

 

#3 가설

비동기, 스레드로 돌리면 응답속도가 빠르다. 이것은 당연한 소리지.. 혹시몰라 gunicorn 으로 실행해보았는데도 느리다..뭘까

 

 

[문제 확인]

확인해보니 쿼리가 동기로 돌아가지만 지연(Lazy)이 있는걸로 확인되었다.

 

성능저하의 원인(N + 1 Problem)

(레퍼런스의 블로그 내용을 발췌했습니다.)

Django REST Framework(이하 DRF)를 사용하면 REST API를 손쉽게 만들 수 있습니다.

 

하지만 개발자가 감안해야 할 부분이 있는데 바로 성능입니다.

 

많은 것을 제공해준다는 것은 내부적으로 처리가 많다는 뜻이고, 이는 때때로 성능저하를 일으킬 수 있다는 것을 의미합니다.

 

ORM 에서 성능 이슈가 발생하면 가장 흔한 원인으로 N + 1 Problem이 언급됩니다.

 

쿼리 한 번으로 N 건의 데이터를 가져왔는데 원하는 데이터를 얻기 위해 이 N 건의 데이터를 데이터 수 만큼 반복해서 2차적으로 쿼리를 수행하는 문제입니다.

 

Django REST Framework 

 

DRF 에서 Serializer는 핵심적인 역할을 합니다.

 

데이터베이스에서 가져온 데이터 Model들을 사용자가 원하는 형식으로 가공해서 Response를 보낼 수 있게 해줍니다.

거기에 Filtering, Field-level validation, Object-level validation 등 편리하고 다양한 기능들을 제공합니다.

 

하지만 이 Serializer가 중첩된 경우 성능 이슈가 발생하기 쉽습니다.

 

Foreign Key 제약이 걸려있는 데이터를 가져오기 위해서 Serializer 안에 또 다른 Serializer를 선언하는 이른바 Nested Serializer를 사용하는 것이 성능저하의 원인이라고 볼 수 있습니다.

 

이 Nested Serializer를 사용했을 때 성능저하가 일어나는 이유는

 

Django ORM 도 다른 ORM과 마찬가지로 Lazy-Loading 방식을 사용합니다.

 

Lazy-Loading 방식을 사용하면 ORM 에서 명령을 실행할 때 마다 데이터베이스에서 데이터를 가져오는 것이 아니라 모든 명령 처리가 끝나고 실제로 데이터를 불러와야 할 시점이 왔을 때 데이터베이스에 쿼리를 실행하는 방식을 의미합니다.

queryset = User.objects.all()
queryset = queryset.filter(user_status='normal')    # 데이터베이스에서 데이터를 아직 가져오지 않음
queryset = queryset.order_by('datetime_signup')     # 여전히 데이터베이스에 쿼리가 실행되지 않음
queryset = self.paginate_queryset(queryset)         # 이제 데이터를 가져와야 하기 때문에 데이터베이스에 쿼리를 날림

출처: https://show-me-the-money.tistory.com/48 [개발자, Trend를 파헤치다.]

 

[해결]

정말 어이없는 문제였다..

쿼리를 금방 끝나는데 리스폰스가 늦게나오고, 리스폰스가 늦게나오니 문제가 있는 전체코드를 주석하고,

일부 코드만 주석을 해제하고 반복하다 쿼리문만 나오면 지연이 발생하길래 당연히 위 문제 중 하나 일 것 이라고 판단했다.

 

문제를 해결하게 된 것은..Pycharm Debug 툴을 사용하면서 미들웨어를 계속 돌고 있는데 이상한 요청이 보이길래

뭐지..? 하고 보니 익숙한 이름....debug_toolbar 가 보였다.. 아차...디버그 툴바에서 요청이나 SQL을 볼 수 있는 기능이 있다.

그러엄...?!! settings 의 debug를 False로 변경하니 디버깅 툴바가 작동이 안되면서 그렇게 오래걸리던 코드가 순식간에 끝나버렸다.

 

이 문제를 해결하면서 미들웨어, 장고 ORM의 동작원리, 디버깅 하는 방법, 디버깅 툴 사용, 쿼리를 좀 더 효율적으로 짜기 위한 노력, 스레드, 비동기, DB 패킷 수신 크기... 등등

덕분에 시간은 오래걸렸지만 많은 지식을 습득할 수 있었다.

 

이 디버깅을 도와주신 팀장님과 동료분께 감사를 표한다. 🙏 

(빠이티티티이이이잉~)

 

Reference

- https://www.slideshare.net/EunhyangKim2/ss-118560530

- https://show-me-the-money.tistory.com/48

댓글

💲 추천 글