Token bucket
Todos os algoritmos vistos até agora tratam a rajada como um problema. Mas muitas rajadas são normais: uma página carregando seis coisas ao mesmo tempo, ou um cliente que ficou ocioso por um minuto e agora tem trabalho a fazer.
O token bucket permite isso de propósito. Os tokens se acumulam a uma taxa constante, uma requisição gasta um token, e um cliente que ficou quieto tem alguns guardados.
Folhas e lagartas
Uma planta segura no máximo cinco folhas e cresce três por dia. Cada lagarta que chega come uma folha, e uma lagarta sem folha disponível não sobrevive.
| Dia | Folhas que crescem | Lagartas | Resultado | Folhas restantes |
|---|---|---|---|---|
| 1 | 3 | 2 | Ambas comem | 1 |
| 2 | 3, total de 4 | 5 | 4 comem, 1 não | 0 |
| 3 | 3 | 0 | Nada acontece | 3 |
| 4 | 2, limitado a 5 | 0 | Nada acontece | 5 |
Três ideias nessa tabela:
- Uma folha é um token, e uma requisição gasta um.
- Os tokens chegam a uma taxa fixa, quer alguém esteja pedindo ou não.
- A planta não consegue guardar mais do que sua capacidade, então no dia quatro crescem duas folhas em vez de três.
A capacidade é o que permite a rajada. Um bucket cheio pode ser gasto de uma só vez, então um cliente que ficou quieto pode enviar cinco requisições num instante e depois precisa esperar mais folhas crescerem.
A taxa de reabastecimento define a média em longo prazo. A capacidade define o tamanho da rajada que você tolera.
Esse teto é a razão de a rajada ter um tamanho limite. Sem ele, um cliente ocioso por uma hora voltaria podendo enviar de uma vez tudo o que acumulou naquela hora.
Construindo o bucket
O estado é composto por dois números, e a lógica é feita de duas decisões.
class TokenBucket {
constructor(capacity, refillRate, refillInterval) {
this.capacity = capacity
this.refillRate = refillRate
this.refillInterval = refillInterval
this.tokens = capacity // começa cheio
this.secondsSinceLastRefill = 0
}
processRequests(numRequests) {
this.secondsSinceLastRefill += 1
if (this.secondsSinceLastRefill >= this.refillInterval) {
this.tokens = Math.min(this.capacity, this.tokens + this.refillRate)
this.secondsSinceLastRefill = 0
}
const accepted = Math.min(numRequests, this.tokens)
const rejected = numRequests - accepted
this.tokens -= accepted
return { accepted, rejected }
}
}As duas chamadas de Math.min são as que fazem o trabalho de verdade.
A primeira limita o reabastecimento à capacidade, então os tokens nunca ultrapassam o teto. A segunda limita a aceitação aos tokens disponíveis, então uma requisição por nove tokens contra um bucket com apenas um aceita um e rejeita oito.
Usar >= na verificação do reabastecimento em vez de == é proposital. Se um tick for perdido por algum motivo, uma comparação exata pularia o reabastecimento e o bucket nunca mais se encheria.
Começar vazio seria mais rígido e faria a primeira visita de qualquer um parecer quebrada.
Os parâmetros mudam tudo
Duas configurações, o mesmo algoritmo.
Capacidade 10, reabastecimento de 6 a cada 3 segundos. Aproximadamente duas por segundo sustentadas, com espaço para dez de uma vez. Acompanhando segundo a segundo:
| Rodada | Solicitadas | Aceitas | Rejeitadas | Tokens restantes |
|---|---|---|---|---|
| 1 | 3 | 3 | 0 | 7 |
| 2 | 5 | 5 | 0 | 2 |
| 3 | 7 | 7 | 0 | 1, depois de reabastecer para 8 |
| 4 | 9 | 1 | 8 | 0 |
Generoso. As rajadas são absorvidas, e esgotar o bucket exige um esforço deliberado.
Capacidade 8, reabastecimento de 2 a cada 10 segundos. Agora as primeiras oito requisições são de graça, e depois quase nada é. Envie cinco, depois mais cinco, e você fica vazio com oito segundos de espera para dois tokens.
Mesmo algoritmo, experiência completamente diferente. Um cliente sob a segunda configuração precisa pensar em quando gastar, porque os tokens são escassos e demoram para voltar.
Isso não é um limite de taxa que molda o tráfego. É um que faz o cliente racionar cada requisição, e o algoritmo não mudou em nada.
Coloque em prática
Um bucket com capacidade 5, reabastecendo 2 tokens a cada 2 segundos, começando cheio.
- Um cliente envia 5 requisições de uma vez. Quantas são aceitas, e o que resta?
- Dois segundos depois ele envia 3. O que acontece?
- O cliente então espera 10 segundos. Quantos tokens ele tem?
- Qual é a taxa sustentada que esse bucket permite, e qual é a maior rajada possível?
Compare suas respostas
1. Todas as 5 aceitas, 0 restantes. O bucket começa cheio e uma requisição gasta um token. Esvaziá-lo em um único instante é exatamente a rajada que a capacidade existe para permitir.
2. Duas aceitas, uma rejeitada. Dois segundos se passaram, então o bucket reabastece 2 tokens. Três requisições chegam contra 2 tokens, então Math.min(3, 2) aceita duas e a terceira é recusada.
3. Cinco, não dez. Dez segundos acumulariam 10 tokens, mas a capacidade limita isso a 5. Essa é a regra do dia quatro: o tempo ocioso não se acumula indefinidamente, o que é justamente o que impede um cliente que ficou quieto por muito tempo de voltar com uma rajada enorme.
4. Um por segundo sustentado, cinco de uma vez. Dois tokens a cada dois segundos é a média em longo prazo, e a capacidade 5 é a maior rajada instantânea possível.
A pergunta quatro é o par que vale a pena guardar. A taxa de reabastecimento e a capacidade respondem a duas perguntas diferentes, e resumir um token bucket a um único número perde uma dessas informações.
Para onde isso vai a seguir
O token bucket deixa o cliente decidir quando gastar, então o tráfego que chega ao seu servidor continua irregular. Limitado, mas irregular.
O leaky bucket segue outro caminho: aceitar a rajada e depois liberá-la a uma taxa constante, de modo que o que o servidor vê seja suave, não importa como o tráfego chegou.

