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.
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.
E depois:
- após alguns minutos, o autoscaler cria um nó extra; e
- a quarta cápsula é colocada no 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:
- Recursos necessários para o funcionamento do sistema operacional e daemons do sistema, tais como SSH, systemd, etc.
- 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.
- Recursos disponíveis para Pods.
- Recursos reservados para o limiar de despejo.
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.
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:
- Um nódulo com quatro cápsulas.
- 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:
- A quinta cápsula é criada imediatamente, e fica no estado de Corrida em menos de 10 segundos.
- A cápsula de suporte foi despejada para criar espaço para a cápsula.
E depois:
- O autoscalador de aglomerado reparou na cápsula de suporte de lugar pendente e providenciou um novo nó.
- A cápsula de suporte de lugar é colocada no nó recém-criado.
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:
- Não funciona fora da caixa. É necessário instalar um Servidor de métricas para agregar e expor as métricas.
- 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
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
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
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