Oto najprostszy algorytm , jeśli chcesz po prostu upuścić wiadomości, gdy nadejdą zbyt szybko (zamiast ustawiać je w kolejce, co ma sens, ponieważ kolejka może stać się dowolnie duża):
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;
W tym rozwiązaniu nie ma datastruktur, timerów itp. I działa ono bezproblemowo :) Aby to zobaczyć, „przydział” rośnie z prędkością maksymalnie 5/8 jednostek na sekundę, czyli maksymalnie pięć jednostek na osiem sekund. Każda przekazana wiadomość odejmuje jedną jednostkę, więc nie możesz wysłać więcej niż pięciu wiadomości w ciągu ośmiu sekund.
Zauważ, że rate
powinna to być liczba całkowita, tj. Bez niezerowej części dziesiętnej, w przeciwnym razie algorytm nie będzie działał poprawnie (rzeczywista stawka nie będzie rate/per
). Np. rate=0.5; per=1.0;
Nie działa, ponieważ allowance
nigdy nie wzrośnie do 1,0. Ale rate=1.0; per=2.0;
działa dobrze.
allowance
. Rozmiar łyżki torate
.allowance += …
Linia jest optymalizacja dodając token każdej stopy ÷ jednej sekundy.Użyj tego dekoratora @RateLimited (ratepersec) przed funkcją, która kolejkuje.
Zasadniczo sprawdza, czy minęła 1 / rate secs od ostatniego razu, a jeśli nie, czeka resztę czasu, w przeciwnym razie nie czeka. To skutecznie ogranicza cię do stawki / sek. Dekorator można zastosować do dowolnej funkcji, która ma ograniczoną cenę.
W twoim przypadku, jeśli chcesz maksymalnie 5 wiadomości na 8 sekund, użyj @RateLimited (0.625) przed funkcją sendToQueue.
źródło
time.clock()
nie ma wystarczającej rozdzielczości w moim systemie, więc dostosowałem kod i zmieniłem go do użyciatime.time()
time.clock()
, który mierzy upływający czas procesora. Czas procesora może działać znacznie szybciej lub znacznie wolniej niż „rzeczywisty” czas. Zamiast tego chcesz użyćtime.time()
, który mierzy czas ściany („rzeczywisty” czas).Token Bucket jest dość prosty do zaimplementowania.
Zacznij od wiadra z 5 tokenami.
Co 5/8 sekund: jeśli w wiadrze jest mniej niż 5 żetonów, dodaj jeden.
Za każdym razem, gdy chcesz wysłać wiadomość: Jeśli wiadro ma ≥1 token, wyjmij jeden token i wyślij wiadomość. W przeciwnym razie poczekaj / upuść wiadomość / cokolwiek.
(oczywiście w rzeczywistym kodzie używałbyś licznika liczb całkowitych zamiast prawdziwych tokenów i możesz zoptymalizować co 5 / 8s krok, przechowując znaczniki czasu)
Czytając ponownie pytanie, jeśli limit szybkości jest całkowicie resetowany co 8 sekund, oto modyfikacja:
Zacznij od sygnatury
last_send
czasowej dawno temu (np. W epoce). Zacznij też od tego samego zasobnika składającego się z 5 tokenów.Zastosuj regułę co 5/8 sekund.
Za każdym razem, gdy wysyłasz wiadomość: Najpierw sprawdź, czy
last_send
≥ 8 sekund temu. Jeśli tak, napełnij wiadro (ustaw na 5 żetonów). Po drugie, jeśli w wiadrze znajdują się tokeny, wyślij wiadomość (w przeciwnym razie upuść / czekaj / itp.). Po trzecie, ustawionelast_send
na teraz.To powinno działać w tym scenariuszu.
Właściwie napisałem bota IRC używając takiej strategii (pierwsze podejście). Jest w Perlu, nie w Pythonie, ale oto kod do zilustrowania:
Pierwsza część dotyczy dodawania tokenów do wiadra. Możesz zobaczyć optymalizację dodawania tokenów na podstawie czasu (od drugiej do ostatniej linii), a następnie ostatnia linia ogranicza zawartość wiadra do maksimum (MESSAGE_BURST)
$ conn to przekazywana struktura danych. Jest to metoda, która działa rutynowo (oblicza, kiedy następnym razem będzie mieć coś do zrobienia, i śpi tak długo lub do momentu, gdy uzyska ruch w sieci). Kolejna część metody zajmuje się wysyłaniem. Jest to dość skomplikowane, ponieważ wiadomości mają przypisane priorytety.
To pierwsza kolejka, która jest uruchamiana bez względu na wszystko. Nawet jeśli spowoduje to utratę połączenia z powodu zalania. Używane do niezwykle ważnych rzeczy, takich jak odpowiadanie na PING serwera. Następnie pozostałe kolejki:
Wreszcie status zasobnika jest zapisywany z powrotem w strukturze danych $ conn (właściwie nieco później w metodzie; najpierw oblicza, jak szybko będzie miał więcej pracy)
Jak widać, rzeczywisty kod obsługi zasobnika jest bardzo mały - około czterech wierszy. Pozostała część kodu to priorytetowa obsługa kolejek. Bot ma kolejki priorytetowe, więc np. Ktoś z nim rozmawiający nie może przeszkodzić mu w wykonywaniu ważnych obowiązków związanych z wyrzucaniem / banowaniem.
źródło
aby zablokować przetwarzanie do momentu wysłania wiadomości, a tym samym kolejkowanie kolejnych wiadomości, piękne rozwiązanie antti można również zmodyfikować w następujący sposób:
po prostu czeka, aż będzie wystarczająca ilość uprawnień do wysłania wiadomości. aby nie rozpoczynać się od dwukrotności stawki, zasiłek może być również zainicjowany wartością 0.
źródło
(1-allowance) * (per/rate)
, musisz dodać tę samą kwotęlast_check
.Zachowaj czas wysłania ostatnich pięciu wierszy. Zatrzymaj wiadomości w kolejce do czasu, gdy piąta najnowsza wiadomość (jeśli istnieje) znajdzie się co najmniej 8 sekund w przeszłości (z last_five jako tablicą czasów):
źródło
Jednym z rozwiązań jest dołączenie znacznika czasu do każdego elementu w kolejce i odrzucenie elementu po upływie 8 sekund. Możesz wykonać to sprawdzenie za każdym razem, gdy kolejka jest dodawana do.
Działa to tylko wtedy, gdy ograniczysz rozmiar kolejki do 5 i odrzucisz wszelkie dodatki, gdy kolejka jest pełna.
źródło
Jeśli ktoś nadal jest zainteresowany, używam tej prostej wywoływalnej klasy w połączeniu z czasowym przechowywaniem wartości klucza LRU, aby ograniczyć liczbę żądań na IP. Używa deque, ale może zostać przepisany, aby był używany z listą.
źródło
Po prostu pythonowa implementacja kodu z zaakceptowanej odpowiedzi.
źródło
Co powiesz na to:
źródło
Potrzebowałem odmiany Scali. Oto ona:
Oto, jak można go użyć:
źródło