İyi bir hız sınırlama algoritması nedir?


155

Bazı sahte kodlar veya daha iyisi Python kullanabilirim. Bir Python IRC bot için bir hız sınırlama kuyruğu uygulamaya çalışıyorum ve kısmen çalışıyor, ancak birisi sınırdan daha az mesaj tetiklerse (örneğin, hız sınırı 8 saniyede 5 mesajdır ve kişi sadece 4 tetikler), ve bir sonraki tetikleyici 8 saniyenin üzerindedir (örn. 16 saniye sonra), bot mesajı gönderir, ancak 8 saniye süresinin dolmasından bu yana gerekli olmamasına rağmen, kuyruk dolar ve 8 saniye bekler.

Yanıtlar:


231

İşte en basit algoritma , mesajları çok hızlı bir şekilde ulaştıklarında bırakmak istiyorsanız (kuyrukları sıralamak yerine, kuyruk rastgele büyüyebileceğinden mantıklıdır):

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;

Bu çözümde veri yapısı, zamanlayıcı vb. Yoktur ve temiz çalışır :) Bunu görmek için, 'izin' en fazla saniyede 5/8 birim, yani sekiz saniyede en fazla beş birim hızında büyür. Yönlendirilen her mesaj bir üniteyi azaltır, böylece sekiz saniyede bir beşten fazla mesaj gönderemezsiniz.

Bunun ratebir tamsayı olması gerekir, yani sıfır olmayan ondalık kısım olmadan veya algoritma düzgün çalışmaz (gerçek oran olmayacaktır rate/per). Örneğin rate=0.5; per=1.0;çalışmaz, çünkü allowanceasla 1.0'a çıkmayacak. Ama iyi rate=1.0; per=2.0;çalışıyor.


4
Ayrıca 'time_passed' boyutunun ve ölçeğinin 'per' ile aynı olması gerektiğini belirtmek gerekir, örneğin saniye.
skaffman

2
Merhaba skaffman, övgü için teşekkürler --- onu
kolumdan

52
Bu standart bir algoritma - kuyruksuz bir token kovası. Kova allowance. Kova boyutu rate. allowance += …Çizgi her belirteç bir ekleme bir optimizasyon olan oran ÷ başına saniye.
derobert

5
@zwirbeltier Yukarıda yazdıklarınız doğru değil. 'Ödenek' her zaman 'rate' ile
sınırlıdır

8
Bu iyidir, ancak oranı aşabilir. Diyelim ki 0 zamanında 5 mesaj iletiyorsunuz, sonra N = 1, 2 için N * (8/5) zamanında ... başka bir mesaj göndererek 8 saniyelik bir sürede
5'ten

48

Sıkıştırma işlevinizden önce bu dekoratör @RateLimited (ratepersec) kullanın.

Temel olarak, bu son zamandan bu yana 1 / rate sn'nin geçip geçmediğini kontrol eder ve eğer değilse, kalan süreyi bekler, aksi takdirde beklemez. Bu etkili bir şekilde hız / sn sınırlar. Dekoratör, hız sınırlı olmasını istediğiniz herhangi bir işleve uygulanabilir.

Sizin durumunuzda, 8 saniyede en fazla 5 mesaj istiyorsanız, sendToQueue işlevinizden önce @RateLimited (0.625) kullanın.

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)

Bu amaçla bir dekoratör kullanma fikrini seviyorum. LastTimeCalled neden bir liste? Ayrıca, birden fazla iş parçacığı aynı RateLimited işlevini çağırdığınızda bu çalışacağından şüpheliyim ...
Stephan202

8
Bu bir liste çünkü şamandıra gibi basit tipler bir kapakla yakalandığında sabittir. Bir liste yaparak liste sabittir, ancak içeriği sabit değildir. Evet, diş için güvenli değildir, ancak kilitlerle kolayca sabitlenebilir.
Carlos A. Ibarra

time.clock()benim sistemimde yeterli çözünürlük yok, bu yüzden kodu adapte ve kullanmak için değiştirdimtime.time()
mtrbean

3
Hız sınırlaması için, time.clock()geçen CPU süresini ölçen kesinlikle kullanmak istemezsiniz . CPU zamanı "gerçek" zamandan çok daha hızlı veya daha yavaş çalışabilir. Bunun time.time()yerine, duvar süresini ("gerçek" zaman) ölçen kullanmak istiyorsunuz .
John Wiseman

1
Gerçek üretim sistemleri için BTW: bir sleep () çağrısı ile bir hız sınırlaması uygulamak, iş parçacığını engelleyeceği ve bu nedenle başka bir istemcinin kullanmasını engelleyeceği için iyi bir fikir olmayabilir.
Maresh

28

Bir Token Kova'nın uygulanması oldukça basittir.

5 jetonlu bir kova ile başlayın.

Her 5/8 saniyede bir: Kepçede 5'ten az jeton varsa bir tane ekleyin.

Her mesaj göndermek istediğinizde: Grupta ≥1 jeton varsa, bir jeton alın ve mesajı gönderin. Aksi takdirde, mesajı bekleyin / bırakın.

(açıkçası, gerçek kodda, gerçek jetonlar yerine bir tamsayı sayacı kullanırsınız ve her 5 / 8s adımını zaman damgalarını kaydederek optimize edebilirsiniz)


Soruyu tekrar okumak, hız limiti her 8 saniyede bir tamamen sıfırlanırsa, işte bir değişiklik:

Bir zaman damgası ile başlayın, last_senduzun zaman önce (örneğin, çağda). Ayrıca, aynı 5 jetonlu kova ile başlayın.

Her 5/8 saniyede bir kuralı vurun.

Her mesaj gönderdiğinizde: İlk olarak, last_send≥ 8 saniye önce olup olmadığını kontrol edin . Öyleyse, kovayı doldurun (5 jetona ayarlayın). İkincisi, kovada jetonlar varsa, mesajı gönderin (aksi takdirde bırakın / bekleyin / vb.). Üçüncü olarak, last_sendşimdi ayarlayın .

Bu senaryo için işe yarayacak.


Aslında böyle bir stratejiyi kullanarak bir IRC bot yazdım (ilk yaklaşım). Perl'de, Python'da değil, ancak göstermek için bazı kodlar:

Buradaki ilk bölüm, kovaya jeton eklemeyi ele alır. Jeton ekleme optimizasyonunu zamana göre (2'den son satıra) ve son satırda kova içeriğini maksimuma (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 aktarılan bir veri yapısıdır. Bu, rutin olarak çalışan bir yöntemin içindedir (bir dahaki sefere yapacak bir şey olduğunda ne zaman hesaplar ve bu kadar uzun süre veya ağ trafiği alana kadar uyur). Yöntemin bir sonraki bölümü gönderme işlemini gerçekleştirir. Oldukça karmaşıktır, çünkü mesajların kendileriyle ilişkili öncelikleri vardır.

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

Bu ne olursa olsun çalıştırılan ilk kuyruk. Sel bağlantımız yüzünden bağlantımız kesilse bile. Sunucunun PING'ine yanıt vermek gibi son derece önemli şeyler için kullanılır. Sonra, sıraların geri kalanı:

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

Son olarak, grup durumu $ conn veri yapısına geri kaydedilir (aslında yöntemde biraz sonra; önce ne kadar süre daha fazla çalışma yapacağını hesaplar)

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

Gördüğünüz gibi, gerçek kova işleme kodu çok küçük - yaklaşık dört satır. Kodun geri kalanı öncelik sırası işlemedir. Botun öncelik sıraları vardır, böylece onunla sohbet eden biri önemli vuruş / yasak görevlerini yapmasını engelleyemez.


Bir şey eksik miyim ... Bu ilk 5 geçtikten sonra her 8 saniyede bir 1 mesajla sınırlı gibi görünüyor
chills42

@ chills42: Evet, soruyu yanlış okudum ... cevabın ikinci yarısına bakın.
derobert

@chills: last_send <8 saniye ise, gruba jeton eklemezsiniz. Grupunuz jeton içeriyorsa, mesajı gönderebilirsiniz; Aksi takdirde yapamazsınız (son 8 saniyede 5 mesaj gönderdiniz)
derobert

3
Bunu önemsemeyen insanlar nedenini açıklarlarsa memnun olurum ... Gördüğünüz sorunları düzeltmek istiyorum, ancak geri bildirim olmadan bunu yapmak zor!
derobert

10

mesaj gönderilinceye kadar işlemeyi engellemek, böylece başka mesajlar kuyruğa almak için antti'nin güzel çözümü de şu şekilde değiştirilebilir:

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;

sadece mesajı göndermek için yeterli izin gelene kadar bekler. oranın iki katı ile başlamaması durumunda, ödenek 0 ile de başlatılabilir.


5
Uyuduğunuzda (1-allowance) * (per/rate), aynı miktarı eklemeniz gerekir last_check.
Alp

2

Son beş satırın gönderildiği zamanı koruyun. Sıraya alınan mesajları, en son beşinci mesaj (varsa) geçmişte en az 8 saniye olana kadar (last_five bir dizi olarak) tutun:

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

Revize ettiğinizden beri değilim.
Pesto

Beş zaman damgası saklıyorsunuz ve bunları bellekte tekrar tekrar kaydırıyorsunuz (veya bağlantılı liste işlemleri yapıyorsunuz). Bir tamsayı sayacı ve bir zaman damgası saklıyorum. Ve sadece aritmetik ve atama yapmak.
derobert

2
Ancak 5 satır göndermeye çalışırken benimki daha iyi çalışacak, ancak zaman diliminde sadece 3 tane daha izin verilecek. Seninki ilk üçünü göndermeye izin verecek ve 4 ve 5'i göndermeden önce 8 saniye beklemeye zorlayacak. Benimki 4 ve 5'in dördüncü ve beşinci en son satırlardan 8 saniye sonra gönderilmesine izin verecek.
Pesto

1
Ancak konuyla ilgili olarak, 5 numaralı en son gönderimi işaret eden, yeni gönderimin üzerine yazarak ve işaretçiyi ileriye doğru hareket ettirerek 5 numaralı dairesel bağlantılı bir liste kullanarak performans iyileştirilebilir.
Pesto

Hız sınırlayıcı hızı olan bir irc botu için sorun yoktur. daha okunabilir olduğundan liste çözümünü tercih ederim. verilen kova cevabı revizyon nedeniyle kafa karıştırıcı, ama onunla da yanlış bir şey yok.
jheriko

2

Bir çözüm, her kuyruk öğesine bir zaman damgası eklemek ve öğeyi 8 saniye geçtikten sonra atmaktır. Kuyruğa her eklendiğinde bu kontrolü yapabilirsiniz.

Bu, yalnızca kuyruk boyutunu 5 ile sınırlandırır ve kuyruk dolu durumdayken eklemeleri atarsanız çalışır.


1

Birisi hala ilgileniyorsa, IP başına istek oranını sınırlamak için bu basit çağrılabilir sınıfı zamanlanmış LRU anahtar değeri depolama ile birlikte kullanıyorum. Bir deque kullanır, ancak bunun yerine bir listeyle kullanılmak üzere yeniden yazılabilir.

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

Kabul edilen cevaptan bir kodun sadece bir python uygulaması.

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

Edilmiş bana önerdi Sana eklemek öneririz kodunuzun kullanım örneği .
Luc

0

Buna ne dersin:

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'da bir varyasyona ihtiyacım vardı. İşte burada:

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

}

İşte nasıl kullanılabilir:

val f = Limiter((5d, 8d), { 
  _: Unit  
    println(System.currentTimeMillis) 
})
while(true){f(())}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.