อัลกอริทึม จำกัด อัตราที่ดีคืออะไร


155

ฉันสามารถใช้โค้ดหลอกบางอย่างหรือดีกว่า Python ฉันพยายามใช้คิว จำกัด อัตราสำหรับบอท Python IRC และทำงานได้บางส่วน แต่ถ้ามีคนทริกเกอร์ข้อความน้อยกว่าขีด จำกัด (เช่นขีด จำกัด อัตราคือ 5 ข้อความต่อ 8 วินาทีและบุคคลเรียกเพียง 4) และทริกเกอร์ถัดไปนั้นใช้เวลาเกิน 8 วินาที (เช่น 16 วินาทีต่อมา) บอทจะส่งข้อความ แต่คิวจะเต็มและบอทรอ 8 วินาทีแม้ว่ามันจะไม่จำเป็นตั้งแต่ 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;

ไม่มี datastructures, ตัวนับและอื่น ๆ ในการแก้ปัญหานี้และทำงานได้อย่างสมบูรณ์ :) เมื่อต้องการดูสิ่งนี้ 'ค่าเผื่อ' จะเพิ่มขึ้นที่ความเร็ว 5/8 หน่วยต่อวินาทีมากที่สุดเช่นสูงสุดห้าหน่วยต่อแปดวินาที ทุกข้อความที่ถูกส่งต่อจะหักหนึ่งหน่วยดังนั้นคุณจะไม่สามารถส่งได้มากกว่าห้าข้อความต่อทุก ๆ แปดวินาที

โปรดทราบว่าrateควรเป็นจำนวนเต็มเช่นไม่มีส่วนทศนิยมที่ไม่เป็นศูนย์มิฉะนั้นอัลกอริทึมจะทำงานไม่ถูกต้อง (อัตราจริงจะไม่เป็นrate/per) เช่นrate=0.5; per=1.0;ไม่ทำงานเพราะallowanceจะไม่เติบโตเป็น 1.0 แต่ใช้rate=1.0; per=2.0;งานได้ดี


4
นอกจากนี้ยังควรชี้ให้เห็นว่าขนาดและมาตราส่วนของ 'time_passed' ต้องเหมือนกับ 'ต่อ' เช่นวินาที
skaffman

2
สวัสดี skaffman ขอบคุณสำหรับคำชมเชย --- ฉันโยนมันออกมาจากแขนของฉัน แต่กับ 99.9% มีใครน่าจะได้ก่อนหน้านี้ขึ้นมาด้วยวิธีการแก้ปัญหาที่คล้ายกัน :)
Antti Huima

52
นั่นคืออัลกอริธึมมาตรฐาน - เป็นที่ฝากโทเค็นโดยไม่มีคิว allowanceถังคือ rateขนาดถัง allowance += …บรรทัดเป็นการเพิ่มประสิทธิภาพของการเพิ่มโทเค็นทุกอัตรา ÷ ต่อวินาที
Derobert

5
@zwirbeltier สิ่งที่คุณเขียนด้านบนไม่เป็นความจริง 'เบี้ยเลี้ยง' ถูก จำกัด โดย 'rate' (ดูที่บรรทัด "// throttle") ดังนั้นจะอนุญาตให้มีการส่งข้อความ 'rate' อย่างแน่นอนในเวลาใดก็ได้เช่น 5
Antti Huima

8
นี่เป็นสิ่งที่ดี แต่อาจเกินอัตรา สมมติว่าในเวลา 0 คุณส่งต่อ 5 ข้อความจากนั้นในเวลา N * (8/5) สำหรับ N = 1, 2, ... คุณสามารถส่งข้อความอื่นส่งผลให้มากกว่า 5 ข้อความในระยะเวลา 8 วินาที
mindvirus

48

ใช้มัณฑนากรนี้ @RateLimited (ratepersec) ก่อนที่ฟังก์ชันของคุณจะจัดคิว

โดยพื้นฐานแล้วสิ่งนี้จะตรวจสอบว่า 1 / อัตราวินาทีได้ผ่านไปแล้วตั้งแต่ครั้งล่าสุดหรือไม่หากไม่รอให้เวลาที่เหลืออยู่ไม่เช่นนั้นจะไม่รอ สิ่งนี้ จำกัด คุณให้คะแนน / วินาทีได้อย่างมีประสิทธิภาพ มัณฑนากรสามารถนำไปใช้กับฟังก์ชั่นที่คุณต้องการอัตรา จำกัด

ในกรณีของคุณหากคุณต้องการสูงสุด 5 ข้อความต่อ 8 วินาทีให้ใช้ @RateLimited (0.625) ก่อนหน้าฟังก์ชั่น sendToQueue ของคุณ

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)

