Parte 3: Como Construir um API Gateway de Microservices Usando OpenResty
API7.ai
February 3, 2023
Neste artigo, a construção do gateway de API de microsserviços chega ao fim. Vamos usar um exemplo mínimo para juntar os componentes selecionados anteriormente e executá-los de acordo com o projeto desenhado!
Configuração e inicialização do NGINX
Sabemos que o gateway de API é usado para lidar com a entrada de tráfego, então primeiro precisamos fazer uma configuração simples no nginx.conf
para que todo o tráfego seja tratado pelo código Lua do gateway.
server {
listen 9080;
init_worker_by_lua_block {
apisix.http_init_worker()
}
location / {
access_by_lua_block {
apisix.http_access_phase()
}
header_filter_by_lua_block {
apisix.http_header_filter_phase()
}
body_filter_by_lua_block {
apisix.http_body_filter_phase()
}
log_by_lua_block {
apisix.http_log_phase()
}
}
}
Aqui usamos o gateway de API de código aberto Apache APISIX como exemplo, então o exemplo de código acima contém a palavra-chave apisix
. Neste exemplo, escutamos a porta 9080
e interceptamos todas as solicitações para essa porta por location /
, processando-as através das fases access
, rewrite
, header filter
, body filter
e log
, chamando as funções correspondentes dos plugins em cada fase. A fase rewrite
é combinada na função apisix.http_access_phase
.
A inicialização do sistema é tratada na fase init_worker
, que inclui a leitura dos parâmetros de configuração, a pré-definição do diretório no etcd, a obtenção da lista de plugins do etcd e a ordenação dos plugins por prioridade, etc. Liste e expliquei as partes principais do código aqui, e você pode ver uma função de inicialização mais completa no GitHub.
function _M.http_init_worker()
-- Inicialização de Roteamento, Serviços e Plugins - as três partes mais importantes
router.init_worker()
require("apisix.http.service").init_worker()
require("apisix.plugins.ext-plugin.init").init_worker()
end
Como você pode ver neste código, a inicialização das partes do roteador e do plugin é um pouco mais complicada, principalmente envolvendo a leitura dos parâmetros de configuração e a tomada de algumas decisões dependendo deles. Como isso envolve a leitura de dados do etcd, usamos ngx.timer
para contornar a restrição de "não poder usar cosocket na fase init_worker
". Se você estiver interessado nesta parte, recomendamos a leitura do código-fonte para entender melhor.
Correspondência de Rotas
No início da fase access
, primeiro precisamos corresponder a rota com base no uri
, host
, args
, cookies
, etc., carregados pela solicitação, às regras de roteamento que foram configuradas.
router.router_http.match(api_ctx)
A única linha de código exposta ao público é a acima, onde o api_ctx
armazena as informações de uri
, host
, args
e cookie
da solicitação. A implementação específica da função de correspondência usa o lua-resty-radixtree
que mencionamos anteriormente. Se nenhuma rota for correspondida, a solicitação não terá um upstream correspondente e retornará 404
.
local router = require("resty.radixtree")
local match_opts = {}
function _M.match(api_ctx)
-- Obtém os parâmetros da solicitação do ctx e os usa como condição de julgamento para a rota
match_opts.method = api_ctx.var.method
match_opts.host = api_ctx.var.host
match_opts.remote_addr = api_ctx.var.remote_addr
match_opts.vars = api_ctx.var
-- Chama a função de julgamento da rota
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
-- Se nenhuma rota for correspondida, retorna 404
if not ok then
core.log.info("not find any matched route")
return core.response.exit(404)
end
return true
end
Carregamento de Plugins
Claro, se a rota for atingida, passamos para a etapa de filtragem e carregamento do plugin, que é o núcleo do gateway de API. Vamos começar com o seguinte código.
local plugins = core.tablepool.fetch("plugins", 32, 0)
-- A lista de plugins no etcd e a lista de plugins no arquivo de configuração local são intersectadas
api_ctx.plugins = plugin.filter(route, plugins)
-- Executa as funções montadas pelo plugin nas fases rewrite e access em sequência
run_plugin("rewrite", plugins, api_ctx)
run_plugin("access", plugins, api_ctx)
Neste código, primeiro solicitamos uma tabela de comprimento 32 através do pool de tabelas, que é uma técnica de otimização de desempenho que introduzimos anteriormente. Em seguida, vem a função de filtro do plugin. Você pode se perguntar por que essa etapa é necessária. Na fase init worker do plugin, não obtivemos a lista de plugins do etcd e os ordenamos?
A filtragem aqui é feita em comparação com a configuração local pelos seguintes dois motivos:
- Primeiro, um plugin recém-desenvolvido precisa ser lançado em canário. Nesse momento, o novo plugin existe na lista do etcd, mas está apenas no estado aberto em alguns dos nós do gateway. Portanto, precisamos fazer uma operação de interseção adicional.
- Para suportar o modo de depuração. Quais plugins são processados pela solicitação do cliente? Qual é a ordem de carregamento desses plugins? Essas informações serão úteis ao depurar, então a função de filtro também determinará se está no modo de depuração e registrará essas informações no cabeçalho da resposta.
Então, no final da fase access
, pegamos esses plugins filtrados e os executamos um por um em ordem de prioridade, como mostrado no código a seguir.
local function run_plugin(phase, plugins, api_ctx)
for i = 1, #plugins, 2 do
local phase_fun = plugins[i][phase]
if phase_fun then
-- O código de chamada principal
phase_fun(plugins[i + 1], api_ctx)
end
end
return api_ctx
end
Ao iterar pelos plugins, você pode ver que fazemos isso em intervalos de 2
. Isso ocorre porque cada plugin terá dois componentes: o objeto do plugin e os parâmetros de configuração do plugin. Agora, vamos olhar para a linha de código principal no exemplo de código acima.
phase_fun(plugins[i + 1], api_ctx)
Se essa linha de código for um pouco abstrata, vamos substituí-la por um plugin limit_count
concreto, o que ficará muito mais claro.
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
Neste ponto, estamos praticamente concluídos com o fluxo geral do gateway de API. Todo esse código está no mesmo arquivo, que contém mais de 400 linhas de código, mas o núcleo do código são as poucas dezenas de linhas que descrevemos acima.
Escrita de Plugins
Agora, há uma coisa a fazer antes que uma demonstração completa possa ser executada, e isso é escrever um plugin. Vamos pegar o plugin limit-count
como exemplo. A implementação completa tem pouco mais de 60 linhas de código, que você pode ver clicando no link. Aqui, vou explicar as linhas de código-chave em detalhes:
Primeiro, vamos introduzir lua-resty-limit-traffic
como a biblioteca base para limitar o número de solicitações.
local limit_count_new = require("resty.limit.count").new
Em seguida, usando o json schema
em rapidjson
para definir quais são os parâmetros deste plugin:
local schema = {
type = "object",
properties = {
count = {type = "integer", minimum = 0},
time_window = {type = "integer", minimum = 0},
key = {type = "string",
enum = {"remote_addr", "server_addr"},
},
rejected_code = {type = "integer", minimum = 200, maximum = 600},
},
additionalProperties = false,
required = {"count", "time_window", "key", "rejected_code"},
}
Esses parâmetros do plugin correspondem à maioria dos parâmetros do resty.limit.count
, que contêm a chave do limite, o tamanho da janela de tempo e o número de solicitações a serem limitadas. Além disso, o plugin adiciona um parâmetro: rejected_code
, que retorna o código de status especificado quando a solicitação é limitada.
Na etapa final, montamos a função de manipulação do plugin na fase rewrite
:
function _M.rewrite(conf, ctx)
-- Obtém o objeto de limite de contagem do cache, se não, usa a função `create_limit_obj` para criar um novo objeto e armazená-lo em cache
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
-- Obtém o valor da chave de `ctx.var` e compõe uma nova chave junto com o tipo de configuração e o número da versão da configuração
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
-- Função para determinar se o limite é atingido
local delay, remaining = lim:incoming(key, true)
if not delay then
local err = remaining
-- Se o valor limite for excedido, retorna o código de status especificado
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- Se o limite não for excedido, libera e define o cabeçalho de resposta correspondente
core.response.set_header("X-RateLimit-Limit", conf.count,
"X-RateLimit-Remaining", remaining)
end
Há apenas uma linha de lógica no código acima que faz a determinação do limite, o resto está aqui para fazer o trabalho de preparação e definir os cabeçalhos de resposta. Se o limite não for excedido, ele continuará a executar o próximo plugin de acordo com a prioridade.
Resumo
Finalmente, vou deixar uma pergunta para reflexão. Sabemos que os gateways de API podem lidar não apenas com tráfego de camada 7, mas também com tráfego de camada 4. Com base nisso, você consegue pensar em alguns cenários de uso para ele? Bem-vindo a deixar seus comentários e compartilhar este artigo para aprender e se comunicar com mais pessoas.
Anterior: Parte 1: Como construir um gateway de API de microsserviços usando OpenResty Parte 2: Como construir um gateway de API de microsserviços usando OpenResty