O método de limitação chama M solicitações em N segundos


137

Eu preciso de um componente / classe que limita a execução de algum método para o máximo de chamadas M em N segundos (ou ms ou nanos, não importa).

Em outras palavras, preciso garantir que meu método seja executado não mais que M vezes em uma janela deslizante de N segundos.

Se você não conhece a classe existente, sinta-se à vontade para postar suas soluções / idéias sobre como implementar isso.



3
Há algumas grandes respostas para este problema no stackoverflow.com/questions/667508/...
skaffman

Preciso garantir que meu método seja executado não mais que M vezes em uma janela deslizante de N segundos. Eu escrevi recentemente um post sobre como fazer isso no .NET. Você pode criar algo semelhante em Java. Melhor Limitação de taxa em .NET
Jack Leitch

A pergunta original parece muito com o problema resolvido nesta postagem do blog: [Regulador de pressão assíncrono multicanal em Java] ( cordinc.com/blog/2010/04/java-multichannel-asynchronous.html ). Para uma taxa de M chamadas em N segundos, o acelerador discutido neste blog garante que qualquer intervalo de duração N na linha do tempo não contenha mais que M chamadas.
Hbf 12/05

Respostas:


81

Eu usaria um buffer de anel de registros de data e hora com um tamanho fixo de M. Cada vez que o método é chamado, você verifica a entrada mais antiga e, se há menos de N segundos no passado, você executa e adiciona outra entrada, caso contrário, dorme para a diferença horária.


4
Encantador. Exatamente o que eu preciso. Tentativas rápidas mostram ~ 10 linhas para implementar isso e um espaço mínimo de memória. Basta pensar na segurança do encadeamento e na fila de solicitações recebidas.
vtrubnikov 10/09/09

5
É por isso que você usa o DelayQueue de java.util.concurrent. Evita o problema de vários encadeamentos que atuam na mesma entrada.
22410 erickson

5
Para um caso com vários segmentos, a abordagem do token bucket pode ser uma escolha melhor, eu acho.
22611 Michael Borgwardt

1
Você sabe como esse algoritmo é chamado se tiver algum nome?
Vlado Pandžić

80

O que funcionou imediatamente para mim foi o Google Guava RateLimiter .

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

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

19
Eu não recomendaria esta solução, pois o Guava RateLimiter bloqueará o thread e isso esgotará o pool de threads facilmente.
Keviddiss # 22/14

18
@kaviddiss se você não deseja bloquear, então usetryAquire()
SLF

7
O problema com a implementação atualmente do RateLimiter (pelo menos para mim) é que ele não permite períodos superiores a 1 segundo e, portanto, taxas de, por exemplo, 1 por minuto.
João B

4
@ John B Tanto quanto eu entendo, você pode conseguir um pedido por minuto com RateLimiter usando RateLimiter.create (60,0) + rateLimiter.acquire (60)
divideByZero

2
O @radiantRazor Ratelimiter.create (1.0 / 60) e o aquisição () alcançam 1 chamada por minuto.
precisa saber é

30

Em termos concretos, você deve conseguir implementá-lo com a DelayQueue. Inicialize a fila com M Delayedinstâncias com o atraso definido inicialmente como zero. À medida que as solicitações ao método chegam, takeum token, que faz com que o método seja bloqueado até que o requisito de limitação seja atendido. Quando um token é obtido, addum novo token para a fila com um atraso de N.


1
Sim, isso faria o truque. Mas não gosto particularmente do DelayQueue porque ele está usando (via PriortyQueue) um hash binário balanceado (o que significa muitas comparações offere possível crescimento da matriz) e é meio pesado para mim. Eu acho que para outros isso pode ser perfeitamente aceitável.
vtrubnikov 10/09/09

5
Na verdade, nesta aplicação, como o novo elemento adicionado ao heap quase sempre será o elemento máximo no heap (ou seja, possui o maior atraso), geralmente é necessária uma comparação por adição. Além disso, a matriz nunca aumentará se o algoritmo for implementado corretamente, uma vez que um elemento é adicionado somente após a obtenção de um elemento.
21411 erickson

3
Achei isso útil também nos casos em que você não deseja que as solicitações ocorram em grandes explosões, mantendo o tamanho M e o atraso N relativamente pequeno em ordem de poucos milis. por exemplo. M = 5, N = 20 ms seria fornecer um meio de venda de 250 / seg kepping rebentar a acontecer em tamanho de 5.
MID

Essa escala será de um milhão de rpm e quando solicitações simultâneas forem permitidas? Eu precisaria adicionar um milhão de elementos atrasados. Além disso, os casos de canto terão alta latência - caso em que vários threads estejam chamando poll () e trancem a cada vez.
Aditya Joshee

@AdityaJoshee Eu não fiz comparações, mas se eu tiver algum tempo, tentarei entender a sobrecarga. Uma coisa a observar é que você não precisa de 1 milhão de tokens que expiram em 1 segundo. Você pode ter 100 tokens que expiram em 10 milissegundos, 10 tokens que expiram em milissegundos, etc. Isso realmente força a taxa instantânea a estar mais próxima da taxa média, suavizando picos, o que pode causar backups no cliente, mas isso é uma consequência natural de limitação de taxa. 1 milhão de RPM dificilmente soa como estrangulamento. Se você pode explicar seu caso de uso, talvez eu tenha melhores idéias.
erickson

21

Leia o algoritmo de bucket de token . Basicamente, você tem um balde com tokens. Toda vez que você executa o método, você recebe um token. Se não houver mais tokens, você bloqueia até obter um. Enquanto isso, há algum ator externo que reabastece os tokens em um intervalo fixo.

Não conheço uma biblioteca para fazer isso (ou algo semelhante). Você pode escrever essa lógica no seu código ou usar o AspectJ para adicionar o comportamento.


3
Obrigado pela sugestão, algo interessante. Mas não é exatamente o que eu preciso. Por exemplo, preciso limitar a execução a 5 chamadas por segundo. Se eu usar o Token bucket e 10 solicitações forem recebidas ao mesmo tempo, as 5 primeiras chamadas receberão todos os tokens disponíveis e serão executadas momentaneamente, enquanto as 5 chamadas restantes serão executadas em um intervalo fixo de 1/5 s. Em tal situação, eu preciso que as 5 chamadas restantes sejam executadas em uma única rajada apenas após 1 segundo.
vtrubnikov 10/09/09

5
E se você adicionou 5 fichas para o balde a cada segundo (ou 5 - (5-remanescente) em vez de 1 a cada 1/5 segundo?
Kevin

@ Kevin sem isso ainda não me daria 'janela deslizante' efeito
vtrubnikov

2
@ valery sim seria. (Lembre-se de limitar os tokens em M)
nos

não há necessidade de um "ator externo". Tudo pode ser feito de um único encadeamento, se você mantiver os metadados em torno dos horários das solicitações.
Marsellus Wallace

8

Se você precisar de um limitador de taxa de janela deslizante baseado em Java que funcione em um sistema distribuído, consulte o projeto https://github.com/mokies/ratelimitj .

Uma configuração suportada pelo Redis, para limitar solicitações por IP a 50 por minuto, seria assim:

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

Consulte https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis para obter mais detalhes sobre a configuração do Redis.


5

Isso depende da aplicação.

Imagine o caso em que vários encadeamentos desejam que um token realize alguma ação com taxa global limitada, sem permissão permitida (ou seja, você deseja limitar 10 ações por 10 segundos, mas não deseja que 10 ações aconteçam no primeiro segundo e permaneça 9 segundos parados).

O DelayedQueue tem uma desvantagem: a ordem na qual os tokens de solicitação de threads podem não ser a ordem em que eles recebem sua solicitação. Se vários threads estiverem bloqueados aguardando um token, não está claro qual deles utilizará o próximo token disponível. Você pode até ter tópicos esperando para sempre, no meu ponto de vista.

Uma solução é ter um intervalo mínimo de tempo entre duas ações consecutivas e executar ações na mesma ordem em que foram solicitadas.

Aqui está uma implementação:

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

o que minTimesignifica aqui? O que isso faz? você pode explicar isso?
flash

minTimeé o tempo mínimo que precisa passar depois que um token é consumido antes que o próximo token possa ser consumido.
Duarte Meneses

3

Embora não seja o que você pediu, ThreadPoolExecutorque foi projetado para limitar solicitações M simultâneas em vez de solicitações M em N segundos, também pode ser útil.


2

Eu implementei um algoritmo de limitação simples.Tente este link, http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

Um breve sobre o algoritmo,

Esse algoritmo utiliza o recurso da fila Java atrasada . Crie um objeto atrasado com o atraso esperado (aqui 1000 / M para TimeUnit de milissegundos ). Coloque o mesmo objeto na fila atrasada que internamente fornece a janela móvel para nós. Então, antes de cada chamada de método pegar o objeto da fila, take é uma chamada de bloqueio que retornará somente após o atraso especificado e, após a chamada do método, não se esqueça de colocar o objeto na fila com tempo atualizado (aqui milissegundos atuais) .

Aqui também podemos ter vários objetos atrasados ​​com atraso diferente. Essa abordagem também fornecerá alto rendimento.


6
Você deve postar um resumo do seu algoritmo. Se o seu link desaparecer, sua resposta se tornará inútil.
JWR

Obrigado, eu adicionei o resumo.
Krishas # 30/16

1

Minha implementação abaixo pode lidar com precisão de tempo de solicitação arbitrária, possui complexidade de tempo O (1) para cada solicitação, não requer nenhum buffer adicional, por exemplo, complexidade de espaço O (1), além de não exigir que o thread em segundo plano libere o token. os tokens são liberados de acordo com o tempo decorrido desde a última solicitação.

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

Tente usar esta abordagem simples:

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

Esta é uma atualização para o código LeakyBucket acima. Isso funciona para mais de 1000 solicitações por segundo.

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

e o mais unittest para acima:

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

o que minTimeNanosignifica aqui? você pode explicar?
flash

0

Aqui está uma versão um pouco avançada do limitador de taxa simples

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

E testes de unidade

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

}

O código é bem simples. Você acabou de criar o limitador com maxRate ou com períodos e taxa. E, em seguida, basta ligar para consumir cada solicitação. Sempre que a taxa não for excedida, o limitador retornará imediatamente ou aguardará algum tempo antes de retornar à menor taxa de solicitação atual. Ele também possui o método de taxa atual, que retorna a média móvel da taxa atual.
Leonid Astakhov

0

Minha solução: Um método utilitário simples, você pode modificá-lo para criar uma classe de wrapper.

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

Retirar do debounce e acelerador de threads JAVA

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.