Como o Apache APISIX é rápido?

Navendu Pottekkat

Navendu Pottekkat

June 12, 2023

Technology

"Alta velocidade", "latência mínima" e "desempenho máximo" são termos frequentemente usados para caracterizar o Apache APISIX. Mesmo quando alguém me pergunta sobre o APISIX, minha resposta sempre inclui "gateway de API nativo em nuvem de alto desempenho".

Os benchmarks de desempenho (vs. Kong, Envoy) confirmam que essas características são realmente precisas (teste você mesmo).

Alta velocidade, latência mínima e desempenho máximo

Testes executados por 10 rodadas com 5000 rotas únicas no Standard D8s v3 (8 vCPUs, 32 GiB de memória).

Mas como o APISIX alcança isso?

Para responder a essa pergunta, precisamos olhar para três coisas: etcd, tabelas hash e árvores radix.

Neste artigo, vamos olhar sob o capô do APISIX e ver o que são essas coisas e como todas elas trabalham juntas para manter o APISIX com desempenho máximo enquanto lida com um tráfego significativo.

etcd como o Centro de Configuração

O APISIX usa o etcd para armazenar e sincronizar configurações.

O etcd foi projetado para funcionar como um armazenamento de chave-valor para configurações de sistemas distribuídos em grande escala. O APISIX foi projetado para ser distribuído e altamente escalável desde o início, e o uso do etcd em vez de bancos de dados tradicionais facilita isso.

Arquitetura do APISIX

Outra característica indispensável para gateways de API é a alta disponibilidade, evitando tempo de inatividade e perda de dados. Isso pode ser alcançado de forma eficiente implantando várias instâncias do etcd para garantir uma arquitetura tolerante a falhas e nativa em nuvem.

O APISIX pode ler/escrever configurações do/para o etcd com latência mínima. As alterações nos arquivos de configuração são notificadas instantaneamente, permitindo que o APISIX monitore apenas as atualizações do etcd em vez de consultar um banco de dados com frequência, o que pode adicionar sobrecarga de desempenho.

Este gráfico resume como o etcd se compara com outros bancos de dados.

Tabelas Hash para Endereços IP

Listas de permissão/bloqueio baseadas em endereços IP são um caso de uso comum para gateways de API.

Para alcançar alto desempenho, o APISIX armazena a lista de endereços IP em uma tabela hash e a usa para correspondência (O(1)) em vez de iterar pela lista (O(N)).

À medida que o número de endereços IP na lista aumenta, o impacto no desempenho de usar tabelas hash para armazenamento e correspondência se torna evidente.

Sob o capô, o APISIX usa a biblioteca lua-resty-ipmatcher para implementar essa funcionalidade. O exemplo abaixo mostra como a biblioteca é usada:

local ipmatcher = require("resty.ipmatcher")
local ip = ipmatcher.new({
    "162.168.46.72",
    "17.172.224.47",
    "216.58.32.170",
})

ngx.say(ip:match("17.172.224.47")) -- true
ngx.say(ip:match("176.24.76.126")) -- false

A biblioteca usa tabelas Lua, que são tabelas hash. Os endereços IP são hasheados e armazenados como índices em uma tabela, e para pesquisar um endereço IP específico, basta indexar a tabela e verificar se o valor é nil ou não.

Armazenando endereços IP em uma tabela hash

Para pesquisar um endereço IP, ele primeiro calcula o hash (índice) e verifica seu valor. Se não estiver vazio, temos uma correspondência. Isso é feito em tempo constante O(1).

Árvores Radix para Roteamento

Por favor, me perdoe por enganá-lo com uma aula de estruturas de dados! Mas ouça-me; é aqui que as coisas ficam interessantes.

Uma área-chave onde o APISIX otimiza o desempenho é a correspondência de rotas.

O APISIX corresponde uma rota com uma solicitação a partir de seu URI, métodos HTTP, host e outras informações (veja roteador). E isso precisa ser eficiente.

Se você leu a seção anterior, uma resposta óbvia seria usar um algoritmo de hash. Mas a correspondência de rotas é complicada porque várias solicitações podem corresponder à mesma rota.

