Avançar para o conteúdo principal
BlogContentores (Kubernetes, Docker)Escala proactiva para os aglomerados de Kubernetes

Escala proactiva para os aglomerados de Kubernetes

Proactive-Scaling-for-Kubernetes-Clusters

Este posto faz parte da nossa série de escalas Kubernetes. Registe-se para assistir ao vivo ou aceder à gravação, e ver os nossos outros posts nesta série:

Quando o seu aglomerado tem poucos recursos, o Cluster Autoscaler providencia um novo nó e adiciona-o ao aglomerado. Se já é um utilizador Kubernetes, deve ter notado que criar e adicionar um nó ao aglomerado demora vários minutos.

Durante este tempo, a sua aplicação pode ser facilmente sobrecarregada com ligações, porque não pode ser mais dimensionada.

Imagem do ecrã mostrando a escala esperada com base nos pedidos por segundo (RPS) versus o planalto de escala real que ocorre enquanto se confia apenas no Cluster Autoscaler.
Pode demorar vários minutos a fornecer uma máquina virtual. Durante este tempo, poderá não ser possível escalar as suas aplicações.

Como é que se pode reparar o longo tempo de espera?

Escala pró-activa, ou: 

  • compreender como funciona o auto-escalador de agregados e maximizar a sua utilidade;
  • utilizando o programador Kubernetes para atribuir cápsulas a um nó; e
  • nós de trabalhadores de provisionamento de forma proactiva para evitar a má escalada.

Se preferir ler o código para este tutorial, pode encontrar isso no GitHub do LearnK8s.

Como funciona o Cluster Autoscaler em Kubernetes

O Cluster Autoscaler não analisa a disponibilidade de memória ou CPU quando desencadeia o autoscaling. Em vez disso, o Cluster Autoscaler reage a eventos e verifica a existência de quaisquer cápsulas não escalonáveis. Uma cápsula é inescalável quando o programador não consegue encontrar um nó que a possa acomodar.

Vamos testar isto, criando um agrupamento.

bash
$ linode-cli lke cluster-create \
 --label learnk8s \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 1 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

Deve prestar atenção aos seguintes detalhes:

  • cada nó tem 4GB de memória e 2 vCPU (i.e. `g6-standard-2`);
  • há um único nó no aglomerado; e
  • o auto-escalador de agregados está configurado para crescer de 1 a 10 nós.

É possível verificar se a instalação é bem sucedida:

bash
$ kubectl get pods -A --kubeconfig=kubeconfig

Exportar o ficheiro kubeconfig com uma variável de ambiente é normalmente mais conveniente.

Pode fazê-lo com:

bash
$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

Excelente!

Implementação de uma aplicação
Vamos implantar uma aplicação que requer 1GB de memória e 250m* de CPU.
Note: m = thousandth of a core, so 250m = 25% of the CPU

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 1
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 1G
             cpu: 250m

Pode submeter o recurso ao agrupamento com:

bash
$ kubectl apply -f podinfo.yaml

Assim que o fizer, poderá reparar nalgumas coisas. Primeiro, três cápsulas estão quase imediatamente a correr, e uma está pendente.

Diagrama mostrando três cápsulas activas num nó, e uma cápsula pendente fora desse nó.

E depois:

  • após alguns minutos, o autoscaler cria um nó extra; e
  • a quarta cápsula é colocada no novo nó.
Diagrama mostrando três cápsulas num nó, e a quarta cápsula implantada num novo nó.
Eventualmente, a quarta cápsula é implantada num novo nó.

Porque é que a quarta cápsula não está implantada no primeiro nó? Vamos investigar os recursos alocáveis.

Recursos atribuíveis em nós de Kubernetes

As cápsulas implantadas no seu cluster Kubernetes consomem memória, CPU, e recursos de armazenamento.

No entanto, no mesmo nó, o sistema operativo e o kubelet requerem memória e CPU.

Num nó operário Kubernetes, a memória e o CPU estão divididos em:

  1. Recursos necessários para o funcionamento do sistema operacional e daemons do sistema, tais como SSH, systemd, etc.
  2. Recursos necessários para o funcionamento dos agentes Kubernetes, tais como o Kubelet, o tempo de funcionamento do recipiente, detector de problemas de nós, etc.
  3. Recursos disponíveis para Pods.
  4. Recursos reservados para o limiar de despejo.
Recursos atribuídos e reservados num nó Kubernetes, consistindo em 1. limiar de despejo; 2. memória e CPU deixados para as cápsulas; 3. memória e CPU reservados para o kubelet; 4. memória e CPU reservados para o SO
Recursos atribuídos e reservados num nó Kubernetes.

Se o seu cluster corre um DaemonSet como o kube-proxy, deverá reduzir ainda mais a memória disponível e o CPU.

Portanto, vamos baixar os requisitos para garantir que todas as cápsulas possam caber num único nó:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 0.8G # <- lower memory
             cpu: 200m    # <- lower CPU

Pode alterar o destacamento com:

bash
$ kubectl apply -f podinfo.yaml

Seleccionar a quantidade certa de CPU e memória para optimizar as suas instâncias pode ser complicado. A calculadora da ferramenta Learnk8s pode ajudá-lo a fazer isto mais rapidamente.

Resolveu um problema, mas e o tempo que leva a criar um novo nó?

Mais cedo ou mais tarde, terá mais de quatro réplicas. Terá realmente de esperar alguns minutos antes que as novas vagens sejam criadas?

A resposta curta é sim.

Linode tem de criar uma máquina virtual a partir do zero, fornecê-la, e ligá-la ao aglomerado. O processo pode facilmente demorar mais de dois minutos.

Mas há uma alternativa.

Poderá criar proactivamente nós já provisionados quando precisar deles.

Por exemplo: poderia configurar o autoscaler para ter sempre um nó de reserva. Quando as cápsulas são implantadas no nó sobressalente, o autoscaler pode criar mais proactivamente. Infelizmente, o auto-calibrador não tem esta funcionalidade incorporada, mas pode facilmente recriá-la.

Pode criar uma cápsula que tenha pedidos iguais ao recurso do nó:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

Pode submeter o recurso ao agrupamento com:

bash
kubectl apply -f placeholder.yaml

Esta cápsula não faz absolutamente nada.

Diagrama que mostra como é utilizada uma cápsula de suporte de lugar para fixar todos os recursos no nó.
É utilizada uma cápsula de reserva de lugar para assegurar todos os recursos no nó.

Apenas mantém o nó totalmente ocupado.

O passo seguinte é certificar-se de que a cápsula do lugar é despejada assim que haja uma carga de trabalho que precise de ser escalonada.

Para isso, pode utilizar uma Classe Prioritária.

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning # <--
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

E voltar a submetê-lo ao agrupamento com:

bash
kubectl apply -f placeholder.yaml

Agora a configuração está completa.

Talvez seja necessário esperar um pouco para que o autoscaler crie o nó, mas neste momento, deverá ter dois nós:

  1. Um nódulo com quatro cápsulas.
  2. Outro com uma cápsula de reserva de lugar.

O que acontece quando se escala o desdobramento a 5 réplicas? Terá de esperar que o autoscaler crie um novo nó?

Vamos testar com:

bash
kubectl scale deployment/podinfo --replicas=5

Deve-se observar:

  1. A quinta cápsula é criada imediatamente, e fica no estado de Corrida em menos de 10 segundos.
  2. A cápsula de suporte foi despejada para criar espaço para a cápsula.
Diagrama mostrando como é despejada a cápsula de suporte do local para dar espaço a cápsulas normais.
A cápsula de suporte é despejada para criar espaço para as cápsulas normais.

E depois:

  1. O autoscalador de aglomerado reparou na cápsula de suporte de lugar pendente e providenciou um novo nó.
  2. A cápsula de suporte de lugar é colocada no nó recém-criado.
Diagrama que mostra como a cápsula pendente desencadeia o auto-calibrador de agrupamento que cria um novo nó.
A cápsula pendente desencadeia o auto-calibrador de agrupamento que cria um novo nó.

