Limites de CPU e aceleração agressiva no Kubernetes

Nota perev. : Este conto preventivo do Omio, o agregador de viagens europeu, leva os leitores da teoria básica aos cativantes meandros práticos da configuração do Kubernetes. A familiaridade com esses casos ajuda não apenas a ampliar os horizontes, mas também a evitar problemas não triviais.



Você já encontrou o fato de que o aplicativo "travou" no local, parou de responder às solicitações de verificação de integridade e não conseguiu entender o motivo desse comportamento? Uma explicação possível é o limite de cota para recursos da CPU. Ele será discutido neste artigo.

TL; DR:
é altamente recomendável que você desative os limites de CPU no Kubernetes (ou desative as cotas CFS no Kubelet) se estiver usando uma versão do kernel Linux com um erro de cota CFS. No núcleo Um bug sério e conhecido que leva a aceleração e atrasos excessivos
.

No Omio, toda a infraestrutura é gerenciada pelo Kubernetes . Todas as nossas cargas com e sem estado funcionam exclusivamente no Kubernetes (usamos o Google Kubernetes Engine). Nos últimos seis meses, começamos a observar desacelerações aleatórias. Os aplicativos congelam ou param de responder às verificações de integridade, perdem a conexão com a rede etc. Esse comportamento há muito nos deixou perplexos e, finalmente, decidimos abordar o problema de perto.

Resumo do artigo:

  • Algumas palavras sobre contêineres e Kubernetes;
  • Como os pedidos e limites de CPU são implementados;
  • Como o limite de CPU funciona em ambientes com vários núcleos;
  • Como rastrear a otimização da CPU;
  • Resolvendo o problema e as nuances.

Algumas palavras sobre contêineres e Kubernetes


Kubernetes, de fato, é o padrão moderno no mundo da infraestrutura. Sua principal tarefa é a orquestração de contêineres.

Recipientes


No passado, tínhamos que criar artefatos como Java JARs / WARs, Python Eggs ou executáveis ​​para lançamento subsequente em servidores. No entanto, para fazê-los funcionar, eles tiveram que fazer um trabalho adicional: instalar o tempo de execução (Java / Python), colocar os arquivos necessários nos lugares certos, garantir a compatibilidade com uma versão específica do sistema operacional, etc. Em outras palavras, você precisava prestar muita atenção ao gerenciamento de configuração (que geralmente causava contendas entre desenvolvedores e administradores de sistema).

Recipientes mudaram tudo.Agora, a imagem do contêiner atua como um artefato. Ele pode ser representado como um tipo de arquivo executável estendido que contém não apenas um programa, mas também um tempo de execução completo (Java / Python / ...), bem como os arquivos / pacotes necessários pré-instalados e prontos para execução. Os contêineres podem ser implantados e executados em vários servidores sem nenhuma etapa adicional.

Além disso, os contêineres funcionam em seu próprio ambiente de sandbox. Eles têm seu próprio adaptador de rede virtual, seu próprio sistema de arquivos com acesso limitado, sua própria hierarquia de processos, suas próprias restrições na CPU e na memória, etc. Tudo isso é conseguido graças a um subsistema especial do kernel do Linux - namespaces (namespace).

Kubernetes


Como afirmado anteriormente, o Kubernetes é uma orquestra de contêineres. Funciona da seguinte maneira: você fornece um pool de máquinas e diz: "Ei, Kubernetes, inicie dez instâncias do meu contêiner com 2 processadores e 3 GB de memória cada e mantenha-os operacionais!" Kubernetes cuida do resto. Ele encontrará capacidades livres, lançará contêineres e os reiniciará se necessário, lançará uma atualização ao alterar versões etc. De fato, o Kubernetes permite abstrair do componente de hardware e torna toda a variedade de sistemas adequada para implantação e operação de aplicativos.


Kubernetes do ponto de vista de um simples leigo

O que são solicitação e limite no Kubernetes


Certo, descobrimos os contêineres e o Kubernetes. Também sabemos que vários contêineres podem estar na mesma máquina.

Você pode desenhar uma analogia com um apartamento comum. Uma sala espaçosa é levada (carros / nós) e arrendada a vários inquilinos (contêineres). Kubernetes atua como um corretor de imóveis. Surge a pergunta: como manter os inquilinos em conflito entre si? E se um deles, por exemplo, decide ocupar o banheiro por meio dia?

É aqui que a solicitação e o limite entram em jogo. Solicitação de CPU é apenas para fins de planejamento. É algo como a "lista de desejos" de um contêiner e é usada para selecionar o nó mais adequado. Ao mesmo tempo, o CPU Limit pode ser comparado a uma concessão - assim que escolhemos um nó para o contêiner,não poderá ir além dos limites estabelecidos. E aqui surge um problema ...

Como solicitações e limites são implementados no Kubernetes


O Kubernetes usa o mecanismo de limitação do kernel (pulando o relógio) para implementar os limites da CPU. Se o aplicativo exceder o limite, a aceleração é ativada (ou seja, recebe menos ciclos da CPU). Solicitações e limites de memória são organizados de maneira diferente, para que sejam mais fáceis de detectar. Para fazer isso, basta verificar o status do último reinício do pod: se é "OOMKilled". Com a otimização da CPU, tudo não é tão simples, pois os K8s apenas disponibilizam métricas para uso, e não para cgroups.

Solicitação de CPU



Como a solicitação de CPU é implementada

Para simplificar, vejamos um processo usando um exemplo de máquina com CPU de 4 núcleos.