Por exemplo, se tivermos uma rota /api/*, então tanto /api/create quanto /api/destroy devem corresponder à rota. Mas isso não é possível com um algoritmo de hash.

Expressões regulares podem ser uma solução alternativa. As rotas podem ser configuradas em uma regex, e ela pode corresponder a várias solicitações sem a necessidade de codificar cada solicitação.

Se pegarmos nosso exemplo anterior, podemos usar a regex /api/[A-Za-z0-9]+ para corresponder a /api/create e /api/destroy. Regexes mais complexas poderiam corresponder a rotas mais complexas.

Mas regex é lenta! E sabemos que o APISIX é rápido. Então, em vez disso, o APISIX usa árvores radix, que são árvores de prefixo compactadas (trie) que funcionam muito bem para pesquisas rápidas.

Vamos ver um exemplo simples. Suponha que temos as seguintes palavras:

  • romane
  • romanus
  • romulus
  • rubens
  • ruber
  • rubicon
  • rubicundus

Uma árvore de prefixo armazenaria isso assim:

Árvore de prefixo

A travessia destacada mostra a palavra "rubens".

Uma árvore radix otimiza uma árvore de prefixo mesclando nós filhos se um nó tiver apenas um nó filho. Nosso exemplo de trie ficaria assim como uma árvore radix:

Árvore radix

A travessia destacada ainda mostra a palavra "rubens". Mas a árvore parece muito menor!

Quando você cria rotas no APISIX, o APISIX as armazena nessas árvores.

O APISIX pode então funcionar perfeitamente porque o tempo necessário para corresponder uma rota depende apenas do comprimento do URI na solicitação e é independente do número de rotas (O(K), K é o comprimento da chave/URI).

Portanto, o APISIX será tão rápido quanto é ao corresponder 10 rotas quando você começar e 5000 rotas quando você escalar.

Este exemplo simples mostra como o APISIX pode armazenar e corresponder rotas usando árvores radix:

Exemplo simples de correspondência de rotas no APISIX

A travessia destacada mostra a rota /user/* onde o * representa um prefixo. Portanto, um URI como /user/navendu corresponderá a essa rota. O código de exemplo abaixo deve dar mais clareza a essas ideias.

O APISIX usa a biblioteca lua-resty-radixtree, que envolve o rax, uma implementação de árvore radix em C. Isso melhora o desempenho em comparação com a implementação da biblioteca em Lua pura.

O exemplo abaixo mostra como a biblioteca é usada:

local radix = require("resty.radixtree")
local rx = radix.new({
    {
        paths = { "/api/*action" },
        metadata = { "metadata /api/action" }
    },
    {
        paths = { "/user/:name" },
        metadata = { "metadata /user/name" },
        methods = { "GET" },
    },
    {
        paths = { "/admin/:name" },
        metadata = { "metadata /admin/name" },
        methods = { "GET", "POST", "PUT" },
        filter_fun = function(vars, opts)
            return vars["arg_access"] == "admin"
        end
    }
})

local opts = {
    matched = {}
}

-- corresponde à primeira rota
ngx.say(rx:match("/api/create", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: create

ngx.say(rx:match("/api/destroy", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: destroy

local opts = {
    method = "GET",
    matched = {}
}

-- corresponde à segunda rota
ngx.say(rx:match("/user/bobur", opts)) -- metadata /user/name
ngx.say("name: ", opts.matched.name) -- name: bobur

local opts = {
    method = "POST",
    var = ngx.var,
    matched = {}
}

-- corresponde à terceira rota
-- o valor para `arg_access` é obtido de `ngx.var`
ngx.say(rx:match("/admin/nicolas", opts)) -- metadata /admin/name
ngx.say("admin name: ", opts.matched.name) -- admin name: nicolas

A capacidade de gerenciar um grande número de rotas de forma eficiente tornou o APISIX o gateway de API escolhido para muitos projetos em grande escala.

Olhando Sob o Capô

Há apenas tanto que posso explicar sobre o funcionamento interno do APISIX em um artigo.

Mas a melhor parte é que as bibliotecas mencionadas aqui e o Apache APISIX são totalmente open source, o que significa que você pode olhar sob o capô e modificar as coisas você mesmo.

E se você puder melhorar o APISIX para obter aquele último pedaço de desempenho, você pode contribuir com as alterações de volta para o projeto e deixar todos se beneficiarem do seu trabalho.

Tags: