web-dev-qa-db-fra.com

Qu'est-ce qu'un bon algorithme de limitation de débit?

Je pourrais utiliser un pseudo-code, ou mieux, Python. J'essaie d'implémenter une file d'attente de limitation de débit pour un Python IRC bot), et cela fonctionne partiellement, mais si quelqu'un déclenche moins de messages que la limite (par exemple , la limite de débit est de 5 messages toutes les 8 secondes, et la personne ne déclenche que 4), et le prochain déclencheur dure plus de 8 secondes (par exemple, 16 secondes plus tard), le bot envoie le message, mais la file d'attente est pleine et attend 8 secondes, même si ce n’est pas nécessaire puisque la période de 8 secondes s’est écoulée.

144
miniman

Ici, le algorithme le plus simple , si vous souhaitez simplement supprimer les messages quand ils arrivent trop rapidement (au lieu de les mettre en file d'attente, ce qui est logique car la file d'attente peut devenir arbitrairement grande):

rate = 5.0; // unit: messages
per  = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds

when (message_received):
  current = now();
  time_passed = current - last_check;
  last_check = current;
  allowance += time_passed * (rate / per);
  if (allowance > rate):
    allowance = rate; // throttle
  if (allowance < 1.0):
    discard_message();
  else:
    forward_message();
    allowance -= 1.0;

Il n'y a pas d'infrastructures de données, de minuteries, etc. dans cette solution et cela fonctionne proprement :) Pour voir cela, la "tolérance" augmente à une vitesse maximale de 5/8 unités par seconde, c'est-à-dire au maximum cinq unités toutes les huit secondes. Chaque message transféré déduit une unité, vous ne pouvez donc pas envoyer plus de cinq messages toutes les huit secondes.

Notez que rate doit être un entier, c’est-à-dire sans partie décimale non nulle, sinon l’algorithme ne fonctionnera pas correctement (le taux réel ne sera pas rate/per). Par exemple. rate=0.5; per=1.0; _ ne fonctionne pas car allowance ne deviendra jamais 1.0. Mais rate=1.0; per=2.0; fonctionne bien.

214
Antti Huima

Utilisez ce décorateur @RateLimited (ratepersec) avant votre fonction qui met en file d'attente.

En gros, cela vérifie si 1/rate secondes se sont écoulés depuis la dernière fois et si ce n’est pas le cas, attend le reste du temps, sinon il n’attendra pas. Cela vous limite effectivement à taux/sec. Le décorateur peut être appliqué à toute fonction de votre choix.

Dans votre cas, si vous souhaitez un maximum de 5 messages par 8 secondes, utilisez @RateLimited (0.625) avant votre fonction sendToQueue.

import time

def RateLimited(maxPerSecond):
    minInterval = 1.0 / float(maxPerSecond)
    def decorate(func):
        lastTimeCalled = [0.0]
        def rateLimitedFunction(*args,**kargs):
            elapsed = time.clock() - lastTimeCalled[0]
            leftToWait = minInterval - elapsed
            if leftToWait>0:
                time.sleep(leftToWait)
            ret = func(*args,**kargs)
            lastTimeCalled[0] = time.clock()
            return ret
        return rateLimitedFunction
    return decorate

@RateLimited(2)  # 2 per second at most
def PrintNumber(num):
    print num

if __== "__main__":
    print "This should print 1,2,3... at about 2 per second."
    for i in range(1,100):
        PrintNumber(i)
43
Carlos A. Ibarra

Un seau à jetons est assez simple à mettre en œuvre.

Commencez avec un seau avec 5 jetons.

Toutes les 5/8 secondes: si le compartiment contient moins de 5 jetons, ajoutez-en un.

Chaque fois que vous souhaitez envoyer un message: Si le compartiment contient ≥ 1 jeton, enlevez un jeton et envoyez le message. Sinon, attendez/déposez le message/peu importe.

(Évidemment, dans le code réel, vous utiliseriez un compteur entier au lieu de vrais jetons et vous pouvez optimiser toutes les étapes tous les 5/8s en stockant des horodatages)


En relisant la question, si la limite de débit est entièrement réinitialisée toutes les 8 secondes, voici une modification:

