Cinco erros ao implantar o primeiro aplicativo no Kubernetes

Cinco erros ao implantar o primeiro aplicativo no KubernetesFalhar por Aris-Dreamer

Muitas pessoas acreditam que basta migrar a aplicação para o Kubernetes (seja usando Helm ou manualmente) e ficarão felizes. Mas não é tão simples.

Equipe Soluções em nuvem Mail.ru traduziu um artigo do engenheiro DevOps Julian Gindi. Ele compartilha quais armadilhas sua empresa encontrou durante o processo de migração para que você não pise no mesmo rake.

Etapa um: configurar solicitações e limites de pod

Vamos começar configurando um ambiente limpo no qual nossos pods serão executados. O Kubernetes faz um ótimo trabalho agendando pods e lidando com condições de falha. Mas descobriu-se que o agendador às vezes não consegue colocar um pod se for difícil estimar quantos recursos ele precisa para funcionar com êxito. É aqui que surgem as solicitações de recursos e limites. Há muito debate sobre a melhor abordagem para definir solicitações e limites. Às vezes realmente parece que é mais arte do que ciência. Esta é a nossa abordagem.

Solicitações de pod - Este é o principal valor usado pelo agendador para posicionar o pod de maneira ideal.

De Documentação do Kubernetes: a etapa de filtragem determina o conjunto de nós onde o pod pode ser agendado. Por exemplo, o filtro PodFitsResources verifica se um nó tem recursos suficientes para satisfazer as solicitações de recursos específicas de um pod.

Usamos solicitações de aplicativos para que possam ser usadas para estimar quantos recursos de fato O aplicativo precisa disso para funcionar corretamente. Desta forma, o agendador pode colocar nós de forma realista. Inicialmente queríamos definir solicitações com margem para garantir que cada pod tivesse um número suficientemente grande de recursos, mas notamos que os tempos de agendamento aumentaram significativamente e alguns pods nunca foram totalmente agendados, como se nenhuma solicitação de recurso tivesse sido recebida para eles.

Nesse caso, o escalonador frequentemente enviava pods e não conseguia reagendá-los porque o plano de controle não tinha ideia de quantos recursos o aplicativo exigiria, um componente-chave do algoritmo de escalonamento.

Limites de pod - este é um limite mais claro para o pod. Representa a quantidade máxima de recursos que o cluster alocará ao contêiner.

Novamente, de documentação oficial: se um contêiner tiver um limite de memória de 4 GiB definido, o kubelet (e o tempo de execução do contêiner) irão aplicá-lo. O tempo de execução não permite que o contêiner use mais do que o limite de recursos especificado. Por exemplo, quando um processo em um contêiner tenta usar mais memória do que a quantidade permitida, o kernel do sistema encerra o processo com um erro de “falta de memória” (OOM).

Um contêiner sempre pode usar mais recursos do que o especificado na solicitação de recurso, mas nunca pode usar mais do que o especificado no limite. Este valor é difícil de definir corretamente, mas é muito importante.

Idealmente, queremos que os requisitos de recursos de um pod mudem ao longo do ciclo de vida de um processo sem interferir em outros processos do sistema – esse é o objetivo de estabelecer limites.

Infelizmente, não posso dar instruções específicas sobre quais valores definir, mas nós próprios seguimos as seguintes regras:

  1. Usando uma ferramenta de teste de carga, simulamos um nível básico de tráfego e monitoramos o uso dos recursos do pod (memória e processador).
  2. Definimos as solicitações de pod para um valor arbitrariamente baixo (com um limite de recursos de cerca de 5 vezes o valor das solicitações) e observamos. Quando as solicitações são muito baixas, o processo não pode ser iniciado, muitas vezes causando erros misteriosos de tempo de execução do Go.

Observe que limites de recursos mais altos dificultam o agendamento porque o pod precisa de um nó de destino com recursos suficientes disponíveis.

Imagine uma situação em que você tenha um servidor web leve com um limite de recursos muito alto, digamos 4 GB de memória. Este processo provavelmente terá que ser escalonado horizontalmente e cada novo módulo terá que ser agendado em um nó com pelo menos 4 GB de memória disponível. Se esse nó não existir, o cluster deverá introduzir um novo nó para processar esse pod, o que pode levar algum tempo. É importante manter a diferença entre solicitações e limites de recursos ao mínimo para garantir um escalonamento rápido e suave.

Etapa dois: configurar testes de atividade e prontidão

