Como resolver problemas de inicialização a frio e pressão de disco no Kubernetes

Como resolver problemas de inicialização a frio e pressão de disco no Kubernetes

Um dia, durante uma atualização planejada do cluster k8s, descobrimos que quase todos os nossos PODs (aproximadamente 500 de 1.000) em novos nós não conseguiam iniciar, e os minutos rapidamente se transformaram em horas. Fomos ativamente procurados pela causa raiz, mas depois de três horas, os PODS ainda estavam no ContainerCreatingstatus. Felizmente, esse não era o ambiente de produção e a janela de manutenção estava programada para o fim de semana. Tivemos tempo para investigar o problema sem nenhuma pressão.

Onde você deve começar sua busca pela causa raiz? Gostaria de saber mais sobre a solução que encontramos? Aperte o cinto e aproveite!

Mais detalhes sobre o problema

O problema era que tínhamos um grande número de imagens docker que precisavam ser extraídas e iniciadas em cada nó do cluster ao mesmo tempo. Isso acontecia porque várias extrações simultâneas de imagens docker em um único nó podem levar a alta utilização do disco e tempos de inicialização a frio estendidos.

De tempos em tempos, o processo de CD leva até 3 horas para puxar as imagens. No entanto, desta vez ele travou completamente, porque a quantidade de PODS durante a atualização do EKS (inline, quando substituímos todos os nós no cluster) era muito alta.

Todos os nossos aplicativos vivem no k8s (baseado em EKS). Para economizar em nossos custos para DEV env, usamos instâncias spot. Usamos a imagem AmazonLinux2 para os nós.

Temos um grande número de ramos de recursos (FBs) no ambiente de desenvolvimento que são continuamente implantados em nossoKubernetescluster. Cada FB tem seu próprio conjunto de aplicativos, e cada aplicativo tem seu próprio conjunto de dependências (dentro de uma imagem).

Em nosso projeto, quase 200 apps e esse número está crescendo. Cada app usa uma das 7 imagens base do docker com um tamanho de ~2 GB. O tamanho total máximo da imagem arquivada (no ECR ) é de cerca de 3 GB.

Todas as imagens são armazenadas no Amazon Elastic Container Registry (ECR). Usamos o tipo de volume EBS gp3 padrão para os nós.

Problemas enfrentados

  • Tempo de inicialização a frio estendido: iniciar um novo pod com uma nova imagem pode levar mais de 1 hora, principalmente quando várias imagens são extraídas simultaneamente em um único nó.
  • Erros ErrImagePull: frequentes ErrImagePullou travados nos ContainerCreatingestados, indicando problemas com a extração de imagens.
  • Alta utilização do disco: a utilização do disco permanece próxima de 100% durante o processo de extração da imagem, principalmente devido à E/S intensiva do disco necessária para descompactação (por exemplo, "unpigz").
  • Problemas com o DaemonSet do sistema: alguns DaemonSets do sistema (como aws-nodeou ebs-csi-node) foram movidos para o estado "não pronto" devido à pressão do disco, impactando a prontidão do nó.
  • Nenhum cache de imagem nos nós: como estamos usando instâncias spot, não podemos usar o disco local para armazenar imagens em cache.

Isso resulta em muitas implantações paralisadas em ramificações de recursos, principalmente porque cada FB tem um conjunto diferente de imagens base.

Após uma rápida investigação, descobrimos que o problema principal era a pressão do disco nos nós pelo unpigzprocesso. Esse processo é responsável por descompactar as imagens do docker. Não alteramos as configurações padrão para o tipo de volume gp3 EBS, porque não é adequado para o nosso caso.

Hotfix para recuperar o cluster

Como primeiro passo, decidimos reduzir o número de PODs nos nós.

  1. Movemos os novos nós para o estado "Cordon"
  2. Remova todos os PODS presos para reduzir a pressão do disco
  3. Execute um por um os PODs para aquecer os nós
  4. Depois disso, movemos os nós aquecidos para o estado normal ("unCordon")
  5. Foram removidos todos os nós no estado travado
  6. Todos os PODS foram iniciados com sucesso usando o cache de imagem do Docker