ฉันชอบความคิดในการใช้มัณฑนากรเพื่อจุดประสงค์นี้ ทำไมเป็นครั้งสุดท้ายที่เรียกว่ารายการ? นอกจากนี้ผมสงสัยนี้จะทำงานเมื่อหลายหัวข้อที่มีการเรียกฟังก์ชัน RateLimited เดียวกัน ...
Stephan202

8
มันเป็นรายการเพราะประเภทง่าย ๆ เช่นลอยเป็นค่าคงที่เมื่อถูกจับโดยการปิด เมื่อทำให้รายการเป็นรายการจะคงที่ แต่เนื้อหาไม่อยู่ในรายการ ใช่มันไม่ได้ปลอดภัยสำหรับเธรด แต่สามารถแก้ไขได้อย่างง่ายดายด้วยการล็อก
Carlos A. Ibarra

time.clock()ระบบของฉันมีความละเอียดไม่เพียงพอดังนั้นฉันจึงปรับรหัสและเปลี่ยนเป็นใช้time.time()
mtrbean

3
สำหรับการ จำกัด อัตราคุณไม่ต้องการใช้อย่างแน่นอนtime.clock()ซึ่งวัดเวลา CPU ที่ผ่านไป เวลาของ CPU สามารถทำงานได้เร็วขึ้นหรือช้าลงกว่าเวลาจริง คุณต้องการใช้time.time()แทนซึ่งจะวัดเวลากำแพง (เวลา "ตามจริง")
John Wiseman

1
BTW สำหรับระบบการผลิตจริง: การใช้อัตรา จำกัด ด้วยการโทร sleep () อาจไม่เป็นความคิดที่ดีเนื่องจากจะบล็อกเธรดและทำให้ไคลเอ็นต์อื่นไม่สามารถใช้งานได้
Maresh

28

Token Bucket นั้นใช้งานง่าย

เริ่มต้นด้วย bucket กับโทเค็น 5 อัน

ทุก 5/8 วินาที: หากที่เก็บข้อมูลมีน้อยกว่า 5 โทเค็นให้เพิ่มอีกหนึ่งอัน

ทุกครั้งที่คุณต้องการส่งข้อความ: หากที่เก็บข้อมูลมีโทเค็น≥1ให้นำโทเค็นหนึ่งอันออกแล้วส่งข้อความ มิฉะนั้นรอ / วางข้อความ / อะไรก็ตาม

(เห็นได้ชัดว่าในรหัสจริงคุณจะต้องใช้ตัวนับจำนวนเต็มแทนโทเค็นจริงและคุณสามารถปรับขั้นตอนทุก ๆ 5/8 โดยการจัดเก็บเวลาประทับ)


อ่านคำถามอีกครั้งหากขีด จำกัด อัตราถูกรีเซ็ตอย่างสมบูรณ์ในแต่ละ 8 วินาทีจากนั้นนี่คือการปรับเปลี่ยน:

เริ่มต้นด้วยการประทับเวลาlast_sendในเวลานานมาแล้ว (เช่นที่ยุค) เริ่มต้นด้วยที่ฝากข้อมูล 5 โทเค็นเดียวกัน

โจมตีกฎทุก ๆ 5/8 วินาที

ทุกครั้งที่คุณส่งข้อความ: อันดับแรกให้ตรวจสอบว่าlast_send≥ 8 วินาทีก่อน ถ้าเป็นเช่นนั้นเติมที่ฝากข้อมูล (ตั้งค่าเป็น 5 โทเค็น) ประการที่สองหากมีโทเค็นอยู่ในที่ฝากข้อมูลให้ส่งข้อความ (มิฉะนั้นปล่อย / รอ / ฯลฯ ) ประการที่สามตั้งค่าlast_sendเป็นตอนนี้

ที่ควรใช้กับสถานการณ์นั้น


ฉันเคยเขียนบอท IRC โดยใช้กลยุทธ์แบบนี้ (แนวทางแรก) มันอยู่ใน Perl ไม่ใช่ Python แต่นี่คือโค้ดที่จะอธิบาย:

ส่วนแรกที่นี่จัดการการเพิ่มโทเค็นลงในที่ฝากข้อมูล คุณสามารถดูการเพิ่มประสิทธิภาพของการเพิ่มโทเค็นตามเวลา (บรรทัดที่ 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;

อย่างที่คุณเห็นรหัสการจัดการถังจริงมีขนาดเล็กมาก - ประมาณสี่บรรทัด ส่วนที่เหลือของรหัสคือการจัดการคิวลำดับความสำคัญ บอทมีคิวที่มีลำดับความสำคัญสูงเช่นมีคนพูดคุยกับมันไม่สามารถป้องกันไม่ให้ทำหน้าที่เตะ / แบนที่สำคัญ


ฉันทำบางสิ่งบางอย่างขาดหายไป ... ดูเหมือนว่านี่จะ จำกัด คุณเป็น 1 ข้อความทุก ๆ 8 วินาทีหลังจากที่คุณผ่าน 5 ครั้งแรก
chills42

@ chills42: ใช่ฉันอ่านคำถามผิด ... ดูครึ่งหลังของคำตอบ
Derobert

@chills: หาก last_send เป็น <8 วินาทีคุณจะไม่เพิ่มโทเค็นใด ๆ หากที่เก็บข้อมูลของคุณมีโทเค็นคุณสามารถส่งข้อความได้ มิฉะนั้นคุณจะไม่ได้ (ที่คุณส่งไปแล้ว 5 ข้อความในช่วง 8 วินาที)
derobert

3
ฉันจะซาบซึ้งถ้าคน downvoting นี้โปรดอธิบายว่าทำไม ... ฉันต้องการแก้ไขปัญหาที่คุณเห็น แต่มันยากที่จะทำโดยไม่มีข้อเสนอแนะ!
Derobert

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;

รอจนกระทั่งมีค่าเผื่อเพียงพอที่จะส่งข้อความ ที่จะไม่เริ่มต้นด้วยอัตราสองเท่าของค่าเผื่ออาจเริ่มต้นด้วย 0


5
เมื่อคุณนอนหลับ(1-allowance) * (per/rate)คุณจะต้องเพิ่มจำนวนที่เท่าlast_checkกัน
Alp

2

รักษาเวลาที่ส่งห้าบรรทัดสุดท้าย กดค้างไว้ที่ข้อความที่อยู่ในคิวจนกว่าข้อความล่าสุดที่ห้า (หากมี) เป็นเวลาอย่างน้อย 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()

ไม่ใช่เพราะคุณแก้ไขมันฉันไม่ได้
Pesto

คุณกำลังจัดเก็บการประทับเวลาห้าครั้งและเลื่อนมันผ่านหน่วยความจำซ้ำ ๆ (หรือดำเนินการกับรายการที่เชื่อมโยง) ฉันกำลังจัดเก็บตัวนับจำนวนเต็มหนึ่งตัวและการประทับเวลาหนึ่งครั้ง และทำเลขคณิตและมอบหมายเท่านั้น
Derobert

2
ยกเว้นว่าเหมืองจะทำงานได้ดีขึ้นหากพยายามส่ง 5 บรรทัด แต่อนุญาตให้เพิ่มอีก 3 บรรทัดในช่วงเวลา คุณจะอนุญาตให้ส่งสามครั้งแรกและบังคับให้รอ 8 วินาทีก่อนที่จะส่ง 4 และ 5 ฉันจะอนุญาตให้ส่ง 4 และ 5 ได้ 8 วินาทีหลังจากบรรทัดที่สี่และห้าล่าสุด
Pesto

1
แต่ในเรื่องนี้ประสิทธิภาพอาจเพิ่มขึ้นด้วยการใช้รายการความยาว 5 เชื่อมโยงแบบวงกลมชี้ไปที่การส่งครั้งที่ห้าครั้งล่าสุดเขียนทับการส่งใหม่และการย้ายตัวชี้ไปข้างหน้า
Pesto

สำหรับบอท irc ที่มีตัว จำกัด อัตราความเร็วจะไม่เป็นปัญหา ฉันชอบโซลูชันรายการเนื่องจากสามารถอ่านได้มากกว่า คำตอบที่ฝากข้อมูลที่ได้รับมีความสับสนเนื่องจากการแก้ไข แต่ไม่มีอะไรผิดปกติกับมัน
jheriko

2

ทางออกหนึ่งคือการแนบประทับเวลากับแต่ละรายการคิวและทิ้งรายการหลังจาก 8 วินาทีผ่านไป คุณสามารถทำการตรวจสอบนี้ทุกครั้งที่มีการเพิ่มคิว

ใช้งานได้ก็ต่อเมื่อคุณ จำกัด ขนาดคิวไว้ที่ 5 และทิ้งส่วนเพิ่มเติมใด ๆ ในขณะที่คิวเต็ม


1

หากใครบางคนยังคงสนใจฉันใช้คลาส callable ที่เรียบง่ายนี้ร่วมกับหน่วยเก็บค่าคีย์ LRU ที่กำหนดเวลาเพื่อ จำกัด อัตราการร้องขอต่อ IP ใช้ deque แต่สามารถเขียนใหม่เพื่อใช้กับรายการแทน

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

เพียงการใช้รหัสหลามจากคำตอบที่ยอมรับได้

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.