Este é outro tópico sutil frequentemente discutido na comunidade Kubernetes. É importante ter um bom entendimento dos testes de atividade e prontidão, pois eles fornecem um mecanismo para que o software funcione sem problemas e minimize o tempo de inatividade. No entanto, eles podem causar um sério impacto no desempenho do seu aplicativo se não forem configurados corretamente. Abaixo está um resumo de como são as duas amostras.

Vivacidade mostra se o contêiner está em execução. Se falhar, o kubelet mata o contêiner e uma política de reinicialização é habilitada para ele. Se o contêiner não estiver equipado com uma sonda Liveness, o estado padrão será bem-sucedido - é o que diz em Documentação do Kubernetes.

As sondagens de atividade devem ser baratas, o que significa que não devem consumir muitos recursos, porque são executadas com frequência e precisam informar ao Kubernetes que o aplicativo está em execução.

Se você definir a opção para execução a cada segundo, isso adicionará 1 solicitação por segundo, portanto, esteja ciente de que serão necessários recursos adicionais para lidar com esse tráfego.

Na nossa empresa, os testes Liveness verificam os componentes principais de uma aplicação, mesmo que os dados (por exemplo, de um banco de dados remoto ou cache) não estejam totalmente acessíveis.

Configuramos os aplicativos com um endpoint de “integridade” que simplesmente retorna um código de resposta 200. Isso é uma indicação de que o processo está em execução e é capaz de processar solicitações (mas ainda não tráfego).

Amostra Prontidão indica se o contêiner está pronto para atender solicitações. Se a investigação de prontidão falhar, o controlador de endpoint removerá o endereço IP do pod dos endpoints de todos os serviços correspondentes ao pod. Isso também é declarado na documentação do Kubernetes.

As sondagens de preparação consomem mais recursos porque devem ser enviadas ao back-end de uma forma que indique que o aplicativo está pronto para aceitar solicitações.

Há muito debate na comunidade sobre o acesso direto ao banco de dados. Dado o overhead (as verificações são realizadas com frequência, mas podem ser ajustadas), decidimos que para algumas aplicações, a prontidão para atender o tráfego só é contabilizada após verificar se os registros são retornados do banco de dados. Testes de prontidão bem projetados garantiram níveis mais elevados de disponibilidade e eliminaram o tempo de inatividade durante a implantação.

Se você decidir consultar o banco de dados para testar a prontidão do seu aplicativo, certifique-se de que seja o mais barato possível. Vamos atender este pedido:

SELECT small_item FROM table LIMIT 1

Aqui está um exemplo de como configuramos esses dois valores no Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Você pode adicionar algumas opções de configuração adicionais:

  • initialDelaySeconds — quantos segundos se passarão entre o lançamento do contêiner e o início das amostras.
  • periodSeconds — intervalo de espera entre execuções de amostra.
  • timeoutSeconds — o número de segundos após os quais a unidade é considerada uma emergência. Tempo limite normal.
  • failureThreshold — o número de falhas de teste antes que um sinal de reinicialização seja enviado ao pod.
  • successThreshold — o número de testes bem-sucedidos antes do pod entrar no estado pronto (após uma falha, quando o pod é iniciado ou se recupera).

Etapa três: configurar políticas de rede padrão para o pod

O Kubernetes tem uma topografia de rede “plana”; por padrão, todos os pods se comunicam diretamente entre si. Em alguns casos isto não é desejável.

Um possível problema de segurança é que um invasor pode usar um único aplicativo vulnerável para enviar tráfego para todos os pods da rede. Tal como acontece com muitas áreas de segurança, o princípio do menor privilégio aplica-se aqui. Idealmente, as políticas de rede deveriam especificar explicitamente quais conexões entre pods são permitidas e quais não são.

Por exemplo, abaixo está uma política simples que nega todo o tráfego de entrada para um namespace específico:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Visualização desta configuração:

Cinco erros ao implantar o primeiro aplicativo no Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Mais detalhes aqui.

Etapa quatro: comportamento personalizado usando ganchos e contêineres de inicialização

Um dos nossos principais objetivos era fornecer implantações para Kubernetes sem tempo de inatividade para os desenvolvedores. Isso é difícil porque existem muitas opções para encerrar aplicativos e liberar os recursos que eles usaram.

Dificuldades particulares surgiram com nginx. Percebemos que quando esses pods eram implantados sequencialmente, as conexões ativas eram interrompidas antes da conclusão bem-sucedida.

