web-dev-qa-db-fra.com

Limite des appels de méthode aux demandes M en N secondes

J'ai besoin d'un composant/classe qui limite l'exécution de certaines méthodes à un maximum de M appels en N secondes (ou ms ou nanos, peu importe).

En d'autres termes, je dois m'assurer que ma méthode n'est pas exécutée plus de M fois dans une fenêtre glissante de N secondes.

Si vous ne connaissez pas la classe existante, n'hésitez pas à publier vos solutions/idées sur la manière de la mettre en œuvre.

118
vtrubnikov

J'utiliserais un ring buffer of timestamps avec une taille fixe de M. Chaque fois que la méthode est appelée, vous vérifiez l'entrée la plus ancienne et, si elle est inférieure à N secondes, vous exécutez et ajoutez une autre entrée. sinon vous dormez pour le décalage horaire.

74
Michael Borgwardt

Ce qui a fonctionné hors de la boîte pour moi était Google Guava TauxLimiter .

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

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

Concrètement, vous devriez pouvoir implémenter ceci avec un DelayQueue . Initialisez la file d'attente avec les instances MDelayed avec leur délai initialement défini à zéro. Au fur et à mesure que les demandes de la méthode entrent, take un jeton, ce qui provoque le blocage de la méthode jusqu'à ce que la limitation requise soit remplie. Quand un jeton a été pris, add un nouveau jeton dans la file d'attente avec un délai de N.

28
erickson

Consultez le répertoire Token bucket algorithm. En gros, vous avez un seau contenant des jetons. Chaque fois que vous exécutez la méthode, vous prenez un jeton. S'il n'y a plus de jetons, vous bloquez jusqu'à ce que vous en obteniez un. Pendant ce temps, un acteur externe reconstitue les jetons à un intervalle fixe.

Je ne suis pas au courant d'une bibliothèque pour faire cela (ou quelque chose de similaire). Vous pouvez écrire cette logique dans votre code ou utiliser AspectJ pour ajouter le comportement.

17
Kevin

Cela dépend de l'application.

Imaginez le cas dans lequel plusieurs threads souhaitent qu'un jeton exécute une action globalement à débit limité avec aucune rafale autorisée (c.-à-d. Que vous souhaitez limiter 10 actions par 10 secondes veulent que 10 actions se produisent dans la première seconde puis restent 9 secondes arrêtées). 

La DelayedQueue présente un inconvénient: l'ordre dans lequel les jetons de requête de threads peuvent ne pas correspondre à l'ordre dans lequel leur requête est remplie. Si plusieurs threads sont bloqués dans l'attente d'un jeton, il est impossible de déterminer lequel prendra le prochain jeton disponible. Vous pourriez même avoir des discussions en attente pour toujours, de mon point de vue.

Une solution consiste à prévoir un intervalle de temps minimal entre deux actions consécutives et à effectuer les actions dans le même ordre que celui demandé.

Voici une implémentation:

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);
        }
    }
}
5
Duarte Meneses

Si vous avez besoin d’un limiteur de vitesse de fenêtre glissante basé sur Java qui fonctionne sur un système distribué, consultez le fichier https://github.com/mokies/ratelimitj project.

Une configuration sauvegardée par Redis, pour limiter les demandes par IP à 50 par minute, ressemblerait à ceci:

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");

Voir https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis pour plus de détails sur la configuration de Redis.

4
user2326162

Bien que ce ne soit pas ce que vous avez demandé, ThreadPoolExecutor , conçu pour limiter à M demandes simultanées au lieu de M demandes en N secondes, pourrait également être utile.

3
Eugene Yokota

Je dois m'assurer que ma méthode est exécuté pas plus de M fois dans un fenêtre glissante de N secondes.

J'ai récemment écrit un article de blog sur la façon de procéder dans .NET. Vous pourrez peut-être créer quelque chose de similaire en Java.

Meilleure limitation de taux dans .NET

3
Jack Leitch

J'ai implémenté un algorithme de limitation simple. Essayez ce lien, http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

Une brève sur l'algorithme,

Cet algorithme utilise la capacité de Java Delayed Queue . Créez un objet delay avec le délai attendu (ici 1000/M pour une milliseconde TimeUnit ) . Placez le même objet. dans la file d'attente retardée qui sera intern fournit la fenêtre mobile pour nous . Alors, avant chaque appel de méthode take l'objet de la file d'attente, take est un appel bloquant qui ne sera retourné qu'après le délai spécifié et après le délai spécifié. appel de méthode n'oubliez pas de mettre l'objet dans la file d'attente avec l'heure mise à jour (ici millisecondes en cours).

Ici, nous pouvons également avoir plusieurs objets retardés avec un retard différent. Cette approche fournira également un débit élevé.

2
Krishas

La question initiale ressemble beaucoup au problème résolu dans cet article de blog: Throttler multicanal asynchrone Java .

Pour un débit de M appels en N secondes, le régulateur décrit dans ce blog garantit que tout intervalle de longueur N sur la ligne de scénario ne contiendra pas plus de M appels.

2
Hbf

Mon implémentation ci-dessous peut gérer une précision temporelle de requête arbitraire, elle a O(1) complexité temporelle pour chaque requête, ne nécessitant aucun tampon supplémentaire, par ex. O(1) complexité de l'espace, de plus, aucun thread d'arrière-plan n'est requis pour libérer le jeton; les jetons sont libérés en fonction du temps écoulé depuis la dernière demande.

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;
        }
    }
}
1
tonywl

Ceci est une mise à jour du code LeakyBucket ci-dessus . Cela fonctionne pour plus de 1000 demandes par seconde.

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

et le plus inconditionnel pour ci-dessus:

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));
  }
}
0
peterreilly

Essayez d'utiliser cette approche simple:

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
SergeZ

Vous pouvez utiliser redis pour cela lorsque le verrouillage est nécessaire dans un système distribué. Deuxième algorithme dans https://redis.io/commands/incr

0
Kanagavelu Sugumar

Apache Camel prend également en charge le mécanisme Throttler comme suit:

from("seda:a").throttle(100).asyncDelayed().to("seda:b");
0
gtonic

Voici une petite version avancée du limiteur de débit simple

/**
 * 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);
    }
}

Et des tests unitaires

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

}
0
Leonid Astakhov