SRE: Planificación de Capacidad, Autoescalamiento y Pruebas de Carga

2026-03-05 | Gabriel Garrido | 11 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introducción

A lo largo de esta serie de SRE cubrimos SLIs y SLOs, gestión de incidentes, observabilidad, y chaos engineering. Todo eso asume que tu sistema tiene suficiente capacidad para servir tráfico. ¿Pero cómo sabés si la tiene? ¿Y qué pasa cuando el tráfico se duplica de un día para el otro?


La planificación de capacidad es el arte de asegurar que tu infraestructura pueda manejar la demanda actual y futura sin sobre-provisionar (desperdiciar plata) o sub-provisionar (degradar el servicio). En Kubernetes, esto significa acertar con los resource requests y limits, configurar autoscalers correctamente, y validar tu setup con pruebas de carga.


En este artículo vamos a cubrir resource requests y limits, el Horizontal Pod Autoscaler (HPA), el Vertical Pod Autoscaler (VPA), KEDA para escalamiento basado en eventos, y pruebas de carga con k6 para asegurarnos de que todo funciona bajo presión.


Vamos al tema.


Resource requests y limits: la base

Antes de poder autoescalar cualquier cosa, necesitás entender resource requests y limits. Son los conceptos más malinterpretados en Kubernetes, y configurarlos mal causa más caídas de las que la mayoría piensa.


  • Requests: Los recursos mínimos que un pod necesita. El scheduler usa esto para decidir dónde ubicar el pod.
  • Limits: Los recursos máximos que un pod puede usar. Si el pod excede su limit de memoria, es OOM-killed.

Acá cómo configurarlos para nuestra aplicación Elixir:


# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tr-web
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: tr-web
  template:
    metadata:
      labels:
        app: tr-web
    spec:
      containers:
        - name: tr-web
          image: kainlite/tr:latest
          resources:
            requests:
              cpu: "250m"      # 0.25 cores de CPU
              memory: "256Mi"  # 256 MB RAM
            limits:
              cpu: "1000m"     # 1 core de CPU
              memory: "512Mi"  # 512 MB RAM
          ports:
            - containerPort: 4000

Errores comunes:


  • Poner requests = limits: Esto te da QoS garantizado pero desperdicia recursos. Solo hacelo para bases de datos críticas.
  • No poner requests: Los pods reciben QoS BestEffort y son los primeros en ser desalojados bajo presión.
  • Poner memory limits muy bajos: Las aplicaciones BEAM usan memoria para tablas ETS, heaps de procesos, y datos binarios. Limits muy ajustados causan OOM kills aleatorios.
  • Poner CPU limits: Hay un consenso creciente de que los CPU limits causan más daño que beneficio por el throttling. Considerá poner solo CPU requests y no CPU limits.

El debate de los CPU limits:


Los CPU limits en Kubernetes usan throttling de CFS (Completely Fair Scheduler). Incluso si el nodo tiene CPU ociosa, un pod que llegue a su CPU limit va a ser throttleado. Esto causa picos de latencia difíciles de debuggear porque todo parece bien desde la perspectiva de uso de recursos.


# Opción A: Con CPU limits (seguro pero puede causar throttling)
resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1000m"
    memory: "512Mi"

# Opción B: Sin CPU limits (mejor performance, requiere buen monitoreo)
resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    memory: "512Mi"  # Mantener memory limits, sacar CPU limits

Si vas por la Opción B, asegurate de tener buen monitoreo para detectar problemas de noisy neighbor.


Dimensionamiento correcto con VPA

El Vertical Pod Autoscaler (VPA) observa el uso real de recursos de tus pods y recomienda o ajusta automáticamente los requests y limits. Es increíblemente útil porque adivinar los valores correctos es difícil.


Creá un recurso VPA en modo recomendación (lo más seguro para arrancar):


# vpa/tr-web-vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: tr-web-vpa
  namespace: default
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: tr-web
  updatePolicy:
    updateMode: "Off"  # Arrancar solo con recomendaciones
  resourcePolicy:
    containerPolicies:
      - containerName: tr-web
        minAllowed:
          cpu: "100m"
          memory: "128Mi"
        maxAllowed:
          cpu: "2000m"
          memory: "1Gi"

Después de unos días recolectando datos, revisá las recomendaciones:


kubectl describe vpa tr-web-vpa

# La salida se ve así:
# Recommendation:
#   Container Recommendations:
#     Container Name: tr-web
#     Lower Bound:
#       Cpu:     150m
#       Memory:  200Mi
#     Target:
#       Cpu:     280m
#       Memory:  310Mi
#     Upper Bound:
#       Cpu:     500m
#       Memory:  450Mi

Los valores “Target” son lo que VPA recomienda para tus requests. Usalos como punto de partida y validá con pruebas de carga.


Horizontal Pod Autoscaler (HPA)

HPA escala la cantidad de pods basándose en métricas. El setup más común escala por uso de CPU o memoria, pero también podés escalar por métricas custom de Prometheus.


HPA básico por CPU:


# hpa/tr-web-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: tr-web-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: tr-web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120

Configuraciones clave:


  • minReplicas: 2: Siempre tené al menos 2 pods para redundancia
  • maxReplicas: 10: Tope para prevenir escalamiento descontrolado (y costos descontrolados)
  • averageUtilization: 70: Escalar cuando el CPU promedio entre pods supera 70%
  • scaleUp stabilization: 60s: Esperar 60 segundos antes de escalar para evitar flapping
  • scaleDown stabilization: 300s: Esperar 5 minutos antes de reducir para manejar oscilaciones de tráfico

HPA con métricas custom de Prometheus:


Escalar por CPU es un instrumento tosco. Para un servicio web, escalar por requests-por-segundo o latencia es mucho más responsivo. Esto requiere el Prometheus Adapter:


# Creá un HPA que escale por requests por segundo por pod:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: tr-web-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: tr-web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    # Escalar por requests por segundo por pod
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "100"  # Escalar si algún pod maneja más de 100 rps

    # También considerar latencia
    - type: Pods
      pods:
        metric:
          name: http_request_duration_p99
        target:
          type: AverageValue
          averageValue: "300m"  # Escalar si p99 > 300ms

Esto es mucho mejor que escalar por CPU porque reacciona a patrones de tráfico reales en lugar de consumo de recursos, que puede ser engañoso (la VM BEAM maneja la memoria de forma diferente a la mayoría de los runtimes).


KEDA: autoescalamiento basado en eventos

KEDA (Kubernetes Event-Driven Autoscaling) lleva al HPA al siguiente nivel soportando docenas de fuentes de eventos. Es particularmente útil para escalar basándose en profundidad de colas, schedules cron, o métricas externas.


Escalamiento basado en métricas de Prometheus (consciente de SLOs):


# keda/tr-web-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: tr-web-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    name: tr-web
  minReplicaCount: 2
  maxReplicaCount: 15
  pollingInterval: 30
  cooldownPeriod: 300
  triggers:
    # Escalar basado en tasa de requests
    - type: prometheus
      metadata:
        serverAddress: http://prometheus.monitoring:9090
        metricName: http_requests_rate
        query: sum(rate(http_requests_total{service="tr-web"}[2m]))
        threshold: "200"
        activationThreshold: "50"

    # Escalar basado en tasa de quemado del presupuesto de error
    - type: prometheus
      metadata:
        serverAddress: http://prometheus.monitoring:9090
        metricName: error_budget_burn_rate
        query: sli:availability:burn_rate5m{service="tr-web"}
        threshold: "5"  # Escalar cuando se quema presupuesto 5x más rápido de lo normal

El trigger de presupuesto de error es particularmente inteligente. Cuando tu servicio está quemando presupuesto de error más rápido de lo esperado (lo que significa que la confiabilidad se está degradando), KEDA agrega más réplicas para absorber la carga. Esto conecta la planificación de capacidad directamente con tus SLOs.


Escalamiento por cron para patrones de tráfico predecibles:


# keda/tr-web-cron-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: tr-web-cron
  namespace: default
spec:
  scaleTargetRef:
    name: tr-web
  minReplicaCount: 2
  maxReplicaCount: 10
  triggers:
    # Escalar durante horario laboral
    - type: cron
      metadata:
        timezone: America/Argentina/Buenos_Aires
        start: "0 8 * * 1-5"   # Lunes a viernes 8am
        end: "0 20 * * 1-5"    # Lunes a viernes 8pm
        desiredReplicas: "4"

Si conocés tus patrones de tráfico (y deberías, de tus datos de observabilidad), el escalamiento proactivo evita la demora que introduce el autoescalamiento reactivo. ¿Por qué esperar a que el CPU suba si sabés que el tráfico aumenta todas las mañanas a las 8am?


Pruebas de carga con k6

Toda la configuración de autoscaling del mundo es inútil si no la validaste bajo carga real. k6 es una excelente herramienta de pruebas de carga que hace fácil definir escenarios.


Prueba de carga básica para el blog:


// load-tests/blog-basic.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // Subir a 50 usuarios en 2 minutos
    { duration: '5m', target: 50 },   // Mantener en 50 usuarios por 5 minutos
    { duration: '2m', target: 100 },  // Subir a 100 usuarios
    { duration: '5m', target: 100 },  // Mantener en 100 usuarios
    { duration: '2m', target: 0 },    // Bajar a 0
  ],
  thresholds: {
    // Umbrales alineados con SLOs
    http_req_duration: ['p(99)<300'],    // 99% de las requests bajo 300ms
    http_req_failed: ['rate<0.001'],     // Menos de 0.1% tasa de error
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:4000';

export default function () {
  // Simular un usuario típico navegando el blog
  const pages = [
    '/blog',
    '/blog/sre-slis-slos-and-automations-that-actually-help',
    '/blog/debugging-distroless-containers-when-your-container-has-no-shell',
  ];

  const page = pages[Math.floor(Math.random() * pages.length)];
  const res = http.get(`${BASE_URL}${page}`);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'body contains content': (r) => r.body.length > 1000,
  });

  sleep(Math.random() * 3 + 1); // Think time aleatorio entre 1-4 segundos
}