Após extensa pesquisa online, descobriu-se que o Kubernetes não espera que as conexões Nginx se esgotem antes de encerrar o pod. Usando um gancho de pré-parada, implementamos a seguinte funcionalidade e nos livramos completamente do tempo de inatividade:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

e aqui nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Outro paradigma extremamente útil é o uso de contêineres init para lidar com a inicialização de aplicações específicas. Isso é especialmente útil se você tiver um processo de migração de banco de dados com uso intensivo de recursos que precisa ser executado antes do início do aplicativo. Você também pode especificar um limite de recursos mais alto para esse processo sem definir esse limite para o aplicativo principal.

Outro esquema comum é acessar segredos em um contêiner de inicialização que fornece essas credenciais ao módulo principal, o que impede o acesso não autorizado aos segredos do próprio módulo principal do aplicativo.

Como de costume, cite a documentação: os contêineres de inicialização executam com segurança códigos ou utilitários personalizados que, de outra forma, reduziriam a segurança da imagem do contêiner do aplicativo. Ao manter as ferramentas desnecessárias separadas, você limita a superfície de ataque da imagem do contêiner do aplicativo.

Etapa cinco: configurando o kernel

Finalmente, vamos falar sobre uma técnica mais avançada.

Kubernetes é uma plataforma extremamente flexível que permite executar cargas de trabalho da maneira que achar melhor. Temos vários aplicativos de alto desempenho que consomem muitos recursos. Após realizar extensos testes de carga, descobrimos que um aplicativo estava lutando para lidar com a carga de tráfego esperada quando as configurações padrão do Kubernetes estavam em vigor.

No entanto, o Kubernetes permite executar um contêiner privilegiado que altera os parâmetros do kernel apenas para um pod específico. Aqui está o que usamos para alterar o número máximo de conexões abertas:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Esta é uma técnica mais avançada que muitas vezes não é necessária. Mas se o seu aplicativo estiver lutando para lidar com uma carga pesada, você pode tentar ajustar algumas dessas configurações. Mais detalhes sobre este processo e definição de valores diferentes – como sempre na documentação oficial.

Em conclusão

Embora o Kubernetes possa parecer uma solução pronta para uso, existem algumas etapas importantes que você precisa seguir para manter seus aplicativos funcionando perfeitamente.

Durante a migração do Kubernetes, é importante seguir o "ciclo de teste de carga": iniciar o aplicativo, fazer o teste de carga, observar as métricas e o comportamento de escalonamento, ajustar a configuração com base nesses dados e, em seguida, repetir o ciclo novamente.

Seja realista sobre o tráfego esperado e tente ir além dele para ver quais componentes quebram primeiro. Com esta abordagem iterativa, apenas algumas das recomendações listadas podem ser suficientes para alcançar o sucesso. Ou pode exigir uma personalização mais profunda.

Sempre se pergunte estas perguntas:

  1. Quantos recursos os aplicativos consomem e como esse volume mudará?
  2. Quais são os requisitos reais de escalabilidade? Quanto tráfego o aplicativo suportará em média? E quanto ao tráfego de pico?
  3. Com que frequência o serviço precisará ser dimensionado horizontalmente? Com que rapidez os novos pods precisam ser colocados online para receber tráfego?
  4. Quão corretamente os pods são desligados? Isso é necessário? É possível conseguir a implantação sem tempo de inatividade?
  5. Como você pode minimizar os riscos de segurança e limitar os danos causados ​​por pods comprometidos? Algum serviço tem permissões ou acesso desnecessário?

Kubernetes fornece uma plataforma incrível que permite aproveitar as melhores práticas para implantar milhares de serviços em um cluster. No entanto, cada aplicação é diferente. Às vezes, a implementação requer um pouco mais de trabalho.

Felizmente, o Kubernetes fornece a configuração necessária para atingir todos os objetivos técnicos. Usando uma combinação de solicitações e limites de recursos, testes de atividade e prontidão, contêineres de inicialização, políticas de rede e ajuste de kernel personalizado, você pode obter alto desempenho junto com tolerância a falhas e escalabilidade rápida.

O que mais ler:

  1. Melhores práticas e práticas recomendadas para execução de contêineres e Kubernetes em ambientes de produção.
  2. Mais de 90 ferramentas úteis para Kubernetes: implantação, gerenciamento, monitoramento, segurança e muito mais.
  3. Nosso canal em torno do Kubernetes no Telegram.

Fonte: habr.com

Adicionar um comentário