Saltar al contenido
Volver al blog
DevOpsHomelabBuenas prácticas

Kubernetes 1.37 lleva Memory QoS a beta

Memory QoS ya viene activado en Kubernetes 1.37, pero proteger o limitar memoria sigue requiriendo configurar el kubelet.

Ismael Catala7 min de lectura

Cuando un servicio importante comparte nodo con todo lo demás

En un homelab es fácil acabar mezclando servicios que no pueden caer con tareas bastante menos importantes. Home Assistant, una base de datos o el controlador del clúster pueden terminar compartiendo nodo con descargas, indexadores o pruebas. Cuando falta memoria, los límites siguen siendo necesarios, pero hasta ahora había poco margen para indicar al kernel qué memoria debía intentar conservar antes de llegar a una situación más agresiva.

Kubernetes 1.37 promueve Memory QoS a beta y activa el feature gate MemoryQoS por defecto. Eso no significa que una actualización vaya a aplicar automáticamente reservas o throttling a todos los Pods. La configuración predeterminada del kubelet no escribe valores para memory.min, memory.low ni memory.high; hay que optar explícitamente por cada comportamiento. (kubernetes.io)

QoS ya no es solo una etiqueta para las expulsiones

Kubernetes clasifica los Pods como Guaranteed, Burstable o BestEffort a partir de sus requests y limits. Esa clasificación ya influía en las decisiones de expulsión cuando un nodo sufría presión de recursos. Con Memory QoS y cgroup v2, también sirve para decidir qué protección o control de memoria aplica el kubelet. (kubernetes.io)

Un Pod Guaranteed requiere que todos sus contenedores tengan requests y limits de CPU y memoria, con los mismos valores en cada recurso. Un Pod Burstable tiene al menos algún request o limit, pero no cumple las condiciones de Guaranteed. Un Pod BestEffort no define requests ni limits de CPU o memoria, así que no recibe protección de memoria con la política de reservas escalonadas. (kubernetes.io)

La protección llega a través de los requests

La opción memoryReservationPolicy: TieredReservation activa la reserva de memoria basada en la clase QoS. Para los Pods Guaranteed, el kubelet configura memory.min con el valor de los requests de memoria. Para los Pods Burstable, configura memory.low con esos requests, que el kernel intentará preservar de forma preferente cuando haya presión de memoria. (kubernetes.io)

La diferencia importa. memory.min representa una protección estricta frente a la reclamación de memoria, mientras que memory.low es una preferencia que el kernel puede ignorar si la presión es extrema. Los Pods BestEffort no reciben ninguna de estas protecciones, lo que encaja bastante bien con cargas prescindibles del homelab. (kubernetes.io)

Para que Kubernetes considere un servicio como Guaranteed, no basta con fijar la memoria. También hay que igualar requests y limits de CPU para todos los contenedores del Pod. Este manifiesto es un ejemplo sencillo de un servicio al que quiero dar esa clasificación:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicio-critico
spec:
  replicas: 1
  selector:
    matchLabels:
      app: servicio-critico
  template:
    metadata:
      labels:
        app: servicio-critico
    spec:
      containers:
        - name: app
          image: nginx:1.29
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "250m"
              memory: "256Mi"

Con TieredReservation, ese Pod puede recibir memory.min a partir de su request de memoria. No es necesario modificar los manifiestos para escribir directamente en cgroups: la decisión se toma en el kubelet según los recursos declarados y la clase QoS resultante. Esto obliga a revisar los requests existentes antes de activar la política, porque pasan a tener más peso operativo que antes. (kubernetes.io)

memory.high permite frenar antes del límite duro

La otra parte de Memory QoS es el throttling mediante memory.high. El kubelet lo controla con memoryThrottlingFactor, un valor mayor que cero y menor o igual que uno. En Pods Burstable, el umbral se calcula a partir del request, el limit y ese factor, de forma que el proceso puede empezar a ser frenado antes de alcanzar memory.max. (kubernetes.io)

La fórmula documentada es esta: memory.high = requests + memoryThrottlingFactor * (limits - requests). Los contenedores Guaranteed no reciben memory.high, porque sus requests y limits ya son iguales. En cargas BestEffort, Kubernetes toma como referencia la memoria asignable del nodo para el cálculo. (kubernetes.io)

Si quiero combinar la reserva escalonada con throttling, la configuración válida del kubelet es esta:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
memoryThrottlingFactor: 0.9
memoryReservationPolicy: TieredReservation

Ambas opciones se pueden usar por separado. Puedo omitir memoryThrottlingFactor si solo quiero protección mediante memory.min y memory.low. También puedo dejar memoryReservationPolicy en None si únicamente me interesa que determinados consumos se frenen antes de alcanzar el límite duro. (kubernetes.io)

El cambio que revisar antes de actualizar

En Kubernetes 1.37, el valor predeterminado de memoryThrottlingFactor pasa a ser null. Por tanto, aunque MemoryQoS quede activado por defecto, el kubelet no configura memory.high salvo que ese valor se declare expresamente. La intención es que actualizar no introduzca throttling de memoria de forma inesperada en cargas que antes funcionaban sin él. (kubernetes.io)

Aquí hay una comprobación importante si ya estabas usando esta función en versiones anteriores. Si el fichero de configuración del kubelet contiene un memoryThrottlingFactor explícito, se conserva y el throttling sigue funcionando. Si dependías del valor anterior sin haberlo declarado, debes añadirlo al fichero de configuración antes o durante la actualización. (kubernetes.io)

La letra pequeña que conviene no saltarse

Memory QoS requiere nodos Linux con cgroup v2. La documentación recomienda kernel 5.9 o superior porque el throttling con memory.high en kernels anteriores puede provocar un problema conocido de livelock. Si tu homelab usa una distribución antigua, comprobar la versión del kernel es tan importante como ajustar los manifiestos de Kubernetes. (kubernetes.io)

Tampoco convierte los requests en una estimación mágica de la memoria que consume una aplicación. Con TieredReservation, la política se aplica a todos los Pods del nodo según su clase QoS; no existe una selección individual de Pods para activar o desactivar esa reserva. Antes de habilitarla en un nodo mixto, conviene pensar qué cargas necesitan protección real y cuáles deberían seguir siendo recuperables bajo presión. (kubernetes.io)

Hay un caso especialmente delicado con Pods Guaranteed que usan mucha caché de página. Como el request y el limit son iguales, memory.min puede coincidir con memory.max; si el límite no deja margen suficiente para esa caché, el kernel puede no recuperar memoria a tiempo y terminar en un OOM kill. La protección no sustituye a dimensionar correctamente el límite de memoria. (kubernetes.io)

También conviene recordar que Memory QoS no elimina los límites ni cambia la clasificación QoS de los Pods. Un contenedor que supera su límite de memoria puede seguir siendo terminado y reiniciado. La función aporta señales más precisas al kernel antes de llegar a ese punto, pero no arregla una aplicación con fugas de memoria ni compensa recursos mal definidos. (kubernetes.io)

Cómo lo aplicaría en un homelab

Yo empezaría identificando los servicios que realmente deben aguantar una pelea por memoria: almacenamiento de estado, automatización doméstica, DNS, monitorización y componentes del propio clúster. Después revisaría que sus Pods sean Guaranteed de verdad, incluyendo sidecars e init containers cuando corresponda. Activaría TieredReservation primero en un nodo de prueba y observaría el comportamiento antes de extenderlo al resto. (kubernetes.io)

Para servicios elásticos o menos críticos, usaría requests y limits que los mantengan como Burstable. Ahí memory.low ofrece una protección menos rígida y memory.high puede servir para empezar a frenar el crecimiento antes de tocar el límite duro. Pero el valor de memoryThrottlingFactor no debería copiarse sin más: es una decisión de capacidad y de latencia para cada nodo. (kubernetes.io)

La mejora de Kubernetes 1.37 no está en que active una opción y ya está. Está en que permite separar mejor los servicios que deben conservar memoria de las cargas que pueden cederla, usando los recursos que ya declaramos en los Pods. Para un homelab con nodos pequeños y muchas piezas compartiendo máquina, esa distinción puede valer más que añadir otro servidor.


Fuente: Kubernetes v1.37: Memory QoS Graduates to Beta

Documentación oficial: Pod Quality of Service Classes y Memory QoS