Recursos avançados do Kubernetes

Primeiramente o Kubernetes possui uma gama enorme de recursos e ferramentas: Se eu parar para ler o medium por meia hora vou encontrar um monte de estruturas e ferramentas novas que são inventadas todo o momento. Esse artigo se atem às principais funcionalidades mas que não são utilizadas necessariamente no dia-a-dia. Desse modo vamos falar sobre recursos avançados no kubernetes, tais como: Resources, HPA, ResourceQuota, NetworkPolicy, Taint e Tolerant, Affinity e Anti-affinity, topology spread constraint e pod disruption budget.

Resources

Os pods podem crescer e consumir todos os recursos do node e gerar problemas críticos no ambiente kubernetes. Para evitar esse problema é comum limitar os recursos que ele pode consumir, seja em memória RAM ou seja em CPU. Mas também é possível indicar qual é o mínimo de recursos. Assim veja a seguir um exemplo de um deployment simples com a definição dos resouces.

apiVersion: apps/v1
kind: Deployment
"metadata":
  name: wordpress
  namespace: app-wordpress

spec:
  selector:
    matchLabels:
      app.kubernetes.io/instance: wordpress-mainapp
  replicas: 1
  template:
    "metadata":
      labels:
        app.kubernetes.io/instance: wordpress-mainapp
    spec:
      containers:
        - name: wordpress
          image: wordpress:php8.1-apache

          resources:
            limits:
              memory: "256Mi"
              cpu: "300m"
            requests:
              memory: "128Mi"
              cpu: "100m"             

Horizontal Pod Autoscaler – HPA

O HPA é uma estrutura do kubernetes para aumentar o diminuir dinamicamente a quantidade de pods dependendo de indicadores como CPU, memória ou outros customizados. A utilização de HPAs é algo realmente recomendado, mas há no mercado soluções alternativas como o Keda.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
"metadata":
  name: nginx-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment             
    name: my-nginx
  minReplicas: 1                 
  maxReplicas: 10                
  behavior:
    scaleDown:
      policies:
      - type: Pods
        value: 1
        periodSeconds: 30
  metrics:                       
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 3

ResourceQuota

Assim como é possível definir num deployment recursos de hardware e de software com o ResourceQuota é possível dizer para todo um namespace o que pode ser consumido. Ele possui um carater ligeiramente diferente mas é uma solução bastante interessante. Com ela é possível, por exemplo, indicar a quantidade máxima de pods do namespace. Veja o exemplo:

apiVersion: v1
kind: ResourceQuota
"metadata":
     name: disable-cross-namespace-affinity
     namespace: foo-ns
 spec:
     hard:
         pods: "0"
	 requests.cpu: "1"
         requests.memory: 1Gi
         limits.cpu: "2"
         limits.memory: 2Gi
         requests.nvidia.com/gpu: 4
	 configmaps: "10"
         persistentvolumeclaims: "4"
         replicationcontrollers: "20"
         secrets: "10"
         services: "10"
         services.loadbalancers: "2"

Network Policy

O Network Policy é uma estrutura de controle de comunicação de rede entre as camadas 3 (Network layer) e 4 (Transport Layer) para o Kubernetes. Portanto ela define regras de comunicação entre outros pods, namespaces e blocos de IP. Vale destacar que na instalação de um cluster k8s é necessário instalar um provedor de rede para que a infraestrutura funcione, tal como Weave net, calico, cilium, etc. Veja um exemplo do uso de NetworkPolicy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
"metadata":
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - ipBlock:
            cidr: 172.17.0.0/16
            except:
              - 172.17.1.0/24
        - namespaceSelector:
            matchLabels:
              project: myproject
        - podSelector:
            matchLabels:
              role: frontend
      ports:
        - protocol: TCP
          port: 6379
  egress:
    - to:
        - ipBlock:
            cidr: 10.0.0.0/24
      ports:
        - protocol: TCP
          port: 5978

Taint e Tolerant

Essa é uma técnica em que o kube-scheduler aloca determinados para determinados pods apenas para determinados nodes. Esse é um dos recursos mais avançados do kubernetes. Um node possui um Taint que indica quem ele é. Já o Pod possui um Tolerant que define regras que dá a ele a possibilidade de ir o não para aquele node.

Inicialmente é necessário criar um tainer com o comando que segue. Após isso é possível utilizar um ou mais tolerants na definição de um pod.

kubectl taint nodes node1 key1=value1:NoSchedule    # adiciona o taint
kubectl taint nodes node1 key1=value1:NoSchedule-   # remove o taint
# Essa é a definição de um pod com um array de tolerations
tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"

Affinity e anti-affinity

É possível indicar que um pod tenha afinidade com um node específico, ou, pelo contrário, ele pode ter uma anti-afinidade. No primeiro caso ele tende a ser alocado no node, e no segudo ele tende a não ser alocado. A definição do critério de afinidade dá a opção da alocação ser obrigatória (ou seja, se não for possível, ele vai ficar esperando) ou opcional (ou seja, se não for possível, ele será alocado em outro node).

# Isso se baseia em labels associados aos nodes
kubectl get nodes --show-labels
# # Essa regra de afinidade indica que o pod só será distribuido em nós com o label 'ssd-enabled=true'
# # A distribuição será obrigatório no agendamento do pod
# # A regra não se aplicará ao pod após estar em execução
# # Se não for possível distribuir o pod para o node em questão, o pod não será distribuído.

affinity:
  nodeAffinity:
  # Obrigatório durante o agendamento
  # Ignorado durante a execução
  requiredDuringSchedullingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
      - key: ssd-enabled
        operator: In
        values:
        - "true"
# # Essa regra de afinidade indica que o pod será distribuido, preferencialmente, para nós com o label 'ssd-enabled=true'
# # A distribuição seguirá esse critério apenas no agendamento do pod
# # A regra não se aplicará ao pod após estar em execução
# # Se não for possível distribuir o pod para o node em questão, o kubernetes escolherá algum outro node por algum critério arbitrário

affinity:
nodeAffinity:
# Obrigatório durante o agendamento
# Ignorado durante a execução
preferredDuringSchedullingIgnoredDuringExecution:
  preference: 
    matchExpressions:
    - key: ssd-enabled
      operator: In
      values:
      - "true"
    weight: 1

Há também a afinidade de Pods. Imagine que a aplicação e o banco devam estar no mesmo node para reduzir a latência. Nesse caso é necessário garantir uma afinidade entre esses nós. O modo de estruturar o yaml é bem semelhante a do nodeAffinity mas utilizando podAffinity.

apiVersion: v1
kind: Pod
"metadata":
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: registry.k8s.io/pause:2.0

Pod Topology Spread Constraints

Imagine o seguinte problema: você tem um cluster com 20 nodes. Ao criar um deployment com 10 replicas é possível que todos os pods sejam agendados para o mesmo node. Com a definição da topologia é possível dizer que a diferença de pods entre os nodes máxima é de n. Imagine n=2: o node 1 tem 2 (total: 2), node 2 tem 2 (total: 4), node 3 tem 2 (total: 6), node 4 tem 2 (total: 8), node 5 tem 2 (total: 10), node 6 tem 0 (10) … Para mais detalhes consulte Pod Topology Spread Constraints.

# Exemplo da definição de um pod
topologySpreadConstraints:

  # diferença máxima
  - maxSkew: 1      

  # label do node utilizada para distribuir                    
  topologyKey: kubernetes.io/hostname
  
  # o que fazer quando não for possível distribuir     
  whenUnsatisfiable: DoNotSchedule

  # label dos nodes que participam dessa topologia
        
  matchLabelKeys:                         
    - app
    - pod-template-hash

Pod Disruption Budget – PDB

Esse é um cenário importante para quando se trabalha com vários nodes mas é necessário remover um nodo para manutenção. Trata-se de um dos recursos mais avançados do Kubernetes. Quando isso ocorre os workloads de um node devem ser drenados para outro node. Ocorre que durante esse processo os workloads podem se comportar incorretamente por não ter necessariamente o mesmo ambiente de recursos. Para tal o PDB existe indicando condições mínimas para o funcionamento das aplicações, mesmo durante a janela de manutenção.

Boas práticas a serem executadas para reduzir o impacto de manutenções e o PDB funcionar do melhor modo:

1 – Garantir que os Resources Limits estejam disponiveis
2 – Colocar a aplicação com replicas para HA
3 – Utilizar anti-afinidade para HA

kubectl get nodes
kubectl drain <node name>
apiVersion: policy/v1
kind: PodDisruptionBudget
"metadata":
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário