45.限流Throttling及原始碼解析

2022-10-25 21:04:31

什麼是限流?

  1. 限流類似於許可權機制,它也決定是否接受當前請求,用於控制使用者端在某段時間內允許向API發出請求的次數,也就是頻率
  2. 假設有使用者端(比如爬蟲程式)短時間發起大量請求,超過了伺服器能夠處理的能力,將會影響其他使用者的正常使用
  3. 為了保證服務的穩定性,並防止介面受到惡意使用者的攻擊,我們可以對介面進行限流
  4. 又或者可以對未經身份驗證的請求設定存取頻率,對經過身份驗證的請求不限制存取頻率
  5. 限流也不止單指限制存取次數的措施,例如付費資料服務的特點存取次數

限流的應用場景

  1. 區分使用者場景,比如匿名和已登入,不同許可權的使用者不同的限流策略
  2. API的不同,根據不同API設定不同的策略
  3. 請求的爆發期和持續期不同的限流策略
  4. 可以同時支援使用多個限流策略
 
 

限流的機制

限流和許可權一樣,執行檢視前會依次檢查所有的限流類,全部通過會執行View,任何一個檢查失敗,會丟擲Exceptions.Throttled異常
在settings中,通過 DEFAULT_THROTTLE_CLASSES 設定限流類,通過DEFAULT_THROTTLE_RATES設定限流頻率
 
 

DRF提供的兩個常用限流類

AnonRateThrottle:對於匿名使用者的限流,使用anon設定頻率
UserRateThrottle:對於登入使用者的限流, 使用user設定頻率
 

全域性限流類設定

REST_FRAMEWORK = {
    # 全域性限流類的設定
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',  # 對於匿名使用者的限流
        'rest_framework.throttling.UserRateThrottle' #對於登入使用者的限流
    ),
    # 限流頻率的設定
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day', # 未認證使用者一天只許存取100次
        'user': '1000/day' # 認證使用者一天可以存取1000次
        }
    }
DEFAULT_THROTTLE_RATES設定限流頻率格式 次數/時間單位
  • second: 按秒設定頻率次數
  • minute:按分鐘設定頻率次數
  • hour:按小時設定頻率次數
  • day: 按天設定頻率次數
 

檢視級別限流類設定

#匯入限流模組
from rest_framework import throttling 


class getInfoList(ModelViewSet):
    # 通過throttle_classes 設定該檢視的限流類
    # 檢視指定會覆蓋settings設定的全域性限流
    throttle_classes = (throttling.UserRateThrottle,)

    def infoList(self):
        ...
 
 

識別請求的使用者端

我們既然要對請求進行限流,那麼肯定要失識 別是誰發來的請求,然後進行對應的措施,不然無法確定請求者身份,那麼就無法得知是不是需要限制的請求,常見的方法有三種
  1. drf利用http報頭的 x-forwarded-for 或者wsgi中的remote-addr變數來唯一標識使用者端的IP地址
  2. 如果 存在x-forwarded-for 屬性,則使用x-forwarded-for ,否則使用remote-addr
  3. 可以使用request.user的屬性來標識請求,比如使用request.user.id 來標記唯一請求
  4. 使用IP地址對使用者端請求進行限流,需要考慮使用偽造代理IP請求的情況
 
 

throttling原始碼解析

throttling原始碼一共有五個類
  1. BaseThrottle: 限流基礎類別
  2. SimpleRateThrottle:頻率校驗類
  3. AnonRateThrottle:匿名使用者限流
  4. UserRateThrottle:認證使用者限流
  5. ScopedRateThrottle:api檢視級別的限流
 

BaseThrottle限流基礎類別

沒有去具體實現某些功能,跟許可權類基礎類別似,只是提供了佔位方法
class BaseThrottle:

    # allow_request原始碼並沒有直接實現功能,只是寫好了方法佔位,待後續繼承實現
    # 該方法主要是處理是否允許請求通過
    # 如果後續繼承基礎類別實現該方法,允許請求通過返回True,不允許請求通過返回False
    def allow_request(self, request, view):
      
        raise NotImplementedError('.allow_request() must be overridden')

    # 獲取IP地址
    def get_ident(self, request):
        # 獲取請求頭中真實IP地址
        xff = request.META.get('HTTP_X_FORWARDED_FOR')
        # 獲取代理IP地址
        remote_addr = request.META.get('REMOTE_ADDR')
        # 獲取設定的允許的最大代理數,預設不設定為None
        num_proxies = api_settings.NUM_PROXIES
       # 如果num_proxies不是None,說明設定了該值
        if num_proxies is not None:
            # 如果設定為0,或者 xff沒有值
            if num_proxies == 0 or xff is None:
                # 返回代理IP地址
                return remote_addr
            #使用代理IP的話會有多個地址,使用逗號分割成一個list
            addrs = xff.split(',')
            ''' 
            通過min函數,拿到允許的代理數和IP地址長度最小的值,使用-變成負數
            在addrs列表中通過該下標取對應值
            '''
            client_addr = addrs[-min(num_proxies, len(addrs))]
            return client_addr.strip()
        # 如果沒有設定允許的代理數 並且xff有值則直接返回,否則返回remote_addr
        return ''.join(xff.split()) if xff else remote_addr

    # 等待時間,告訴使用者端被限流,等待多久可以存取
    # 後續繼承實現,可選
    def wait(self):
    
        return None

SimpleRateThrottle

頻率控制類,繼承了BaseThrottle,新增和重寫了一些方法,重點是新增了get_cache_key 方法,但必須自己實現該方法
class SimpleRateThrottle(BaseThrottle):
    # 限流需要用到快取,使用drf預設的快取
    # 如果其他繼承類想修改快取機制,cache = caches['快取名'] 進行修改
    cache = default_cache
    # time.time方法,但是並沒有()進行範例呼叫
    # 類似計時器功能,在這裡留好,後續呼叫
    timer = time.time  
    # 快取設定,字串格式化方法後續傳參使用
    cache_format = 'throttle_%(scope)s_%(ident)s'
    # scope預設沒有設定,該值是DEFAULT_THROTTLE_RATES中對應限流類的key
    scope = None
    # 限流頻率預設的設定值
    THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES

    def __init__(self):
        if not getattr(self, 'rate', None):
            # 從下面get_rate()方法獲取存取頻率限制的引數
            self.rate = self.get_rate()
            
        # 通過self.parse_rate方法獲取限制的頻率及持續時間賦值給num_requests
        self.num_requests, self.duration = self.parse_rate(self.rate)
    # 獲取當前請求的標識
    def get_cache_key(self, request, view):
        raise NotImplementedError('.get_cache_key() must be overridden')
        
    # 獲取settings頻率設定限流類對應的key
    def get_rate(self):
       # 如果沒有scope,丟擲異常
        if not getattr(self, 'scope', None):
            msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
                   self.__class__.__name__)
            raise ImproperlyConfigured(msg)

        try:
            # 從 self.THROTTLE_RATES 中獲取設定的scope
            return self.THROTTLE_RATES[self.scope]
        except KeyError:
            msg = "No default throttle rate set for '%s' scope" % self.scope
            raise ImproperlyConfigured(msg)
    # 獲取限流頻率設定及持續時間
    def parse_rate(self, rate):
         # 如果沒有設定頻率限制,直接返回None
        if rate is None:
            return (None, None)
        # 在settings設定頻率我們使用 num/type 設定值
        # 字串使用/分割 ,獲取兩個對應的值
        num, period = rate.split('/')
        num_requests = int(num)
        #settings中設定時間單位以天為單位可以是day也可以是d
        # period[0]獲取第一個字元為key,以秒為單位換算,秒就1,分就是60,天就是86400 
        # 如果需要擴充套件月、年等時間,可以擴充套件原始碼
        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
        # 返回頻率和持續時間
        return (num_requests, duration)

    # 是否允許請求通過,執行返回True,否則返回False
    def allow_request(self, request, view):
        # 如果沒有設定限流頻率,直接返回True
        if self.rate is None:
            return True
        # 獲取使用者標識賦值給self.key
        self.key = self.get_cache_key(request, view)
        # 沒有使用者標識直接返回True
        if self.key is None:
            return True
        # 獲取歷史存取時間戳
        self.history = self.cache.get(self.key, [])
        # 獲取當前時間戳
        self.now = self.timer()

        # while迴圈,如果歷史存取時間戳有值,拿到歷史時間戳[-1]的資料,如果小於等於當前時間戳減去持續時間,彈出最後一個時間戳
        # 當前時間-持續時間,就相當於需要限制的時間區間,如果歷史時間戳小於等於該區間,才不會繼續pop
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        # 如果歷史存取時間戳的列表長度大於等於我們設定頻率數量,說明到了頻率上限
        if len(self.history) >= self.num_requests:
            # 返回self.throttle_failure 對應False
            return self.throttle_failure()
        # 返回self.throttle_success 對應True
        return self.throttle_success()
    # 頻率未到達上限時返回該方法
    def throttle_success(self):
        # 在歷史請求時間戳列表,將當前時間插入該列表  
        self.history.insert(0, self.now)
        # 更新快取內容
        self.cache.set(self.key, self.history, self.duration)
        # 返回True
        return True
    # 頻率到達上限時返回該方法
    def throttle_failure(self):
       
        return False
    # 返回還需要多長時間可以進行下一次請求,可選方法
    def wait(self):
     
        if self.history:
            # 如果歷史請求時間戳有值,剩餘時間等於持續時間減去(當前時間-第一次請求)
            remaining_duration = self.duration - (self.now - self.history[-1])
        else:
            # 剩餘的時間等於持續時間
            remaining_duration = self.duration
        # 允許請求的次數 等於 允許的次數-已請求的次數+1
        available_requests = self.num_requests - len(self.history) + 1
        # 如果允許請求的次數小於等於0,返回None
        if available_requests <= 0:
            return None

        return remaining_duration / float(available_requests)

