LostCatBox

DRF 기본편 12

Word count: 945Reading time: 5 min
2020/01/24 Share

EP 12. PostAPIView 차근차근 응답시간 줄여보기

관련 문서

원문 : Web API performance: profiling Django REST framework 장고 공식문서

  • Performance and optimization
  • Database access optimization

왜 최적화를 해야하나요?

  • 보다빠르게동작하는프로그램을위해!!!

    • 보다낮은CPU타임
    • 보다낮은메모리소모
  • 개발비용이가장큰리소스입니다.=>개발시간+인건비

  • 최적화를통해성능은높아지지만,유지관리성이낮아질수도있습니다.=>가성비를 체크해보세요.

  • 한영역의개선이다른부분을희생시킬수있습니다.

    ex) CPU 연산을 아꼈는데, 메모리 소모가 늘었다.

시작하기에 앞서

Throttle와 Pagination은 꺼주시고, 현재, DB Record갯수가 적기에 … 뻥튀기

1
2
3
4
5
6
7
8
from ep08.models import Post

post = Post.objects.first()

for i in range(100):
post.id = None # Django Model은 id=None일 경우, CREATE를 수행합니다.

post.save()

주의: 실제 서비스에서는 id=None과 같은 코드는 절대 쓰지 마세요.

측정 Metric

다음 코드를 통해, 요청 처리시의 각 부분에 대한 시간을 측정해보겠습니다.

  • Database Lookup (db_time) : 데이터베이스 Fetch 수행시간(DB에서 가져오는시간)
  • Serialization (serializer_time) : Serializer 직렬화 수행시간
  • API View (api_view_time) : APIView 수행시간(인증관련처리, 페이징처리등등)
  • Response rendering (render_time) : Response 렌더링 수행시간
  • Django request/response : request/response 수행시간

스크린샷 2020-01-24 오후 2.45.00

total시간은 요청이 요청되고 응답이 다생성되고 닫기까지의 시간

각자 시간이 계산되서 적힘.

수행시간 비교 (단위:)

스크린샷 2020-01-24 오후 2.45.34

(미들웨어는 웹페이지를 위한 미들웨어들을 제거, 뷰를 부를때는 미들웨어가반드시 포함되므로 제거할수록 빨라짐)(HttpResponse를 상속받은 Response를 쓰지않고 원본인 HttpResponse 씀)

측정을 위한 코드 코드 측정코드

  • 코드1/3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from rest_framework.response import Response

class PostViewSet(ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer

def dispatch(self, request, *args, **kwargs):
global cbv
cbv = self
dispatch_start = time.time()
response = super().dispatch(request, *args, **kwargs)

render_start = time.time()
response.render()
self.render_time = time.time() - render_start

self.dispatch_time = time.time() - dispatch_start
self.api_view_time = self.dispatch_time - (self.render_time + self.serializer_time + self.db_time)

return response
  • 코드 2/3
1
2
3
4
5
6
7
8
9
10
11
def list(self, request, *args, **kwargs): 
db_start = time.time()
post_list = list(self.queryset) #원래 queryset은 lazy 작동하지만 list등으로 변환을 줘서 즉시 DB Fetch를 이루워지게함 #실제 프로젝트에서는 lazy한게좋다
self.db_time = time.time() - db_start

serializer_start = time.time()
serializer = self.get_serializer(self.queryset, many=True)
data = serializer.data
self.serializer_time = time.time() - serializer_start

return Response(data)
  • 코드3/3

    주의 : 아래코드는 Single Request에서만 동작합니다. 동시 요청에서는 제대로

    동작하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.core.signals import request_started, request_finished

def started_fn(sender, **kwargs): global started
started = time.time()

def finished_fn(sender, **kwargs):
request_response_time = (time.time() - started) - cbv.dispatch_time

total = cbv.db_time + cbv.serializer_time + cbv.api_view_time + cbv.render_time + request_response_time

print('Database Lookup - db_time : {:.6f}s, {:>4.1f}%'.format(cbv.db_time, 100*(cbv.db_time/total)))
print('Serialization - serializer_time : {:.6f}s, {:>4.1f}%'.format(cbv.serializer_time, 100*(cbv.serializer_time/total)))
print('API View -api_view_time. : {:.6f}s, {:>4.1f}%'.format(cbv.api_view_time, 100*(cbv.api_view_time/total)))
print('Response rendering - render_time : {:.6f}s,{:>4.1f}%'.format(cbv.render_time, 100*(cbv.render_time/total)))
print('Django request/response : {:.6f}s,{:>4.1f}%'.format(request_response_time, 100*(request_response_time/total)))






request_started.connect(started_fn) # 요청 처리 시작
request_finished.connect(finished_fn) # 요청 처리 끝

기본 코드, 수행

“GET /ep08/post/?format=json HTTP/1.1” 200 6497

스크린샷 2020-01-24 오후 3.05.28

=> DB Lookup 및 DRF Serializer의 시간비율이 가장 높네요.

Serializer를 제거해봅시다.

하지만 개발생산성이있으므로, Serializer를 쓰더라도 적절한 캐싱을 통해서 성능뽑기가능

QuerySet.values(필드명)를 통해, 원하는 필드만 가져오기

1
2
3
4
5
6
7
8
9
10
11
12
13
def list(self, request, *args, **kwargs):
db_start = time.time()
# post_list = list(self.queryset)
data = self.queryset.values('author__username', 'message') #DB에서그냥 바로 목록가져오게함
self.db_time = time.time() - db_start

# serializer_start = time.time()
# serializer = self.get_serializer(self.queryset, many=True)
# data = serializer.data
# self.serializer_time = time.time() - serializer_start
self.serializer_time = 0

return Response(data))

