# Kubernetes Scheduling - Técnicas de agendamento

Um dos principais recursos do Kubernetes é seu sistema de Scheduling, responsável por alocar recursos de maneira eficiente para os contêineres em execução. Em termos simples, o Scheduling no Kubernetes é o processo pelo qual o sistema decide em qual nó (node) um contêiner específico deve ser executado, levando em consideração vários critérios, como recursos disponíveis, requisitos de aplicativos e políticas de alocação.

Ao compreender como o Scheduling funciona no Kubernetes, os desenvolvedores e administradores de sistemas podem otimizar a utilização dos recursos, garantir a escalabilidade e a confiabilidade dos aplicativos, além de facilitar a manutenção e a implantação contínua.

> Este artigo é um resumo do curso que fiz sobre Kubernetes. Decidi focar no tema de *Scheduling* por considerá-lo essencial para entender como o Kubernetes gerencia a alocação de recursos e organiza os pods nos nós do cluster, revelando aspectos fundamentais do funcionamento interno da plataforma.

## Agendamento manual

O agendamento manual de um pod no cluster se dá quando o serviço do `kube-scheduler` não está em execução no momento, que é o serviço padrão do Kubernetes para o escalonamento, nesse caso o agendamento não será possível de forma automática.

Para isso, é possível informar manualmente na definição do arquivo de manifesto em qual nó o pod em questão será agendado, isso é feito durante a criação do pod.

Na definição do arquivo do pod, use a propriedade `nodeName` com o valor do nome do nó, por exemplo `node01`.

```yaml
# file: myapp.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels: 
    app: nginx
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 8080
  nodeName: node01
```

Agendamento manual de um pod usando o atributo nodeName

Com esse manifesto, podemos aplicar ao cluster e veremos que o pod será escalado no nó `node01`, o mesmo que informamos manualmente.

```bash
kubectl apply -f myapp.yaml
'pod/nginx' created.

kubectl get pods -o wide
NAME         READY   STATUS      RESTARTS   AGE    IP            NODE
nginx        1/1     Running     0          35s    10.244.0.4    node01
```

## Labels e Selectors

### Labels

As **labels** (rótulos) são pares chave/valor que são adicionados no objeto com o objetivo de agrupá-los. Você pode usar para especificar atributos de identificação dos objetos que podem ser relevantes para os usuários, por exemplo, agrupar serviços por seu ambiente com uma label `environment=dev` , ou um conjunto de aplicações que executam rotinas de processamento de vídeo, poderia conter uma label `tier=video-processing`. Labels também pode ser usado para selecionar subconjuntos de objetos.

```yaml
metadata:
  labels:
    environment: "dev"
    tier: "video-processing"
```

Definição de labels

### Selectors

Os **selectors** (seletores), são filtros que podem ser aplicados em cima das labels existentes de um objeto, através dos critérios dos filtros você teria o resultado dos grupos de pods.

Temos abaixo o manifesto de um pod usando a imagem do Nginx, com duas labels e um pod usando a imagem do Mysql. A seguir podemos ver como interagir com os objetos usando os seletores.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: webserver
  labels:
    environment: dev
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: mysql
  labels:
    environment: prod
    app: mysql
spec:
  containers:
  - name: mysql
    image: mysql
    ports:
    - containerPort: 3306
```

Definição de Pods com Nginx e Mysql

Podemos listar todas as labels adicionadas à um objeto com o parâmetro `--show-labels`.

```bash
kubectl get pods --show-labels
NAME               READY   STATUS             RESTARTS           AGE   LABELS
nginx              1/1     Running            0				     2m    app=nginx,enviroment=dev
```

Para listar todos os objetos filtrando por uma determinada label, pode ser usado a flag `--selector` ou `-l` que é a sua abreviação. Os seguintes operadores são suportados: `'='` , `'=='` e `'!='`.

```bash
kubectl get pods -l app=nginx
NAME        READY   STATUS    RESTARTS   AGE
webserver   1/1     Running   0          32s

kubectl get pods --selector app!=nginx
NAME    READY   STATUS    RESTARTS   AGE
mysql   1/1     Running   0          41s

