简体   繁体   English

什么是好的限速算法?

[英]What's a good rate limiting algorithm?

I could use some pseudo-code, or better, Python. I am trying to implement a rate-limiting queue for a Python IRC bot, and it partially works, but if someone triggers less messages than the limit (eg, rate limit is 5 messages per 8 seconds, and the person triggers only 4), and the next trigger is over the 8 seconds (eg, 16 seconds later), the bot sends the message, but the queue becomes full and the bot waits 8 seconds, even though it's not needed since the 8 second period has lapsed.我可以使用一些伪代码,或者更好,Python。我正在尝试为 Python IRC 机器人实现一个速率限制队列,它部分工作,但如果有人触发的消息少于限制(例如,速率限制为 5每 8 秒发送一次消息,而该人仅触发 4 次),下一次触发超过 8 秒(例如,16 秒后),机器人发送消息,但队列已满,机器人等待 8 秒,即使因为 8 秒时间已经过去,所以不需要它。

Here the simplest algorithm , if you want just to drop messages when they arrive too quickly (instead of queuing them, which makes sense because the queue might get arbitrarily large):这是最简单的算法,如果您只想在消息到达太快时丢弃消息(而不是排队,这是有道理的,因为队列可能会变得任意大):

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;

There are no datastructures, timers etc. in this solution and it works cleanly:) To see this, 'allowance' grows at speed 5/8 units per seconds at most, ie at most five units per eight seconds.在这个解决方案中没有数据结构、计时器等,它工作得很干净:) 看到这一点,“津贴”最多以每秒 5/8 个单位的速度增长,即每八秒最多五个单位。 Every message that is forwarded deducts one unit, so you can't send more than five messages per every eight seconds.转发的每条消息都会扣除一个单位,因此每八秒发送的消息不能超过五条。

Note that rate should be an integer, ie without non-zero decimal part, or the algorithm won't work correctly (actual rate will not be rate/per ).请注意, rate应该是 integer,即没有非零小数部分,否则算法将无法正常工作(实际速率不会是rate/per )。 Eg rate=0.5; per=1.0;例如rate=0.5; per=1.0; rate=0.5; per=1.0; does not work because allowance will never grow to 1.0.不起作用,因为allowance永远不会增长到 1.0。 But rate=1.0; per=2.0;但是rate=1.0; per=2.0; rate=1.0; per=2.0; works fine.工作正常。

Use this decorator @RateLimited(ratepersec) before your function that enqueues.在排队的 function 之前使用这个装饰器 @RateLimited(ratepersec)。

Basically, this checks if 1/rate secs have passed since the last time and if not, waits the remainder of the time, otherwise it doesn't wait.基本上,这会检查自上次以来是否经过了 1/rate secs,如果没有,则等待剩余时间,否则不等待。 This effectively limits you to rate/sec.这有效地限制了您的速率/秒。 The decorator can be applied to any function you want rate-limited.装饰器可以应用于您想要限速的任何 function。

In your case, if you want a maximum of 5 messages per 8 seconds, use @RateLimited(0.625) before your sendToQueue function.在您的情况下,如果您希望每 8 秒最多 5 条消息,请在您的 sendToQueue function 之前使用 @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)

A Token Bucket is fairly simple to implement.令牌桶实现起来相当简单。

Start with a bucket with 5 tokens.从一个有 5 个令牌的存储桶开始。

Every 5/8 seconds: If the bucket has less than 5 tokens, add one.每 5/8 秒:如果桶中的令牌少于 5 个,则添加一个。

Each time you want to send a message: If the bucket has ≥1 token, take one token out and send the message.每次要发送消息:如果bucket中的token≥1,取出一个token发送消息。 Otherwise, wait/drop the message/whatever.否则,等待/丢弃消息/无论如何。

(obviously, in actual code, you'd use an integer counter instead of real tokens and you can optimize out the every 5/8s step by storing timestamps) (显然,在实际代码中,您将使用 integer 计数器而不是真正的令牌,您可以通过存储时间戳优化每 5/8 秒的步骤)


Reading the question again, if the rate limit is fully reset each 8 seconds, then here is a modification:再次阅读问题,如果速率限制每 8 秒完全重置一次,那么这里是一个修改:

Start with a timestamp, last_send , at a time long ago (eg, at the epoch).从很久以前的时间戳last_send开始(例如,在时代)。 Also, start with the same 5-token bucket.此外,从相同的 5 令牌桶开始。

Strike the every 5/8 seconds rule.执行每 5/8 秒规则。

Each time you send a message: First, check if last_send ≥ 8 seconds ago.每次发送消息:首先,检查last_send是否≥ 8 秒前。 If so, fill the bucket (set it to 5 tokens).如果是这样,请填充存储桶(将其设置为 5 个令牌)。 Second, if there are tokens in the bucket, send the message (otherwise, drop/wait/etc.).其次,如果桶中有令牌,则发送消息(否则,丢弃/等待/等)。 Third, set last_send to now.第三,将last_send设置为 now。

That should work for that scenario.这应该适用于那种情况。


I've actually written an IRC bot using a strategy like this (the first approach).我实际上已经使用这样的策略(第一种方法)编写了一个 IRC 机器人。 Its in Perl, not Python, but here is some code to illustrate:它在 Perl,而不是 Python,但这里有一些代码来说明:

The first part here handles adding tokens to the bucket.这里的第一部分处理向存储桶添加令牌。 You can see the optimization of adding tokens based on time (2nd to last line) and then the last line clamps bucket contents to the maximum (MESSAGE_BURST)您可以看到基于时间添加令牌的优化(第 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 is a data structure which is passed around. $conn 是一个被传递的数据结构。 This is inside a method that runs routinely (it calculates when the next time it'll have something to do, and sleeps either that long or until it gets network traffic).这是在一个例行运行的方法中(它计算下一次它什么时候有事情要做,并且休眠那么长时间或者直到它获得网络流量)。 The next part of the method handles sending.该方法的下一部分处理发送。 It is rather complicated, because messages have priorities associated with them.它相当复杂,因为消息具有与之相关的优先级。

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

That's the first queue, which is run no matter what.这是第一个队列,无论如何都会运行。 Even if it gets our connection killed for flooding.即使它使我们的连接因洪水而被杀死。 Used for extremely important things, like responding to the server's PING.用于极其重要的事情,例如响应服务器的 PING。 Next, the rest of the queues:接下来,队列的rest:

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

Finally, the bucket status is saved back to the $conn data structure (actually a bit later in the method; it first calculates how soon it'll have more work)最后,桶状态被保存回 $conn 数据结构(实际上在该方法中稍晚一点;它首先计算它多久会有更多工作)

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

As you can see, the actual bucket handling code is very small — about four lines.如您所见,实际的桶处理代码非常小——大约四行。 The rest of the code is priority queue handling.代码的rest是优先队列处理。 The bot has priority queues so that eg, someone chatting with it can't prevent it from doing its important kick/ban duties.该机器人具有优先级队列,因此与它聊天的人无法阻止它执行重要的踢/禁止职责。

to block processing until the message can be sent, thus queuing up further messages, antti's beautiful solution may also be modified like this:要阻塞处理直到可以发送消息,从而使更多消息排队,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;

it just waits until enough allowance is there to send the message.它只是等到有足够的余量来发送消息。 to not start with two times the rate, allowance may also initialized with 0.为了不以两倍的费率开始,津贴也可以用0初始化。

One solution is to attach a timestamp to each queue item and to discard the item after 8 seconds have passed.一种解决方案是为每个队列项目附加一个时间戳,并在 8 秒后丢弃该项目。 You can perform this check each time the queue is added to.您可以在每次添加队列时执行此检查。

This only works if you limit the queue size to 5 and discard any additions whilst the queue is full.仅当您将队列大小限制为 5 并在队列已满时丢弃任何添加时,这才有效。

Keep the time that the last five lines were sent.保留最后五行发送的时间。 Hold the queued messages until the time the fifth-most-recent message (if it exists) is a least 8 seconds in the past (with last_five as an array of times):保留排队的消息,直到第五个最近的消息(如果存在)过去至少 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()

If someone still interested, I use this simple callable class in conjunction with a timed LRU key value storage to limit request rate per IP.如果有人仍然感兴趣,我使用这个简单的可调用 class 和定时 LRU 键值存储来限制每个 IP 的请求率。 Uses a deque, but can rewritten to be used with a list instead.使用双端队列,但可以重写为与列表一起使用。

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

Just a python implementation of a code from accepted answer.只是来自已接受答案的代码的 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

I needed a variation in Scala.我需要 Scala 的变体。 Here it is:这里是:

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

}

Here is how it can be used:以下是它的使用方法:

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

yet another solution另一种解决方案

from collections import deque
from datetime import timedelta
from time import sleep

class RateLimiter:
    def __init__(self, items: int, per: timedelta = timedelta(seconds=1)):
        self.items = items
        self.per = per
        self.deque = deque(maxlen=items)

    def count(self):
        now = datetime.now()
        self.deque.append(now)

    def time_to_wait(self) -> timedelta:
        if len(self.deque) < self.deque.maxlen:
            return timedelta(0)
        now = datetime.now()
        per = now - self.deque[0]
        return max(timedelta(0), self.per - per)

    def throttle(self):
        sleep(self.time_to_wait().total_seconds())
        self.count()

if __name__ == '__main__':
    rate_limiter = RateLimiter(items=3, per=timedelta(seconds=3))

    for i in range(10):
        rate_limiter.throttle()
        print(f'{i}')

How about this:这个怎么样:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM