APISIX: Migrar operações do etcd de HTTP para gRPC
February 10, 2023
Limitações das Operações HTTP do Apache APISIX com etcd
Quando o etcd estava na versão 2.x, a interface de API que ele expunha era HTTP 1 (vamos nos referir a ela como HTTP a partir de agora). Após o etcd ser atualizado para a versão 3.x, ele mudou o protocolo de HTTP para gRPC. Para usuários que não suportam gRPC, o etcd fornece o gRPC-Gateway para proxy de solicitações HTTP como gRPC para acessar as novas APIs gRPC.
Quando o APISIX começou a usar o etcd, ele utilizava a API v2 do etcd. No APISIX 2.0 (2020), atualizamos a exigência do etcd da versão 2.x para 3.x. A compatibilidade do etcd com HTTP nos poupou esforço para a atualização da versão. Precisamos apenas modificar o código nos métodos de chamada e processamento de respostas. No entanto, ao longo dos anos, também encontramos alguns problemas relacionados à API HTTP do etcd. Ainda existem algumas diferenças sutis. Percebemos que ter um gRPC-gateway não significa que ele pode suportar perfeitamente o acesso HTTP.
Aqui está uma lista de problemas relacionados que encontramos com o etcd nos últimos anos:
- gRPC-gateway desabilitado por padrão. Devido à negligência do mantenedor, a configuração padrão do etcd não habilita o gRPC-gateway em alguns projetos. Então, tivemos que adicionar instruções no documento para verificar se o etcd atual tem o gRPC-gateway habilitado. Veja https://github.com/apache/apisix/pull/2940.
- Por padrão, o gRPC limita as respostas a 4MB. O etcd remove essa restrição no SDK que fornece, mas não no gRPC-gateway. Acontece que o etcdctl oficial (construído no SDK que ele fornece) funciona bem, mas o APISIX não. Veja https://github.com/etcd-io/etcd/issues/12576.
- Mesmo problema - desta vez com o número máximo de solicitações para a mesma conexão. A implementação do HTTP2 do Go tem uma configuração
MaxConcurrentStreams
que controla o número de solicitações que um único cliente pode enviar simultaneamente, com padrão de 250. Qual cliente normalmente enviaria mais de 250 solicitações ao mesmo tempo? Então, o etcd sempre usou essa configuração. No entanto, o gRPC-gateway, o "cliente" que faz proxy de todas as solicitações HTTP para a interface gRPC local, pode exceder esse limite. Veja https://github.com/etcd-io/etcd/issues/14185. - Após o etcd habilitar o mTLS, o etcd usa o mesmo certificado tanto como certificado do servidor quanto como certificado do cliente: o certificado do servidor para o gRPC-gateway e o certificado do cliente quando o gRPC-gateway acessa a interface gRPC. Se a extensão de autenticação do servidor estiver habilitada no certificado, mas a extensão de autenticação do cliente não estiver habilitada, ocorrerá um erro na verificação do certificado. Mais uma vez, acessar diretamente com o etcdctl funciona bem (já que o certificado não será usado como certificado do cliente neste caso), mas o APISIX não. Veja https://github.com/etcd-io/etcd/issues/9785.
- Após habilitar o mTLS, o etcd permite a configuração de políticas de segurança das informações do usuário nos certificados. Como mencionado acima, o gRPC-gateway usa um certificado de cliente fixo ao acessar a interface gRPC, em vez das informações do certificado usadas para acessar a interface HTTP no início. Assim, esse recurso não funcionará naturalmente, pois o certificado do cliente é fixo e não será alterado. Veja https://github.com/apache/apisix/issues/5608.
Podemos resumir os problemas em dois pontos:
- O gRPC-gateway (e talvez outras tentativas de converter HTTP para gRPC) não é uma solução mágica que resolve todos os problemas.
- Os desenvolvedores do etcd não dão ênfase suficiente ao método HTTP para gRPC. E seu maior usuário, o Kubernetes, não usa esse recurso.
Para resolver esse problema, precisamos usar o etcd diretamente através do gRPC, para que não precisemos passar pelo caminho HTTP do gRPC-Gateway reservado para compatibilidade.
Superando os Desafios da Migração para gRPC
Bug no lua-protobuf
Nosso primeiro problema durante o processo de migração foi um bug inesperado em uma biblioteca de terceiros. Como a maioria dos aplicativos OpenResty, usamos lua-protobuf para decodificar/codificar protobuf.
Após integrar o arquivo proto do etcd, descobrimos que haveria falhas ocasionais no código Lua, relatando um erro de "estouro de tabela". Como essa falha não pode ser reproduzida de forma confiável, nosso primeiro instinto foi procurar um exemplo mínimo reproduzível. Curiosamente, se você usar o arquivo proto do etcd sozinho, não consegue reproduzir o problema. Essa falha parece ocorrer apenas quando o APISIX está em execução.
Após algum debugging, localizei o problema no lua-protobuf ao analisar o campo oneof
do arquivo proto. O lua-protobuf tentava pré-alocar o tamanho da tabela ao analisar, e o tamanho alocado é calculado de acordo com um valor específico. Havia uma certa chance de que esse valor fosse um número negativo. Então, o LuaJIT converteria esse número para um número positivo grande ao alocar, resultando em um erro de "estouro de tabela". Eu relatei o problema ao autor e mantivemos um fork com uma solução alternativa internamente.
O autor do lua-protobuf foi muito receptivo, fornecendo uma correção no dia seguinte e lançando uma nova versão alguns dias depois. Acontece que, quando o lua-protobuf limpava os arquivos proto que não eram mais usados, ele deixava de limpar alguns campos, resultando em um número negativo irracional quando o oneof
era processado posteriormente. O problema só ocorria de vez em quando, e por que não podia ser reproduzido ao usar o arquivo proto do etcd sozinho, porque ele perdia as etapas de limpeza desses campos.
Alinhamento com o Comportamento HTTP
Durante o processo de migração, descobri que a API existente não retorna exatamente o resultado da execução, mas uma resposta HTTP com status de resposta e corpo. E então, o chamador precisa processar a resposta HTTP por si mesmo.
Se as respostas fossem em gRPC, elas precisariam ser encapsuladas com uma camada de resposta HTTP para alinhar com a lógica de processamento. Caso contrário, o chamador precisaria modificar o código em vários lugares para se adaptar ao novo formato de resposta (gRPC). Especialmente considerando que as operações baseadas em HTTP do etcd também precisam ser suportadas simultaneamente.
Embora adicionar uma camada adicional para ser compatível com a resposta HTTP não seja desejado, precisamos contornar isso. Além disso, também precisamos fazer algum processamento na resposta gRPC. Por exemplo, quando não há dados correspondentes, o HTTP não retorna nenhum dado, mas o gRPC retorna uma tabela vazia. Isso também precisa ser ajustado para alinhar com os comportamentos do HTTP.
De Conexão Curta para Conexão Longa
Nas operações baseadas em HTTP do etcd, o APISIX usa conexões curtas, então não há necessidade de considerar o gerenciamento de conexões. Tudo o que precisamos fazer é iniciar uma nova conexão sempre que precisamos e fechá-la quando terminamos.
Mas o gRPC não pode fazer isso. Um dos principais propósitos da migração para o gRPC é alcançar o multiplexing, o que não pode ser alcançado se uma nova conexão gRPC for criada para cada operação. Aqui, precisamos agradecer ao gRPC-go, por sua capacidade de gerenciamento de conexão embutida, que pode reconectar automaticamente uma vez que a conexão é interrompida. Assim, podemos usar o gRPC-go para reutilizar a conexão. E apenas os requisitos de negócios precisam ser considerados no nível do APISIX.
As operações do etcd do APISIX podem ser divididas em duas categorias: uma são as operações CRUD (criar, excluir, modificar, consultar) nos dados do etcd; a outra é sincronizar a configuração do plano de controle. Embora, teoricamente, essas duas operações do etcd possam compartilhar a mesma conexão gRPC, decidimos dividi-las em duas conexões para separar as responsabilidades. Para a conexão das operações CRUD, como o APISIX precisa ser tratado separadamente na inicialização e após a inicialização, uma instrução if
é adicionada ao obter uma nova conexão. Se houver uma incompatibilidade (ou seja, a conexão atual foi criada na inicialização enquanto precisamos de uma conexão após a inicialização), então fechamos a conexão atual e criamos uma nova. Desenvolvi um novo método de sincronização para a sincronização da configuração, de modo que cada recurso use um stream sob a conexão existente para observar o etcd.
Benefícios da Migração para gRPC
Um benefício óbvio após a migração para o gRPC é que o número de conexões necessárias para operar o etcd é drasticamente reduzido. Ao operar o etcd através do HTTP, o APISIX só podia usar conexões curtas. E ao sincronizar a configuração, cada recurso teria uma conexão separada.
Após mudar para o gRPC, pudemos usar a função de multiplexing do gRPC, e cada recurso usa apenas um único stream em vez de uma conexão completa. Dessa forma, o número de conexões não aumenta mais com o número de recursos. Considerando que o desenvolvimento subsequente do APISIX introduzirá mais tipos de recursos, por exemplo, a versão mais recente 3.1 adicionou secrets
, a redução no número de conexões ao usar o gRPC será mais significativa.
Ao usar o gRPC para sincronização, cada processo tem apenas uma (duas, se o subsistema de stream estiver habilitado) conexão para sincronização de configuração. Na figura abaixo, podemos ver que os dois processos têm quatro conexões, duas das quais são para sincronização de configuração, a API Admin usa uma conexão, e a conexão restante é para o agente privilegiado relatar informações do servidor.
Para comparação, a figura abaixo mostra 22 conexões necessárias para usar o método original de sincronização de configuração, mantendo os outros parâmetros inalterados. Além disso, essas conexões são conexões curtas.
A única diferença entre essas duas configurações é se o gRPC está habilitado para operações do etcd:
etcd:
use_grpc: true
host:
- "http://127.0.0.1:2379"
prefix: "/apisix"
...
Além de reduzir o número de conexões, usar o gRPC para acessar o etcd diretamente em vez do gRPC-gateway pode resolver uma série de problemas limitados arquitetonicamente, como a autenticação mTLS mencionada no início do artigo. Também haverá menos problemas após usar o gRPC, porque o Kubernetes usa o gRPC para operar o etcd. Se houver um problema, ele será descoberto pela comunidade do Kubernetes.
Claro, como o método gRPC ainda é relativamente novo, o APISIX inevitavelmente terá alguns novos problemas ao operar o etcd através do gRPC. Atualmente, o padrão ainda é usar o método original baseado em HTTP para operar o etcd por padrão. Os usuários têm a opção de configurar use_grpc
sob o etcd como true no config.yaml
por si mesmos. Você pode tentar se o método gRPC é melhor. Também continuaremos a coletar feedback de várias fontes para melhorar a operação do etcd baseada em gRPC. Quando descobrirmos que a abordagem gRPC é madura o suficiente, a tornaremos a abordagem padrão.
Para Maximizar o APISIX, Você Precisa do API7
Você ama o desempenho do Apache APISIX, não as despesas de gerenciá-lo. Você pode se concentrar no seu negócio principal sem se preocupar com configuração, manutenção e atualização.
Nossa equipe é composta por criadores e contribuidores do Apache APISIX, mantenedores principais do OpenResty e NGINX, membros do Kubernetes e especialistas da indústria em infraestrutura em nuvem. Você tem as melhores pessoas por trás das cenas.
Você quer acelerar seu desenvolvimento com confiança? Para maximizar o suporte ao APISIX, você precisa do API7. Fornecemos suporte aprofundado para o APISIX e soluções de gerenciamento de API com base em suas necessidades!
Entre em contato conosco agora: https://api7.ai/contact.