N秒でMリクエストへのスロットリングメソッド呼び出し


137

一部のメソッドの実行をN秒で最大M回の呼び出しに制限するコンポーネント/クラスが必要です(またはmsかnanosでも構いません)。

つまり、メソッドがN秒のスライディングウィンドウでM回以上実行されないようにする必要があります。

既存のクラスがわからない場合は、ソリューション/アイデアをどのように実装するかを自由に投稿してください。




>私のメソッドがN秒のスライディングウィンドウでM回以上実行されないようにする必要があります。私は最近、これを.NETで行う方法についてブログ投稿を書きました。Javaで同様のものを作成できる場合があります。.NETでのレート制限の改善
Jack Leitch

元の質問は、このブログ投稿で解決された問題のように聞こえます:[Java Multi-Channel Asynchronous Throttler](cordinc.com/blog/2010/04/java-multichannel-asynchronous.html)。Mの割合がN秒に呼び出すために、スロットラーがいることを、このブログの保証で議論任意のタイムライン上の長さNの間隔はM・コールよりも多く含まれていません。
Hbf、

回答:


81

Mの固定サイズのタイムスタンプのリングバッファーを使用します。メソッドが呼び出されるたびに、最も古いエントリを確認し、それが過去N秒未満の場合は、実行して別のエントリを追加します。それ以外の場合はスリープします。時間差のため。


4
美しい。ちょうど私が必要なもの。クイック試行では、これを実装するための最大10行が示され、メモリフットプリントは最小限に抑えられます。スレッドの安全性と着信要求のキューイングについて考える必要があるだけです。
vtrubnikov 2009

5
そのため、java.util.concurrentのDelayQueueを使用します。同じエントリで複数のスレッドが動作する問題を防ぎます。
エリクソン2009

5
マルチスレッドの場合、トークンバケットアプローチの方が適していると思います。
マイケルボルグワート

1
このアルゴリズムがなんらかの名前を持っている場合、どのように呼び出されるか知っていますか?
VladoPandžić18年

80

私にとって最初からうまくいったのは、Google Guava RateLimiterでした

// Allow one request per second
private RateLimiter throttle = RateLimiter.create(1.0);

private void someMethod() {
    throttle.acquire();
    // Do something
}

19
Guava RateLimiterがスレッドをブロックし、スレッドプールを簡単に使い果たしてしまうため、このソリューションはお勧めしません。
kaviddiss 2014年

18
ブロックしたくない場合は、@ kaviddissを使用してくださいtryAquire()
slf

7
現在(少なくとも私にとって)RateLimiterの実装の問題は、1秒を超える期間が許可されないため、たとえば1分あたり1回のレートが許容されないことです。
ジョンB

4
@ジョンB私が理解している限り、RateLimiter.create(60.0)+ rateLimiter.acquire(60)を使用すると、RateLimiterを使用して1分あたり1つのリクエストを達成できます
divideByZero

2
@radiantRazor Ratelimiter.create(1.0 / 60)およびquire()は、1分あたり1回の呼び出しを実現します。
bizentass

30

具体的には、これをで実装できるはずDelayQueueです。M Delayed遅延を最初にゼロに設定したインスタンスでキューを初期化します。メソッドへのリクエストが入ると、takeトークン。これにより、スロットリングの要件が満たされるまでメソッドがブロックされます。トークンが取得されるとadd、新しいトークンが遅延してキューに追加されますN


1
はい、これでうまくいきます。しかし、DelayQueueは(PriortyQueueを介して)バランスの取れたバイナリハッシュ(多くの比較offerと配列の増加の可能性があることを意味します)を使用しているため、特に私は好きではありません。他の人にとっては、これで問題ないかもしれません。
vtrubnikov 2009

5
実際、このアプリケーションでは、ヒープに追加される新しい要素はほとんど常にヒープ内の最大要素になる(つまり、遅延が最も長い)ため、追加ごとに1つの比較が必要です。また、1つの要素は1つの要素を取得した後にのみ追加されるため、アルゴリズムが正しく実装されていれば、配列は決して大きくなりません。
エリクソン2009

3
これは、サイズMと遅延Nを数ミリ単位のオーダーで比較的小さく保つことにより、リクエストが大きなバーストで発生しないようにする場合にも役立ちます。例えば。M = 5、N = 20msの250 /秒keppingのPUTを介して提供するには、5の大きさで発生するバースト
FUD

これは、100万rpmで、同時リクエストが許可される場合に拡張されますか?100万のdelayedElementsを追加する必要があります。また、コーナーケースはレイテンシが高くなります。複数のスレッドがpoll()を呼び出し、毎回ロックする場合です。
Aditya Joshee

@AdityaJosheeベンチマークはしていませんが、時間があれば、オーバーヘッドの意味を理解しようとします。ただし、1秒で有効期限が切れる100万のトークンは必要ありません。10ミリ秒で期限切れになる100個のトークン、ミリ秒で期限切れになる10個のトークンなどが存在する可能性があります。これにより、実際に瞬間レートが平均レートに近づき、スパイクが滑らかになり、クライアントでバックアップが発生する可能性がありますが、これは自然な結果ですレート制限の。100万RPMは、スロットリングのようには聞こえません。ユースケースを説明できれば、もっと良いアイデアがあるかもしれません。
エリクソン

21

上の記事を読むまでのトークンバケットアルゴリズム。基本的に、トークンが入ったバケットがあります。メソッドを実行するたびに、トークンを取得します。トークンがない場合は、トークンを取得するまでブロックします。一方、一定の間隔でトークンを補充する外部アクターがあります。

私はこれを行うためのライブラリー(または同様のもの)を知りません。このロジックをコードに書き込むか、AspectJを使用して動作を追加できます。


3
提案をありがとう、興味深いアルゴ。しかし、それはまさに私が必要としているものではありません。たとえば、実行を毎秒5コールに制限する必要があります。トークンバケットを使用していて、10個のリクエストが同時に入った場合、最初の5回の呼び出しですべての利用可能なトークンが取得されて一時的に実行され、残りの5回の呼び出しは1/5秒の固定間隔で実行されます。そのような状況では、1秒経過した後でのみ、残りの5つのコールをシングルバーストで実行する必要があります。
vtrubnikov 2009

5
1/5秒ごとに1つではなく、毎秒5つのトークンをバケットに追加した場合(または5-(残り5つ)の場合)
Kevin

@Kevinいいえ、これでもまだ「スライディングウィンドウ」の効果はありません
vtrubnikov

2
@valeryはい、そうです。(ただし、トークンはMでキャップすることを忘れないでください)
nos

「外部俳優」の必要はありません。リクエスト時間についてメタデータを維持すれば、すべてをシングルスレッドで実行できます。
Marsellus Wallace

8

分散システム全体で動作するJavaベースのスライディングウィンドウレートリミッターが必要な場合は、https://github.com/mokies/ratelimitjプロジェクトをご覧ください

IPによるリクエストを1分あたり50に制限するRedisのバックアップ構成は次のようになります。

import com.lambdaworks.redis.RedisClient;
import es.moki.ratelimitj.core.LimitRule;

RedisClient client = RedisClient.create("redis://localhost");
Set<LimitRule> rules = Collections.singleton(LimitRule.of(1, TimeUnit.MINUTES, 50)); // 50 request per minute, per key
RedisRateLimit requestRateLimiter = new RedisRateLimit(client, rules);

boolean overLimit = requestRateLimiter.overLimit("ip:127.0.0.2");

Redis構成の詳細については、https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redisを参照してください


5

これはアプリケーションによって異なります。

その場合想像して複数のスレッドがトークンがいくつか行わないしたいグローバル・レート・限られた行動をして許可バーストなし(つまり、あなたは10秒ごとに10回の行動を制限したいが、あなたは10個のアクションが第一、第二に起こり、その後ままにしたくないし9秒停止)。

