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

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

Ответ 1

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

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. Но rate=1.0; per=2.0; отлично работает.

Ответ 2

Используйте этот декоратор @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)

Ответ 3

Знак Token Bucket довольно прост для реализации.

Начните с ведра с 5 жетонами.

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

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

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


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

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

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

Каждый раз, когда вы отправляете сообщение: сначала проверьте, не было ли last_send ≥ 8 секунд назад. Если да, заполните ковш (установите его на 5 токенов). Во-вторых, если в ковше есть токены, отправьте сообщение (в противном случае, отбросить/подождать/и т.д.). В-третьих, установите 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;

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

Ответ 4

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

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.

Ответ 5

Сохраняйте время отправки последних пяти строк. Удерживайте сообщения в очереди до тех пор, пока сообщение с пятым самым последним (если оно существует) занимает менее 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()

Ответ 6

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

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

Ответ 7

Если кто-то все еще интересуется, я использую этот простой класс вызываемых вызовов в сочетании с временным хранилищем ключей 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")

Ответ 8

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

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

Ответ 9

Мне нужно изменение в 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(())}

Ответ 10

Просто реализация кода на основе 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