Melhorando a resposta do Kubernetes em situações de scale-out

Rafa Leo
6 min readMar 17, 2022

--

navio de carga enfrentando uma tempestade

Você pode ler este post em Inglês aqui.

Conseguir responder rapidamente a situações de scale-out sempre foi um desafio, serviços de nuvem chegaram para resolver esse problema, mas ainda sim, continua sendo desafiador às vezes.

Deixe me contar uma história…

A um tempo atrás, meu time recebeu a missão de reduzir o tempo de rollout dos deployments através do nosso sistema de CI, porque era bem comum ver deployments rodando muito mais do que o necessário devido ao tempo gasto pelo cluster-autoscaler para iniciar novas instancias (Note que eu adoro o cluster-autoscaler, não estou aqui para criticar a desempenho dele).

Nós estávamos executando o K8S no EKS e usando o cluster-autoscaler iniciar instâncias de self-managed node groups (também conhecidos como Auto Scaling Groups) e atender nossas demandas de forma automatizada, e isso tinha um custo: TEMPO

Detalhando o problema e estimando esforços

Se você já usou o cluster-autoscaler, deve saber que sempre que há pods unschedulable, ele solicita novas instâncias para o ASG mais adequado, que requer algum tempo para fornecer à capacidade adicional (no nosso caso, geralmente era cerca de 2 minutos). Portanto, frequentemente nossos deployments tinham que esperar dois, quatro ou até SEIS minutos, apenas esperando a AWS fornecer instancias (considerando ambientes de desenvolvimento, staging e produção).

Devo dizer também que essa não era nossa principal prioridade ou mesmo era alinhado com nossos OKRs, mas foi uma importante melhoria na developer expecience para nossa plataforma.

Nós consideramos algumas opções:

  • Ser um dos primeiros a adotar o karpenter (que estava se tornando GA na época)
  • Adotar warm pools
  • Superdimensionar nosso cluster, deixando capacidade ociosa disponível para situações como essa, usando o cluster-proportional-autoscaler para gerenciar a quantidade de capacidade ociosa dinamicamente.

Em um sprint especifico, concordamos em priorizar este assunto e implementar uma solução que coubesse em um sprint, decidimos que talvez o cluster-proportional-autoscaler fosse a resposta!

O Cluster Proportional Autoscaler (CPA) é:

This container image watches over the number of schedulable nodes and cores of the cluster and resizes the number of replicas for the required resource. This functionality may be desirable for applications that need to be autoscaled with the size of the cluster, such as DNS and other services that scale with the number of nodes/pods in the cluster.

Deixe-me resumir o raciocínio aqui:

  • O problema da “lentidão no rollout” estava sendo causado devido ao nosso cluster operar com alto comprometimento de recursos (não necessariamente uso, mas comprometimento), e era bastante comum para novos deployments terem de esperar por novos recursos.
  • E se mantivéssemos recursos sempre disponíveis para novos deployments? Parece interessante (apesar do 💸), o problema era: como atingir isso usando o CA? nada fácil!
  • Então, nossa ideia era: um deployment fake consumindo recursos cujo numero de réplicas aumenta ou diminui conforme os requisitos de cada cluster (cluster maiores com mais recurso disponíveis, e cluster menores com menos.) ok… até aqui tudo bem… mas como?

Detalhes de implementação:

  • Realizar o deploy de uma app fictícia com a priority class mais baixa possível para que ela possa ser evicted sempre que necessário. (não inferior ao parâmetro expendable-pods-priority-cutoff do CA, por favor!)
  • Definir Request/Limit desta app fictícia de forma equivalente à capacidade disponível no instance type utilizado.
  • Usar o CPA para acompanhar o status do cluster e criar novas réplicas da app fictícia quando necessário conforme os requisitos do cluster, considerando uma margem de 5% da capacidade computacional ociosa.

Simples, não é?

O mantra para a solução de problemas: Keep it Simple

A ideia realmente tinha potencial, mas significaria um novo componente com pelo menos 2 novos deploys, tempo de implementação, etc., então, considerando o tamanho de nossos clusters, pensamos um pouco mais e decidimos abandonar o CPA por um tempo e usar números estáticos para capacidade ociosa (também foi bom para os custos 🤑), economizando tempo em nosso sprint para tópicos relacionados a KR.

A solução final foi algo assim:

Falando sobre números, antes do deployment do “overprovisioning”:

Vamos usar o deploy greetings-app que já tem 2 réplicas nesta demonstração:

kubectl scale deploy gree-46f4-web -n greetings-app --replicas=3
deployment.apps/gree-46f4-web scaled

