適切なレート制限アルゴリズムとは何ですか?
擬似コード、またはそれ以上のPythonを使用できます。 Python IRC=ボット、およびボット、それは部分的に動作しますが、誰かが制限より少ないメッセージをトリガーした場合(例えば、レート制限は8秒ごとに5メッセージであり、ユーザーがトリガーするのは4)のみであり、次のトリガーが8秒を超える場合(たとえば、16秒後)、ボットはメッセージを送信しますが、キューはいっぱいになり、ボットは待機します8秒が経過しているため、必要ではありませんが、8秒。
ここで 最も単純なアルゴリズム 、メッセージがあまりにも早く到着したときに単にドロップしたい場合(キューを任意に大きくする可能性があるため、キューに入れる代わりに):
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
ではありません)。例えば。 allowance
は決して1.0にならないため、rate=0.5; per=1.0;
は機能しません。ただし、rate=1.0; per=2.0;
は正常に機能します。
エンキューする関数の前に、このデコレーター@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 __== "__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つ追加します。
メッセージを送信するたびに:バケットに1つ以上のトークンがある場合、トークンを1つ取り出してメッセージを送信します。それ以外の場合は、メッセージを待機/ドロップします。
(明らかに、実際のコードでは、実際のトークンの代わりに整数カウンターを使用し、タイムスタンプを保存することで5/8秒ごとのステップを最適化できます)
質問をもう一度読んで、レート制限が8秒ごとに完全にリセットされる場合、ここに変更があります。
タイムスタンプで始まるlast_send
、昔(エポックなど)。また、同じ5トークンバケットから始めます。
5/8秒ごとの規則を打ちます。
メッセージを送信するたびに:まず、last_send
≥8秒前。その場合、バケットを埋めます(5トークンに設定します)。次に、バケットにトークンがある場合は、メッセージを送信します(そうでない場合は、drop/wait/etc。)。第三に、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行です。残りのコードは、優先キューの処理です。ボットには優先キューがあるため、たとえば、チャットをしている誰かが重要なキック/禁止の義務を果たすことを防ぐことはできません。
メッセージを送信できるようになるまで処理をブロックして、さらにメッセージをキューに入れるために、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で初期化される場合があります。
1つの解決策は、各キューアイテムにタイムスタンプを添付し、8秒が経過した後にアイテムを破棄することです。このチェックは、キューが追加されるたびに実行できます。
これは、キューサイズを5に制限し、キューがいっぱいの間に追加を破棄する場合にのみ機能します。
最後の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()
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
まだ興味がある人は、この単純な呼び出し可能クラスを時限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")
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(())}
これはどう:
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;
}