Prueba de validación de autoscaling:


Esta es la prueba de carga más importante. Valida que tu HPA arranca correctamente bajo carga:


// load-tests/autoscale-validation.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter } from 'k6/metrics';

const errorCount = new Counter('errors');

export const options = {
  stages: [
    // Fase 1: Línea base (debería correr con min replicas)
    { duration: '2m', target: 10 },

    // Fase 2: Rampa para disparar scale-up
    { duration: '1m', target: 200 },

    // Fase 3: Carga alta sostenida (HPA debería escalar)
    { duration: '10m', target: 200 },

    // Fase 4: Rampa de bajada (HPA debería eventualmente reducir)
    { duration: '2m', target: 10 },

    // Fase 5: Carga baja sostenida
    { duration: '5m', target: 10 },
  ],
  thresholds: {
    http_req_duration: ['p(99)<500'],  // Incluso bajo carga, p99 < 500ms
    http_req_failed: ['rate<0.005'],   // Menos de 0.5% errores
    errors: ['count<50'],              // Menos de 50 errores totales
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:4000';

export default function () {
  const res = http.get(`${BASE_URL}/blog`);

  const success = check(res, {
    'status is 200': (r) => r.status === 200,
    'latency < 500ms': (r) => r.timings.duration < 500,
  });

  if (!success) {
    errorCount.add(1);
  }

  sleep(Math.random() * 2 + 0.5);
}

Corré la prueba mientras mirás tu HPA:


# Terminal 1: Correr la prueba de carga
k6 run --env BASE_URL=https://your-app.example.com load-tests/autoscale-validation.js

# Terminal 2: Mirar HPA
kubectl get hpa tr-web-hpa --watch

# Terminal 3: Mirar pods escalando
kubectl get pods -l app=tr-web --watch

Lo que querés ver:


  • Durante Fase 1: 2 réplicas (minReplicas), bajo uso de recursos
  • Durante Fase 2-3: HPA escala a 4-6 réplicas dentro de 2-3 minutos de alta carga
  • Durante Fase 3: La latencia se mantiene dentro del SLO incluso con alta carga
  • Durante Fase 4-5: HPA gradualmente reduce a 2 réplicas después de la ventana de estabilización

Planificación de capacidad como práctica

Más allá del autoescalamiento, la planificación de capacidad es una práctica continua:


1. Rastreá tendencias de utilización de recursos

Si tu utilización promedio está consistentemente por encima del 80%, necesitás más capacidad. Si está consistentemente por debajo del 20%, estás sobre-provisionado y desperdiciando plata.


2. Revisá después de cada cambio importante

Después de lanzar una feature nueva, verificá si los patrones de uso de recursos cambiaron. Un job nuevo en background podría aumentar el uso de memoria. Un endpoint nuevo podría aumentar el uso de CPU en horas pico.


3. Planificá para el crecimiento

Si tu tráfico crece 10% mes a mes, tu maxReplicas del autoscaler necesita acomodar ese crecimiento. Revisá tus límites máximos trimestralmente y ajustá.


Juntando todo

Acá está el setup completo de gestión de capacidad:


  1. VPA en modo recomendación te dice qué recursos tus pods realmente necesitan
  2. Resource requests se configuran basándose en recomendaciones de VPA y se validan con pruebas de carga
  3. HPA con métricas custom escala pods basándose en tráfico (no solo CPU)
  4. Triggers cron de KEDA escalan proactivamente para patrones de tráfico conocidos
  5. Cluster Autoscaler agrega nodos cuando los pods no pueden ser schedulados
  6. Buffer pods aseguran capacidad instantánea para eventos de scale-up
  7. Pruebas de carga con k6 validan todo el pipeline de escalamiento regularmente

Esto te da un sistema que maneja picos de tráfico automáticamente, dimensiona recursos basándose en uso real, y te da confianza en que tus SLOs se van a mantener bajo carga.


Notas finales

La planificación de capacidad no tiene que ser adivinanza. Con recomendaciones de VPA, autoescalamiento alineado con SLOs, y pruebas de carga regulares, podés tener confianza en que tu infraestructura maneja cualquier tráfico que llegue.


La conclusión más importante: el autoescalamiento no es sustituto de entender tu workload. Conocé tus patrones de tráfico, probá tus límites de escalamiento, y siempre tené un plan para lo que pasa cuando el tráfico excede tu capacidad máxima. La respuesta a “¿qué pasa si recibimos 10x de tráfico?” nunca debería ser “no sé.”


Esto concluye nuestra serie de cinco partes sobre SRE. Desde SLIs/SLOs hasta gestión de incidentes, observabilidad, chaos engineering, y ahora planificación de capacidad, tenés las herramientas y prácticas para correr sistemas confiables a cualquier escala.


¡Espero que te haya resultado útil y lo hayas disfrutado! ¡Hasta la próxima!


Errata

Si encontrás algún error o tenés alguna sugerencia, por favor mandame un mensaje para que se corrija.

También podés revisar el código fuente y los cambios en las fuentes acá



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-03-05 | Gabriel Garrido