
O gerenciamento de recursos de cluster é sempre um tópico complexo. Como explicar a necessidade de configurar recursos para o usuário que implanta seus aplicativos no cluster? Talvez seja mais fácil automatizar isso?
Descrição do problema
O gerenciamento de recursos é uma tarefa importante no contexto da administração de cluster do Kubernetes. Mas por que é importante se o Kubernetes faz todo o trabalho duro para você? Porque não é. O Kubernetes fornece ferramentas convenientes para resolver muitos problemas ... se você usar essas ferramentas. Para cada pod no seu cluster, você pode especificar os recursos necessários para seus contêineres. E o Kubernetes usará essas informações para distribuir instâncias do seu aplicativo entre os nós do cluster.
Poucas pessoas levam a sério o gerenciamento de recursos no Kubernetes. Isso é normal para um cluster levemente carregado com alguns aplicativos estáticos. Mas e se você tiver um cluster muito dinâmico? Onde os aplicativos vêm e vão, onde o espaço para nome é criado e excluído o tempo todo? Um cluster com um grande número de usuários que podem criar seu próprio espaço para nome e implantar aplicativos? Bem, neste caso, em vez de uma orquestração estável e previsível, você terá várias falhas aleatórias nos aplicativos e, às vezes, até nos componentes do próprio Kubernetes!
Aqui está um exemplo desse cluster:

