수학과의 좌충우돌 프로그래밍

[Django] Throttling 본문

웹프로그래밍/DRF

[Django] Throttling

ssung.k 2020. 1. 8. 21:47

Throttle 이란 특정 조건 하에 최대 호출 회수를 결정하는 클래스 입니다.

여러 이유로 지정 기간 내에 호출 횟수를 제한하는 것이죠.

 

Rate

rate 는 지정 기간 내의 최대 호출 횟수를 말합니다.

표기하는 방법은 {숫자}/{간격} 다음과 같이 표기합니다.

여기서 숫자는 지정 간격 내의 최대 요청 제한 횟수를 말하고, 간격은 횟수를 초기화하는 시간을 말합니다.

이 때 간격은 문자의 맨 앞글자만을 참조합니다. 따라서 dday 는 똑같이 동작합니다.

사용할 수 있는 간격 키워드로는 다음과 같습니다.

  • s : 초
  • m : 분
  • h : 시
  • d : 일
# ex

"10/s" # 초당 10회
"100000/day" # 하루당 10만회

 

Rates 제한 메커니즘

django 에서 제공하는 Throttle 들은 모두 SingleRateThrottle 를 상속받습니다.

그리고 SingleRateThrottle 는 아래의 매커니즘으로 제한을 유지합니다.

  1. SingleRateThrottle에서는 요청한 시간의 timestamp를 list로 유지

  2. 매 요청 시마다

    2-1 cache 에서 timestamp list를 가져옵니다.

    2-2 체크범위 밖의 timestamp값은 모두 버립니다.

    2-3 timestamp list의 크기가 허용범위보다 클 경우, 요청을 거부합니다.

    2-4 timestamp list의 크기가 허용범위보다 작을 경우, 현재 timestamptimestamp list에 추가하고, cache에 다시 저장합니다.

 

Django Throttle

django 에서 기본 제공하는 Throttle 은 다음과 같습니다.

아래서 나오는 scopeThrottle 에 대한 alias 입니다.

  • AnonRateThrottle

    • 인증요청에는 제한을 두지 않고 비인증요청에는 IP 별로 횟수 제한
    • Throttle 클래스 별로 scope 1개만 지정 가능
    • default scope : anon
  • UserRateThrottle

    • 인증요청에는 유저 별로 횟수를 제한하고, 비인증요청에는 IP 별로 횟수 제한
    • Throttle 클래스 별로 scope 1개만 지정 가능
    • default scope : user
  • ScopedRateThrottle

    • 인증요청에는 유저 별로 횟수를 제한하고, 비인증 요청에는 IP 별로 횟수 제한
    • 여러 APIView내 throttle_scope값을 읽어들어 APIView별로 다른 Scope을 적용

 

Throttle 에 대한 default settings 은 아래와 같습니다.

아무것도 지정이 되어있지 않습니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_THROTTLE_RATES': {
        'anon': None,
        'user': None,
    },
}

 

Throttle TEST

settings 에서 default 값을 바꿔 Throttle 을 설정해보고 테스트 해보겠습니다.

다음과 같이 settings 에 추가하면 모든 APIView 에 대해서 최대 호출 횟수가 제한됩니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'user': '3/day',
    },
}

 

3/day 하루에 3번까지만 허용했으므로 네 번째 요청에 대해서는 거부 됩니다.

아래는 네 번째 요청에 대한 응답입니다.

HTTP/1.1 429 Too Many Requests
{
    "detail": "Request was throttled. Expected available in 86396 seconds."
}

 

물론 각 APIView 별로 개별 설정도 가능합니다.

throttle_classes 에 해당하는 throttle 를 지정해줍니다.

# views.py

from rest_framework.throttling import UserRateThrottle

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

 

클라이언트 IP

Throttle 마다 다르지만 비인증요청에 대해서는 IP 를 기준으로 카운트하게 됩니다.

이 때 REMOTE_ADDR WSGI 변수값을 참조하게 되면 문제가 발생합니다.

이는 로드 밸랜서를 통할 경우 여러 유저들의 REMOTE_ADDR 의 값이 동일해지고, 이는 여러 유저가 같은 timestamp 를 가지게 되기 때문입니다.

그렇기 때문에 X-Forwarded-For 헤더값과 REMOTE_ADDR WSGI 변수값을 참조해서, 클라이언트 IP를 확정합니다.

X-Forwarded-For 헤더값이 REMOTE_ADDR 값에 우선합니다.

로드 밸런서란 ?

하나의 인터넷 서비스가 발생하는 트래픽이 많을 때 여러 대의 서버가 분산처리하여 서버의 로드율 증가, 부하량, 속도저하 등을 고려하여 적절히 분산처리하여 해결해주는 서비스

xff = request.META.get('HTTP_X_FORWARDED_FOR')
remote_addr = request.META.get('REMOTE_ADDR')
# 생략
if xff:
    client_ip = ''.join(xff.split()) 
else:
    client_ip = remote_addr

 

ScopedRateThrottle 활용

API 마다 다른 Rate 적용를 적용해보도록 하겠습니다.

ScopedRateThrottle 사용하지 않는 경우

settings 에서는 rate 의 키값을 설정해줍니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_THROTTLE_RATES': {
        'contact': '1000/day',
        'upload': '20/day',
    },
}

 

새로 throttles.py 파일을 만들어 throttle 를 관리해줍니다.

새로 정의한 Throttle 클래스의 scope 를 설정해줍니다.

# throttles.py

class CotactRateThrottle(UserRateThrottle):
    scope = 'contact'

class UploadRateThrottle(UserRateThrottle):
    scope = 'upload'

 

views 에서는 이를 설정해주면 됩니다.

# views.py

class ContactListView(APIView):
    throttle_classes = [CotactRateThrottle]

class ContactDetailView(APIView):
    throttle_classes = [ContactRateThrottle]

class UploadView(APIView):
    throttle_classes = [UploadRateThrottle]

 

ScopedRateThrottle 사용하는 경우

하지만 throttles 가 많이 필요하다면 코드가 길어지게 됩니다.

이를 위해 ScopedRateThrottle 를 사용하면 코드를 간결하게 할 수 있습니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'contact': '1000/day',
        'upload': '20/day',
    },
}

 

# views.py

class ContactListView(APIView):
    throttle_scope = 'contact'

class ContactDetailView(APIView):
    throttle_scope = 'contact'

class UploadView(APIView):
    throttle_scope = 'upload'

 

Cache

매 요청시마다 cache 에서 timestamp list 값을 get/set 하므로 cache의 성능이 굉장히 중요합니다.

SimpleRateThrottle에는 다음 코드와 같이 장고 디폴트 cache를 쓰도록 설정되어있습니다.

from django.core.cache import cache as default_cache

class SimpleRateThrottle(BaseThrottle):
    cache = default_cache

 

django 프로젝트는 디폴트로 로컬 메모리 cache를 사용하고 있어 서버가 재시작되면 캐시가 모두 초기화됩니다. 이 외에도 여러 cache가 있습니다.

  • Memcached 서버 지원

    • django.core.cache.backends.memcached.MemcachedCache
    • django.core.cache.backends.memcached.PyLibMCCache
  • 데이터베이스 캐시

    • django.core.cache.backends.db.DataabseCache
  • 파일 시스템 캐시

    • django.core.cache.backends.filebased.FileBasedCache
  • 로컬 메모리 캐시

    • django.core.cache.backends.locmem.LocMemCache
Comments