Porquê criar proactivamente um único nó quando se poderia ter mais?

É possível escalar a cápsula de suporte de lugar para várias réplicas. Cada réplica irá pré-provisionar um nó Kubernetes pronto a aceitar cargas de trabalho padrão. No entanto, esses nós ainda contam contra a sua conta de nuvem, mas ficam ociosos e não fazem nada. Portanto, deve ter cuidado e não criar muitos deles.

Combinando o Cluster Autoscaler com o Pod Autoscaler Horizontal

Para compreender a implicação desta técnica, vamos combinar o autoscaler de cluster com o Autoscaler de Pod Horizontal (HPA). O HPA foi concebido para aumentar as réplicas nas suas implantações.

À medida que a sua candidatura recebe mais tráfego, poderá ter o autoscaler a ajustar o número de réplicas para tratar de mais pedidos.

Quando as cápsulas esgotarem todos os recursos disponíveis, o autoscalador de agrupamento irá desencadear a criação de um novo nó para que o HPA possa continuar a criar mais réplicas.

Vamos testar isto, criando um novo agrupamento:

bash
$ linode-cli lke cluster-create \
 --label learnk8s-hpa \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 3 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa

É possível verificar se a instalação é bem sucedida:

bash
$ kubectl get pods -A --kubeconfig=kubeconfig-hpa

Exportar o ficheiro kubeconfig com uma variável de ambiente é mais conveniente.

Pode fazê-lo com:

bash
$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods

Excelente!

Vamos usar o Helm para instalar Prometheus e raspar as métricas dos destacamentos.
Pode encontrar as instruções sobre como instalar o Hel m no seu site oficial.

bash
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus

Kubernetes oferece ao HPA um controlador para aumentar e diminuir as réplicas de forma dinâmica.

Infelizmente, a HPA tem alguns inconvenientes:

  1. Não funciona fora da caixa. É necessário instalar um Servidor de métricas para agregar e expor as métricas.
  2. Não pode utilizar as consultas PromQL fora da caixa.

Felizmente, pode usar a KEDA, que estende o controlador HPA com algumas características extra (incluindo a leitura de métricas de Prometheus).

KEDA é um autoscaler feito de três componentes:

  • Um Escalador
  • Um Adaptador de Métrica
  • Um Controlador
Diagrama mostrando a arquitectura KEDA
Arquitectura KEDA.

Pode instalar a KEDA com Helm:

bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

Agora que Prometheus e KEDA estão instalados, vamos criar uma implantação.

Para esta experiência, irá utilizar um aplicativo concebido para lidar com um número fixo de pedidos por segundo. 

Cada cápsula pode processar, no máximo, dez pedidos por segundo. Se a cápsula receber o 11º pedido, deixará o pedido pendente e processá-lo-á mais tarde.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
     annotations:
       prometheus.io/scrape: "true"
   spec:
     containers:
       - name: podinfo
         image: learnk8s/rate-limiter:1.0.0
         imagePullPolicy: Always
         args: ["/app/index.js", "10"]
         ports:
           - containerPort: 8080
         resources:
           requests:
             memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 8080
 selector:
   app: podinfo

Pode submeter o recurso ao agrupamento com:

bash
$ kubectl apply -f rate-limiter.yaml

Para gerar algum tráfego, irá utilizar Locust.

A seguinte definição de YAML cria um cluster de teste de carga distribuída:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: locust-script
data:
 locustfile.py: |-
   from locust import HttpUser, task, between
 
   class QuickstartUser(HttpUser):
       @task
       def hello_world(self):
           self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-primary
 template:
   metadata:
     labels:
       app: locust-primary
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--master"]
         ports:
           - containerPort: 5557
             name: comm
           - containerPort: 5558
             name: comm-plus-1
           - containerPort: 8089
             name: web-ui
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script
---
apiVersion: v1
kind: Service
metadata:
 name: locust
spec:
 ports:
   - port: 5557
     name: communication
   - port: 5558
     name: communication-plus-1
   - port: 80
     targetPort: 8089
     name: web-ui
 selector:
   app: locust-primary
 type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-worker
 template:
   metadata:
     labels:
       app: locust-worker
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--worker", "--master-host=locust"]
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script

Pode submetê-lo ao agrupamento com:

bash
$ kubectl locust.yaml

Gafanhoto lê o seguinte locustfile.py, que é armazenado num ConfigMap:

py
from locust import HttpUser, task, between
 
class QuickstartUser(HttpUser):
 
   @task
   def hello_world(self):
       self.client.get("/")

O ficheiro não faz nada de especial para além de fazer um pedido para um URL. Para se ligar ao painel de bordo do Locust, é necessário o endereço IP do seu equilibrador de carga.

Pode recuperá-la com o seguinte comando:

bash
$ kubectl get service locust -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

Abra o seu navegador e introduza esse endereço IP.

Excelente!

Falta uma peça: o Pod Autoscaler Horizontal.
O Autoscaler KEDA envolve o Autoscaler Horizontal com um objecto específico chamado ScaledObject.

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: podinfo
spec:
scaleTargetRef:
  kind: Deployment
  name: podinfo
minReplicaCount: 1
maxReplicaCount: 30
cooldownPeriod: 30
pollingInterval: 1
triggers:
- type: prometheus
  metadata:
    serverAddress: http://prometheus-server
    metricName: connections_active_keda
    query: |
      sum(increase(http_requests_total{app="podinfo"}[60s]))
    threshold: "480" # 8rps * 60s

A KEDA faz a ponte entre as métricas recolhidas por Prometheus e alimenta-as a Kubernetes.

Finalmente, cria um Pod Autoscaler Horizontal (HPA) com essas métricas.

Pode inspeccionar manualmente o HPA com:

bash
$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo

Pode submeter o objecto com:

bash
$ kubectl apply -f scaled-object.yaml

É tempo de testar se a escalada funciona.

No painel de instrumentos do Locust, lançar uma experiência com as seguintes configurações:

  • Número de utilizadores: 300
  • Taxa de reprodução: 0.4
  • Anfitrião: http://podinfo
Gif de gravação de ecrã que demonstra escalonamento com cápsulas pendentes usando autoscaler.
Combinando o agrupamento e o autoscalador de vagem horizontal.

O número de réplicas está a aumentar!

Excelente! Mas reparou?

Após as escalas de implantação para 8 cápsulas, tem de esperar alguns minutos antes de serem criadas mais cápsulas no novo nó.

Neste período, os pedidos por segundo estagnam porque as actuais oito réplicas só podem tratar de dez pedidos cada uma.

Vamos reduzir a escala e repetir a experiência:

bash
kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods

Desta vez, vamos fornecer em excesso o nó com a cápsula de suporte do lugar:

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.9G

Pode submetê-lo ao agrupamento com:

bash
kubectl apply -f placeholder.yaml

Abrir o painel de instrumentos do Locust e repetir a experiência com os seguintes parâmetros:

  • Número de utilizadores: 300
  • Taxa de reprodução: 0.4
  • Anfitrião: http://podinfo
Gif
Combinando o agrupamento e o autoscalador de vagens horizontais com o fornecimento excessivo.

Desta vez, novos nós são criados em segundo plano e os pedidos por segundo aumentam sem aplanar. Excelente trabalho!

Vamos recapitular o que aprendeu neste post:

  • o autoscaler de cluster não rastreia o consumo de CPU ou memória. Em vez disso, monitora as cápsulas pendentes;
  • pode criar uma cápsula que utiliza a memória total e a CPU disponível para fornecer um nó Kubernetes de forma proactiva;
  • Os nós Kubernetes têm recursos reservados para kubelet, sistema operativo e limiar de despejo; e
  • pode combinar Prometheus com a KEDA para escalar a sua cápsula com uma consulta PromQL.

Quer seguir juntamente com a nossa série de webinar Kubernetes em escala? Registe-se para começar, e saiba mais sobre a utilização da KEDA para escalar os clusters Kubernetes a zero.

Comentários

Deixe uma resposta

O seu endereço de correio electrónico não será publicado. Os campos obrigatórios estão marcados com *