Algoritmos de janela deslizante
O estouro na fronteira da janela fixa vem de dividir o tempo em blocos. Uma requisição cai em um bloco ou em outro, e um intervalo de tempo que atravessa a divisa pode carregar o dobro do limite.
Pare de usar blocos e o problema desaparece. Os dois algoritmos aqui medem contra uma janela que termina na requisição que está sendo decidida, e eles se diferenciam apenas na quantidade de coisas que precisam lembrar.
Log de janela deslizante
Guarde um timestamp para cada requisição aceita. Quando uma nova chega, olhe para trás o comprimento de uma janela, conte o que há ali dentro e decida.
A janela é calculada do zero a cada vez, então ela desliza logo atrás de cada requisição. Mesmo estacionamento, janela de trinta segundos, limite de cinco:
| Tempo | Chegam | Janela olha até | Contagem na janela | Resultado |
|---|---|---|---|---|
| 0s | 1 | 0s | 0 | Aceita |
| 5s | 2 | 0s | 1 | Ambas aceitas, contagem agora 3 |
| 15s | 2 | 0s | 3 | Ambas aceitas, contagem agora 5 |
| 25s | 1 | 0s | 5 | Rejeitada, no limite |
| 35s | 1 | 5s | 4 | Aceita, a requisição de 0s já saiu da janela |
| 40s | 3 | 10s | 3 | Duas aceitas, a terceira rejeitada |
Aos 35 segundos a janela começa em 5, então a requisição de 0 fica fora dela e deixa de contar. Esse envelhecimento gradual é todo o mecanismo: a capacidade volta aos poucos, à medida que requisições antigas saem, em vez de tudo de uma vez em um reset.
Nenhum intervalo de trinta segundos jamais contém mais de cinco requisições. Essa é a garantia que uma janela fixa não consegue dar.
Aqui a borda fica logo atrás da requisição que está sendo decidida, então ela se move a cada vez. Ninguém ganha um reset, e ninguém precisa esperar por um também.
Contador de janela deslizante
O custo do log é a lista de timestamps. O contador consegue a maior parte da precisão usando apenas dois inteiros.
Volte às janelas fixas, mantenha uma contagem para a atual e para a anterior, e depois pondere a contagem anterior pela fração dela que ainda cai dentro de um olhar retrospectivo deslizante:
x = quanto já avançamos na janela atual, como uma fração
weighted = (1 - x) * previousCount + currentCount
aceitar se weighted + 1 <= limitCom uma janela de trinta segundos, um limite de 5, e uma janela anterior que se encheu completamente:
| Tempo | Dentro da janela | 1 - x | Ponderado | Mais esta requisição | Resultado |
|---|---|---|---|---|---|
| 35s | 5s, ou seja 1/6 | 5/6 | 5/6 × 5 + 0 = 4,17 | 5,17 | Rejeitada, acima de 5 |
| 40s | 10s, ou seja 1/3 | 2/3 | 2/3 × 5 + 0 = 3,33 | 4,33 | Aceita |
| 40s de novo | 10s, ou seja 1/3 | 2/3 | 2/3 × 5 + 1 = 4,33 | 5,33 | Rejeitada |
O termo (1 - x) * previousCount é a estimativa: à medida que a janela atual se enche, menos da janela anterior permanece à vista, então a contribuição dela se esvai suavemente.
Dois contadores por cliente, e nenhum timestamp.
Em vez de checar quais requisições específicas ainda estão dentro do intervalo, ela assume que estavam distribuídas uniformemente e pega uma fração. Mais barato, e quase certo.
Onde eles discordam
O contador assume que as requisições da janela anterior estavam distribuídas uniformemente. Raramente é esse o caso, e qual algoritmo é mais permissivo depende de onde elas realmente se concentraram.
Requisições perto do início da janela anterior. O log olha para trás trinta segundos de verdade, vê que essas requisições iniciais ficam fora desse intervalo, e permite mais. O contador ainda cobra uma fração delas, então recusa.
O log é mais permissivo aqui, porque o contador está tratando a capacidade como mais restrita do que ela realmente é.
Requisições perto do fim da janela anterior. Agora o log ainda as tem à vista e recusa, enquanto o contador já esvaiu a maior parte da contagem anterior. Aqui o contador é mais permissivo, porque está subestimando o quão recente foi aquele tráfego.
Então o erro vai para os dois lados, e é isso que o torna aceitável. Não é um viés sistemático a favor dos clientes nem do servidor.
Às vezes esse chute é generoso e às vezes é rígido, e isso depende inteiramente de quando as requisições realmente chegaram.
Experimente
Uma janela de trinta segundos, um limite de 5, e uma janela anterior que aceitou 5 requisições. Nenhuma requisição ainda na janela atual.
- Usando o contador, uma requisição aos 33 segundos é aceita?
- Em que ponto da janela atual a primeira requisição passa a ser aceitável?
- Mesmo tráfego, mas as 5 requisições da janela anterior chegaram todas no primeiro segundo dela. Qual algoritmo permite a requisição aos 33 segundos, e por quê?
Compare suas respostas
1. Rejeitada. Aos 33 segundos você está 3 segundos dentro de uma janela de 30 segundos, então x é 1/10. A contagem ponderada é 0,9 × 5 + 0 = 4,5, e somando esta requisição dá 5,5, acima do limite de 5.
2. Aos 36 segundos. Você precisa que (1 - x) × 5 + 1 <= 5, então (1 - x) × 5 <= 4, então 1 - x <= 0,8 e x >= 0,2. Um quinto de uma janela de trinta segundos são 6 segundos, colocando a primeira requisição aceitável aos 36 segundos.
3. O log permite; o contador ainda recusa. O log olha para trás até os 3 segundos. As cinco requisições anteriores chegaram todas antes disso, então já saíram da janela e a contagem é 0.
O contador não tem ideia de quando elas chegaram, assume uma distribuição uniforme, e ainda cobra 4,5 contra elas. Esse é o custo concreto da aproximação: o cliente não enviou nada nos últimos trinta segundos e mesmo assim é recusado.
Para onde isso vai a seguir
Os dois algoritmos aplicam uma média, e os dois tratam um burst como algo a ser evitado. Só que boa parte do tráfego é legitimamente em bursts: uma página carregando seis recursos de uma vez não é abuso.
Token bucket adota a visão oposta, permitindo um burst de propósito e depois fazendo o cliente esperar para ganhar outro.

