EKS | Instâncias spot Com Zero Downtime

Instâncias spot Com Zero Downtime

Como Arquiteto Cloud, uma das minhas principais funções é garantir a alta disponibilidade de nossos serviços para ajudar nossa empresa a atingir suas metas de negócios.

No ano passado, um desses objetivos era reduzir os custos de cloud, pois nossos serviços devem atender a SLAs rigorosos com um ambiente de produção altamente disponível.

Neste artigo, vou orientá-lo sobre como reduzir significativamente os custos em seus clusters EKS, usando instâncias spot do AWS EC2 e, esperamos, dar a você a confiança necessária para usar instâncias spot com nodepool altamente disponíveis no ambiente de produção.

 

- O que são instâncias spot ?

As instâncias spot do EC2 representam capacidade de computação na AWS, onde são oferecidas com desconto de 60 a 80% sobre o preço sob demanda.

São gerenciados em nodepool de instâncias spot, onde são conjuntos de instâncias do EC2 com o mesmo tipo de instância solicitada, sistema operacional e zona de disponibilidade (AZ).

Se um nodepool de instâncias spot não estiver mais disponível, a instância spot poderá ser interrompida, recebendo uma notificação de encerramento com um aviso de dois minutos antes de ser encerrada.

 

- Provisionamento de instância spot

Com as instâncias spot, cada tipo de instância em cada zona de disponibilidade é um nodepool com seu próprio preço spot, com base na capacidade disponível.

Infelizmente, o componente autoscaler de nodes (Cluster Autoscaler), não oferece suporte a spot Fleet, portanto, teremos que escolher uma estratégia diferente para executar instâncias spot: AWS Auto Scaling Groups (ASG).

 

- Auto Scaling Groups

Um ASG contém uma coleção de instâncias do Amazon EC2 que são tratadas como um grupo lógico, pois nesta atuação específica usamos o kops para configurar nossos clusters, então demonstrarei como instalar ASGs de instâncias spot com kops InstanceGroups.

Não vamos nos aprofundar em kops neste artigo, pois se você usar outras ferramentas de instalação do Kubernetes, também poderá configurar seus ASGs para serem executados em instâncias spot com pequenos ajustes de configuração, portanto, isso não será abordado aqui.

Criaremos o InstanceGroup com o comando abaixo através de seu manifesto a seguir:

kops create ig spot-nodes-xlarge
apiVersion: kops.k8s.io/v1alpha2
kind: InstanceGroup
metadata:
 labels:
   kops.k8s.io/cluster: sample.prod.k8s.local
 name: spot-nodes-xlarge
spec:
 image: kope.io/k8s-1.15-debian-stretch-amd64-hvm-ebs-2020-07-20
 machineType: c3.xlarge
 maxSize: 10
 minSize: 0
 mixedInstancesPolicy:
   instances:
   - c3.xlarge
   - c4.xlarge
   - c5.xlarge
   - c5a.xlarge
   onDemandAboveBase: 0
   onDemandBase: 0
   spotAllocationStrategy: capacity-optimized
 nodeLabels:
   kops.k8s.io/instancegroup: spot-nodes-xlarge
   lifecycle: "spot"
 role: Node
 subnets:
 - us-east-1a
 - us-east-1b
 - us-east-1c

Após a configuração deste InstanceGroup, o Kops criará um EC2 ASG com uma mixedInstancesPolicy, utilizando vários tipos de instância spot em um InstanceGroup.

A estratégia de alocação "capacity-optimized", permite que o ASG selecione os tipos de instância com a maior capacidade disponível durante a expansão.

Isso reduzirá a chance de interrupções do spot.

Devido às limitações do Cluster Autoscaler, onde qual tipo de instância expandir, seria importante escolher instâncias do mesmo tamanho (vCPU e memória) para cada InstanceGroup.

Podemos usar o amazon-ec2-instance-selector para nos ajudar a selecionar os tipos e famílias de instâncias relevantes com um número suficiente de vCPUs e memória. Por exemplo, para obter um grupo de instâncias com 8 vCPUs e 32 GB de RAM, podemos executar o seguinte comando:

ec2-instance-selector --vcpus 8 --memory 32768 --gpus 0 --current-generation true -a x86_64 --deny-list '.*n.*'

 

- Cluster Autoscaler

Cluster Autoscaler (CA) é uma ferramenta que dimensiona automaticamente o tamanho do cluster, alterando a capacidade desejada dos ASGs.

Ele aumentará o cluster quando houver pods que não funcionem devido a recursos insuficientes e o reduzirá quando houver nodes no cluster que foram subutilizados por um longo período de tempo.

Na seção anterior, criamos um InstanceGroup de nodes, mas na maioria dos casos um InstanceGroup não é suficiente e precisaremos de mais grupos, por exemplo, para diferentes tamanhos de máquina, nodes com GPU ou para um grupo com uma única AZ para dar suporte a volumes persistentes.

Quando houver mais de um grupo de nodes e a CA identificar que precisa aumentar o cluster devido a pods não programáveis, ela terá que decidir qual grupo expandir. Queremos que a CA sempre prefira adicionar instâncias spot em vez de On-demand.