Um design original de CI/CD

A ideia principal da solução é aquecer os nós antes do processo de CD começar pela maior parte da imagem do docker (camada de dependências JS), que usa como imagem raiz para todos os nossos aplicativos. Temos pelo menos 7 tipos de imagens raiz com as dependências JS, que estão relacionadas ao tipo do aplicativo.

Em nosso pipeline de CI/CD, temos 3 pilares:

  1. Na Initetapa de TI: preparamos o ambiente/variáveis, definimos o conjunto de imagens a serem reconstruídas, etc…
  2. Na Buildetapa: construímos as imagens e as enviamos para o ECR
  3. Na Deployetapa: implantamos as imagens no k8s (atualização de implantações, etc…)

Nossos feature branches (FB) bifurcaram-se do mainbranch. No processo de CI, sempre analisamos o conjunto de imagens que foram alteradas no FB e as reconstruímos. O mainbranch é sempre estável, pois a definição, deve haver sempre a versão mais recente das imagens base.

Nós construímos separadamente as imagens docker de dependências JS (para cada ambiente) e as enviamos para o ECR para reutilizá-las como a imagem raiz (base) no Dockerfile. Temos cerca de 5 a 10 tipos de imagem docker de dependências JS.

O FB é implantado no cluster k8s para o namespace separado, mas para os nós comuns para o FB. O FB pode ter ~200 apps, com o tamanho da imagem de até 3 GB.

Temos o sistema de dimensionamento automático de cluster, que dimensiona os nós no cluster com base na carga ou PODS pendentes com o nodeSelector e a tolerância adequados. Usamos as instâncias spot para os nós.

Implementação do processo de aquecimento

Há requisitos para o processo de aquecimento:

Obrigatório:

  • Resolução de problemas: aborda e resolve ContainerCreatingproblemas.
  • Desempenho aprimorado: reduz significativamente o tempo de inicialização utilizando imagens base pré-aquecidas (dependências JS).

É bom ter melhorias:

  • Flexibilidade: permite alterações fáceis no tipo de nó e sua vida útil (por exemplo, SLA alto ou tempo de vida útil estendido).
  • Transparência: fornece métricas claras sobre uso e desempenho.
  • Eficiência de custos: economiza custos excluindo o VNG imediatamente após a exclusão do ramo de recurso associado.
  • Isolamento: Esta abordagem garante que outros ambientes não sejam afetados.

Solução

Após analisar os requisitos e restrições, decidimos implementar um processo de aquecimento que pré-aqueceria os nós com as imagens de cache JS base. Esse processo seria acionado antes do início do processo de CD, garantindo que os nós estejam prontos para a implantação do FB e que tenhamos uma chance máxima de atingir o cache.

Dividimos essa melhoria em três grandes etapas:

  1. Crie o conjunto de nós (Grupo de Nós Virtuais) para cada FB
  2. Adicione imagens base ao script cloud-init para os novos nós
  3. Adicione uma etapa de pré-implantação para executar o DaemonSet com a initContainersseção para baixar as imagens do docker necessárias para os nós antes do início do processo de CD.

Um pipeline de CI/CD atualizado

Etapa de inicialização 1.1. (nova etapa) Implantação de inicialização: se for a primeira inicialização do FB, crie um novo conjunto pessoal de instâncias de nó (em nossos termos, é Virtual Node Group ou VNG) e baixe todas as imagens base JS (5–10 imagens) do branch principal. É justo fazer isso, porque bifurcamos o FB do branch principal. Um ponto importante: não é uma operação de bloqueio.

Etapa de pré-implantação 3.1. (nova etapa) Baixe imagens base JS recém-assadas com a tag FB específica do ECR. Pontos importantes: É uma operação de bloqueio, porque devemos reduzir a pressão do disco. Uma por uma, baixamos as imagens base para cada nó relacionado.

Etapa de implantação Não há alterações nesta etapa. Mas, graças à etapa anterior, já temos todas as camadas de imagem docker pesadas nos nós necessários.