DelayedQueueには欠点があります。スレッドがトークンを要求する順序は、スレッドが要求を実行する順序とは異なる場合があります。複数のスレッドがトークンを待ってブロックされている場合、次の使用可能なトークンを取得するスレッドが明確ではありません。私の考えでは、スレッドを永久に待機させることもできます。

1つの解決策は、2つの連続するアクションの間に最小の時間間隔を設け要求されたのと同じ順序でアクションを実行することです。

ここに実装があります:

public class LeakyBucket {
    protected float maxRate;
    protected long minTime;
    //holds time of last action (past or future!)
    protected long lastSchedAction = System.currentTimeMillis();

    public LeakyBucket(float maxRate) throws Exception {
        if(maxRate <= 0.0f) {
            throw new Exception("Invalid rate");
        }
        this.maxRate = maxRate;
        this.minTime = (long)(1000.0f / maxRate);
    }

    public void consume() throws InterruptedException {
        long curTime = System.currentTimeMillis();
        long timeLeft;

        //calculate when can we do the action
        synchronized(this) {
            timeLeft = lastSchedAction + minTime - curTime;
            if(timeLeft > 0) {
                lastSchedAction += minTime;
            }
            else {
                lastSchedAction = curTime;
            }
        }

        //If needed, wait for our time
        if(timeLeft <= 0) {
            return;
        }
        else {
            Thread.sleep(timeLeft);
        }
    }
}

minTimeここではどういう意味ですか?それは何をするためのものか?それについて説明できますか?
フラッシュ

minTimeトークンが消費されてから次のトークンが消費されるまでに経過する必要がある最小時間です。
ドゥアルテメネセス


2

単純なスロットルアルゴリズムを実装しました。このリンクを試してください 。http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

アルゴリズムの概要、

このアルゴリズムは、Java 遅延キューの機能を利用します。作成遅延(ミリ秒のためにここでは1000 / M予想遅延でオブジェクトをするTimeUnit)。同じオブジェクトを遅延キューに入れると、インターンが移動ウィンドウを提供します。次に、各メソッド呼び出しキューからオブジェクトを取得する前に、指定された遅延の後でのみ返されるブロッキング呼び出しがあり、メソッド呼び出しの後に、更新された時間(ここでは現在のミリ秒)でオブジェクトをキューに入れることを忘れないでください。 。

ここでは、異なる遅延を持つ複数の遅延オブジェクトを使用することもできます。このアプローチは、高いスループットも提供します。


6
アルゴリズムの概要を投稿してください。あなたのリンクが消えると、あなたの答えは役に立たなくなります。
jwr '20 / 10/20

おかげで、私は概要を追加しました。
Krishas

1

以下の私の実装は、任意のリクエスト時間精度を処理でき、リクエストごとにO(1)時間の複雑さを持ち、追加のバッファー、たとえばO(1)スペースの複雑さを必要としません。さらに、トークンを解放するバックグラウンドスレッドを必要としません。トークンは、最後のリクエストから経過した時間に従って解放されます。

class RateLimiter {
    int limit;
    double available;
    long interval;

    long lastTimeStamp;

    RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;

        available = 0;
        lastTimeStamp = System.currentTimeMillis();
    }

    synchronized boolean canAdd() {
        long now = System.currentTimeMillis();
        // more token are released since last request
        available += (now-lastTimeStamp)*1.0/interval*limit; 
        if (available>limit)
            available = limit;

        if (available<1)
            return false;
        else {
            available--;
            lastTimeStamp = now;
            return true;
        }
    }
}

0

この簡単なアプローチを試してください:

