Cumprimentos a todos! Hoje gostaríamos de compartilhar nossa experiência usando o Google Kubernetes Engine para gerenciar nossos clusters Kubernetes. Nós o usamos nos últimos três anos em produção e estamos satisfeitos por não precisarmos mais nos preocupar em gerenciar esses clusters nós mesmos.
Atualmente, temos todos os nossos ambientes de teste e clusters de infraestrutura exclusivos sob o controle do Kubernetes. Hoje, queremos falar sobre como encontramos um problema em nosso cluster de teste e como esperamos que este artigo economize tempo e esforço de outras pessoas.
Devemos fornecer informações sobre nossa infraestrutura de teste para entender totalmente nosso problema. Temos mais de cinco ambientes de teste permanentes e estamos implantando ambientes para desenvolvedores mediante solicitação. O número de módulos durante a semana chega a 6.000 durante o dia e continua crescendo. Como a carga é instável, empacotamos os módulos muito bem para economizar custos, e revender recursos é nossa melhor estratégia.
Essa configuração funcionou bem para nós até que um dia recebemos um alerta e não conseguimos excluir um namespace. A mensagem de erro que recebemos sobre a exclusão do namespace foi:
$ kubectl delete namespace arslanbekov Error from server (Conflict): Operation cannot be fulfilled on namespaces "arslanbekov": The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.
Mesmo usando a opção de exclusão forçada não resolveu o problema:
$ kubectl get namespace arslanbekov -o yaml apiVersion: v1 kind: Namespace metadata: ... spec: finalizers: - kubernetes status: phase: Terminating
Para resolver o problema de namespace travado, seguimos um guia . Ainda assim, essa solução temporária não era ideal, pois nossos desenvolvedores deveriam poder criar e excluir seus ambientes à vontade, usando a abstração de namespace.
Determinados a encontrar uma solução melhor, decidimos investigar mais. O alerta indicou um problema de métrica, que confirmamos executando um comando:
$ kubectl api-resources --verbs=list --namespaced -o name error: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request
Descobrimos que o pod do servidor de métricas estava apresentando um erro de falta de memória (OOM) e um erro de pânico nos logs:
apiserver panic'd on GET /apis/metrics.k8s.io/v1beta1/nodes: killing connection/stream because serving request timed out and response had been started goroutine 1430 [running]:
O motivo estava nos limites dos recursos do pod:
O contêiner estava encontrando esses problemas devido à sua definição, que era a seguinte (bloco de limites):
resources: limits: cpu: 51m memory: 123Mi requests: cpu: 51m memory: 123Mi
O problema era que apenas 51 milhões de CPU foram alocados ao contêiner, o que é aproximadamente equivalente a 0,05 de uma CPU principal , e isso não era suficiente para lidar com as métricas de um número tão grande de pods. Principalmente o agendador CFS é usado.
Normalmente, corrigir esses problemas é direto e envolve simplesmente alocar mais recursos para o pod. No entanto, no GKE, essa opção não está disponível na IU ou por meio da CLI gcloud. Isso ocorre porque o Google protege os recursos do sistema de serem modificados, o que é compreensível, considerando que todo o gerenciamento é feito por eles.
Descobrimos que não éramos os únicos enfrentando esse problema e encontramos um problema semelhante em que o autor tentou alterar a definição do pod manualmente. Ele teve sucesso, mas nós não. Quando tentamos alterar os limites de recursos no arquivo YAML, o GKE os reverteu rapidamente.
Precisávamos encontrar outra solução.
Nosso primeiro passo foi entender por que os limites de recursos foram definidos para esses valores. O pod consistia em dois contêineres: metrics-server
e o addon-resizer
. Este último era responsável por ajustar os recursos à medida que os nós eram adicionados ou removidos do cluster, atuando como um zelador do dimensionamento automático vertical do cluster.
Sua definição de linha de comando foi a seguinte:
command: - /pod_nanny - --config-dir=/etc/config - --cpu=40m - --extra-cpu=0.5m - --memory=35Mi - --extra-memory=4Mi ...
Nesta definição, CPU e memória representam os recursos de linha de base, enquanto extra-cpu
e extra-memory
representam recursos adicionais por nó. Os cálculos para 180 nós seriam os seguintes:
0.5m * 180 + 40m=~130m
A mesma lógica é aplicada aos recursos de memória.
Infelizmente, a única forma de aumentar os recursos era adicionando mais nós, o que não queríamos fazer. Então, decidimos explorar outras opções.
Apesar de não conseguir resolver totalmente o problema, queríamos estabilizar a implantação o mais rápido possível . Aprendemos que algumas propriedades na definição YAML podem ser alteradas sem serem revertidas pelo GKE. Para resolver isso, aumentamos o número de réplicas de 1 para 5 , adicionamos uma verificação de integridade e ajustamos a estratégia de distribuição de acordo com este artigo .
Essas ações ajudaram a reduzir a carga na instância do servidor de métricas e garantiram que sempre tivéssemos pelo menos um pod de trabalho que pudesse fornecer métricas. Levamos algum tempo para reconsiderar o problema e refrescar nossos pensamentos. A solução acabou sendo simples e óbvia em retrospecto.
Nós nos aprofundamos nas partes internas do addon-resizer e descobrimos que ele pode ser configurado por meio de um arquivo de configuração e parâmetros de linha de comando. À primeira vista, parecia que os parâmetros da linha de comando deveriam substituir os valores de configuração, mas não era o caso.
Ao investigar, descobrimos que o arquivo de configuração foi conectado ao pod por meio dos parâmetros de linha de comando do contêiner addon-resizer:
--config-dir=/etc/config
O arquivo de configuração foi mapeado como um ConfigMap com o nome metrics-server-config
no namespace do sistema, e o GKE não reverte essa configuração.
Adicionamos recursos por meio dessa configuração da seguinte maneira:
apiVersion: v1 data: NannyConfiguration: |- apiVersion: nannyconfig/v1alpha1 kind: NannyConfiguration baseCPU: 100m cpuPerNode: 5m baseMemory: 100Mi memoryPerNode: 5Mi kind: ConfigMap metadata:
E funcionou! Isso foi uma vitória para nós.
Deixamos dois pods com verificações de integridade e uma estratégia de tempo de inatividade zero enquanto o cluster estava sendo redimensionado e não recebemos mais nenhum alerta depois de fazer essas alterações.