Implementar

  1. Crie um novo conjunto de nós para cada FB por meio de chamada de API (para o sistema de dimensionamento automático de terceiros) do nosso pipeline de CI.

Problemas resolvidos:

  • Isolamento: Cada FB tem seu próprio conjunto de nós, garantindo que o ambiente não seja afetado por outros FBs.
  • Flexibilidade: Podemos alterar facilmente o tipo de nó e sua vida útil.
  • Eficiência de custos: podemos excluir os nós imediatamente após a exclusão do FB.
  • Transparência: Podemos rastrear facilmente o uso e o desempenho dos nós (cada nó tem uma tag relacionada ao FB).
  • Uso efetivo das instâncias spot: a instância spot começa com imagens base já predefinidas, ou seja, depois que o nó spot é iniciado, já existem imagens base no nó (da ramificação principal).
  1. Baixe todas as imagens base JS do branch principal para os novos nós via cloud-initscript.

Problemas resolvidos:

  • Resolução do problema: A pressão do disco acabou, porque atualizamos o cloud-initscript adicionando o download das imagens base do branch principal. Isso nos permite atingir o cache na primeira inicialização do FB.
  • Uso efetivo das instâncias spot: A instância spot está começando com dados atualizados cloud-init. Isso significa que, depois que o nó spot começa, já existem imagens base no nó (do branch principal).
  • Desempenho aprimorado: o processo de CD pode continuar a criar novas imagens sem problemas.

Etapa de pré-implantação

Precisamos dessa etapa porque as imagens do FB são diferentes das imagens do branch principal. Precisamos baixar as imagens base do FB para os nós antes do processo de CD começar. Isso ajudará a mitigar os tempos estendidos de inicialização a frio e a alta utilização do disco que podem ocorrer quando várias imagens pesadas são extraídas simultaneamente.

Objetivos da etapa de pré-implantação:

  • Prevenir pressão de disco: Baixe sequencialmente as imagens mais pesadas do docker. Após a etapa init-deploy, já temos as imagens base nos nós, o que significa que temos uma grande chance de atingir o cache.
  • Melhore a eficiência da implantação: garanta que os nós sejam pré-aquecidos com imagens do Docker essenciais, resultando em tempos de inicialização do POD mais rápidos (quase imediatamente).
  • Melhore a estabilidade: minimize as chances de encontrar ErrImagePullerros ContainerCreatinge garanta que os conjuntos de daemons do sistema permaneçam em um estado "pronto".

Detalhes da etapa de pré-implantação:

  • No CD criamos um DaemonSet com a initContainersseção. A initContainersseção é executada antes do início do contêiner principal, garantindo que as imagens necessárias sejam baixadas antes do início do contêiner principal.
  • No CD, estamos continuamente verificando o status do daemonSet. Se o daemonSet estiver em um estado "pronto", prosseguimos com a implantação. Caso contrário, esperamos que o daemonSet esteja pronto.

Conteúdo Relacionado

O Rails 8 sempre foi um divisor de águas...
Os aplicativos da Web são uma pedra fundamental da...
Os desenvolvedores Java enfrentam uma variedade de erros relacionados...
Com várias décadas de experiência, adoro criar aplicativos corporativos...
A escalabilidade é um fator crítico quando se trata...
Ao trabalhar em um projeto de código aberto no...
A Inteligência Artificial (IA) tem se tornado cada vez...
A maioria das organizações enfrenta desafios ao se adaptar...
Quando nós, desenvolvedores, encontramos alguns bugs em nossos logs,...
A cibersegurança é um tópico cada vez mais importante...
A experiência do desenvolvedor (DX) é um tópico cada...
Ao relatar estatísticas resumidas para resultados de testes de...
Explorando as Engrenagens do Kernel Semântico Falei um pouco...
A arquitetura de software evoluiu drasticamente nas últimas décadas,...
Como você previne alucinações de grandes modelos de linguagem...
O conceito de "jardim digital" tem ganhado cada vez...
Voltar para o blog

Deixe um comentário

Os comentários precisam ser aprovados antes da publicação.