AnonRateThrottle

匿名限流類:繼承了SimpleRateThrottle,重寫了 get_cache_key 方法
AnonRateThrottle 只會限制未經身份驗證的使用者。傳入的請求的IP地址用於生成一個唯一的金鑰。
允許的請求頻率由以下各項之一確定(按優先順序):
  1. 類的 rate 屬性,可以通過繼承 AnonRateThrottle 並設定該屬性來修改這個值,優先順序高
  2. settings組態檔中 DEFAULT_THROTTLE_RATES['anon'] 設定項的值。優先順序低
  3. anonratetrottle 適用於想限制來自未知使用者的請求頻率的情況
class AnonRateThrottle(SimpleRateThrottle):
    # 設定頻率控制的key為anon
    scope = 'anon'
    # 重寫get_cache_key方法
    def get_cache_key(self, request, view):
        # 如果請求使用者是經過認證的使用者,不需要進行限流,直接返回None
        if request.user.is_authenticated:
            return None  
        # 如果使用者是未經認證的使用者,將該類的scope和 使用者的IP地址傳入SimpleRateThrottle的self.cache_format類屬性
        return self.cache_format % {
            'scope': self.scope,
            'ident': self.get_ident(request)
        }

UserRateThrottle

認證使用者限流類:繼承了SimpleRateThrottle,僅僅是重寫了 get_cache_key 方法
UserRateThrottle 用於限制已認證的使用者在整個API中的請求頻率。使用者ID用於生成唯一的金鑰。未經身份驗證的請求將使用傳入的請求的IP地址生成一個唯一的金鑰
允許的請求頻率由以下各項之一確定(按優先順序):
  1. 類的 rate 屬性,可以通過繼承 UserRateThrottle 並設定該屬性來修改這個值,優先順序高
  2. settings組態檔中 DEFAULT_THROTTLE_RATES['user'] 設定項的值。優先順序低

   # 設定頻率控制的key位anon
    scope = 'user'
    # 重寫get_cache_key方法
    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            # 如果請求使用者是認證使用者,設定使用者的唯一標識賦值給ident
            ident = request.user.pk
        else:
            #如果請求使用者是非認證使用者,通過get_ident獲取請求ip賦值給ident
            ident = self.get_ident(request)
        # 設定SimpleRateThrottle中self.cache_format的值
        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

ScopedRateThrottle

使用者對於每個檢視的存取頻次:繼承了SimpleRateThrottle,重寫了 get_cache_key 和allow_request 方法
ScopedRateThrottle 類用於限制對APIs特定部分的存取,也就是檢視級別的限流,不是全域性性的
只有當正在存取的檢視包含 throttle_scope 屬性時,才會應用此限制。然後,通過將檢視的「scope」屬性值與唯一的使用者ID或IP地址連線,生成唯一的金鑰。
允許的請求頻率由 scope 屬性的值在 DEFAULT_THROTTLE_RATES 中的設定確定
class ScopedRateThrottle(SimpleRateThrottle):
    
    scope_attr = 'throttle_scope'

    def __init__(self):

        pass

    def allow_request(self, request, view):
        #  從view獲取self.scope_attr賦值給scope,如果view中沒有指定,設定為None
        self.scope = getattr(view, self.scope_attr, None)
        # 如果沒有設定scope,直接返回True
        if not self.scope:
            return True
        # 獲取settings頻率設定限流類對應的key  
        self.rate = self.get_rate()
        # 獲取頻率限制、持續時長
        self.num_requests, self.duration = self.parse_rate(self.rate)
        # 呼叫父類別的allow_request 返回對應的結果
        return super().allow_request(request, view)
    # 獲取使用者唯一標識
    def get_cache_key(self, request, view):
         # 如果是認證使用者 ident=使用者唯一標識
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            # 非認證使用者返回請求的ip
            ident = self.get_ident(request)
        # 設定父類別的類屬性
        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }
 

自定義限流類

上面原始碼的類,我們一般使用的是後三個,如果原始碼提供的限流類無法滿足我們的需求,我們可以寫自定義的限流類

自定義限流類的步驟:

  1. 繼承BaseThrottle類或者根據場景繼承其他限流類
  2. 實現allow_request方法,如果請求被允許,那麼返回True,否則返回False
  3. wait方法,是否實現根據自己場景
  4. 獲取唯一標識的方法可以使用原始碼自由的,也可以自定義
 
 

場景案例1

假設我們的請求需要同時進行多個認證使用者的限流措施,比如每小時限制100次,同時每天限制1000次
# 每小時的限流類
class UserHourRateThrottle(UserRateThrottle): 
    scope = 'userHour' 
# 每天的限流類
class UserDayRateThrottle(UserRateThrottle):
    scope = 'userDay' 
# settings中進行設定
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
    # 設定我們自定義的限流類或者再view中進行區域性的設定
    'testApi.throttles.UserHourRateThrottle', 
    'testApi.throttles.UserDayRateThrottle'
    ),
    
'DEFAULT_THROTTLE_RATES': {
    'userHour': '100/hour', # 每小時最多100次
    'userDay': '1000/day' # 每天最多100次
}
}

場景案例2

隨機限制
import random
class RandomRateThrottle(throttling.BaseThrottle):
    def allow_request(self, request, view):
        # 如果隨機的數位 不等於1,返回True,否則返回False
        return random.randint(1, 10) != 1
# 之後在settings進行設定或者區域性設定