public class SimpleThrottler {

private static final int T = 1; // min
private static final int N = 345;

private Lock lock = new ReentrantLock();
private Condition newFrame = lock.newCondition();
private volatile boolean currentFrame = true;

public SimpleThrottler() {
    handleForGate();
}

/**
 * Payload
 */
private void job() {
    try {
        Thread.sleep(Math.abs(ThreadLocalRandom.current().nextLong(12, 98)));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.err.print(" J. ");
}

public void doJob() throws InterruptedException {
    lock.lock();
    try {

        while (true) {

            int count = 0;

            while (count < N && currentFrame) {
                job();
                count++;
            }

            newFrame.await();
            currentFrame = true;
        }

    } finally {
        lock.unlock();
    }
}

public void handleForGate() {
    Thread handler = new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1 * 900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                currentFrame = false;

                lock.lock();
                try {
                    newFrame.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    });
    handler.start();
}

}



0

これは、上記のLeakyBucketコードの更新です。これは、1秒あたり1000を超えるリクエストで機能します。

import lombok.SneakyThrows;
import java.util.concurrent.TimeUnit;

class LeakyBucket {
  private long minTimeNano; // sec / billion
  private long sched = System.nanoTime();

  /**
   * Create a rate limiter using the leakybucket alg.
   * @param perSec the number of requests per second
   */
  public LeakyBucket(double perSec) {
    if (perSec <= 0.0) {
      throw new RuntimeException("Invalid rate " + perSec);
    }
    this.minTimeNano = (long) (1_000_000_000.0 / perSec);
  }

  @SneakyThrows public void consume() {
    long curr = System.nanoTime();
    long timeLeft;

    synchronized (this) {
      timeLeft = sched - curr + minTimeNano;
      sched += minTimeNano;
    }
    if (timeLeft <= minTimeNano) {
      return;
    }
    TimeUnit.NANOSECONDS.sleep(timeLeft);
  }
}

上記のユニットテスト:

import com.google.common.base.Stopwatch;
import org.junit.Ignore;
import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class LeakyBucketTest {
  @Test @Ignore public void t() {
    double numberPerSec = 10000;
    LeakyBucket b = new LeakyBucket(numberPerSec);
    Stopwatch w = Stopwatch.createStarted();
    IntStream.range(0, (int) (numberPerSec * 5)).parallel().forEach(
        x -> b.consume());
    System.out.printf("%,d ms%n", w.elapsed(TimeUnit.MILLISECONDS));
  }
}

minTimeNanoここでの意味は何ですか?説明できますか?
フラッシュ

0

これは、シンプルなレートリミッターの少し高度なバージョンです。

/**
 * Simple request limiter based on Thread.sleep method.
 * Create limiter instance via {@link #create(float)} and call {@link #consume()} before making any request.
 * If the limit is exceeded cosume method locks and waits for current call rate to fall down below the limit
 */
public class RequestRateLimiter {

    private long minTime;

    private long lastSchedAction;
    private double avgSpent = 0;

    ArrayList<RatePeriod> periods;


    @AllArgsConstructor
    public static class RatePeriod{

        @Getter
        private LocalTime start;

        @Getter
        private LocalTime end;

        @Getter
        private float maxRate;
    }


    /**
     * Create request limiter with maxRate - maximum number of requests per second
     * @param maxRate - maximum number of requests per second
     * @return
     */
    public static RequestRateLimiter create(float maxRate){
        return new RequestRateLimiter(Arrays.asList( new RatePeriod(LocalTime.of(0,0,0),
                LocalTime.of(23,59,59), maxRate)));
    }

    /**
     * Create request limiter with ratePeriods calendar - maximum number of requests per second in every period
     * @param ratePeriods - rate calendar
     * @return
     */
    public static RequestRateLimiter create(List<RatePeriod> ratePeriods){
        return new RequestRateLimiter(ratePeriods);
    }

    private void checkArgs(List<RatePeriod> ratePeriods){

        for (RatePeriod rp: ratePeriods ){
            if ( null == rp || rp.maxRate <= 0.0f || null == rp.start || null == rp.end )
                throw new IllegalArgumentException("list contains null or rate is less then zero or period is zero length");
        }
    }

    private float getCurrentRate(){

        LocalTime now = LocalTime.now();

        for (RatePeriod rp: periods){
            if ( now.isAfter( rp.start ) && now.isBefore( rp.end ) )
                return rp.maxRate;
        }

        return Float.MAX_VALUE;
    }



    private RequestRateLimiter(List<RatePeriod> ratePeriods){

        checkArgs(ratePeriods);
        periods = new ArrayList<>(ratePeriods.size());
        periods.addAll(ratePeriods);

        this.minTime = (long)(1000.0f / getCurrentRate());
        this.lastSchedAction = System.currentTimeMillis() - minTime;
    }

    /**
     * Call this method before making actual request.
     * Method call locks until current rate falls down below the limit
     * @throws InterruptedException
     */
    public void consume() throws InterruptedException {

        long timeLeft;

        synchronized(this) {
            long curTime = System.currentTimeMillis();

            minTime = (long)(1000.0f / getCurrentRate());
            timeLeft = lastSchedAction + minTime - curTime;

            long timeSpent = curTime - lastSchedAction + timeLeft;
            avgSpent = (avgSpent + timeSpent) / 2;

            if(timeLeft <= 0) {
                lastSchedAction = curTime;
                return;
            }

            lastSchedAction = curTime + timeLeft;
        }

        Thread.sleep(timeLeft);
    }

    public synchronized float getCuRate(){
        return (float) ( 1000d / avgSpent);
    }
}

そしてユニットテスト

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RequestRateLimiterTest {


    @Test(expected = IllegalArgumentException.class)
    public void checkSingleThreadZeroRate(){

        // Zero rate
        RequestRateLimiter limiter = RequestRateLimiter.create(0);
        try {
            limiter.consume();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void checkSingleThreadUnlimitedRate(){

        // Unlimited
        RequestRateLimiter limiter = RequestRateLimiter.create(Float.MAX_VALUE);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) < 1000));
    }

    @Test
    public void rcheckSingleThreadRate(){

        // 3 request per minute
        RequestRateLimiter limiter = RequestRateLimiter.create(3f/60f);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 3; i++ ){

            try {
                limiter.consume();
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) >= 60000 ) & ((ended - started) < 61000));
    }



    @Test
    public void checkSingleThreadRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ));
    }

    @Test
    public void checkMultiThreadedRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreaded32RateLimit(){

        // 0,2 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(0.2f);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(8);
        ExecutorService exec = Executors.newFixedThreadPool(8);

        for ( int i = 0; i < 8; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 2; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreadedRateLimitDynamicRate(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {

                Random r = new Random();
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                        Thread.sleep(r.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

}

コードはかなり単純です。maxRateまたは期間とレートでリミッターを作成するだけです。そして、呼び出しはすべての要求を消費します。レートを超えない場合、リミッターはすぐに戻るか、しばらく待ってから現在の要求レートを下げます。また、現在のレートのスライド平均を返す現在のレートメソッドもあります。
Leonid Astakhov

0

私の解決策:単純なutilメソッド。これを変更して、ラッパークラスを作成できます。

public static Runnable throttle (Runnable realRunner, long delay) {
    Runnable throttleRunner = new Runnable() {
        // whether is waiting to run
        private boolean _isWaiting = false;
        // target time to run realRunner
        private long _timeToRun;
        // specified delay time to wait
        private long _delay = delay;
        // Runnable that has the real task to run
        private Runnable _realRunner = realRunner;
        @Override
        public void run() {
            // current time
            long now;
            synchronized (this) {
                // another thread is waiting, skip
                if (_isWaiting) return;
                now = System.currentTimeMillis();
                // update time to run
                // do not update it each time since
                // you do not want to postpone it unlimited
                _timeToRun = now+_delay;
                // set waiting status
                _isWaiting = true;
            }
            try {
                Thread.sleep(_timeToRun-now);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // clear waiting status before run
                _isWaiting = false;
                // do the real task
                _realRunner.run();
            }
        }};
    return throttleRunner;
}

JAVAスレッドのデバウンスとスロットルから取得

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.