kubectl get pods --selector app=nginx,environment=dev
NAME        READY   STATUS    RESTARTS   AGE
webserver   1/1     Running   0          47

kubectl get pods --selector app=mysql,environment=dev
No resources found in default namespace.
```

Um ponto interessante para entendermos, é que o Kubernetes internamente usa as `labels` e `selector` para vincular um `replica set` em um `pod`, um `service` no `deployment`, e assim por diante.

## Taints e Tolerations

Os **taints** são uma forma de marcar (ou manchar) um nó para que esse não receba novos pods que não correspondam a um determinado critério (tolerância), ou seja, repelir os pods.

As **tolerations** seriam marcar um pod que seja tolerante a essa restrição, ou seja, para um determinado nó que foi marcado para não receber pods, um pod com tolerations irá ignorar essa restrição e será agendado nesses nós marcados.

**Exemplo:**

O nó `node01` for marcado da seguinte forma:

```bash
kubectl taint nodes node01 monitoring=prometheus:NoSchedule
```

Dessa forma, somente pods que contenham uma tolerância poderá ser agendado nesse nó. No exemplo a seguir, esse pod contém uma tolerância e será agendado no nó que foi marcado.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-toleration
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "monitoring"
    operator: "Equal"
    value: "prometheus"
    effect: "NoSchedule"
```

Definição de Pod com tolerância ao nó

### Vamos entender um pouco mais...

Uma tolerância corresponde a uma mancha (taint) se as chaves forem iguais e os efeitos forem os mesmos, e:

* O `operator` é `Exists` (nesse caso, o `value` não deve ser especificado), ou
    
* O `operator` é `Equal` e os valores devem ser iguais.
    

Para o campo `effect`, são permitidos os seguintes valores:

`NoExecute`:

Isso afeta os pods que já estão em execução no nó da seguinte maneira:

* Os pods que não toleram a contaminação são despejados imediatamente.
    
* Os pods que toleram a contaminação sem especificar `tolerationsSeconds` em sua especificação de tolerância permanecem vinculados para sempre.
    
* Os pods que toleram a contaminação com a propriedade `tolerationsSeconds` especificada, permanecem vinculados pelo período informado, após esse tempo, os pods são despejados dos nós.
    

`NoSchedule`:

Nenhum pod será agendado no nó contaminado a menos que tenha uma tolerância correspondente. Os pods em execução não são removidos.

`PreferNoSchedule`:

Essa é uma versão preferencial do `NoSchedule`, o plano de controle tentará *evitar* colocar o pod que não tolere a contaminação, mas não é garantido.

### Despejos baseados em contaminação

O controlador de nó contamina automaticamente um nó quando certas condições são verdadeiras. Exemplo:

* [`node.kubernetes.io/not-ready`](http://node.kubernetes.io/not-ready): o nó não está pronto, corresponde a condição/status do nó `Ready` ser `false`.
    
* [`node.kubernetes.io/unreachable`](http://node.kubernetes.io/unreachable): o nó está inacessível pelo controlador de nó, corresponde a condição/status do nó `Ready` ser `Unkown`.
    
* [`node.kubernetes.io/memory-pressure`](http://node.kubernetes.io/memory-pressure): o nó tem pressão de memória.
    
* [`node.kubernetes.io/disk-pressure`](http://node.kubernetes.io/disk-pressure): o nó tem pressão de disco.
    
* [`node.kubernetes.io/pid-pressure`](http://node.kubernetes.io/pid-pressure): o nó tem pressão PID.
    
* [`node.kubernetes.io/network-unvailable`](http://node.kubernetes.io/network-unvailable): a rede do nó está indisponível.
    
* [`node.kubernetes.io/unschedulable`](http://node.kubernetes.io/unschedulable): o nó não é programável.
    

Os pods que são criados a partir de um `DaemonSet` são criados com tolerâncias para que evitem de serem removidos no caso de uma contaminação, por exemplo.

O pod já vem com essa duas tolerâncias:

* [`node.kubernetes.io/unreachable`](http://node.kubernetes.io/unreachable)
    
* [`node.kubernetes.io/no-ready`](http://node.kubernetes.io/no-ready)
    

## NodeSelector

O seletor de nó é a forma mais simples para determinar em qual nó o pod deve ser agendado através de seus rótulos. Você pode adicionar a propriedade `nodeSelector` à especificação do pod e informar qual o rótulo correspondente para que o pod seja agendado, se vários nós corresponderem ao rótulo informado, o scheduler do Kubernetes decidirá em qual nó o pod será colocado com base em outros fatores, como capacidade de recursos e afinidade.

Para visualizar todos as labels existentes em um nó, use o seguinte comando:

```bash
kubectl get nodes --show-labels

NAME      STATUS    ROLES    AGE     VERSION        LABELS
worker1   Ready     <none>   1d      v1.24.0        ...,kubernetes.io/hostname=worker1
```

Para adicionar uma label ao nó, execute o comando:

```bash
kubectl label nodes worker1 disktype=ssd 

kubectl get nodes --show-labels

NAME      STATUS    ROLES    AGE     VERSION        LABELS
worker1   Ready     <none>   1d      v1.24.0        ...,disktype=ssd
```

Agora a propriedade `nodeSelector`, pode ser usada da seguinte maneira:

```yaml
...
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    disktype: ssd
```

Definição de um Pod com a propriedade nodeSelector

Dessa forma, o pod será agendado em nó que corresponder a esse rótulo.

## Node Affinity e anti-affinity

Afinidade e anti-afinidade expandem os tipos de restrições que você pode definir para o agendamento. Alguns benefícios de usar afinidade e anti-afinidade:

* Poder usar lógicas mais elaboradas para escolher o nó de destino, ao contrário do `nodeSelector` que apenas nós com as labels especificadas são escolhidas.
    
* Poder escolher se a regra é flexível ou preferencial para que o agendador agende um pod mesmo que não consiga encontrar um nó correspondente.
    

### Afinidade de nó

A afinidade do nó tem o mesmo conceito da propriedade `nodeSelector`, que permite restringir em quais nós o pod dever se agendado. Existem dois tipos de afinidade de nó:

* `requiredDuringSchedulingIgnoredDuringExecution`: O agendador irá agendar o pod conforme a regra definida. Caso não encontre um nó que atenda as condições, o pod não será agendado.
    
* `preferredDuringSchedulingIgnoredDuringExecution`: O agendador irá tentar encontrar um nó que atenda a regra definida. Caso não encontre um nó que corresponda as condições, o agendar ainda irá agendar o pod.
    

Dado o seguinte manifesto, temos:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity
spec:
  containers:
  - name: nginx
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd
```

Definição de Pod com a propriedade nodeAffinity

* Este pod só será agendado em nós que tenham o rótulo `disktype=ssd`.
    
* Se não houver nós com este rótulo, o pod ficará em estado `pending`.
    

Para entender melhor sobre cada tipo de afinidade, vamos dividir em duas partes cada propriedade do tipo de afinidade:

1. Primeira parte é o início de cada tipo de afinidade:
    

`requiredDuringScheduling`: Nesse trecho, estamos dizendo para o agendador que as condições definidas são obrigadas durante o agendamento, ou seja, deverá atender estritamente as regras para que o agendamento possa ser realizado.

`preferredDuringScheduling`: Nesse trecho, estamos dizendo para o agendador que se as condições propostas ao pod não for possível atender, se por exemplo, o label `disktype=ssd` não corresponder a nenhum nó, então o agendador poderá escolher outro nó para agendar esse pod.

2. Segunda parte é o final de cada tipo de afinidade:
    

`ignoreDuringExecution`: Nesse trecho, estamos dizendo ao Kubernetes que se um pod que já está em execução em um nó e uma label é removida desse nó, esse pod não será removido. Exemplo: o pod foi agendado no nó `worker1` devido a label `disktype=ssd` que correspondeu aos critérios do pod, se essa label for removida, o agendador não irá remover esse pod do nó, pois será ignorado durante a execução.

### Anti-afinidade de nó

Anti-afinidade de nó funciona no mesmo conceito de afinidade de nó, só que ao invés de trabalhar com regras de **"este pod deve"**, será da forma de **"este pod não deve"** ser agendado em um determinado nó.

Dado o seguinte manifesto:

```yaml
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: NotIn
            values:
            - linux
```

Definição de Pod com anti-affinity

No exemplo acima, estamos instruindo o Kubernetes a **não agendar o pod em nós que estejam executando o sistema operacional Linux**. Ele restringe o agendamento apenas para nós que rodam outro sistema operacional, como Windows.

Se não houver nós com sistema operacional diferente de Linux, o pod ficará pendente e não será agendado.

## Requests e limits

Os recursos de solicitações e limites são informações de consumo de CPU e memória que você pode especificar opcionalmente para um contêiner.

Ao usar a solicitação de recurso para os contêineres em um pod, o agendador usa essas informações para decidir em qual nó o pod deve ser agendado.

O agendador irá sempre buscar um nó que tenha recursos disponíveis para que o pod seja agendado. Caso não encontre nenhum nó, o pod ficará com o status `Pending` e provavelmente se visualizar o status do pod, usando o comando `kubectl describe pod` verá que houve uma falha no agendamento devido a CPU insuficiente `Insufficient cpu (3)`.

### Recursos de CPU

As solicitações e limites de CPU são medidos em unidade de CPU, no Kubernetes, 1 unidade de CPU equivale a 1 núcleo de CPU físico ou 1 núcleo virtual.

São permitidas usar solicitações fracionárias, por exemplo `0.5`, está solicitando metade do tempo de CPU, em relação a uma CPU de 1 core.

### Recursos de Memória

As solicitações e limites de memória são medidos em bytes, você pode informar a memória como um número inteiro simples ou como um número de ponto fixo usando os sufixos de quantidade.

Exemplo de uso:

```yaml
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
```

Definição de Pod com requests e limits

`resources`: Esta seção configura os recursos de CPU e memória que o contêiner solicita e o limite máximo que pode usar. Ela contém dois subitens:

* `requests`: Define a quantidade mínima de recursos que o contêiner precisa para ser executado. Isso garante que, quando o pod for agendado, o nó terá pelo menos essa quantidade de recursos disponível.
    
* `limits`: Define a quantidade máxima de recursos que o contêiner pode usar. O contêiner pode consumir mais do que o `request`, mas nunca ultrapassará o valor do `limit`.
    

### Análise Detalhada dos Recursos

#### `requests`

* `memory: "64Mi"`: Especifica que o contêiner solicita 64 MiB (Mebibytes) de memória. Isso significa que o Kubernetes tentará agendar este pod apenas em nós que tenham pelo menos 64 MiB de memória disponível.
    
* `cpu: "250m"`: Especifica que o contêiner solicita 250 milicores (ou 0,25 core) de CPU. Esse é o recurso de CPU mínimo que o Kubernetes garantirá para o contêiner, mas ele pode usar mais até o limite especificado.
    

#### `limits`

* `memory: "128Mi"`: Especifica que o contêiner terá um limite máximo de 128 MiB de memória. Se o contêiner tentar usar mais do que isso, o Kubernetes poderá terminá-lo com um erro de `OutOfMemory` (OOM).
    
* `cpu: "500m"`: Especifica que o contêiner terá um limite máximo de 500 milicores (ou 0,5 core) de CPU. Isso significa que o contêiner poderá consumir até metade de um núcleo de CPU, mas não mais do que isso.
    

## Limit Range

Por padrão os contêineres são executados com recursos de computação ilimitados no cluster. O limit range é uma política para restringir as alocações de recursos que você pode especificar para objetos como `Pod` e `PersistenVolumeClaim` em um namespace específico.

Um `LimitRange` fornece restrições que podem:

* Definir o uso mínimo e máximo de recursos de computação por pod ou contêiner em um namespace.
    
* Aplicar solicitação de armazenamento mínimo e máximo por `PersistentVolumeClaim` em um namespace.
    
* Definir uma proporção entre solicitação e limite para um recurso em um namespace.
    
* Definir solicitação/limite padrão para recursos de computação em um namespace e injetá-los automaticamente em contêineres em tempo de execução.
    

```yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: resources-limits
  namespace: default
spec:
  limits:
  - type: Container
    max:
      cpu: "2"           # Máximo de 2 cores de CPU por contêiner
      memory: "1Gi"      # Máximo de 1 GiB de memória por contêiner
    min:
      cpu: "200m"        # Mínimo de 200 milicores (0.2 cores) de CPU por contêiner
      memory: "100Mi"    # Mínimo de 100 MiB de memória por contêiner
    default:
      cpu: "500m"        # Solicitação padrão de CPU se não especificado
      memory: "200Mi"    # Solicitação padrão de memória se não especificado
    defaultRequest:
      cpu: "300m"        # Request padrão de CPU para contêineres que não especificarem um valor
      memory: "150Mi"    # Request padrão de memória para contêineres que não especificarem um valor
```

Definição de LimitRange para Contêiner no namespace default

### LimitRange para PersistentVolumeClaim

Outro uso comum do LimitRange é para definir limites em PersistentVolumeClaims (PVCs). Isso pode garantir que o armazenamento seja usado de forma responsável.

```yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: storage-limits
  namespace: default
spec:
  limits:
  - type: PersistentVolumeClaim
    max:
      storage: "10Gi"    # Máximo de 10 GiB de armazenamento por PVC
    min:
      storage: "1Gi"     # Mínimo de 1 GiB de armazenamento por PVC
```

Definição de LimitRange para PVCs no namespace default

## Resource Quotas

A cota por recursos é definido por um objeto `ResourceQuota`, e fornece restrições que limitam o consumo de recursos por namespace. Ele pode limitar a quantidade de objetos que podem ser criados em um namespace por tipo.

Para recursos de CPU e Memória, `ResourceQuota` impõe que cada novo pod nesse namespace defina um limite para esse recurso. Se você aplicar uma cota de recursos em um namespace para CPU ou memória, o `requests` e `limits` sempre deverão ser especificados para o pod enviado. Caso contrário a admissão do pod poderá ser rejeitada.

```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: resource-quota
  namespace: default
spec:
  hard:
    pods: "10"                      # Limita o número máximo de pods para 10
    requests.cpu: "2"               # Limita o total de requests de CPU para 2 cores
    requests.memory: "1Gi"          # Limita o total de requests de memória para 1 GiB
    limits.cpu: "4"                 # Limita o total de CPU para 4 cores
    limits.memory: "2Gi"            # Limita o total de memória para 2 GiB
```

Definição de ResourceQuota para o namespace default

1. `pods: "10"`: Limita o número total de pods que podem ser criados no namespace `default` a 10. Se um usuário tentar criar mais do que 10 pods, o Kubernetes irá bloquear a criação.
    
2. `requests.cpu: "2"`: Define que a soma de todas as solicitações de CPU (`requests.cpu`) dos pods no namespace não pode ultrapassar 2 núcleos. Ou seja, se os pods no namespace já estiverem usando 2 núcleos de CPU solicitados, não será possível agendar novos pods que aumentem essa demanda.
    
3. `requests.memory: "1Gi"`: Limita o total de memória solicitada pelos pods no namespace para 1 GiB. Esse é o valor máximo acumulado de `requests.memory` que todos os pods combinados podem solicitar.
    
4. `limits.cpu: "4"`: Define que o consumo máximo de CPU para o namespace é de 4 núcleos. Esse limite considera o uso de `limits.cpu`, o que significa que o Kubernetes permitirá a criação de pods até que o consumo total de CPU atinja esse valor.
    
5. `limits.memory: "2Gi"`: Define que o total máximo de memória, usando `limits.memory`, é de 2 GiB. Isso limita o uso total de memória dentro do namespace.
    

## Conclusão

Entender as diferentes formas de agendamento de pods no Kubernetes é essencial para aproveitar ao máximo a infraestrutura e garantir que os recursos sejam utilizados de maneira inteligente e equilibrada.

Desde as políticas de afinidade e anti-afinidade até quotas e limites de recursos, cada técnica contribui para um ambiente mais resiliente e adaptado às necessidades da sua aplicação.

Até a próxima.
