Como regra, sempre há a necessidade de fornecer um conjunto dedicado de recursos a qualquer aplicativo para sua operação correta e estável. Mas e se vários aplicativos funcionarem com as mesmas capacidades ao mesmo tempo? Como fornecer os recursos mínimos necessários para cada um deles? Como posso limitar o consumo de recursos? Como distribuir corretamente a carga entre nós? Como garantir o mecanismo de escala horizontal em caso de aumento de carga na aplicação?

Você precisa começar com quais tipos básicos de recursos existem no sistema - é claro, tempo do processador e RAM. Nos manifestos do k8s, esses tipos de recursos são medidos nas seguintes unidades:
- CPU - nos núcleos
- RAM - em bytes
Além disso, para cada recurso, há uma oportunidade de definir dois tipos de requisitos - solicitações e limites . Solicitações - descreve os requisitos mínimos para os recursos livres do nó executarem o contêiner (e a lareira como um todo), enquanto os limites definem um limite estrito nos recursos disponíveis para o contêiner.
É importante entender que no manifesto não é necessário definir explicitamente os dois tipos, e o comportamento será o seguinte:
- Se apenas os limites do recurso forem definidos explicitamente, as solicitações para esse recurso terão automaticamente um valor igual a limites (isso pode ser verificado chamando entidades de descrição). I.e. de fato, a operação do contêiner será limitada pela mesma quantidade de recursos necessária para executar.
- Se apenas solicitações forem explicitamente definidas para um recurso, nenhuma restrição será definida sobre ele - ou seja, o contêiner é limitado apenas pelos recursos do próprio nó.
Também é possível configurar o gerenciamento de recursos não apenas no nível de um contêiner específico, mas também no nível do namespace, usando as seguintes entidades:
- LimitRange - descreve a política de restrição no nível do contêiner / lareira em ns e é necessária para descrever as restrições padrão no contêiner / lareira, além de impedir a criação de contêineres / lareiras obviamente gordas (ou vice-versa), limitar seu número e determinar a possível diferença nos valores dentro dos limites e pedidos
- ResourceQuotas - descreva a política de restrição em geral para todos os contêineres em ns e é usada, como regra, para diferenciar recursos entre ambientes (útil quando os ambientes não são rigidamente delimitados no nível dos nós)
A seguir, exemplos de manifestos em que os limites de recursos são definidos:
No nível do contêiner específico:
containers: - name: app-nginx image: nginx resources: requests: memory: 1Gi limits: cpu: 200m
I.e. nesse caso, para iniciar um contêiner com nginx, você precisará pelo menos da presença de 1G OP e 0,2 CPU livre no nó, enquanto o contêiner máximo pode consumir 0,2 CPU e todo o OP disponível no nó.
No nível inteiro ns:
apiVersion: v1 kind: ResourceQuota metadata: name: nxs-test spec: hard: requests.cpu: 300m requests.memory: 1Gi limits.cpu: 700m limits.memory: 2Gi
I.e. a soma de todos os contêineres de solicitação nos ns padrão não pode exceder 300m para a CPU e 1G para o OP e a soma de todo o limite é 700m para a CPU e 2G para o OP.
Restrições padrão para contêineres em ns:
apiVersion: v1 kind: LimitRange metadata: name: nxs-limit-per-container spec: limits: - type: Container defaultRequest: cpu: 100m memory: 1Gi default: cpu: 1 memory: 2Gi min: cpu: 50m memory: 500Mi max: cpu: 2 memory: 4Gi
I.e. no espaço de nome padrão para todos os contêineres, por padrão, a solicitação será definida como 100m para a CPU e 1G para o OP, limite - 1 CPU e 2G. Ao mesmo tempo, também foi estabelecida uma restrição sobre os possíveis valores em solicitação / limite para a CPU (50m <x <2) e RAM (500M <x <4G).
Limitações no nível da lareira ns:
apiVersion: v1 kind: LimitRange metadata: name: nxs-limit-pod spec: limits: - type: Pod max: cpu: 4 memory: 1Gi
I.e. para cada lareira nos ns padrão, será definido um limite de 4 vCPU e 1G.
Agora, gostaria de dizer quais vantagens a instalação dessas restrições pode nos dar.
O mecanismo de balanceamento de carga entre nós
Como você sabe, o componente k8s, como o agendador , que funciona de acordo com um determinado algoritmo, é responsável pela distribuição dos lares pelos nós. Esse algoritmo no processo de escolha do nó ideal para execução passa por dois estágios:
- Filtragem
- Ranking
I.e. de acordo com a política descrita, os nós são selecionados inicialmente no qual uma lareira pode ser iniciada com base em um conjunto de predicados (incluindo se o nó possui recursos suficientes para executar uma lareira - PodFitsResources) e, em seguida, são concedidos pontos para cada um desses nós, de acordo com as prioridades (incluindo, quanto mais recursos livres o nó tiver - mais pontos ele será atribuído - LeastResourceAllocation / LeastRequestedPriority / BalancedResourceAllocation) e será executado no nó com mais pontos (se vários nós atenderem a essa condição ao mesmo tempo, será selecionado um aleatório).
Ao mesmo tempo, você precisa entender que o planejador, ao avaliar os recursos disponíveis do nó, se concentra nos dados armazenados no etcd - ou seja, pela quantidade do recurso solicitado / limite de cada pod em execução neste nó, mas não pelo consumo real de recursos. Esta informação pode ser obtida na saída do comando kubectl describe node $NODE
do kubectl describe node $NODE
, por exemplo:
Aqui vemos todos os pods em execução em um nó específico, bem como os recursos solicitados por cada um dos pods. E aqui está a aparência dos logs do planejador ao iniciar o pod cronjob-cron-events-1573793820-xt6q9 (essas informações aparecem no log do planejador ao definir o 10º nível de log nos argumentos do comando start --v = 10):
gaivota larga I1115 07:57:21.637791 1 scheduling_queue.go:908] About to try and schedule pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 I1115 07:57:21.637804 1 scheduler.go:453] Attempting to schedule pod: nxs-stage/cronjob-cron-events-1573793820-xt6q9 I1115 07:57:21.638285 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s5 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638300 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s6 is allowed, Node is running only 20 out of 110 Pods. I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s3 is allowed, Node is running only 20 out of 110 Pods. I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s4 is allowed, Node is running only 17 out of 110 Pods. I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638365 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s12 is allowed, Node is running only 9 out of 110 Pods. I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s11 is allowed, Node is running only 11 out of 110 Pods. I1115 07:57:21.638385 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s1 is allowed, Node is running only 19 out of 110 Pods. I1115 07:57:21.638402 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s2 is allowed, Node is running only 21 out of 110 Pods. I1115 07:57:21.638383 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638335 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, Node is running only 18 out of 110 Pods. I1115 07:57:21.638408 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s13 is allowed, Node is running only 8 out of 110 Pods. I1115 07:57:21.638478 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638505 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638577 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638583 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s7 is allowed, Node is running only 25 out of 110 Pods. I1115 07:57:21.638932 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 9 I1115 07:57:21.638946 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 8 I1115 07:57:21.638961 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: BalancedResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 9 I1115 07:57:21.638971 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7 I1115 07:57:21.638975 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: LeastResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 8 I1115 07:57:21.638990 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7 I1115 07:57:21.639022 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: TaintTolerationPriority, Score: (10) I1115 07:57:21.639030 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: TaintTolerationPriority, Score: (10) I1115 07:57:21.639034 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: TaintTolerationPriority, Score: (10) I1115 07:57:21.639041 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: NodeAffinityPriority, Score: (0) I1115 07:57:21.639053 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: NodeAffinityPriority, Score: (0) I1115 07:57:21.639059 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: NodeAffinityPriority, Score: (0) I1115 07:57:21.639061 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639063 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639073 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639077 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639085 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639088 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639103 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639109 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639114 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639127 1 generic_scheduler.go:781] Host nxs-k8s-s10 => Score 100037 I1115 07:57:21.639150 1 generic_scheduler.go:781] Host nxs-k8s-s8 => Score 100034 I1115 07:57:21.639154 1 generic_scheduler.go:781] Host nxs-k8s-s9 => Score 100037 I1115 07:57:21.639267 1 scheduler_binder.go:269] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10" I1115 07:57:21.639286 1 scheduler_binder.go:279] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10": all PVCs bound and nothing to do I1115 07:57:21.639333 1 factory.go:733] Attempting to bind cronjob-cron-events-1573793820-xt6q9 to nxs-k8s-s10
Aqui vemos que, inicialmente, o planejador realiza a filtragem e forma uma lista de 3 nós nos quais é possível executar (nxs-k8s-s8, nxs-k8s-s9, nxs-k8s-s10). Em seguida, calcula os pontos de acordo com vários parâmetros (incluindo BalancedResourceAllocation, LeastResourceAllocation) para cada um desses nós, a fim de determinar o nó mais adequado. No final, ele é planejado no nó com mais pontos (aqui, dois nós ao mesmo tempo têm o mesmo número de pontos 100037, portanto, um aleatório é selecionado - nxs-k8s-s10).
Conclusão : se os pods funcionam no nó para o qual não há restrições, então para os k8s (do ponto de vista do consumo de recursos) isso será equivalente a se esses pods estavam completamente ausentes nesse nó. Portanto, se você possui um pod condicionalmente com um processo voraz (por exemplo, wowza) e não há restrições para isso, uma situação pode surgir quando, de fato, o usuário consumiu todos os recursos do nó, mas para o k8s esse nó é considerado descarregado e ele receberá o mesmo número de pontos na classificação (ou seja, em pontos com uma avaliação dos recursos disponíveis), bem como um nó que não possui campos de trabalho, o que pode levar a uma distribuição desigual da carga entre os nós.
Despejo da lareira
Como você sabe, cada um dos pods recebe uma das três classes de QoS:
- garantido - é atribuído quando a solicitação e o limite são definidos para cada contêiner na lareira para memória e CPU, e esses valores devem corresponder
- estourável - pelo menos um contêiner na lareira tem solicitação e limite, enquanto solicitação <limite
- melhor esforço - quando nenhum recipiente na lareira é limitado em recursos
Ao mesmo tempo, quando há uma escassez de recursos (disco, memória) no nó, o kubelet começa a classificar e despejar os pods de acordo com um determinado algoritmo que leva em consideração a prioridade do pod e sua classe de QoS. Por exemplo, se estivermos falando sobre RAM, os pontos de classe de QoS serão atribuídos com base no seguinte princípio:
- Garantido : -998
- Melhor esforço : 1000
- Burstable : min (max (2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
I.e. com a mesma prioridade, o kubelet expelirá os pods com a melhor classe de esforço de QoS do nó.
Conclusão : se você deseja reduzir a probabilidade de remoção do pod necessário do nó em caso de recursos insuficientes, em seguida, juntamente com a prioridade, também deve definir a solicitação / limite para ele.
Mecanismo de auto-dimensionamento horizontal (HPA)
Quando a tarefa é aumentar e diminuir automaticamente o número de pod, dependendo do uso de recursos (sistema - CPU / RAM ou usuário - rps), uma entidade k8s como HPA (Horizontal Pod Autoscaler) pode ajudar em sua solução. O algoritmo é o seguinte:
- As leituras atuais do recurso observado (currentMetricValue) são determinadas
- Os valores desejados para o recurso (desejadoMétricoValorado) são determinados, que são configurados para recursos do sistema usando solicitação
- O número atual de réplicas é determinado (currentReplicas)
- A fórmula a seguir calcula o número de réplicas desejado (réplicas desejadas)
allowedReplicas = [currentReplicas * (currentMetricValue / desejadoMetricValue)]
No entanto, a escala não ocorrerá quando o coeficiente (currentMetricValue / desejadoMetricValue) estiver próximo de 1 (podemos definir o erro permitido, por padrão, é 0,1).
Considere hpa usando o aplicativo de teste de aplicativo (descrito como Implantação), onde é necessário alterar o número de réplicas, dependendo do consumo da CPU:
Manifesto de aplicativo
kind: Deployment apiVersion: apps/v1beta2 metadata: name: app-test spec: selector: matchLabels: app: app-test replicas: 2 template: metadata: labels: app: app-test spec: containers: - name: nginx image: registry.nixys.ru/generic-images/nginx imagePullPolicy: Always resources: requests: cpu: 60m ports: - name: http containerPort: 80 - name: nginx-exporter image: nginx/nginx-prometheus-exporter resources: requests: cpu: 30m ports: - name: nginx-exporter containerPort: 9113 args: - -nginx.scrape-uri - http://127.0.0.1:80/nginx-status
I.e. vemos que, com o aplicativo, ele é iniciado inicialmente em duas instâncias, cada uma contendo dois contêineres nginx e nginx-exportador, para cada um dos quais são solicitados pedidos de CPU.
Manifesto HPA
apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler metadata: name: app-test-hpa spec: maxReplicas: 10 minReplicas: 2 scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: app-test metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 30
I.e. criamos um hpa que monitora o aplicativo de teste de implantação e ajusta o número de lares com o aplicativo com base no indicador da CPU (esperamos que o lar consuma 30% por cento da CPU solicitada por ele), enquanto o número de réplicas está entre 2 e 10.
Agora, consideraremos o mecanismo de operação hpa se aplicarmos uma carga a uma das lareiras:
Total, temos o seguinte:
- Valor desejado (valorMétrico desejado) - de acordo com as configurações de hpa, temos 30%
- Valor atual (currentMetricValue) - para cálculo, o controlador-gerente calcula o valor médio do consumo de recursos em%, ou seja, condicionalmente faz o seguinte:
- Obtém os valores absolutos das métricas do coração do servidor de métricas, ou seja, 101m e 4m
- Calcula o valor absoluto médio, ou seja, (101m + 4m) / 2 = 53m
- Obtém o valor absoluto para o consumo de recursos desejado (para isso, a solicitação de todos os contêineres é somada) 60m + 30m = 90m
- Calcula a porcentagem média de consumo de CPU em relação à lareira da solicitação, ou seja, 53m / 90m * 100% = 59%
Agora temos todo o necessário para determinar se é necessário alterar o número de réplicas, para isso calculamos o coeficiente:
ratio = 59% / 30% = 1.96
I.e. o número de réplicas deve ser aumentado ~ 2 vezes e compor [2 * 1,96] = 4.
Conclusão: Como você pode ver, para que esse mecanismo funcione, um pré-requisito é incluir a disponibilidade de solicitações para todos os contêineres na lareira observada.
O mecanismo de dimensionamento automático horizontal de nós (Cluster Autoscaler)
Para neutralizar o impacto negativo no sistema durante explosões de carga, a presença de um hpa ajustado não é suficiente. Por exemplo, de acordo com as configurações no gerenciador do controlador hpa, decide que o número de réplicas precisa ser aumentado em 2 vezes, no entanto, não há recursos livres nos nós para executar esse número de pods (ou seja, o nó não pode fornecer os recursos solicitados para as solicitações de pod) e esses pods insira o estado Pendente.
Nesse caso, se o provedor tiver IaaS / PaaS apropriado (por exemplo, GKE / GCE, AKS, EKS etc.), uma ferramenta como o Node Autoscaler pode nos ajudar. Permite definir o número máximo e mínimo de nós no cluster e ajustar automaticamente o número atual de nós (acessando a API do provedor de nuvem para solicitar / excluir nós) quando houver falta de recursos no cluster e os pods não puderem ser agendados (eles estão no estado Pendente).
Conclusão: para poder dimensionar automaticamente os nós, é necessário especificar solicitações nos contêineres da lareira, para que os k8s possam avaliar corretamente a carga dos nós e, consequentemente, informar que não há recursos no cluster para iniciar a próxima lareira.
Conclusão
Deve-se observar que definir limites de recursos para o contêiner não é um pré-requisito para o lançamento bem-sucedido do aplicativo, mas ainda é melhor fazer isso pelos seguintes motivos:
- Para uma operação mais precisa do planejador em termos de balanceamento de carga entre os nós k8s
- Para reduzir a probabilidade de um evento de despejo da lareira
- Para fornos de aplicativos de dimensionamento automático horizontal (HPA)
- Para dimensionamento automático horizontal de nós (escalonamento automático de cluster) para provedores de nuvem
Leia também outros artigos em nosso blog: