Limites de taxa com Python e Redis



Neste artigo, veremos alguns algoritmos de limite de taxa baseados em Python e Redis, da implementação mais simples ao avançado algoritmo Generic Cell Rate Algorithm (GCRA ). Usaremos redis-py



para interagir com Redis ( pip install redis) . Sugiro clonar meu repositório para experimentar as restrições de consulta.



Limite de tempo



A primeira abordagem para limitar o número de solicitações por período é usar um algoritmo de tempo limitado: para cada chave de limitação (chave de taxa, algo único, como um apelido ou endereço IP), um contador (inicialmente definindo o valor limite) e uma data de expiração são armazenados separadamente (período), que diminuem conforme as solicitações são recebidas.



Com Python e Redis, você pode implementar esse algoritmo da seguinte maneira:



  1. Verificando a existência da chave de restrição.
  2. Se existir, inicialize-o com um valor limite ( Redis SETNX ) e uma data de validade ( Redis EXPIRE ).
  3. Diminuímos esse valor com cada solicitação subsequente ( Redis DECRBY ).
  4. As consultas são interrompidas apenas quando o valor cai abaixo de zero.
  5. Após um período de tempo especificado, a chave é excluída automaticamente.


from datetime import timedelta
from redis import Redis

def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
    if r.setnx(key, limit):
        r.expire(key, int(period.total_seconds()))
    bucket_val = r.get(key)
    if bucket_val and int(bucket_val) > 0:
        r.decrby(key, 1)
        return False
    return True


Você pode ver o trabalho desse código ao emular o limite de 20 requisições em 30 segundos (para deixar mais claro, coloquei a função em um módulo).



import redis
from datetime import timedelta
from ratelimit import time_bucketed

r = redis.Redis(host='localhost', port=6379, db=0)
requests = 25

for i in range(requests):
    if time_bucketed.request_is_limited(r, 'admin', 20, timedelta(seconds=30)):
        print ('Request is limited')
    else:
        print ('Request is allowed')


Apenas as primeiras 20 solicitações não serão limitadas, depois delas você terá que esperar 30 segundos para que novas solicitações possam ser enviadas novamente.



A desvantagem dessa abordagem é que ela não limita a frequência, mas o número de solicitações feitas pelo usuário em um determinado período. Se todo o limite se esgotar imediatamente, você terá que aguardar o fim do período.



Algoritmo de balde com vazamento



Existe uma abordagem em que podemos limitar a frequência com muito cuidado: este é o algoritmo de intervalo atual . O princípio de operação é o mesmo de um balde com vazamento real: despejamos água (solicitações) no balde até as bordas, enquanto parte da água flui constantemente pelo orifício no fundo (geralmente implementado usando um processo em segundo plano). Se a quantidade de água despejada no balde for maior do que a quantidade de água despejando, o balde está cheio e, quando estiver cheio, novos pedidos não são mais aceitos.



Como o algoritmo funciona



Você pode pular esta abordagem para obter uma solução mais elegante que não requer um processo separado para emular o vazamento: o algoritmo Generic Cell Rate.



Algoritmo de controle de taxa de célula generalizada



O GCRA foi criado no setor de telecomunicações, onde é denominado Modo de Transferência Assíncrona (ATM). Foi usado por gerentes de rede ATM para atrasar ou descartar células - pequenos pacotes de dados de tamanho fixo - que chegavam a uma taxa maior do que um limite especificado.



O GCRA rastreia o limite restante usando o chamado Tempo Teórico de Chegada (TAT) de cada solicitação:



tat = current_time + period


e limita a próxima solicitação se o tempo de chegada for menor que o TAT atual. Isso funciona bem se a frequência for 1 solicitação / período, quando as solicitações são divididas em períodos. Mas, na realidade, a frequência geralmente é calculada como um limite / período. Por exemplo, se a frequência é de 10 solicitações / 60 segundos, o usuário pode fazer 10 solicitações nos primeiros 6 segundos. E com uma frequência de 1 solicitação / 6 segundos, ele terá que aguardar 6 segundos entre as solicitações.



Para poder enviar um grupo de solicitações em um curto período e manter a limitação de seu número por um período com um limite> 1, cada solicitação deve ser dividida pela relação período / limite, e então o próximo tempo teórico de chegada ( new_tat) será calculado de forma diferente. Vamos denotar a hora de chegada da solicitação como t:



  • new_tat = tat + period / limitse as solicitações são agrupadas ( t <= tat)
  • new_tat = t + period / limitse as solicitações não forem agrupadas ( t > tat)


Conseqüentemente:



new_tat = max(tat, t) + period / limit


O pedido será limitado, se new_tatmaior do que a soma do tempo atual e o período: new_tat > t + period. Quando new_tat = tat + period / limitchegarmos tat + period / limit > t + period. Portanto, você precisa restringir as consultas apenas quando tat - t > period - period / limit.



      period — period / limit
      <----------------------->
--|----------------------------|--->
  t                           TAT


Nesse caso, não precisamos atualizar o TAT, pois as solicitações limitadas não têm tempo de chegada teórico.



Agora vamos construir a versão final do código!



from datetime import timedelta
from redis import Redis

def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
    period_in_seconds = int(period.total_seconds())
    t = r.time()[0]
    separation = round(period_in_seconds / limit)
    r.setnx(key, 0)
    tat = max(int(r.get(key)), t)
    if tat - t <= period_in_seconds - separation:
        new_tat = max(tat, t) + separation
        r.set(key, new_tat)
        return False
    return True


Usamos o Redis TIME porque o GCRA depende do tempo e precisamos nos certificar de que o horário atual seja consistente em várias implantações (discrepâncias de relógio entre máquinas diferentes podem levar a falsos positivos).



Este código demonstra o desempenho GCRA em 10 solicitações / 60 seg.



import redis
from datetime import timedelta
from ratelimit import gcra

r = redis.Redis(host='localhost', port=6379, db=0)
requests = 10

for i in range(requests):
    if gcra.request_is_limited(r, 'admin', 10, timedelta(minutes=1)):
        print ('Request is limited')
    else:
        print ('Request is allowed')


O algoritmo não limita as 10 primeiras solicitações, mas você terá que esperar pelo menos 6 segundos para fazer a próxima solicitação. Tente executar o script após algum tempo e altere o valor do limite e do período (por exemplo, limit = 1e period=timedelta(seconds=6)).



Para manter a implementação limpa, não adicionei um bloqueio entre o recebimento e o envio de chamadas. Mas é necessário para várias solicitações com a mesma chave e ao mesmo tempo. Com Lua, você pode adicionar bloqueio como um gerenciador de contexto.



def request_is_limited(r: Redis, key: str, limit: int, period: timedelta):
    period_in_seconds = int(period.total_seconds())
    t = r.time()[0]
    separation = round(period_in_seconds / limit)
    r.setnx(key, 0)
    try:
        with r.lock('lock:' + key, blocking_timeout=5) as lock:
            tat = max(int(r.get(key)), t)
            if tat - t <= period_in_seconds - separation:
                new_tat = max(tat, t) + separation
                r.set(key, new_tat)
                return False
            return True
    except LockError:
        return True


O código completo está no GitHub .



All Articles