A CA usa o expansor para escolher qual grupo dimensionar, com isso a AWS com o CA oferece 4 estratégias de expansão diferentes para selecionar o grupo de nodes, ao qual novos nodes serão adicionados:

Os expansor podem ser selecionados, configurando através dos argumentos da CA, ou seja,

--expander=priority

O Priority Expansor seleciona o grupo de node que recebeu a prioridade mais alta pelo usuário nos valores armazenados em ConfigMap, que deve ser criado antes do pod de CA e deve ser nomeado como cluster-autoscaler-priority-expander

O exemplo do ConfigMap é o seguinte:

apiVersion: v1
kind: ConfigMap
metadata:
 name: cluster-autoscaler-priority-expander
 namespace: kube-system
data:
 priorities: |-
   20:
   - spot-nodes.*
   10:
   - .*

Ao definir .*spot-nodes.* (regex para nomes de grupos de nodes), com a prioridade mais alta, informamos ao CA Expander para sempre preferir expandir os grupos de nodes spot.

A CA respeita o nodeSelector e o requiredDuringSchedulingIgnoredDuringExecution nodeAffinity, portanto, considerará apenas Após a configuração deste InstanceGroup, a kops criará um EC2 ASG com uma mixedInstancesPolicy, utilizando vários tipos de instância spot em um InstanceGroup que satisfaçam esses requisitos de expansão.

Se não houver instâncias spot disponíveis, a CA não conseguirá escalar os grupos spot e, em vez disso, escalará os grupos sob demanda com menos prioridade. Com essa abordagem, você obterá um mecanismo de retorno completo e automático para o mecanismo sob demanda.

 

- Instance Termination Handler

Instance Termination Handler

Agora vamos preparar nosso cluster para lidar com interrupções spot.

Usaremos o AWS Node Termination Handler para essa finalidade, pois executará um pod em cada node de instância spot (um DaemonSet) que detecta um aviso de interrupção S pot observando os metadados do AWS EC2.

Um novo recurso chamado EC2 Instance rebalance recommendation na tradução livre “Recomendação de rebalanceamento de instância do EC2”, foi anunciado recentemente pela AWS.

Um sinal notifica você quando uma instância spot está em alto risco de interrupção.

Esse sinal pode chegar antes do aviso de interrupção da instância spot de dois minutos, dando a você a oportunidade de reequilibrar proativamente seu nodepool antes do aviso de interrupção.

Se um aviso de recomendação de interrupção ou rebalanceamento for detectado, será acionadouma drenagem do node.

A drenagem do node despeja com segurança todos os pods hospedados nele. Quando um pod é despejado usando a API de despejo, ele é encerrado normalmente, respeitando a configuração de terminationGracePeriodSeconds em seu PodSpec.

Cada um dos pods removidos será reprogramado em um node diferente para que todas as implantações voltem à capacidade desejada.

Um exemplo simples de instalação do Helm aws-node-termination-handler é o seguinte:

helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm upgrade --install aws-node-termination-handler \
--namespace kube-system \
--set nodeSelector.lifecycle=spot \
--set enableSpotInterruptionDraining="true" \
--set enableRebalanceMonitoring="true" \
eks/aws-node-termination-handler

 

- Prevenção de inatividade de serviço

Até agora, temos um cluster híbrido que pode dimensionar automaticamente instâncias spot, fazer fallback para sob demanda, se necessário, e lidar com despejos de pod normal quando um node spot é recuperado.

Em um ambiente de produção em que muitos serviços precisam permanecer ativos 100% do tempo, a drenagem de nodes aleatórios pode levar a uma catástrofe com bastante facilidade.

Por exemplo, se:

Nas seções a seguir, você entenderá melhor como evitar que esses cenários ocorram.

 

- Affinity Rules

Pod affinity and anti-affinity do pod são regras que permitem especificar como os pods devem ser agendados em relação a outros pods.

As regras são definidas usando custom labels em nodes e seletores de label selectors em pods.

Por exemplo, usando regras de afinidade, você pode distribuir pods de um serviço entre nodes ou AZ.

Há dois tipos de regras de afinidade de pod:

Preferencial especifica que o scheduler tentará impor as regras, mas não há garantia. Obrigatório, por outro lado, especifica que a regra deve ser atendida antes que um pod possa ser agendado.

No exemplo a seguir, usamos o tipo podAntiAffinity preferido:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: redis
spec:
 selector:
   matchLabels:
     app: redis
 replicas: 3
 template:
   metadata:
     labels:
       app: redis
   spec:
     containers:
     - name: redis-server
       image: redis:6.0-alpine
     affinity:
       podAntiAffinity:
         preferredDuringSchedulingIgnoredDuringExecution:
         - weight: 30
           podAffinityTerm:
             labelSelector:
               matchExpressions:
               - key: app
                 operator: In
                 values:
                 - "redis"
             topologyKey: failure-domain.beta.kubernetes.io/zone
         - weight: 20
           podAffinityTerm:
             labelSelector:
               matchExpressions:
               - key: app
                 operator: In
                 values:
                 - "redis"
             topologyKey: beta.kubernetes.io/instance-type
         - weight: 10
           podAffinityTerm:
             labelSelector:
               matchExpressions:
               - key: app
                 operator: In
                 values:
                 - "redis"
             topologyKey: kubernetes.io/hostname