Commencez avec un horodatage, last_send, jadis (à l’époque, par exemple). De plus, commencez avec le même seau de 5 jetons.

Frappez la règle toutes les 5/8 secondes.

Chaque fois que vous envoyez un message: vérifiez d'abord si last_send ≥ 8 secondes auparavant. Si c'est le cas, remplissez le seau (réglez-le sur 5 jetons). Deuxièmement, s'il y a des jetons dans le compartiment, envoyez le message (sinon, déposez/attendez/etc.). Troisièmement, définissez last_send jusqu'à maintenant.

Cela devrait fonctionner pour ce scénario.


J'ai en fait écrit un bot IRC) en utilisant une stratégie comme celle-ci (la première approche). Il est en Perl, pas en Python, mais voici du code pour illustrer:

La première partie traite ici de l’ajout de jetons au seau. Vous pouvez voir l'optimisation de l'ajout de jetons en fonction du temps (2e à la dernière ligne), puis la dernière ligne fixe le contenu du compartiment au maximum (MESSAGE_BURST).

    my $start_time = time;
    ...
    # Bucket handling
    my $bucket = $conn->{fujiko_limit_bucket};
    my $lasttx = $conn->{fujiko_limit_lasttx};
    $bucket += ($start_time-$lasttx)/MESSAGE_INTERVAL;
    ($bucket <= MESSAGE_BURST) or $bucket = MESSAGE_BURST;

$ conn est une structure de données qui est transmise. Cela fait partie d'une méthode qui s'exécute régulièrement (il calcule quand la prochaine fois qu'il aura quelque chose à faire et dort si longtemps ou jusqu'à ce qu'il reçoive du trafic réseau). La partie suivante de la méthode gère l’envoi. C'est assez compliqué, car les messages ont des priorités qui leur sont associées.

    # Queue handling. Start with the ultimate queue.
    my $queues = $conn->{fujiko_queues};
    foreach my $entry (@{$queues->[PRIORITY_ULTIMATE]}) {
            # Ultimate is special. We run ultimate no matter what. Even if
            # it sends the bucket negative.
            --$bucket;
            $entry->{code}(@{$entry->{args}});
    }
    $queues->[PRIORITY_ULTIMATE] = [];

C'est la première file d'attente, qui est exécutée quoi qu'il arrive. Même si notre connexion est mise à mort pour cause d'inondation. Utilisé pour des tâches extrêmement importantes, telles que la réponse à la commande PING du serveur. Ensuite, le reste des files d'attente:

    # Continue to the other queues, in order of priority.
    QRUN: for (my $pri = PRIORITY_HIGH; $pri >= PRIORITY_JUNK; --$pri) {
            my $queue = $queues->[$pri];
            while (scalar(@$queue)) {
                    if ($bucket < 1) {
                            # continue later.
                            $need_more_time = 1;
                            last QRUN;
                    } else {
                            --$bucket;
                            my $entry = shift @$queue;
                            $entry->{code}(@{$entry->{args}});
                    }
            }
    }

Enfin, le statut du compartiment est sauvegardé dans la structure de données $ conn (en fait, un peu plus tard dans la méthode; il calcule d’abord le temps qu’il aura plus de travail)

    # Save status.
    $conn->{fujiko_limit_bucket} = $bucket;
    $conn->{fujiko_limit_lasttx} = $start_time;

Comme vous pouvez le constater, le code de manipulation du godet est très petit - environ quatre lignes. Le reste du code est la gestion de la file d'attente prioritaire. Le bot a des files d’attente prioritaires afin que, par exemple, une personne qui discute avec lui ne puisse l’empêcher de s’acquitter de ses tâches importantes de kick/ban.

24
derobert

pour bloquer le traitement jusqu'à ce que le message puisse être envoyé, mettant ainsi en file d'attente d'autres messages, la belle solution d'antti peut également être modifiée comme suit:

rate = 5.0; // unit: messages
per  = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds

when (message_received):
  current = now();
  time_passed = current - last_check;
  last_check = current;
  allowance += time_passed * (rate / per);
  if (allowance > rate):
    allowance = rate; // throttle
  if (allowance < 1.0):
    time.sleep( (1-allowance) * (per/rate))
    forward_message();
    allowance = 0.0;
  else:
    forward_message();
    allowance -= 1.0;

il attend juste que suffisamment d’allocation soit disponible pour envoyer le message. pour ne pas commencer avec deux fois le taux, l’allocation peut également être initialisée avec 0.

9
san

Une solution consiste à attacher un horodatage à chaque élément de la file d'attente et à le supprimer au bout de 8 secondes. Vous pouvez effectuer cette vérification à chaque fois que la file d'attente est ajoutée à.

Cela ne fonctionne que si vous limitez la taille de la file d'attente à 5 et supprimez tous les ajouts tant qu'elle est pleine.

2
jheriko

Gardez l'heure à laquelle les cinq dernières lignes ont été envoyées. Tenez les messages en file d'attente jusqu'à ce que le cinquième message le plus récent (s'il existe) soit au moins 8 secondes dans le passé (avec last_five comme tableau de fois):

