什么是好的速率限制算法?


155

我可以使用一些伪代码,或者更好的Python。我正在尝试为Python IRC机器人实现一个限速队列,并且部分起作用,但是如果有人触发的消息少于限制(例如,限速为每8秒5条消息,而该人仅触发4条消息),并且下一个触发时间超过8秒(例如16秒后),机器人将发送消息,但队列已满,机器人将等待8秒,即使由于8秒钟的时间已过去也不需要它。

Answers:


231

这是最简单的算法,如果您只想在消息到达太快时就丢弃它们(而不是对其进行排队,这很有意义,因为队列可能会变得任意大):

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;效果很好。


4
还需要指出的是,“ time_passed”的尺寸和小数位数必须与“ per”相同,例如秒。
skaffman

2
嗨,skaffman,谢谢你的夸奖-我把它扔出袖子,但是有99.9%的可能性早些时候有人提出了类似的解决方案:)
Antti Huima 2009年

52
那是一种标准算法,它是一个令牌桶,没有队列。桶是allowance。铲斗尺寸为rate。该allowance += …行是对每个速率 ÷ 每秒添加令牌的优化。
derobert

5
@zwirbeltier你上面写的是错误的。“津贴”始终以“费率”为上限(请查看“ //油门”行),因此它将仅允许在任何特定时间(即5
发送一连串

7
这很好,但是可以超出速率。假设在时间0,您转发了5条消息,然后在时间N *(8/5)时,N = 1,2,...您可以发送另一条消息,从而在8秒内发送了5条以上的消息
mindvirus

47

在函数加入之前,使用此装饰器@RateLimited(ratepersec)。

基本上,这检查自上次以来是否过去了1 / rate秒,如果没有,则等待其余时间,否则不等待。这有效地限制了您的速率/秒。装饰器可以应用于您要限制速率的任何功能。

对于您的情况,如果每8秒最多需要5条消息,请在sendToQueue函数之前使用@RateLimited(0.625)。

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)

我喜欢为此目的使用装饰器的想法。为什么lastTimeCalled是一个列表?另外,我怀疑当多个线程调用相同的RateLimited函数时,这种方法是否会起作用……
Stephan202,2009年

8
这是一个列表,因为像float这样的简单类型在被闭包捕获时是常量。通过使其成为列表,列表是恒定的,但其内容不是恒定的。是的,它不是线程安全的,但可以使用锁轻松修复。
Carlos A. Ibarra

time.clock()在我的系统上没有足够的分辨率,因此我改写了代码并更改为使用time.time()
mtrbean 2014年

3
对于速率限制,您绝对不希望使用time.clock(),它可以测量经过的CPU时间。CPU时间可以比“实际”时间快得多或慢得多。您想使用time.time()它来衡量墙壁时间(“实际”时间)。
John Wiseman

1
顺便说一句,在实际的生产系统中:用sleep()调用实现速率限制可能不是一个好主意,因为它将阻塞线程,从而阻止另一个客户端使用它。
Maresh

28

令牌桶很容易实现。

从带有5个令牌的存储桶开始。

每5/8秒:如果存储桶中的令牌少于5个,则添加一个。

每次您要发送消息时:如果存储桶中的令牌≥1,则取出一个令牌并发送消息。否则,请等待/丢弃消息/任何内容。

(显然,在实际代码中,您将使用整数计数器代替实际的令牌,并且可以通过存储时间戳来优化每5/8秒的步长)


再次阅读问题,如果速率限制每8秒被完全重置一次,则可以进行以下修改:

last_send很久以前的某个时间(例如,纪元)开始,以一个时间戳记开始。同样,从相同的5令牌桶开始。

每5/8秒执行一次规则。

每次发送消息时:首先,检查是否last_send≥8秒。如果是这样,请填充存储桶(将其设置为5个令牌)。其次,如果存储桶中有令牌,则发送消息(否则,丢弃/等待/等)。第三,设置last_send为现在。

那应该适合那种情况。


我实际上已经使用这种策略(第一种方法)编写了IRC机器人。它在Perl中而不是Python中,但是这里有一些代码来说明:

第一部分处理将令牌添加到存储桶的过程。您可以看到基于时间(从第二行到最后一行)添加令牌的优化,然后最后一行将存储桶内容限制为最大值(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;

如您所见,实际的存储桶处理代码非常小-大约四行。其余代码是优先级队列处理。该机器人具有优先级队列,因此,例如,与它聊天的人无法阻止其执行重要的踢/禁止任务。


我错过了什么吗?看来这会使您在经历前5个问题后每8秒只能收到1条消息
chills42,2009年

@ chills42:是的,我看错了问题...请看答案的后半部分。
derobert

@chills:如果last_send <8秒,则不会在存储桶中添加任何令牌。如果您的存储桶中包含令牌,则可以发送消息;否则,您将无法(您在过去8秒钟内已发送了5条消息)
derobert

3
如果对此表示不满的人们请解释一下,我将不胜感激。我想解决您遇到的任何问题,但是如果没有反馈,这很难做到!
derobert

10

为了阻止处理,直到消息可以发送为止,从而使更多消息排队,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初始化。


5
睡觉时(1-allowance) * (per/rate),您需要添加相同的量last_check
阿尔卑斯山

2

保留最后五行的发送时间。保留排队的消息,直到最近的第五条消息(如果存在)过去至少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()

自从您修改它以来,我不是。
香蒜酱

您要存储五个时间戳,并在内存中反复移动它们(或执行链接列表操作)。我要存储一个整数计数器和一个时间戳。而且只做算术和赋值。
derobert

2
除非我的系统尝试发送5条线路,但在这段时间内只允许再发送3条线路,否则其功能会更好。您的将允许发送前三个,并在发送4和5之前强制等待8秒。我的将允许在最近的第四行和第五行之后的8秒发送4和5。
香蒜酱

1
但就此而言,可以通过使用长度为5的循环链接列表(指向最近的第五个发送),在新发送中覆盖它以及将指针向前移动来提高性能。
香蒜酱

对于具有速率限制器速度的irc机器人来说,这不是问题。我更喜欢列表解决方案,因为它更具可读性。由于修订,给出的存储桶答案令人困惑,但也没有错。
jheriko

2

一种解决方案是将时间戳记附加到每个队列项目,并在经过8秒后丢弃该项目。您可以在每次添加队列时执行此检查。

仅当将队列大小限制为5并在队列已满时丢弃所有添加项时,此方法才有效。


1

如果仍然有兴趣,我可以将此简单的可调用类与定时LRU键值存储结合使用,以限制每个IP的请求速率。使用双端队列,但可以重写为与列表一起使用。

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

只是接受的答案中的代码的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

建议我建议您添加代码用法示例
卢克,

0

这个怎么样:

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

我需要一个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(())}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.