Ao definir pesos diferentes, o scheduler tentará primeiro distribuir essas 3 réplicas redis em diferentes AZs (label do node failure-domain.beta.kubernetes.io/zone).

Se não houver espaço disponível em zonas separadas, ele continuará tentando agendá-los em diferentes tipos de instância (label do node do tipo de instância).

Por fim, se nenhum local estiver disponível em AZs ou tipos de instância separados, ele tentará distribuir as réplicas em nodes separados (label do node do nome do host).

Especificar essas regras para implantações críticas nos ajudará a distribuir os pods de acordo com a lógica do pool de instâncias spot e minimizar a chance de vários encerramentos do mesmo componente ao mesmo tempo.

 

- PodDisruptionBudge

PodDisruptionBudget (PDB) é um objeto de API que indica o número máximo de interrupções que podem ser causadas a uma coleção de pods. O PDB pode nos ajudar a limitar o número de despejos simultâneos e evitar uma interrupção do serviço.

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: redis-pdb
spec:
minAvailable: 1
selector:
  matchLabels:
    app: redis

No exemplo acima, estamos configurando a API de despejo para negar interrupções de pods redis se houver apenas um pod pronto no cluster.

Portanto, se, por exemplo, o deployment do redis tiver 3 réplicas em um ou vários nodes que estão sendo drenados simultaneamente, o Kubernetes primeiro despejará dois pods e depois continuará para o terceiro, somente depois que um dos pods reprogramados estiver pronto em outro node.

Você só pode especificar maxUnavailable ou minAvailable em um único PDB. Ambos podem ser expressos como números inteiros ou como porcentagem.

 

- Super Provisionamento do Cluster

Cluster Headroom

Depois que uma instância spot for recuperada e o node estiver sendo drenado, o Kubernetes tentará agendar os pods removidos.

Na maioria das vezes, devido ao tamanho do cluster que vem com a CA, o agendador não encontrará espaço suficiente para todos os pods removidos e alguns deles aguardarão em estado pendente até que a CA acione uma expansão e novos nós estejam prontos.

Esses preciosos minutos de espera podem ser evitados implementando o headroom do cluster (ou super provisionamento de cluster).

Antes de entrarmos na implementação, você deve estar familiarizado com o Pod Priority.

Em resumo, os pods podem ter prioridade. Se um pod não puder ser agendado, o k8s pode despejar pods de prioridade mais baixa para possibilitar o agendamento de um pod pendente de prioridade mais alta.

Para implementar um headroom de cluster, executamos pods de cluster-overprovisioning "fictício", com baixa prioridade para reservar espaço extra no cluster. Esses pods salvarão o local necessário para pods críticos que são despejados quando um nó está sendo drenado. Os pods de provisionamento excessivo obterão valores de solicitação de recursos e executarão um processo linux de pause, para que eles economizem ativamente espaço extra no cluster sem consumir nenhum recurso.

Quando os pods de provisionamento excessivo são substituídos por pods de alta prioridade, seu status muda para pendente e eles se tornam os que aguardam novos nodes em vez do workload crítico.

A maior parte disso pode ser feito com o helm chart do cluster-overprovisioner que adicionará duas PriorityClasses e o deployment do over-provisioner configurada com uma baixa priorityClass.

O PriorityClass mais alto criado aqui será o globalDefault, para que todos os pods sem um conjunto de priorityClassName sejam mais altos do que os pods com provisionamento excessivo.

values.yaml para o chart do helm:

fullnameOverride: "overprovision"
deployments:
- name: spot
  replicaCount: 1
  resources:
    requests:
      cpu: 2
      memory: 4Gi
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: lifecycle
            operator: In
            values:
            - "spot"

Para garantir que as réplicas de cluster-overprovisioning contem escalas automáticas com base no tamanho das instâncias spot do cluster, podemos implantar uma ferramenta muito útil chamada cluster-proportional-autoscaler que permite dimensionar uma deployment com base no tamanho do cluster.

Execute em seu cluster com os seguintes argumentos:

/cluster-proportional-autoscaler
--namespace={{ .Release.Namespace }}
--configmap=overprovisioning-scale
--target=deployment/overprovision-spot
--nodelabels=lifecycle=spot
--logtostderr=true
--v=1

Com um ConfigMap:

kind: ConfigMap
apiVersion: v1
metadata:
 name: overprovisioning-scale
 namespace: {{ .Release.Namespace | quote }}
data:
 linear: |-
   {
     "coresPerReplica": 50
   }

Neste exemplo, definimos o cluster-proportional-autoscaler para dimensionar o deployment do ponto de cluster-overprovisioning para uma réplica para cada 50 núcleos de CPU de todos os nodes da instância spot.

As configurações do coresPerReplica no cluster-overprovisioning devem ser ajustadas com base em suas necessidades de espaço livre.

 

- Considerações

Nos casos abaixo, você pode considerar a não execução de alguns aplicativos em instâncias spot: