Thuật toán giới hạn tỷ lệ tốt là gì?


155

Tôi có thể sử dụng một số mã giả, hoặc tốt hơn, Python. Tôi đang cố gắng thực hiện hàng đợi giới hạn tốc độ cho bot IRC của Python và nó hoạt động một phần, nhưng nếu ai đó kích hoạt ít tin nhắn hơn giới hạn (ví dụ: giới hạn tốc độ là 5 tin nhắn trong 8 giây và người đó chỉ kích hoạt 4), và kích hoạt tiếp theo là hơn 8 giây (ví dụ: 16 giây sau), bot gửi tin nhắn, nhưng hàng đợi đã đầy và bot chờ 8 giây, mặc dù không cần thiết vì khoảng thời gian 8 giây đã hết.

Câu trả lời:


230

Ở đây thuật toán đơn giản nhất , nếu bạn chỉ muốn thả tin nhắn khi chúng đến quá nhanh (thay vì xếp hàng chúng, điều này có ý nghĩa vì hàng đợi có thể lớn tùy ý):

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;

Không có cơ sở dữ liệu, bộ hẹn giờ, v.v. trong giải pháp này và nó hoạt động sạch sẽ :) Để thấy điều này, 'phụ cấp' tăng trưởng với tốc độ tối đa 5/8 đơn vị mỗi giây, tức là nhiều nhất là năm đơn vị mỗi tám giây. Mỗi tin nhắn được chuyển tiếp sẽ trừ một đơn vị, vì vậy bạn không thể gửi nhiều hơn năm tin nhắn mỗi tám giây.

Lưu ý rằng ratephải là một số nguyên, nghĩa là không có phần thập phân khác không, hoặc thuật toán sẽ không hoạt động chính xác (tỷ lệ thực tế sẽ không rate/per). Ví dụ: rate=0.5; per=1.0;không hoạt động vì allowancesẽ không bao giờ tăng lên 1.0. Nhưng rate=1.0; per=2.0;hoạt động tốt.


4
Cũng đáng chỉ ra rằng kích thước và tỷ lệ của 'time_passed' phải giống với 'per', ví dụ: giây.
skaffman

2
Xin chào skaffman, cảm ơn vì những lời khen ngợi --- Tôi đã ném nó ra khỏi tay áo của tôi nhưng với xác suất 99,9% ai đó đã đưa ra một giải pháp tương tự :)
Antti Huima

51
Đó là một thuật toán tiêu chuẩn, đó là một thùng mã thông báo, không có hàng đợi. Cái xô là allowance. Kích thước xô là rate. Các allowance += …dòng là một tối ưu hóa thêm một mã thông báo mỗi tỷ lệ ÷ mỗi giây.
derobert

5
@zwirbeltier Những gì bạn viết ở trên là không đúng sự thật. 'Trợ cấp' luôn bị giới hạn bởi 'tốc độ' (nhìn vào "// ga" dòng) vì vậy nó sẽ chỉ cho phép một sự bùng phát chính xác 'tốc độ' thông điệp tại bất kỳ thời điểm cụ thể, tức là 5.
Antti Huima

7
Điều này là tốt, nhưng có thể vượt quá tỷ lệ. Giả sử tại thời điểm 0 bạn chuyển tiếp 5 tin nhắn, sau đó tại thời điểm N * (8/5) cho N = 1, 2, ... bạn có thể gửi một tin nhắn khác, dẫn đến hơn 5 tin nhắn trong khoảng thời gian 8 giây
mindvirus

47

Sử dụng trang trí này @RateLrict (Ratepersec) trước chức năng của bạn.

Về cơ bản, kiểm tra này nếu 1 / tốc độ giây đã trôi qua kể từ lần cuối cùng và nếu không, hãy đợi phần còn lại của thời gian, nếu không thì không chờ đợi. Điều này có hiệu quả giới hạn bạn để đánh giá / giây. Trình trang trí có thể được áp dụng cho bất kỳ chức năng nào bạn muốn giới hạn tỷ lệ.

Trong trường hợp của bạn, nếu bạn muốn có tối đa 5 tin nhắn trong 8 giây, hãy sử dụng @RateLrict (0.625) trước chức năng sendToQueue của bạ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)

Tôi thích ý tưởng sử dụng một trang trí cho mục đích này. Tại sao làm LastTimeCalled một danh sách? Ngoài ra, tôi nghi ngờ điều này sẽ làm việc khi nhiều chủ đề đang kêu gọi cùng chức năng RateLimited ...
Stephan202

8
Đó là một danh sách vì các loại đơn giản như float không đổi khi được đóng bởi một bao đóng. Bằng cách làm cho nó một danh sách, danh sách là không đổi, nhưng nội dung của nó thì không. Vâng, nó không an toàn cho luồng nhưng có thể dễ dàng sửa bằng khóa.
Carlos A. Ibarra

time.clock()không có đủ độ phân giải trên hệ thống của tôi, vì vậy tôi đã điều chỉnh mã và thay đổi để sử dụngtime.time()
mtrbean

3
Để giới hạn tốc độ, bạn chắc chắn không muốn sử dụng time.clock(), đo thời gian CPU đã trôi qua. Thời gian CPU có thể chạy nhanh hơn hoặc chậm hơn nhiều so với thời gian "thực tế". Bạn muốn sử dụng time.time()thay thế, đo thời gian treo tường (thời gian "thực tế").
John Wiseman

1
BTW cho các hệ thống sản xuất thực tế: thực hiện giới hạn tốc độ với lệnh gọi ngủ () có thể không phải là ý tưởng hay vì nó sẽ chặn luồng và do đó ngăn khách hàng khác sử dụng nó.
Maresh

28

Một Token Xô khá đơn giản để thực hiện.

Bắt đầu với một cái xô có 5 mã thông báo.

Cứ sau 5/8 giây: Nếu thùng có ít hơn 5 mã thông báo, hãy thêm một thùng.

Mỗi lần bạn muốn gửi tin nhắn: Nếu nhóm có to1 mã thông báo, hãy lấy một mã thông báo ra và gửi tin nhắn. Nếu không, chờ / thả tin nhắn / bất cứ điều gì.

(rõ ràng, trong mã thực tế, bạn sẽ sử dụng bộ đếm số nguyên thay vì mã thông báo thực và bạn có thể tối ưu hóa từng bước 5/8 bằng cách lưu trữ dấu thời gian)


Đọc lại câu hỏi, nếu giới hạn tốc độ được đặt lại hoàn toàn sau mỗi 8 giây, thì đây là một sửa đổi:

Bắt đầu với dấu thời gian, last_sendtại một thời điểm cách đây rất lâu (ví dụ: ở thời đại). Ngoài ra, hãy bắt đầu với nhóm 5 mã thông báo tương tự.

Tấn công cứ sau 5/8 giây.

Mỗi lần bạn gửi tin nhắn: Đầu tiên, hãy kiểm tra xem last_send8 giây trước. Nếu vậy, hãy đổ đầy xô (đặt thành 5 mã thông báo). Thứ hai, nếu có mã thông báo trong nhóm, hãy gửi tin nhắn (nếu không, thả / chờ / v.v.). Thứ ba, thiết lập last_sendđến bây giờ.

Điều đó sẽ làm việc cho kịch bản đó.


Tôi thực sự đã viết một bot IRC bằng cách sử dụng một chiến lược như thế này (cách tiếp cận đầu tiên). Nó ở Perl, không phải Python, nhưng đây là một số mã để minh họa:

Phần đầu tiên ở đây xử lý việc thêm mã thông báo vào nhóm. Bạn có thể thấy tối ưu hóa việc thêm mã thông báo dựa trên thời gian (dòng thứ 2 đến dòng cuối cùng) và sau đó dòng cuối cùng kẹp nội dung xô tối đa (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 là một cấu trúc dữ liệu được truyền qua. Đây là một phương thức chạy thường xuyên (nó sẽ tính toán khi lần sau nó sẽ có việc gì đó và ngủ lâu hoặc cho đến khi nó có lưu lượng truy cập mạng). Phần tiếp theo của phương thức xử lý việc gửi. Nó khá phức tạp, bởi vì các tin nhắn có các ưu tiên liên quan đến chúng.

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

Đó là hàng đầu tiên, được chạy không có vấn đề gì. Ngay cả khi nó được kết nối của chúng tôi bị giết vì lũ lụt. Được sử dụng cho những việc cực kỳ quan trọng, như phản hồi PING của máy chủ. Tiếp theo, phần còn lại của hàng đợi:

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

Cuối cùng, trạng thái nhóm được lưu trở lại cấu trúc dữ liệu $ Conn (thực tế là một lát sau trong phương thức; trước tiên, nó sẽ tính toán thời gian nó sẽ có nhiều công việc hơn)

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

Như bạn có thể thấy, mã xử lý xô thực tế rất nhỏ - khoảng bốn dòng. Phần còn lại của mã là xử lý hàng đợi ưu tiên. Bot có các hàng đợi ưu tiên để ví dụ, ai đó trò chuyện với nó không thể ngăn nó thực hiện các nhiệm vụ cấm / cấm quan trọng.


Tôi có thiếu thứ gì không ... có vẻ như điều này sẽ giới hạn bạn 1 tin nhắn cứ sau 8 giây sau khi bạn vượt qua 5
ớn lạnh42

@ chills42: Vâng, tôi đọc câu hỏi sai ... xem nửa sau của câu trả lời.
derobert

@chills: Nếu last_send là <8 giây, bạn không thêm bất kỳ mã thông báo nào vào nhóm. Nếu thùng của bạn chứa mã thông báo, bạn có thể gửi tin nhắn; nếu không thì bạn không thể (bạn đã gửi 5 tin nhắn trong 8 giây qua)
derobert

3
Tôi đánh giá cao nếu mọi người hạ thấp điều này sẽ giải thích tại sao ... Tôi muốn khắc phục mọi sự cố bạn thấy, nhưng điều đó khó thực hiện mà không có phản hồi!
derobert

10

để chặn xử lý cho đến khi tin nhắn có thể được gửi, do đó xếp hàng các tin nhắn tiếp theo, giải pháp tuyệt đẹp của antti cũng có thể được sửa đổi như sau:

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;

nó chỉ đợi cho đến khi có đủ tiền trợ cấp để gửi tin nhắn. để không bắt đầu với hai lần tỷ lệ, trợ cấp cũng có thể được khởi tạo bằng 0.


5
Khi bạn ngủ (1-allowance) * (per/rate), bạn cần thêm số tiền đó vào last_check.
Alp

2

Giữ thời gian mà năm dòng cuối cùng được gửi. Giữ các tin nhắn được xếp hàng cho đến thời điểm tin nhắn gần đây thứ năm (nếu nó tồn tại) là ít nhất 8 giây trong quá khứ (với last_five là một mảng thời gian):

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

Không phải vì bạn sửa đổi nó.
Pesto

Bạn đang lưu trữ năm tem thời gian và liên tục chuyển chúng qua bộ nhớ (hoặc thực hiện các hoạt động danh sách được liên kết). Tôi đang lưu trữ một bộ đếm số nguyên và một dấu thời gian. Và chỉ làm số học và chỉ định.
derobert

2
Ngoại trừ việc tôi sẽ hoạt động tốt hơn nếu cố gắng gửi 5 dòng nhưng chỉ cho phép thêm 3 dòng trong khoảng thời gian. Bạn sẽ cho phép gửi ba người đầu tiên và buộc phải chờ 8 giây trước khi gửi 4 và 5. Của tôi sẽ cho phép 4 và 5 được gửi 8 giây sau các dòng thứ tư và thứ năm gần đây nhất.
Pesto

1
Nhưng về chủ đề này, hiệu suất có thể được cải thiện thông qua việc sử dụng danh sách được liên kết vòng tròn có độ dài 5, chỉ đến lần gửi gần đây thứ năm, ghi đè lên lần gửi mới và di chuyển con trỏ về phía trước.
Pesto

đối với một bot irc với tốc độ giới hạn tốc độ không phải là vấn đề. tôi thích giải pháp danh sách vì nó dễ đọc hơn. câu trả lời xô được đưa ra là khó hiểu vì sửa đổi, nhưng cũng không có gì sai với nó.
jheriko

2

Một giải pháp là gắn dấu thời gian cho từng mục xếp hàng và loại bỏ mục đó sau 8 giây đã trôi qua. Bạn có thể thực hiện kiểm tra này mỗi khi hàng đợi được thêm vào.

Điều này chỉ hoạt động nếu bạn giới hạn kích thước hàng đợi xuống còn 5 và loại bỏ mọi bổ sung trong khi hàng đợi đã đầy.


1

Nếu ai đó vẫn quan tâm, tôi sử dụng lớp có thể gọi đơn giản này kết hợp với bộ lưu trữ giá trị khóa LRU được định thời gian để giới hạn tốc độ yêu cầu trên mỗi IP. Sử dụng một deque, nhưng có thể viết lại để được sử dụng với một danh sách thay thế.

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

Chỉ cần một con trăn thực hiện một mã từ câu trả lời được chấp nhận.

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

Nó đã được đề xuất với tôi rằng tôi đề nghị bạn thêm một ví dụ sử dụng mã của bạn .
Luc

0

Còn cái này thì sao:

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

Tôi cần một biến thể trong Scala. Đây là:

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

}

Đây là cách nó có thể được sử dụng:

val f = Limiter((5d, 8d), { 
  _: Unit  
    println(System.currentTimeMillis) 
})
while(true){f(())}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.