Limitação Dinâmica de Taxa no OpenResty
API7.ai
January 6, 2023
No artigo anterior, apresentei os algoritmos de leaky bucket
e token bucket
, que são comuns para lidar com tráfego em rajadas. Além disso, aprendemos como realizar a limitação de taxa de solicitações usando a configuração do NGINX. No entanto, usar a configuração do NGINX está apenas no nível de usabilidade e ainda há um longo caminho para ser útil.
O primeiro problema é que a chave de limitação de taxa é limitada a um intervalo de variáveis do NGINX e não pode ser definida de forma flexível. Por exemplo, não há como definir diferentes limites de velocidade para diferentes províncias e diferentes canais de clientes, o que é uma necessidade comum com o NGINX.
Outro problema maior é que a taxa não pode ser ajustada dinamicamente, e cada alteração requer a recarga do serviço NGINX. Como resultado, limitar a velocidade com base em diferentes períodos só pode ser implementado de forma precária por meio de scripts externos.
É importante entender que a tecnologia serve ao negócio e, ao mesmo tempo, o negócio impulsiona a tecnologia. No momento do nascimento do NGINX, havia pouca necessidade de ajustar a configuração dinamicamente; era mais sobre proxy reverso, balanceamento de carga, baixo uso de memória e outras necessidades semelhantes que impulsionaram o crescimento do NGINX. Em termos de arquitetura e implementação de tecnologia, ninguém poderia ter previsto a enorme explosão de demanda por controle dinâmico e granular em cenários como a Internet móvel, IoT e microsserviços.
O uso de scripts Lua pelo OpenResty compensa a falta do NGINX nessa área, tornando-o um complemento eficaz. É por isso que o OpenResty é tão amplamente usado como substituto do NGINX. Nos próximos artigos, continuarei apresentando mais cenários e exemplos dinâmicos no OpenResty. Vamos começar vendo como usar o OpenResty para implementar a limitação de taxa dinâmica.
No OpenResty, recomendamos o uso de lua-resty-limit-traffic para limitar o tráfego. Ele inclui limit-req
(limitar a taxa de solicitações), limit-count
(limitar o número de solicitações) e limit-conn
(limitar conexões simultâneas); e fornece limit.traffic
para agregar esses três métodos.
Limitar a taxa de solicitações
Vamos começar olhando para o limit-req
, que usa um algoritmo de leaky bucket para limitar a taxa de solicitações.
Na seção anterior, apresentamos brevemente o código de implementação chave do algoritmo de leaky bucket nesta biblioteca resty, e agora aprenderemos como usar essa biblioteca. Primeiro, vamos ver o seguinte código de exemplo.
resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 200, 100)
local delay, err = lim:incoming("key", true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
return ngx.exit(500)
end
if delay >= 0.001 then
ngx.sleep(delay)
end'
Sabemos que o lua-resty-limit-traffic
usa um shared dict
para armazenar e contar chaves, então precisamos declarar o espaço de 100m
para my_limit_req_store
antes de podermos usar o limit-req
. Isso é semelhante para limit-conn
e limit-count
, que precisam de espaços separados de shared dict
para serem distinguidos.
limit_req.new("my_limit_req_store", 200, 100)
A linha de código acima é uma das mais críticas. Ela significa que um shared dict
chamado my_limit_req_store
é usado para armazenar estatísticas, e a taxa por segundo é definida como 200
, de modo que, se exceder 200
mas for menor que 300
(este valor é calculado a partir de 200 + 100
), será enfileirado, e se exceder 300
, será rejeitado.
Após a configuração, temos que processar a solicitação do cliente. lim: incoming("key", true)
está aqui para fazer isso. incoming
tem dois parâmetros, que precisamos ler em detalhes.
O primeiro parâmetro, a chave especificada pelo usuário para a limitação de taxa, é uma constante de string no exemplo acima, o que significa que a limitação de taxa deve ser uniforme para todos os clientes. Se você quiser limitar a taxa de acordo com diferentes províncias e canais, é muito simples usar ambas as informações como a chave, e o seguinte é o pseudo-código para alcançar esse requisito.
local province = get_ province(ngx.var.binary_remote_addr)
local channel = ngx.req.get_headers()["channel"]
local key = province .. channel
lim:incoming(key, true)
Claro, você também pode personalizar o significado da chave e as condições para chamar incoming
, para que possa obter um efeito muito flexível de limitação de taxa.
Vamos olhar para o segundo parâmetro da função incoming
, e é um valor booleano. O padrão é false
, o que significa que a solicitação não será registrada no shared dict
para estatísticas; é apenas um exercício. Se for definido como true, terá um efeito real. Portanto, na maioria dos casos, você precisará defini-lo explicitamente como true.
Você pode se perguntar por que esse parâmetro existe. Considere um cenário em que você configura duas instâncias diferentes de limit-req
com chaves diferentes, uma chave sendo o nome do host e a outra sendo o endereço IP do cliente. Então, quando uma solicitação do cliente é processada, os métodos incoming
dessas duas instâncias são chamados em ordem, conforme indicado pelo seguinte pseudo-código.
local limiter_one, err = limit_req.new("my_limit_req_store", 200, 100)
local limiter_two, err = limit_req.new("my_limit_req_store", 20, 10)
limiter_one :incoming(ngx.var.host, true)
limiter_two:incoming(ngx.var.binary_remote_addr, true)
Se a solicitação do usuário passar pela detecção de limite de limiter_one
mas for rejeitada pela detecção de limiter_two
, então a chamada da função limiter_one:incoming
deve ser considerada um exercício e não precisamos contá-la.
Nesse caso, a lógica do código acima não é rigorosa o suficiente. Precisamos fazer um exercício de todos os limitadores antecipadamente, para que, se um limite de limitador for acionado que possa rejeitar a solicitação do cliente, ele possa retornar diretamente.
for i = 1, n do
local lim = limiters[i]
local delay, err = lim:incoming(keys[i], i == n)
if not delay then
return nil, err
end
end
É sobre isso que se trata o segundo argumento da função incoming
. Este código é o código central do módulo limit.traffic
, que é usado para combinar vários limitadores de taxa.
Limitar o número de solicitações
Vamos dar uma olhada no limit.count
, uma biblioteca que limita o número de solicitações. Ela funciona como o GitHub API Rate Limiting, que limita o número de solicitações do usuário em uma janela de tempo fixa. Como de costume, vamos começar com um código de exemplo.
local limit_count = require "resty.limit.count"
local lim, err = limit_count.new("my_limit_count_store", 5000, 3600)
local key = ngx.req.get_headers()["Authorization"]
local delay, remaining = lim:incoming(key, true)
Você pode ver que limit.count
e limit.req
são usados de forma semelhante. Começamos definindo um shared dict
no nginx.conf
.
lua_shared_dict my_limit_count_store 100m;
Então new
um objeto limitador, e finalmente usamos a função incoming
para determinar e processar.
No entanto, a diferença é que o segundo valor de retorno da função incoming em limit-count
representa as chamadas restantes, e podemos adicionar campos ao cabeçalho de resposta para dar uma indicação melhor ao cliente.
ngx.header["X-RateLimit-Limit"] = "5000"
ngx.header["X-RateLimit-Remaining"] = remaining
Limitar o número de conexões simultâneas
limit.conn
é uma biblioteca para limitar o número de conexões simultâneas. Ela difere das duas bibliotecas mencionadas anteriormente por ter uma API especial leaving
, que descreverei brevemente aqui.
Limitar a taxa de solicitação e o número de solicitações, como mencionado acima, pode ser feito diretamente na fase de access
. Diferente de limitar o número de conexões simultâneas, que requer não apenas determinar se o limite foi excedido na fase de access
, mas também chamar a API leaving
na fase de log
.
log_by_lua_block {
local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
local key = ctx.limit_conn_key
local conn, err = lim:leaving(key, latency)
}
No entanto, o código central dessa API é bastante simples, que é a seguinte linha de código que decrementa o número de conexões em um. Se você não limpar na fase de log
, o número de conexões continuará subindo e logo atingirá o limite de simultaneidade.
local conn, err = dict:incr(key, -1)
Combinação de limitadores de taxa
Este é o fim da introdução de cada um desses três métodos. Finalmente, vamos ver como combinar limit.rate
, limit.conn
e limit.count
. Aqui precisamos usar a função combine
em limit.
traffic`.
local lim1, err = limit_req.new("my_req_store", 300, 200)
local lim2, err = limit_req.new("my_req_store", 200, 100)
local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
local limiters = {lim1, lim2, lim3}
local host = ngx.var.host
local client = ngx.var.binary_remote_addr
local keys = {host, client, client}
local delay, err = limit_traffic.combine(limiters, keys, states)
Este código deve ser fácil de entender com o conhecimento que você acabou de adquirir. O código central da função combine
, que já mencionamos na análise de limit.rate
acima, é principalmente implementado com a ajuda da função drill
e da função uncommit
. Essa combinação permite que você defina diferentes limites e chaves para vários limitadores para atender a requisitos de negócios mais complexos.
Resumo
Não apenas o limit.traffic
suporta os três limitadores de taxa mencionados hoje, mas desde que o limitador de taxa tenha as APIs incoming
e uncommit
, ele pode ser gerenciado pela função combine
do limit.traffic
.
Finalmente, vou deixar uma tarefa para você. Você pode escrever um exemplo que combine os limitadores de taxa de token e bucket que apresentamos antes? Sinta-se à vontade para escrever sua resposta na seção de comentários para discutir comigo, e você também é bem-vindo a compartilhar este artigo com seus colegas e amigos para aprender e se comunicar juntos.