Что такое хороший алгоритм ограничения скорости?

Я мог бы использовать некоторый псевдо-код или, лучше, Python. Я пытаюсь реализовать очередь ограничения скорости для бота Python IRC, и она частично работает, но если кто-то запускает меньше сообщений, чем лимит (например, ограничение скорости составляет 5 сообщений в 8 секунд, а человек запускает только 4), а следующий триггер находится в течение 8 секунд (например, 16 секунд спустя), бот отправляет сообщение, но очередь становится полной, и бот ждет 8 секунд, хотя это не требуется, так как 8-секундный период прошедший.

10 ответов


на простейший алгоритм, если вы хотите просто отбросить сообщения, когда они прибывают слишком быстро (вместо того, чтобы выстраивать их в очередь, что имеет смысл, потому что очередь может стать сколь угодно большой):

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;

нет структур, таймеры и т. д. в этом решении и он работает чисто :) чтобы увидеть это, "пособие" растет со скоростью не более 5/8 единиц в секунду, т. е. не более пяти единиц в восемь секунд. Каждое пересылаемое сообщение вычитает одну единицу, поэтому вы не можете отправляйте более пяти сообщений каждые восемь секунд.

отметим, что rate должно быть целым числом, т. е. без ненулевой десятичной части, или алгоритм не будет работать правильно (фактическая скорость не будет rate/per). Е. Г. rate=0.5; per=1.0; не работает, потому что allowance никогда не вырастет до 1.0. Но!--5--> работает нормально.


используйте этот декоратор @RateLimited (ratepersec) перед вашей функцией, которая запрашивает.

в принципе, это проверяет, прошел ли 1 / rate secs с момента последнего времени, а если нет, ждет оставшуюся часть времени, иначе он не ждет. Это эффективно ограничивает вас в скорости / сек. Декоратор может быть применен к любой функции, которую вы хотите ограничить.

в вашем случае, если вы хотите максимум 5 сообщений в 8 секунд, используйте @RateLimited (0.625) перед 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 __name__ == "__main__":
    print "This should print 1,2,3... at about 2 per second."
    for i in range(1,100):
        PrintNumber(i)

ведро токенов довольно просто реализовать.

начните с ведра с 5 токенов.

каждые 5/8 секунд: если ведро имеет менее 5 токенов, добавьте один.

каждый раз, когда вы хотите отправить сообщение: Если ведро имеет ≥1 токен, возьмите один токен и отправьте сообщение. В противном случае подождите/отбросьте сообщение/что угодно.

(очевидно, в фактическом коде вы бы использовали целочисленный счетчик вместо реальных токенов, и вы можете оптимизировать каждые 5 / 8s шаг, сохраняя метки времени)


чтение вопроса снова, если ограничение скорости полностью сбрасывается каждые 8 секунд, то вот модификация:

начните с отметки времени,last_send, в давние времена (например, в эпоху). Кроме того, начните с того же 5-маркерного ведра.

ударьте каждые 5/8 секунд правило.

каждый раз, когда вы отправляете сообщение: Во-первых, проверьте, если last_send ≥ 8 секунд назад. Если так, наполнять ведра (по 5 жетонов). Во-вторых, если есть являются токенами в ведре, отправляют сообщение (в противном случае, drop/wait/etc.). В-третьих, set last_send по настоящее время.

это должно сработать для этого сценария.


Я на самом деле написал IRC-бот, используя такую стратегию (первый подход). Его в Perl, а не Python, но вот некоторый код для иллюстрации:

первая часть здесь обрабатывает добавление токенов в ведро. Вы можете увидеть оптимизацию добавления токенов на основе времени (от 2-й до последней строки), а затем последней линия зажимает содержимое ведра до максимума (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-это структура данных, которая передается. Это внутри метода, который работает регулярно (он вычисляет, когда в следующий раз ему будет что-то делать, и спит либо так долго, либо пока не получит сетевой трафик). Следующая часть метода обрабатывает отправку. Это довольно сложно, потому что сообщения имеют связанные с ними приоритеты.

    # 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] = [];

это первая очередь, которая не неважно что. Даже если это убьет нашу связь из-за наводнения. Используется для чрезвычайно важных вещей, таких как ответ на PING сервера. Далее остальные очереди:

    # 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}});
                    }
            }
    }

наконец, состояние ведра сохраняется обратно в структуру данных $conn (на самом деле немного позже в методе; он сначала вычисляет, как скоро у него будет больше работы)

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

как вы можете видеть, фактический код обработки ведра очень мал-около четырех строк. Остальная часть кода является приоритетной обработка очереди. Бот имеет приоритетные очереди, так что, например, кто-то в чате с ним не может помешать ему выполнять свои важные обязанности по удару/запрету.


чтобы заблокировать обработку до тех пор, пока сообщение не будет отправлено, таким образом, выстраивая дальнейшие сообщения в очередь, красивое решение antti также может быть изменено следующим образом:

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;

Он просто ждет, пока не будет достаточно средств для отправки сообщения. чтобы не начинать с двухкратной ставки, пособие также может быть инициализировано с 0.


сохранить время отправки последних пяти строк. Удерживайте сообщения в очереди до тех пор, пока пятое самое последнее сообщение (если оно существует) не будет как минимум 8 секунд в прошлом (с last_five как массив раз):

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()

одним из решений является прикрепление метки времени к каждому элементу очереди и отбрасывание элемента через 8 секунд. Эту проверку можно выполнять при каждом добавлении очереди.

это работает, только если вы ограничиваете размер очереди до 5 и отбрасываете любые добавления, пока очередь заполнена.


Если кто-то все еще заинтересован, я использую этот простой вызываемый класс в сочетании с временным хранилищем значений ключей LRU для ограничения скорости запроса на IP. Использует deque, но может быть переписан для использования со списком.

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")

Как насчет этого:

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;
}

мне нужна была вариация в Scala. Вот это:

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)
  }

}

вот как его можно использовать:

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

просто реализация python кода из принятого ответа.

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