Escalando o Kubernetes para usar milhares de CRDs
(blog.upbound.io)O Crossplane, criado pela Upbound, fornece um control plane de nuvem no Kubernetes. Por isso, ele tem centenas de CRDs (Custom Resource Definitions) para corresponder a recursos de nuvem como AWS, Azure e GCP. No Crossplane, eles são chamados de MRs (Managed Resources).
Mesmo usuários avançados de Kubernetes costumavam operar uma quantidade razoável de CRs na casa das dezenas, mas como no Crossplane é preciso usar centenas de MRs, eles começaram a investigar até onde vai o limite de quantos CRDs o Kubernetes consegue lidar.
Isso pode ser dividido em problemas do lado do cliente e do lado do servidor.
Problemas do lado do cliente
- No cliente, o problema está no processo de discovery.
- Clientes como o kubectl fazem discovery para descobrir quais APIs o servidor oferece, e para isso precisam percorrer todos os endpoints de API uma vez.
- Os CRs são expostos como endpoints de API.
- Para consultar um MR como
https://example.org/apis/rds.aws.upbound.io/v1/instances/cool-db, é preciso encontrar os grupos de API suportados emhttps://example.org/apis/, depois encontrar as versões suportadas emhttps://example.org/apis/rds.aws.upbound.ioe então encontrar os CRs suportados emhttps://example.org/apis/rds.aws.upbound.io/v1. - Os MRs do Crossplane para provedores de nuvem AWS, Azure e GCP somam cerca de 2.000 e estão divididos em 300 grupos e versões de API.
- O cliente acaba enviando 300 requisições HTTP para fazer o discovery.
- Nas condições de rede atuais isso não é um grande problema, mas os problemas encontrados foram rate limit e cache.
Rate limit do cliente
- Havia um rate limit médio de 5 requisições por segundo (com burst de até 100), e o cache de discovery era invalidado a cada 10 minutos.
- Isso pode ser resolvido aumentando o rate limit; ele continua em 5 requisições por segundo por padrão, mas agora pode ser elevado até 300.
- Na época do kubectl v1.22 foi aberta uma issue pedindo aumento desse limite, e o cache de discovery também foi ajustado de 10 minutos para 5 horas, permitindo aproveitar o limite ampliado do cliente no Kubernetes v1.25.
Cache do cliente
- Mesmo desativando o rate limit nos testes, consultar 300 grupos de API levava quase 20 segundos.
- No início parecia um problema de rede, mas depois descobriram que era causado pela leitura dos arquivos de cache.
- Isso foi corrigido no Kubernetes 1.25, ficando 25 vezes mais rápido no macOS e 2 vezes mais rápido no Linux.
Melhorias futuras no cliente
- Faz sentido aplicar rate limit no cliente, mas na prática isso não protege adequadamente o servidor.
- O API Priority and Fairness (AP&F), introduzido no Kubernetes 1.20, fornece filas e modelagem de tráfego no lado do servidor para proteger o servidor de API.
- Um único endpoint HTTP agregado para discovery foi aprovado em um KEP e deve ser suportado em alpha na versão 1.26.
Problemas do lado do servidor
Cálculo do esquema OpenAPI
- Depois de registrar centenas de CRDs, foi observado que as requisições à API ficavam lentas por quase uma hora.
- Por meio de profiling, descobriram que esse problema vinha da lógica de cálculo do esquema OpenAPI v2.
- Quando um CRD é adicionado ou atualizado, o controlador OpenAPI constrói a especificação swagger do CR, junta isso com os swaggers de todos os CRs em uma grande especificação única, serializa em JSON e a expõe em
/openapi/v2. - Isso foi alterado para que
/openapi/v2fosse calculado sob demanda, apenas quando um endpoint relacionado a um CR real fosse solicitado. - Essa correção entrou na v1.24.0 e recebeu backport para 1.20.13, 1.21.7 e 1.22.4.
Cliente etcd
- Esse foi um novo gargalo encontrado depois de resolver o problema do OpenAPI.
- Foi descoberto que o servidor de API consumia 4 MiB de memória por CRD.
- Isso é ainda mais problemático em Kubernetes gerenciados como GKE e EKS, porque CPU e memória do servidor de API são limitadas. Se forem necessários mais recursos, o servidor de API é expandido automaticamente, mas infelizmente a adição de CRDs não é um fator usado para decidir esse escalonamento. Por isso, ele não escala a menos que o servidor de API fique repetidamente sendo morto por OOM.
- Em testes no GKE, AKE e EKS, o self-healing funcionava, mas o servidor de API ficava indisponível de 5 segundos até 1 hora. O cluster não parava completamente, mas toda a reconciliação era interrompida.
- O profiling mostrou que a biblioteca de logging Zap consumia 20% da memória.
- O servidor de API cria um cliente etcd para cada versão de CR, e cada cliente etcd cria um logger Zap.
- Como resultado, além do aumento de memória por loggers duplicados, também eram criadas conexões TCP desnecessárias entre o servidor de API e o etcd.
- Os maintainers concordaram que o certo seria usar um único cliente etcd para todos os endpoints de CR, mas como o lançamento do Kubernetes 1.25 estava próximo, era difícil corrigir tudo, então foi feita uma mudança menor para que todos os clientes etcd compartilhassem um único logger.
-Isso deve ser incluído na 1.25 e receber backport para 1.22, 1.23 e 1.24. Isso deve reduzir o uso de memória em 20%.
Melhorias futuras no lado do servidor
- Está previsto mudar de um cliente etcd criado por versão de CR para um cliente por transporte (um por cluster etcd).
- Também estão colaborando com as equipes de engenharia do GKE, EKS e AKE para lidar com a instalação de muitos CRDs do Crossplane.
2 comentários
Gratuidade -> invalidação
Cliente -> Cliente