now = time.time()
if len(last_five) == 0 or (now - last_five[-1]) >= 8.0:
    last_five.insert(0, now)
    send_message(msg)
if len(last_five) > 5:
    last_five.pop()
2
Pesto

Juste une python implémentation d'un code de réponse acceptée.

import time

class Object(object):
    pass

def get_throttler(rate, per):
    scope = Object()
    scope.allowance = rate
    scope.last_check = time.time()
    def throttler(fn):
        current = time.time()
        time_passed = current - scope.last_check;
        scope.last_check = current;
        scope.allowance = scope.allowance + time_passed * (rate / per)
        if (scope.allowance > rate):
          scope.allowance = rate
        if (scope.allowance < 1):
          pass
        else:
          fn()
          scope.allowance = scope.allowance - 1
    return throttler
1
Anton Shelin

Si quelqu'un est toujours intéressé, j'utilise cette classe appelable simple avec un stockage de valeur de clé LRU temporisé pour limiter le débit de requête par IP. Utilise un deque, mais peut être réécrit pour être utilisé avec une liste.

from collections import deque
import time


class RateLimiter:
    def __init__(self, maxRate=5, timeUnit=1):
        self.timeUnit = timeUnit
        self.deque = deque(maxlen=maxRate)

    def __call__(self):
        if self.deque.maxlen == len(self.deque):
            cTime = time.time()
            if cTime - self.deque[0] > self.timeUnit:
                self.deque.append(cTime)
                return False
            else:
                return True
        self.deque.append(time.time())
        return False

r = RateLimiter()
for i in range(0,100):
    time.sleep(0.1)
    print(i, "block" if r() else "pass")
1
sanyi

J'avais besoin d'une variation de Scala. C'est ici:

case class Limiter[-A, +B](callsPerSecond: (Double, Double), f: A ⇒ B) extends (A ⇒ B) {

  import Thread.sleep
  private def now = System.currentTimeMillis / 1000.0
  private val (calls, sec) = callsPerSecond
  private var allowance  = 1.0
  private var last = now

  def apply(a: A): B = {
    synchronized {
      val t = now
      val delta_t = t - last
      last = t
      allowance += delta_t * (calls / sec)
      if (allowance > calls)
        allowance = calls
      if (allowance < 1d) {
        sleep(((1 - allowance) * (sec / calls) * 1000d).toLong)
      }
      allowance -= 1
    }
    f(a)
  }

}

Voici comment cela peut être utilisé:

val f = Limiter((5d, 8d), { 
  _: Unit ⇒ 
    println(System.currentTimeMillis) 
})
while(true){f(())}
0
Landon Kuhn

Que dis-tu de ça:

long check_time = System.currentTimeMillis();
int msgs_sent_count = 0;

private boolean isRateLimited(int msgs_per_sec) {
    if (System.currentTimeMillis() - check_time > 1000) {
        check_time = System.currentTimeMillis();
        msgs_sent_count = 0;
    }

    if (msgs_sent_count > (msgs_per_sec - 1)) {
        return true;
    } else {
        msgs_sent_count++;
    }

    return false;
}
0
jredner