수행결과

“GET /ep08/post/?format=json HTTP/1.1” 200 6598

스크린샷 2020-01-24 오후 3.06.54

=> 해당 부분이 0.8초에서 0.00019초로 줄었네요.

Tip: 하지만, Serializer으로 얻는 막대한 개발생산성이 있으며, Serializer를 쓰더라도 적절한 캐싱을 통해 극복할 수 있습니다.

DB/Serializer 대신에 캐싱

데이터가 변경되지 않는다면, 캐싱을 통해 성능을 높일 수 있습니다.

cache는 django에서 기본설정으로 django.conf.global_settings.py에서 cache를 보면 locmemcache인 것을 알수있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.core.cache import cache  #cache 접근

# 중략

def list(self, request, *args, **kwargs):
db_start = time.time()

data = cache.get('post_list_data') #값이 있다면 가져오고 아니면 None반환됨

if data is None: #None이라면
data = self.queryset.values('author__username', 'message') #data를 DB에서 가져오고
cache.set('post_list_data', data, 60) #그 값을 cache에다가 넣음

self.db_time = time.time() - db_start

self.serializer_time = 0

return Response(data)

수행결과

스크린샷 2020-01-24 오후 3.08.59

Tip: 코드 최적화가 먼저입니다. 캐시는 적당히. 무분별한 캐시는 마약과도 같습니다.

APIView에 필요한 설정만 넣기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from rest_framework.negotiation import BaseContentNegotiation
from rest_framework.renderers import JSONRenderer

class IgnoreClientContentNegotiation(BaseContentNegotiation):
def select_parser(self, request, parsers):
"Select the first parser in the `.parser_classes` list."
return parsers[0]

def select_renderer(self, request, renderers, format_suffix):
"Select the first renderer in the `.renderer_classes` list."
return (renderers[0], renderers[0].media_type)

class PostViewSet(ModelViewSet): #디폴트값들 없앰, 최소한으로
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = []
authentication_classes = []
renderer_classes = [JSONRenderer]
content_negotiation_class = IgnoreClientContentNegotiation

수행결과

스크린샷 2020-01-24 오후 3.11.09

미들웨어 제거하기

본 프로젝트가 API 기능만 할 경우, 장고 웹페이지를 위한 기능을 꺼볼 수 있겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 프로젝트/settings.py
INSTALLED_APPS = [
# 'debug_toolbar',
]

MIDDLEWARE = [
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
# 'django.middleware.security.SecurityMiddleware',
# 'django.contrib.sessions.middleware.SessionMiddleware',
# 'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
# 'django.contrib.auth.middleware.AuthenticationMiddleware',
# 'django.contrib.messages.middleware.MessageMiddleware',
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

수행결과

스크린샷 2020-01-24 오후 3.12.16

Django 기본 HttpResponse 쓰기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
from django.http import HttpResponse

def list(self, request, *args, **kwargs):
db_start = time.time()
data = cache.get('post_list_data')
if data is None:
data = list(self.queryset.values('author__username', 'message')) # QuerySet은 JSON 직렬화 불가, 따라서 list로 바꿔줌>>이제 json직렬화가능해짐
cache.set('post_list_data', data, 60)


self.db_time = time.time() - db_start

self.serializer_time = 0

#json으로 직렬화해주는것이 가능한 data타입을 줘야함
return HttpResponse(json.dumps(data), content_type='application/json; charset=utf-8')

수행 결과

스크린샷 2020-01-24 오후 3.15.39

정리

  • 필요에따라Serializer/Response를쓰지않고직접처리

    • 생산성을통한성능의희생
  • 조회요청의경우,적절한캐싱은 성능을높여줍니다.

    • 주의) 로직으로캐싱된내용을적절히만료시켜야만합니다. (예를 들면 만약 지워지거나하면 다시캐싱해줘야함)
  • 어떤설정이적용되는지정확히알고,필요한설정만적용하기.

    • APIView 설정 최소화
    • 미들웨어제거
  • 제일 중요한것은 DB query를 필요한 만큼 요청하고 생성하는지만 체크하는 것(성능폭풍상승)

CATALOG
  1. 1. EP 12. PostAPIView 차근차근 응답시간 줄여보기
    1. 1.1. 관련 문서
    2. 1.2. 왜 최적화를 해야하나요?
    3. 1.3. 시작하기에 앞서
    4. 1.4. 측정 Metric
    5. 1.5. 수행시간 비교 (단위:초)
    6. 1.6. 측정을 위한 코드 코드 측정코드
    7. 1.7. 기본 코드, 수행
    8. 1.8. Serializer를 제거해봅시다.
      1. 1.8.1. 수행결과
    9. 1.9. DB/Serializer 대신에 캐싱
      1. 1.9.1. 수행결과
    10. 1.10. APIView에 필요한 설정만 넣기
      1. 1.10.1. 수행결과
    11. 1.11. 미들웨어 제거하기
      1. 1.11.1. 수행결과
    12. 1.12. Django 기본 HttpResponse 쓰기
      1. 1.12.1. 수행 결과
    13. 1.13. 정리