Você vê três lareiras no estado “Terminando”. Mas essa não é a remoção habitual de lareiras - elas ficam presas nesse estado porque o daemon de contêiner em seu nó foi atingido por algo que demanda muito recursos.
Esses problemas podem ser resolvidos lidando adequadamente com a falta de recursos, mas este não é o tópico deste artigo (há um bom artigo ) e também não é uma bala de prata para resolver todos os problemas com recursos.
O principal motivo para esses problemas é incorreto ou falta de gerenciamento de recursos no cluster. E se esse tipo de problema não for um desastre para implantações, porque elas criarão facilmente um novo problema, então para entidades como DaemonSet ou mais ainda para StatefulSet, esses congelamentos serão fatais e exigirão intervenção manual.
Você pode ter um cluster enorme com muita CPU e memória. Quando você executa muitos aplicativos sem as configurações adequadas de recursos, há uma chance de que todos os pods com uso intenso de recursos sejam colocados no mesmo nó. Eles lutarão por recursos, mesmo que os nós restantes do cluster permaneçam praticamente livres.
Você também pode ver casos menos críticos em que alguns aplicativos são afetados por seus vizinhos. Mesmo se os recursos desses aplicativos "inocentes" foram configurados corretamente, um erro de digitação pode vir e matá-los. Um exemplo desse cenário:
- Seu aplicativo solicita 4 GB de memória, mas inicialmente leva apenas 1 GB.
- Uma subida errante, sem uma configuração de recurso, é atribuída ao mesmo nó.
- Vagando sob consome toda a memória disponível.
- Seu aplicativo está tentando alocar mais memória e falha porque não há mais.
Outro caso bastante popular é a reavaliação. Alguns desenvolvedores fazem solicitações enormes em manifestos "apenas por precaução" e nunca usam esses recursos. O resultado é um desperdício de dinheiro.
Teoria da decisão
Horror! Certo?
Felizmente, o Kubernetes oferece uma maneira de impor algumas restrições aos pods, especificando configurações de recursos padrão, bem como valores mínimos e máximos. Isso é implementado usando o objeto LimitRange . LimitRange é uma ferramenta muito conveniente quando você tem um número limitado de namespaces ou controle total sobre o processo de criação deles. Mesmo sem a configuração adequada dos recursos, seus aplicativos terão uso limitado. Lareiras “inocentes” e adequadamente ajustadas estarão seguras e protegidas de vizinhos prejudiciais. Se alguém implantar um aplicativo ganancioso sem uma configuração de recurso, esse aplicativo receberá valores padrão e provavelmente falhará. E isso é tudo! O aplicativo não arrastará mais ninguém.
Assim, temos uma ferramenta para controlar e forçar a configuração de recursos para lareiras, agora parece que estamos seguros. Então Na verdade não. O fato é que, conforme descrito anteriormente, nossos espaços para nome podem ser criados pelos usuários e, portanto, o LimitRange pode não estar presente nesses espaços para nome, pois ele deve ser criado em cada espaço para nome separadamente. Portanto, precisamos de algo não apenas no nível do espaço para nome, mas também no nível do cluster. Mas ainda não existe essa função no Kubernetes.
Por isso, decidi escrever minha solução para esse problema. Deixe-me apresentá-lo - operador de limite. Este é um operador criado com base na estrutura do Operator SDK , que usa o recurso personalizado ClusterLimit e ajuda a proteger todos os aplicativos "inocentes" no cluster. Usando esse operador, você pode controlar os valores padrão e os limites de recursos para todos os espaços para nome usando a quantidade mínima de configuração. Também permite escolher exatamente onde aplicar a configuração usando o namespaceSelector.
Exemplo ClusterLimitapiVersion: limit.myafq.com/v1alpha1 kind: ClusterLimit metadata: name: default-limit spec: namespaceSelector: matchLabels: limit: "limited" limitRange: limits: - type: Container max: cpu: "800m" memory: "1Gi" min: cpu: "100m" memory: "99Mi" default: cpu: "700m" memory: "900Mi" defaultRequest: cpu: "110m" memory: "111Mi" - type: Pod max: cpu: "2" memory: "2Gi"
Com essa configuração, o operador criará um LimitRange apenas no espaço para nome com o rótulo limit: limited
. Isso será útil para fornecer restrições mais rigorosas em um grupo específico de namespaces. Se namespaceSelector não for especificado, o operador aplicará um LimitRange a todos os namespaces. Se você deseja configurar o LimitRange manualmente para um espaço para nome específico, pode usar a anotação "limit.myafq.com/unlimited": true
isso instruirá o operador a ignorar esse espaço para nome e não a criar LimitRange automaticamente.
Exemplo de script para usar o operador:
- Crie o ClusterLimit padrão com restrições liberais e sem namespaceSelector - ele será aplicado em qualquer lugar.
- Para um conjunto de namespaces com aplicativos leves, crie um ClusterLimit adicional, mais rigoroso, com namespaceSelector. Coloque etiquetas nesses espaços de nomes de acordo.
- Em um espaço para nome com aplicativos que consomem muitos recursos, coloque a anotação "limit.myafq.com/unlimited": true e configure LimitRange manualmente com limites muito mais amplos do que o especificado no ClusteLimit padrão.
O importante a saber sobre vários LimitRange em um espaço para nome:
Quando uma sub é criada em um espaço para nome com vários LimitRange, os maiores padrões são adotados para configurar seus recursos. Mas os valores máximo e mínimo serão verificados de acordo com o mais rigoroso LimitRange.
Exemplo prático
O operador rastreará todas as alterações em todos os namespace, ClusterLimits, filhos LimitRanges e iniciará a coordenação do estado do cluster com qualquer alteração nos objetos monitorados. Vamos ver como isso funciona na prática.
Para começar, crie sob sem restrições:
saída do kubectl run / get ❯() kubectl run --generator=run-pod/v1 --image=bash bash pod/bash created ❯() kubectl get pod bash -o yaml apiVersion: v1 kind: Pod metadata: labels: run: bash name: bash namespace: default spec: containers: - image: bash name: bash resources: {}
Nota: parte da saída do comando foi omitida para simplificar o exemplo.
Como você pode ver, o campo "recursos" está vazio, o que significa que esse sub pode ser iniciado em qualquer lugar.
Agora, criaremos o ClusterLimit padrão para todo o cluster com valores bastante liberais:
default-limit.yaml apiVersion: limit.myafq.com/v1alpha1 kind: ClusterLimit metadata: name: default-limit spec: limitRange: limits: - type: Container max: cpu: "4" memory: "5Gi" default: cpu: "700m" memory: "900Mi" defaultRequest: cpu: "500m" memory: "512Mi"
E também mais rigoroso para um subconjunto de namespaces:
restritivo-limite.yaml apiVersion: limit.myafq.com/v1alpha1 kind: ClusterLimit metadata: name: restrictive-limit spec: namespaceSelector: matchLabels: limit: "restrictive" limitRange: limits: - type: Container max: cpu: "800m" memory: "1Gi" default: cpu: "100m" memory: "128Mi" defaultRequest: cpu: "50m" memory: "64Mi" - type: Pod max: cpu: "2" memory: "2Gi"
Em seguida, crie os namespaces e pods neles para ver como ele funciona.
Espaço de nome normal com restrição padrão:
apiVersion: v1 kind: Namespace metadata: name: regular
E um espaço para nome um pouco mais limitado, de acordo com a legenda - para aplicativos leves:
apiVersion: v1 kind: Namespace metadata: labels: limit: "restrictive" name: lightweight
Se você olhar para os logs do operador imediatamente após criar o espaço para nome, poderá encontrar algo assim no spoiler:
logs do operador {...,"msg":"Reconciling ClusterLimit","Triggered by":"/regular"} {...,"msg":"Creating new namespace LimitRange.","Namespace":"regular","LimitRange":"default-limit"} {...,"msg":"Updating namespace LimitRange.","Namespace":"regular","Name":"default-limit"} {...,"msg":"Reconciling ClusterLimit","Triggered by":"/lightweight"} {...,"msg":"Creating new namespace LimitRange.","Namespace":"lightweight","LimitRange":"default-limit"} {...,"msg":"Updating namespace LimitRange.","Namespace":"lightweight","Name":"default-limit"} {...,"msg":"Creating new namespace LimitRange.","Namespace":"lightweight","LimitRange":"restrictive-limit"} {...,"msg":"Updating namespace LimitRange.","Namespace":"lightweight","Name":"restrictive-limit"}
A parte ausente do log contém mais 3 campos que não são relevantes no momento
Como você pode ver, a criação de cada espaço para nome iniciou a criação do novo LimitRange. Um espaço para nome mais limitado possui dois LimitRange - padrão e mais rigoroso.
Agora vamos tentar criar um par de lares nesses espaços de nomes.
saída do kubectl run / get ❯() kubectl run --generator=run-pod/v1 --image=bash bash -n regular pod/bash created ❯() kubectl get pod bash -o yaml -n regular apiVersion: v1 kind: Pod metadata: annotations: kubernetes.io/limit-ranger: 'LimitRanger plugin set: cpu, memory request for container bash; cpu, memory limit for container bash' labels: run: bash name: bash namespace: regular spec: containers: - image: bash name: bash resources: limits: cpu: 700m memory: 900Mi requests: cpu: 500m memory: 512Mi
Como você pode ver, apesar de não termos alterado a maneira como o pod é criado, o campo de recurso agora está preenchido. Você também pode observar a anotação criada automaticamente pelo LimitRanger.
Agora crie under em um espaço para nome leve:
saída do kubectl run / get ❯() kubectl run --generator=run-pod/v1 --image=bash bash -n lightweight pod/bash created ❯() kubectl get pods -n lightweight bash -o yaml apiVersion: v1 kind: Pod metadata: annotations: kubernetes.io/limit-ranger: 'LimitRanger plugin set: cpu, memory request for container bash; cpu, memory limit for container bash' labels: run: bash name: bash namespace: lightweight spec: containers: - image: bash name: bash resources: limits: cpu: 700m memory: 900Mi requests: cpu: 500m memory: 512Mi
Observe que os recursos na lareira são os mesmos do exemplo anterior. Isso ocorre no caso de vários LimitRange, valores padrão menos rigorosos serão usados ao criar pods. Mas por que então precisamos de um LimitRange mais limitado? Será usado para verificar os valores máximo e mínimo de recursos. Para demonstrar, tornaremos nosso ClusterLimit limitado ainda mais limitado:
restritivo-limite.yaml apiVersion: limit.myafq.com/v1alpha1 kind: ClusterLimit metadata: name: restrictive-limit spec: namespaceSelector: matchLabels: limit: "restrictive" limitRange: limits: - type: Container max: cpu: "200m" memory: "250Mi" default: cpu: "100m" memory: "128Mi" defaultRequest: cpu: "50m" memory: "64Mi" - type: Pod max: cpu: "2" memory: "2Gi"
Preste atenção à seção:
- type: Container max: cpu: "200m" memory: "250Mi"
Agora, definimos 200m de CPU e 250Mi de memória como máximo para o contêiner na lareira. E agora, novamente, tente criar em:
❯() kubectl run --generator=run-pod/v1 --image=bash bash -n lightweight Error from server (Forbidden): pods "bash" is forbidden: [maximum cpu usage per Container is 200m, but limit is 700m., maximum memory usage per Container is 250Mi, but limit is 900Mi.]
Nosso sub possui valores grandes definidos pelo LimitRange padrão e não pôde iniciar porque não passou na verificação de recursos máximos permitidos.
Este foi um exemplo de uso do operador de limite. Tente você mesmo e brinque com o ClusterLimit na sua instância local do Kubernetes.
No repositório do operador de limite do GitHub , você pode encontrar o manifesto para a implantação do operador, bem como o código-fonte. Se você deseja expandir a funcionalidade do operador, as pull-quests e as feature-quests são bem-vindas!
Conclusão
O gerenciamento de recursos no Kubernetes é fundamental para a estabilidade e a confiabilidade de seus aplicativos. Personalize seus recursos da lareira sempre que possível. E use LimitRange para garantir casos quando isso não for possível. Automatize a criação do LimitRange usando o Operador de limite.
Siga estas dicas e seu cluster estará sempre a salvo do caos sem recursos de lareiras perdidas.