良いレート制限アルゴリズムとは何ですか?


155

疑似コード、またはそれ以上のPythonを使用できます。私はPython IRCボットのレート制限キューを実装しようとしていますが、部分的には機能しますが、トリガーされるメッセージが制限よりも少ない場合(たとえば、レート制限は8秒あたり5メッセージで、トリガーされるのは4秒のみ)、次のトリガーが8秒を超えると(たとえば、16秒後)、ボットはメッセージを送信しますが、キューがいっぱいになり、ボットは8秒待機します。

回答:


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ユニット、つまり最大で8秒間に5ユニットの速度で増加します。転送されるメッセージごとに1ユニットが差し引かれるので、8秒ごとに5つを超えるメッセージを送信することはできません。

rate整数でなければならないことに注意してください。つまり、ゼロ以外の小数部がないと、アルゴリズムは正しく機能しません(実際のレートはにならないrate/per)。たとえば、1.0にrate=0.5; per=1.0;はならないため、機能しませんallowance。しかし、rate=1.0; per=2.0;正常に動作します。


4
また、 'time_passed'の次元とスケールは 'per'と同じでなければなりません(例:秒)。
skaffman 2009年

2
こんにちはskaffman、お世辞に感謝します---私は袖からそれを投げましたが、99.9%の確率で誰かが以前に同様の解決策を考え出しました:)
Antti Huima

52
これは標準的なアルゴリズムであり、キューのないトークンバケットです。バケットはallowanceです。バケットサイズはrateです。allowance += …ラインは、すべてのトークンの追加の最適化である ÷ あたりの秒。
derobert

5
@zwirbeltier上記の記述は正しくありません。「手当」それが唯一の任意の特定の時間に正確に「率」メッセージのバーストを許可しますので、常に「率」(「//スロットル」の行を見て)によってキャップされている、すなわち5
アンティHuima

8
これは良いですが、レートを超える可能性があります。時間0で5つのメッセージを転送し、時間N *(8/5)でN = 1、2、...とすると、別のメッセージを送信できるため、8秒間に5つ以上のメッセージが送信されます
mindvirus

48

エンキューする関数の前にこのデコレータ@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

8
floatのような単純な型は、クロージャーによってキャプチャされたときに定数であるため、これはリストです。リストにすることで、リストは一定になりますが、その内容は一定ではありません。はい、スレッドセーフではありませんが、ロックで簡単に修正できます。
カルロスA.イバラ

time.clock()私のシステムでは十分な解像度がないため、コードを調整して使用するように変更しましたtime.time()
mtrbean

3
レート制限の場合time.clock()、CPUの経過時間を測定するは絶対に使用しないでください。CPU時間は、「実際の」時間よりもはるかに速くまたは遅く実行できます。time.time()代わりに、実時間(「実際の」時間)を測定するものを使用します。
John Wiseman

1
実際の本番システムのBTW:スレッドをブロックし、別のクライアントがスレッドを使用できないようにするため、sleep()呼び出しでレート制限を実装することはお勧めできません。
Maresh、2016

28

トークンバケットの実装はかなり簡単です。

5トークンのバケットから始めます。

5/8秒ごと:バケットのトークンが5つ未満の場合は、トークンを1つ追加します。

メッセージを送信するたびに:バケットに1つ以上のトークンがある場合、トークンを1つ取り出してメッセージを送信します。それ以外の場合は、待機/メッセージのドロップ/何でも。

(明らかに、実際のコードでは、実際のトークンの代わりに整数カウンターを使用し、タイムスタンプを格納することで5/8秒ごとのステップを最適化できます)


質問をもう一度読んで、レート制限が8秒ごとに完全にリセットされた場合は、次のように変更します。

タイムスタンプでlast_send、かなり前の時点(たとえば、エポック)から始めます。また、同じ5トークンのバケットから始めます。

5/8秒ごとのルールを実行します。

メッセージを送信するたび:まず、last_send8秒以上前かどうかを確認します。その場合は、バケットに入力します(5トークンに設定します)。次に、バケットにトークンがある場合は、メッセージを送信します(それ以外の場合は、ドロップ/待機など)。第三に、last_send今に設定します。

それはそのシナリオでうまくいくはずです。


私は実際にこのような戦略(最初のアプローチ)を使用してIRCボットを作成しました。これはPythonではなくPerlですが、以下に示すコードをいくつか示します。

ここの最初の部分は、バケットへのトークンの追加を処理します。時間に基づいてトークンを追加する最適化(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;

ご覧のとおり、実際のバケット処理コードは非常に小さく、約4行です。コードの残りの部分は優先キューの処理です。ボットには優先キューがあるため、たとえば、ボットとチャットしている人がボットの重要なキック/禁止作業を妨げることはできません。


私は何かを見逃していますか...これは最初の5を通過した後8秒ごとに1メッセージに制限するように見えます
chills42

@ chills42:はい、質問を間違って読みました...回答の後半を参照してください。
derobert 2009年

@chills:last_sendが8秒未満の場合、バケットにトークンを追加しません。バケットにトークンが含まれている場合は、メッセージを送信できます。それ以外の場合はできません(過去8
秒間に

3
これに反対票を投じた人々が理由を説明していただければ幸いです...表示された問題を修正したいのですが、フィードバックなしではそれは困難です!
derobert 2009年

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;

メッセージを送信するための十分な余裕ができるまで待つだけです。2倍のレートで開始しないようにするために、アローワンスも0で初期化できます。


5
あなたが眠るとき(1-allowance) * (per/rate)、あなたはそれに同じ量を加える必要がありますlast_check
Alp 2015

2

最後の5行が送信された時間を記録します。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()

あなたがそれを改訂して以来、私はそうではありません。
ペスト

5つのタイムスタンプを保存し、それらをメモリ内で繰り返しシフトします(またはリンクリスト操作を実行します)。1つの整数カウンタと1つのタイムスタンプを保存しています。そして、算術演算と代入のみを行います。
derobert 2009年

2
ただし、期間内に許可されているのは5行だけですが、3行しか許可されない場合は機能が向上します。あなたの最初の3つを送信することを許可し、4と5を送信する前に8秒の待機を強制します。鉱山は、4番目と5番目に最近の行の8秒後に4と5を送信できるようにします。
ペスト

1
しかし、この件に関しては、長さ5の循環リンクリストを使用して、5番目に新しい送信を指し、新しい送信で上書きし、ポインタを1つ前に移動することで、パフォーマンスを改善できます。
ペスト

レートリミッターの速度を持つircボットの場合は問題ありません。読みやすいので、リストソリューションの方が好きです。与えられたバケットの回答は改訂のために混乱していますが、それにも問題はありません。
jheriko 2009年

2

1つの解決策は、各キューアイテムにタイムスタンプを添付し、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

されてきた私に提案し、私はあなたが追加することを提案することをあなたのコードの使用例を
Luc、

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(())}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.