O K8s usa o mecanismo cgroups para controlar a alocação de recursos (memória e processador). Um modelo hierárquico está disponível para ele: um descendente herda os limites do grupo pai. Os detalhes da distribuição são armazenados no sistema de arquivos virtual ( /sys/fs/cgroup). No caso do processador, isso /sys/fs/cgroup/cpu,cpuacct/*.

O K8s usa o arquivo cpu.sharepara alocar recursos do processador. No nosso caso, o grupo de controle raiz recebe 4096 compartilhamentos de recursos da CPU - 100% da energia disponível do processador (1 núcleo = 1024; esse é um valor fixo). O grupo raiz distribui os recursos proporcionalmente, dependendo das quotas de descendentes prescritas emcpu.share, e aqueles, por sua vez, fazem o mesmo com seus descendentes etc. Tipicamente Kubernetes raiz do grupo de controlo tem três nó filho: system.slice, user.slicee kubepods. Os dois primeiros subgrupos são usados ​​para distribuir recursos entre cargas críticas do sistema e programas de usuários fora dos K8s. O último - - kubepodsé criado pelo Kubernetes para distribuir recursos entre os pods.

O diagrama acima mostra que o primeiro e o segundo subgrupos receberam 1024 compartilhamentos, com 4096 compartilhamentos alocados ao subgrupo kuberpod . Como isso é possível: afinal, o grupo raiz tem apenas 4096 compartilhamentos disponíveis e a soma das compartilhamentos de seus descendentes excede significativamente esse número ( 6144)? O fato é que o valor faz sentido lógico; portanto, o Linux Scheduler (CFS) o utiliza para alocar proporcionalmente os recursos da CPU. No nosso caso, os dois primeiros grupos recebem 680 ações reais (16,6% de 4096) e o kubepod recebe as 2736 ações restantes . Em caso de inatividade, os dois primeiros grupos não usarão os recursos alocados.

Felizmente, o planejador possui um mecanismo para evitar a perda de recursos da CPU não utilizados. Ele transfere capacidades "inativas" para o pool global, das quais são distribuídas entre grupos que precisam de capacidades adicionais do processador (a transferência ocorre em lotes para evitar perdas por arredondamento). Um método semelhante se aplica a todos os descendentes de descendentes.

Esse mecanismo garante uma distribuição justa da energia do processador e garante que nenhum processo "roube" recursos de outros.

Limite de CPU


Apesar de as configurações dos limites e solicitações nos K8s parecerem semelhantes, sua implementação é fundamentalmente diferente: esta é a parte mais enganosa e menos documentada.

O K8s usa o mecanismo de cotas do CFS para implementar limites. Suas configurações são especificadas nos arquivos cfs_period_use cfs_quota_usno diretório cgroup (o arquivo também está localizado lá cpu.share).

Por outro lado cpu.share, a cota é baseada em um período de tempo e não na energia disponível do processador. cfs_period_usdefine a duração do período (época) - é sempre 100.000 μs (100 ms). K8s tem a capacidade de alterar esse valor, mas atualmente está disponível apenas na versão alfa. O planejador usa a era para reiniciar as cotas usadas. Segundo arquivocfs_quota_us, define o tempo disponível (cota) em cada época. Observe que também é indicado em microssegundos. A cota pode exceder a duração da época; em outras palavras, pode ser superior a 100 ms.

Vejamos dois cenários em máquinas de 16 núcleos (o tipo mais comum de computadores que temos no Omio):


Cenário 1: 2 threads e um limite de 200 ms. Sem limitação O


cenário 2: 10 flui e um limite de 200 ms. A aceleração inicia após 20 ms, o acesso aos recursos do processador é reiniciado após outros 80 ms.

Suponha que você defina o limite da CPU para 2 núcleos; O Kubernetes converterá esse valor para 200 ms. Isso significa que o contêiner pode usar no máximo 200 ms de tempo da CPU sem limitar.

E aqui começa a diversão. Como mencionado acima, a cota disponível é de 200 ms. Se você tiver dez threads em execução paralela em uma máquina de 12 núcleos (veja a ilustração do cenário 2), enquanto todos os outros pods estiverem inativos, a cota será esgotada em apenas 20 ms (desde 10 * 20 ms = 200 ms) e todos os threads deste pod é acelerador pelos próximos 80 ms. O bug do agendador já mencionado agrava a situação , devido à qual a otimização excessiva ocorre e o contêiner não consegue nem calcular a cota existente.

Como avaliar a otimização em pods?


Basta ir ao pod e correr cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods - o número total de períodos do planejador;
  • nr_throttled- o número de períodos de estrangulamento na composição nr_periods;
  • throttled_time - tempo de aceleração acumulado em nanossegundos.



O que realmente está acontecendo?


Como resultado, obtemos alta aceleração em todas as aplicações. Às vezes, é uma vez e meia mais forte que o calculado!

Isso leva a vários erros - falhas de prontidão nas verificações, travamentos de contêiner, interrupções na conexão de rede, tempos limite nas chamadas de serviço. Por fim, isso se traduz em aumento da latência e aumento de erros.

Decisão e consequências


Tudo é simples aqui. Abandonamos os limites da CPU e começamos a atualizar o kernel do SO em clusters para a versão mais recente na qual o bug foi corrigido. O número de erros (HTTP 5xx) em nossos serviços caiu imediatamente de forma significativa:

Erros HTTP 5xx



Erros HTTP 5xx de um serviço crítico

Tempo de resposta P95



Atraso de solicitação de serviço crítico, percentil 95

Custos operacionais



Número de horas gastas

Qual é o problema?


Conforme declarado no início do artigo:

Você pode fazer uma analogia com um apartamento comum ... Kubernetes atua como corretor de imóveis. Mas como manter os inquilinos em conflito entre si? E se um deles, por exemplo, decide ocupar o banheiro por meio dia?

Essa é a pegadinha. Um contêiner negligente pode absorver todos os recursos disponíveis do processador na máquina. Se você tiver uma pilha de aplicativos inteligente (por exemplo, JVM, Go, Node VM estiver configurada corretamente), isso não será um problema: você poderá trabalhar nessas condições por um longo período de tempo. Porém, se os aplicativos forem pouco otimizados ou nem otimizados ( FROM java:latest), a situação pode ficar fora de controle. No Omio, automatizamos Dockerfiles básicos com configurações padrão adequadas para a pilha de idiomas principais, portanto não havia esse problema.

Recomendamos que você monitore as métricas de USE (uso, saturação e erros), atrasos da API e taxas de erro. Verifique se os resultados estão conforme o esperado.

Referências


Essa é a nossa história. Os seguintes materiais ajudaram muito a entender o que está acontecendo:


Relatório de erros do Kubernetes:


Você encontrou problemas semelhantes em sua prática ou tem experiência com otimização em ambientes de produção em contêiner? Compartilhe sua história nos comentários!

PS do tradutor


Leia também no nosso blog:


All Articles