Lesson  in  Kubernetes 101

Requests, limits y QoS Classes

El contrato del Pod con el clúster: pide recursos con requests, ponles techo con limits, y comprueba en la práctica las tres clases de calidad de servicio y qué le pasa a un contenedor que se pasa de memoria.

En la primera lección viste sin entenderla una línea del describe: QoS Class: BestEffort. Ahora toca entenderla, porque detrás de esas dos palabras está el contrato que tu Pod firma con el clúster, y ese contrato decide dos cosas serias: dónde te programan y a quién matan primero cuando el nodo se queda sin memoria.

Dos campos, dos significados muy distintos:

  • requests: lo que el Pod pide. Es lo único que mira el scheduler para decidir si cabe en un nodo. No es un consumo real: es una reserva.
  • limits: el techo. Es lo que vigila el kernel del nodo mientras el contenedor corre.

Y aquí está el detalle que más gente confunde: el scheduler no mira los limits, y el kernel no mira los requests. Son dos mundos que ni se hablan.

Trabaja desde la pestaña dev-machine.

Paso 1: Las tres clases, en un solo archivo

Kubernetes no te pregunta qué clase quieres: la deduce de lo que escribas. Crea qos.yaml con los tres casos:

cat << 'EOF' > qos.yaml
apiVersion: v1
kind: Pod
metadata:
  name: reports
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
---
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        cpu: 200m
        memory: 128Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: db
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 100m
        memory: 128Mi
EOF

El YAML, explicado con preguntas y respuestas

¿Qué significa 100m de CPU?

Cien milicores. Un milicore es la milésima parte de un núcleo, así que 100m es la décima parte de uno. La CPU es un recurso comprimible: se puede repartir en fracciones de tiempo. 1 es un núcleo entero; 500m es medio.

¿Y 128Mi no es lo mismo que 128M?

No, y la diferencia se paga. Mi es un mebibyte (1024 al cuadrado, base binaria); M es un megabyte (un millón, base decimal). Kubernetes acepta los dos, pero mezclarlos en la misma hoja de cálculo de capacidad es una fuente inagotable de descuadres. Usa siempre Mi y Gi.

¿Cómo se calcula la QoS Class exactamente?

Con tres reglas, en este orden:

  1. Guaranteed: todos los contenedores del Pod declaran requests y limits iguales, tanto en CPU como en memoria. Ni uno solo puede faltar.
  2. Burstable: al menos un contenedor declara algún request o limit, pero no se cumple lo anterior.
  3. BestEffort: nadie declara nada.

Nótese lo estricta que es la primera: basta con que un contenedor de un Pod de cinco tenga el limit de CPU un milicore por encima de su request para que todo el Pod caiga a Burstable.

¿Para qué sirve la clase, si mis Pods ya tienen los recursos que pedí?

Cuando un nodo se queda sin memoria, el kubelet tiene que desalojar Pods para sobrevivir, y los desaloja por clase: primero los BestEffort (no prometieron nada, no se les debe nada), después los Burstable que estén consumiendo por encima de sus requests, y en último lugar los Guaranteed. Tu clase de QoS es, literalmente, tu puesto en la cola de desalojo.

Aplícalo y pregunta a la API por el veredicto:

kubectl apply -f qos.yaml
kubectl get pods -o custom-columns=POD:.metadata.name,QOS:.status.qosClass

Tres Pods, tres clases, y tú no has escrito la palabra "QoS" en ningún sitio.

Paso 2: Pasarse del limit de CPU y pasarse del de memoria no es lo mismo

Aquí está la asimetría que hay que llevarse grabada. Crea tragamemoria.yaml:

cat << 'EOF' > tragamemoria.yaml
apiVersion: v1
kind: Pod
metadata:
  name: tragamemoria
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    command: ["sh", "-c", "echo empiezo a comer memoria; tail /dev/zero"]
    resources:
      requests:
        cpu: 50m
        memory: 32Mi
      limits:
        cpu: 50m
        memory: 32Mi
EOF

Ese tail /dev/zero es un truco clásico: lee ceros infinitos y los va acumulando en memoria, así que el contenedor crece sin parar hasta chocar con su techo de 32Mi.

kubectl apply -f tragamemoria.yaml
kubectl get pod tragamemoria --watch

Ese --watch se queda escuchando: en cuanto veas la columna RESTARTS subir, sal con Ctrl-C y pregunta por el motivo real del corte:

kubectl get pod tragamemoria -o jsonpath='{.status.containerStatuses[0].lastState}' | jq .

OOMKilled. El kernel del nodo no negocia: cuando un contenedor supera su limit de memoria, lo mata. Y como el restartPolicy por defecto es Always, el kubelet lo reinicia, vuelve a comer memoria, vuelve a caer, y acabas en el CrashLoopBackOff que ya conoces, pero con una causa nueva en tu catálogo.

Compáralo con lo que ocurre al pasarse de CPU:

¿Qué le pasa a un contenedor que quiere más CPU que su limit?

Nada dramático: se le estrangula (throttling). El kernel simplemente le da menos tiempo de procesador, así que la aplicación va lenta, pero sigue viva. La CPU es comprimible, la memoria no. Por eso:

  • Un limit de CPU mal calculado se manifiesta como latencia inexplicable, y es dificilísimo de diagnosticar si no lo tienes en el radar (mira la métrica de throttling del contenedor).
  • Un limit de memoria mal calculado se manifiesta como OOMKilled, que al menos es honesto y aparece en el describe.
Note

💡 El debate eterno: ¿hay que poner limit de CPU? Hay una escuela muy defendible que dice que no: request de CPU siempre (para que el scheduler haga bien su trabajo), pero sin limit, porque estrangular una aplicación que podría usar CPU ociosa del nodo no beneficia a nadie. Con la memoria no hay debate: limit siempre, porque un solo Pod con una fuga puede tirar el nodo entero y llevarse por delante a los vecinos.

Resumen

  • requests = lo que se reserva y lo único que mira el scheduler. limits = el techo que vigila el kernel.
  • La QoS Class no se declara, se deduce: Guaranteed (requests == limits en todo), Burstable (algo declarado), BestEffort (nada).
  • La clase decide el orden de desalojo cuando el nodo se queda sin memoria. BestEffort cae primero.
  • Pasarse de CPU: throttling, la aplicación va lenta. Pasarse de memoria: OOMKilled, la aplicación cae.
  • Sin requests no hay scheduling sensato, no hay cuotas justas y no hay HPA (lo verás en el módulo de escalado).
Previous lesson
Probes, el estado del Pod