Como podemos verificar nos eventos a seguir, o pod levou 97 segundos para iniciar e, dos quais 91 segundos estavam aguardando um nó.

Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 117s (x2 over 117s) default-scheduler 0/13 nodes are available: 1 node(s) had taint {kind: monitoring}, that the pod didn't tolerate, 2 Insufficient cpu, 2 node(s) had taint {kind: consul-server}, that the pod didn't tolerate, 3 node(s) didn't match pod affinity/anti-affinity, 3 node(s) didn't match pod anti-affinity rules, 5 node(s) had taint {kind: ingress}, that the pod didn't tolerate.
Normal TriggeredScaleUp 109s cluster-autoscaler pod triggered scale-up: [{t-medium 1->2 (max: 20)}]
Warning FailedScheduling 37s (x3 over 57s) default-scheduler 0/14 nodes are available: 1 node(s) had taint {kind: monitoring}, that the pod didn't tolerate, 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn't tolerate, 2 Insufficient cpu, 2 node(s) had taint {kind: consul-server}, that the pod didn't tolerate, 3 node(s) didn't match pod affinity/anti-affinity, 3 node(s) didn't match pod anti-affinity rules, 5 node(s) had taint {kind: ingress}, that the pod didn't tolerate.
Normal Scheduled 26s default-scheduler Successfully assigned greetings-app/gree-46f4-web-7b9f94b67f-9czsl to ip-10-0-0-70.ec2.internal
Normal Pulling 21s kubelet Pulling image "123456789123.dkr.ecr.us-east-1.amazonaws.com/greetings-app:0d1c10f3"
Normal Pulled 20s kubelet Successfully pulled image "123456789123.dkr.ecr.us-east-1.amazonaws.com/greetings-app:0d1c10f3" in 832.344624ms
Normal Created 20s kubelet Created container web
Normal Started 20s kubelet Started container web

Depois do deployment “overprovisioning”:

Vamos usar o deploy greetings-app que já tem 3 réplicas nesta demonstração:

$ kubectl scale deploy gree-46f4-web -n greetings-app --replicas=4
deployment.apps/gree-46f4-web scaled

Como podemos verificar nos eventos a seguir, o pod levou 15 segundos para iniciar, mas suas primeiras tentativas falharam por falta de recursos, porem após apenas 13 segundos encontrou um nó:

$ kubectl get events -n greetings-app
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m54s (x2 over 2m54s) default-scheduler 0/13 nodes are available: 1 Insufficient memory, 1 node(s) had taint {kind: monitoring}, that the pod didn't tolerate, 2 Insufficient cpu, 2 node(s) had taint {kind: consul-server}, that the pod didn't tolerate, 3 node(s) didn't match pod affinity/anti-affinity, 3 node(s) didn't match pod anti-affinity rules, 5 node(s) had taint {kind: ingress}, that the pod didn't tolerate.
Normal Scheduled 2m41s default-scheduler Successfully assigned greetings-app/gree-46f4-web-7b9f94b67f-ljl82 to ip-10-0-0-235.ec2.internal
Normal Pulling 2m40s kubelet Pulling image "123456789123.dkr.ecr.us-east-1.amazonaws.com/greetings-app:0d1c10f3"
Normal Pulled 2m39s kubelet Successfully pulled image "123456789123.dkr.ecr.us-east-1.amazonaws.com/greetings-app:0d1c10f3" in 758.707555ms
Normal Created 2m39s kubelet Created container web
Normal Started 2m39s kubelet Started container web

Da perspectiva do pod overprovisioning, podemos verificar através de seus eventos que ele foi evicted para conceder seus recursos ao novo pod greetings-app.

$ kubectl get events -n kube-system -wLAST SEEN   TYPE     REASON      OBJECT                                           MESSAGE
0s Normal Preempted pod/node-std-overprovisioning-6c85d99cb4-4vgnv Preempted by greetings-app/gree-46f4-web-7b9f94b67f-ljl82 on node ip-10-73-34-235.ec2.internal
0s Normal Killing pod/node-std-overprovisioning-6c85d99cb4-4vgnv Stopping container reserved-resources

O novo Pod de overprovisioning teve que esperar algum tempo para ficar online novamente, pois exigia capacidade adicional. Ele esperou precisamente 85 segundos por um nó.

$ kubectl describe pod node-std-overprovisioning-6c85d99cb4-jz4rh -n kube-systemEvents:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 94s (x2 over 94s) default-scheduler 0/13 nodes are available: 13 Insufficient cpu, 13 Insufficient memory.
Normal TriggeredScaleUp 83s cluster-autoscaler pod triggered scale-up: [{t-medium 1->2 (max: 20)}]
Warning FailedScheduling 19s (x3 over 39s) default-scheduler 0/14 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn't tolerate, 13 Insufficient cpu, 13 Insufficient memory.
Normal Scheduled 8s default-scheduler Successfully assigned kube-system/node-std-overprovisioning-6c85d99cb4-jz4rh to ip-10-0-0-0.ec2.internal
Normal Pulling 4s kubelet Pulling image "k8s.gcr.io/pause:3.1"
Normal Pulled 3s kubelet Successfully pulled image "k8s.gcr.io/pause:3.1" in 783.123073ms
Normal Created 3s kubelet Created container reserved-resources
Normal Started 3s kubelet Started container reserved-resources

Conclusão

Alcançamos os resultados que esperávamos em muito pouco tempo e tivemos uma grande redução no tempo de implantação.

Para nossa surpresa, logo após iniciarmos uma campanha estratégica que nos levou diretamente a um aumento de tráfego, fizemos um pequeno ajuste no número de nós ociosos e quando todos os HPAs começaram a fazer seu trabalho, adivinhe quem forneceu toda a capacidade adicional necessária: a deployment FICTÍCIO!!! Que reduziu drasticamente o tempo gasto no scale out das nossas apps.

Isso definitivamente resolveu o nosso problema inicial (diminuir o tempo de rollout de deploys), mas também nos deu condições de atingir melhor os objetivos do negócio, com um esforço muito baixo, podendo focar no que eram de fato prioridades.

--

--