Kubernetes 1.35: Redimensionamento de Pod In-Place é GA — Escale Verticalmente Sem Reiniciar

Kubernetes 1.35: In-Place Pod Resize é GA — Escale Verticalmente Sem Reiniciar

Atualize CPU e memória de Pods em execução sem recriá-los


O Problema: Mudanças “Simples” que Causam Disrupções

Um serviço em produção mostra throttling de CPU. A latência P95 está subindo. A solução é óbvia: aumentar o limite de CPU de 500m para 700m.

Em versões anteriores do Kubernetes, essa “mudança simples” desencadeava uma cascata de eventos indesejados:

# Mudar isto...
resources:
  limits:
    cpu: "500m"
  
# ...para isto
resources:
  limits:
    cpu: "700m"

Consequências no K8s ≤1.34:

  • Pod completamente recriado
  • Conexões ativas encerradas
  • Cache em memória perdido
  • Estado local eliminado
  • Jobs em progresso interrompidos

Nenhum código foi alterado. Nenhum comportamento foi alterado. Apenas um número foi ajustado. Mas o sistema tratou esse ajuste como um deployment completo.

Kubernetes 1.35 muda isso fundamentalmente.


A Solução: In-Place Resource Update (GA)

A partir do Kubernetes 1.35, a atualização in-place de recursos de Pod é GA (Generally Available). Isso significa que CPU e memória podem ser modificadas em Pods em execução, e o nó aplica os novos valores sem o ciclo automático de recriação.

Características principais:

  • Mudanças de CPU aplicadas em tempo real (sem restart)
  • Mudanças de memória configuráveis (com ou sem restart de container)
  • Pod nunca é recriado — apenas os cgroups são ajustados
  • Estado, conexões e cache preservados
  • Observável via status do Pod e conditions

Arquitetura: Como Funciona o Resize

O processo envolve vários componentes do Kubernetes trabalhando em coordenação:

O Novo Modelo Mental: Desired vs Actual vs Allocated

Kubernetes 1.35 introduz uma distinção importante em como os recursos são reportados:

Campo Descrição
spec.containers[].resources O que o operador deseja (desired)
status.containerStatuses[].allocatedResources O que o nó reservou
status.containerStatuses[].resources O que o container está usando
status.conditions Estado do resize (Pending, InProgress)
status.observedGeneration Confirmação de que kubelet processou a mudança

Essa visibilidade permite saber exatamente em qual estado o resize está em qualquer momento.


Configuração: resizePolicy

O comportamento do resize é configurado por tipo de recurso usando resizePolicy na spec do container:

Spec de Pod com Resize Policy

apiVersion: v1
kind: Pod
metadata:
  name: app-con-resize
spec:
  containers:
    - name: app
      image: minha-app:latest
      ports:
        - containerPort: 8080
      resizePolicy:
        - resourceName: cpu
          restartPolicy: NotRequired      # CPU em tempo real
        - resourceName: memory
          restartPolicy: RestartContainer # Memória com restart
      resources:
        requests:
          cpu: "300m"
          memory: "256Mi"
        limits:
          cpu: "300m"
          memory: "256Mi"

Recomendação para produção:

  • CPU: NotRequired — As mudanças de CPU são seguras de aplicar em tempo real
  • Memory: RestartContainer — Mais previsível do que esperar que a app libere memória

Demo: Verificando que o Resize Funciona

Para demonstrar que o resize realmente funciona sem restart, é possível criar um servidor simples que exponha seu PID e os limites de cgroup atuais.

Servidor de Demo (Go)

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
)

func read(path string) string {
    b, err := os.ReadFile(path)
    if err != nil {
        return fmt.Sprintf("unavailable (%v)", err)
    }
    return strings.TrimSpace(string(b))
}

func handler(w http.ResponseWriter, r *http.Request) {
    pid := os.Getpid()
    
    // cgroup v2 paths
    cpuMax := read("/sys/fs/cgroup/cpu.max")
    memMax := read("/sys/fs/cgroup/memory.max")
    
    io.WriteString(w, fmt.Sprintf("pid=%d\n", pid))
    io.WriteString(w, fmt.Sprintf("cpu.max=%s\n", cpuMax))
    io.WriteString(w, fmt.Sprintf("memory.max=%s\n", memMax))
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("listening on :8080")
    http.ListenAndServe(":8080", nil)
}

Dockerfile

FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o app .

FROM alpine:3.20
WORKDIR /app
COPY --from=build /src/app /app/app
EXPOSE 8080
ENTRYPOINT ["/app/app"]

Desplegar e Verificar

# Criar o Pod
kubectl apply -f pod-resize-demo.yaml

# Port-forward para acessar
kubectl port-forward pod/app-con-resize 8080:8080 &

# Verificar estado inicial
curl localhost:8080
# pid=7
# cpu.max=30000 100000
# memory.max=268435456

Executando o Resize

No Kubernetes 1.35, o resize é executado contra o subresource resize do Pod:

Aumentar CPU (Sem Restart)

kubectl patch pod app-con-resize --subresource resize --type merge -p '
{
  "spec": {
    "containers": [
      {
        "name": "app",
        "resources": {
          "requests": { "cpu": "700m" },
          "limits":   { "cpu": "700m" }
        }
      }
    ]
  }
}'

Verificar que Funcionou

# O endpoint deve mostrar:
# - Mesmo PID (sem restart)
# - Novo valor de cpu.max
curl localhost:8080
# pid=7                    <- MESMO PID
# cpu.max=70000 100000     <- NOVO LIMITE
# memory.max=268435456

# Confirmar que não houve restart
kubectl get pod app-con-resize -o jsonpath='{.status.containerStatuses[0].restartCount}'
# 0

Se o PID se mantém e cpu.max mudou, o resize in-place funcionou corretamente.


Aumentar Memória (Com Restart de Container)

Com a política RestartContainer para memória:

kubectl patch pod app-con-resize --subresource resize --type merge -p '
{
  "spec": {
    "containers": [
      {
        "name": "app",
        "resources": {
          "requests": { "memory": "512Mi" },
          "limits":   { "memory": "512Mi" }
        }
      }
    ]
  }
}'

Neste caso:

  • restartCount será incrementado
  • O PID mudará
  • Mas o Pod NÃO será recriado — volumes e networking são mantidos

Observabilidade Durante o Resize

Kubernetes 1.35 fornece visibilidade do estado do resize via conditions:

kubectl describe pod app-con-resize

Conditions relevantes:

  • PodResizePending — O resize foi solicitado mas ainda não foi aplicado
  • PodResizeInProgress — O kubelet está aplicando a mudança
# Ver o estado detalhado
kubectl get pod app-con-resize -o jsonpath='{.status.conditions}' | jq
```This removes the uncertainty. There's no longer any guessing whether the change was applied — the system reports it explicitly.

---

## Limitations to Consider

The feature is powerful but has clear limits:

| Limitation | Description |
|------------|----------------|
| **QoS Class** | Cannot change post-creation (Guaranteed/Burstable/BestEffort) |
| **Init containers** | Do not support resize |
| **Ephemeral containers** | Do not support resize |
| **Sidecars** | Yes, support resize |
| **Windows Pods** | Not supported |
| **Memory decrease** | Best-effort without restart (the app must free memory) |
| **Node constraints** | Some CPU/Memory managers can block changes |

These limitations are part of what makes the feature safe. A feature that promises everything becomes dangerous. A feature that declares its limits is operable.

---

## Scheduler Protection

A valid concern: what happens if the resize is pending but the scheduler assumes it's already applied?

Kubernetes prevents this by being conservative during incomplete resizes. When scheduling, it considers the **maximum** between:
- What was requested (desired)
- What was allocated (allocated)
- What was applied (actual)

This prevents overcommit based on changes that haven't yet completed.

---

## Operational Impact

The most significant change is not technical — it's cultural.

**Before K8s 1.35:**
- Teams avoided resize until it was urgent
- Engineers over-provisioned to avoid touching resources afterward
- "Right-sizing" was a project, not a habit
- On-call delayed simple fixes out of fear of disruption

**With K8s 1.35:**
- CPU corrections without restart cost
- Faster iteration over resource configuration
- Response to throttling without maintenance window
- Resize becomes a normal operation, not an event

---

## Command Summary

```bash
# Apply CPU resize
kubectl patch pod POD_NAME --subresource resize --type merge -p '
{
  "spec": {
    "containers": [{
      "name": "CONTAINER_NAME",
      "resources": {
        "requests": { "cpu": "NEW_VALUE" },
        "limits": { "cpu": "NEW_VALUE" }
      }
    }]
  }
}'

# Check resize status
kubectl describe pod POD_NAME | grep -A5 Conditions

# View current vs desired resources
kubectl get pod POD_NAME -o jsonpath='{.status.containerStatuses[0].resources}'

# Confirm there was no restart
kubectl get pod POD_NAME -o jsonpath='{.status.containerStatuses[0].restartCount}'

Conclusion

Kubernetes 1.35 solves a problem that should never have existed: the need to restart a process just because a resource limit was adjusted.

With in-place resize GA:

  • CPU can be adjusted without any restart
  • Memory can be configured for container restart (not Pod)
  • Full observability of resize state
  • Protection against overcommit during pending changes

Vertical scaling finally behaves like an adjustment, not a deployment.


Resources


Published on yoDEV.dev — The community of developers from Latin America