Ignacio
Sun, 28 Dec 2025 14:55:04 +0000
BackEnd

Kubernetes 1.35: In-Place Pod Resize es GA — Escala Verticalmente Sin Reiniciar

Actualiza CPU y memoria de Pods en ejecución sin recrearlos

El Problema: Cambios “Simples” que Causan Disrupciones

Un servicio en producción muestra throttling de CPU. La latencia P95 está subiendo. La solución es obvia: aumentar el límite de CPU de 500m a 700m.

En versiones anteriores de Kubernetes, este “cambio simple” desencadenaba una cascada de eventos no deseados:

# Cambiar esto...
resources:
  limits:
    cpu: "500m"

# ...a esto
resources:
  limits:
    cpu: "700m"

Consecuencias en K8s ≤1.34:

Pod completamente recreado

Conexiones activas terminadas

Cache en memoria perdido

Estado local eliminado

Jobs en progreso interrumpidos

No se cambió código. No se cambió comportamiento. Solo se ajustó un número. Pero el sistema trató ese ajuste como un deployment completo.

Kubernetes 1.35 cambia esto fundamentalmente.

La Solución: In-Place Resource Update (GA)

A partir de Kubernetes 1.35, el in-place update de recursos de Pod es GA (Generally Available). Esto significa que CPU y memoria pueden modificarse en Pods en ejecución, y el nodo aplica los nuevos valores sin el ciclo automático de recreación.

Características clave:

Cambios de CPU aplicados en caliente (sin restart)

Cambios de memoria configurables (con o sin restart de container)

Pod nunca es recreado — solo se ajustan los cgroups

Estado, conexiones y cache preservados

Observable via Pod status y conditions

Arquitectura: Cómo Funciona el Resize

El proceso involucra varios componentes de Kubernetes trabajando en coordinación:

El Nuevo Modelo Mental: Desired vs Actual vs Allocated

Kubernetes 1.35 introduce una distinción importante en cómo se reportan los recursos:

Campo | Descripción
spec.containers[].resources | Lo que el operador desea (desired)
status.containerStatuses[].allocatedResources | Lo que el nodo reservó
status.containerStatuses[].resources | Lo que el container está usando
status.conditions | Estado del resize (Pending, InProgress)
status.observedGeneration | Confirmación de que kubelet procesó el cambio

Esta visibilidad permite saber exactamente en qué estado está el resize en cualquier momento.

Configuración: resizePolicy

El comportamiento del resize se configura por tipo de recurso usando resizePolicy en el spec del container:

Spec de Pod con Resize Policy

apiVersion: v1
kind: Pod
metadata:
  name: app-con-resize
spec:
  containers:
    - name: app
      image: mi-app:latest
      ports:
        - containerPort: 8080
      resizePolicy:
        - resourceName: cpu
          restartPolicy: NotRequired      # CPU en caliente
        - resourceName: memory
          restartPolicy: RestartContainer # Memoria con restart
      resources:
        requests:
          cpu: "300m"
          memory: "256Mi"
        limits:
          cpu: "300m"
          memory: "256Mi"

Recomendación para producción:

CPU: NotRequired — Los cambios de CPU son seguros de aplicar en caliente

Memory: RestartContainer — Más predecible que esperar que la app libere memoria

Demo: Verificando que el Resize Funciona

Para demostrar que el resize realmente funciona sin restart, se puede crear un servidor simple que exponga su PID y los límites de cgroup actuales.

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 y Verificar

# Crear el Pod
kubectl apply -f pod-resize-demo.yaml

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

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

Ejecutando el Resize

En Kubernetes 1.35, el resize se ejecuta contra el subresource resize del Pod:

Aumentar CPU (Sin 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 Funcionó

# El endpoint debe mostrar:
# - Mismo PID (sin restart)
# - Nuevo valor de cpu.max
curl localhost:8080
# pid=7                    <-- MISMO PID
# cpu.max=70000 100000     <-- NUEVO LÍMITE
# memory.max=268435456

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

Si el PID se mantiene y cpu.max cambió, el resize in-place funcionó correctamente.

Aumentar Memoria (Con Restart de Container)

Con la política RestartContainer para memoria:

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

En este caso:

restartCount incrementará

El PID cambiará

Pero el Pod NO será recreado — volumes y networking se mantienen

Observabilidad Durante el Resize

Kubernetes 1.35 proporciona visibilidad del estado del resize via conditions:

kubectl describe pod app-con-resize

Conditions relevantes:

PodResizePending — El resize fue solicitado pero no aplicado aún

PodResizeInProgress — El kubelet está aplicando el cambio

# Ver el estado detallado
kubectl get pod app-con-resize -o jsonpath='{.status.conditions}' | jq

Esto elimina la incertidumbre. Ya no hay que adivinar si el cambio fue aplicado — el sistema lo reporta explícitamente.

Limitaciones a Considerar

El feature es potente pero tiene límites claros:

Limitación | Descripción
QoS Class | No cambia post-creación (Guaranteed/Burstable/BestEffort)
Init containers | No soportan resize
Ephemeral containers | No soportan resize
Sidecars | Sí soportan resize
Windows Pods | No soportado
Memory decrease | Best-effort sin restart (la app debe liberar memoria)
Node constraints | Algunos CPU/Memory managers pueden bloquear cambios

Estas limitaciones son parte de lo que hace el feature seguro. Un feature que promete todo se vuelve peligroso. Un feature que declara sus límites es operable.

Protección del Scheduler

Una preocupación válida: ¿qué pasa si el resize está pendiente pero el scheduler asume que ya se aplicó?

Kubernetes previene esto siendo conservador durante resizes incompletos. Al schedulear, considera el máximo entre:

Lo solicitado (desired)

Lo asignado (allocated)

Lo aplicado (actual)

Esto evita overcommit basado en cambios que aún no se completaron.

Impacto Operacional

El cambio más significativo no es técnico — es cultural.

Antes de K8s 1.35:

Equipos evitaban resize hasta que fuera urgente

Ingenieros sobreaprovisionaban para evitar tocar recursos después

“Right-sizing” era un proyecto, no un hábito

On-call retrasaba fixes simples por miedo a la disrupción

Con K8s 1.35:

Correcciones de CPU sin costo de restart

Iteración más rápida sobre configuración de recursos

Respuesta a throttling sin ventana de mantenimiento

Resize se convierte en una operación normal, no un evento

Resumen de Comandos

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

# Verificar estado del resize
kubectl describe pod POD_NAME | grep -A5 Conditions

# Ver recursos actuales vs deseados
kubectl get pod POD_NAME -o jsonpath='{.status.containerStatuses[0].resources}'

# Confirmar que no hubo restart
kubectl get pod POD_NAME -o jsonpath='{.status.containerStatuses[0].restartCount}'

Conclusión

Kubernetes 1.35 resuelve un problema que nunca debió existir: la necesidad de reiniciar un proceso solo porque se ajustó un límite de recursos.

Con in-place resize GA:

CPU puede ajustarse sin ningún restart

Memory puede configurarse para restart de container (no de Pod)

Observabilidad completa del estado del resize

Protección contra overcommit durante cambios pendientes

El escalado vertical finalmente se comporta como un ajuste, no como un deployment.

Recursos

KEP-1287: In-Place Pod Vertical Scaling

Kubernetes 1.35 Release Notes

Pod Resource Management

Publicado en yoDEV.dev — La comunidad de desarrolladores de Latinoamérica