Escalado horizontal de servicios Go en Kubernetes: de la configuración de HPA a las métricas personalizadas
Introducción: por qué los servicios Go necesitan escalado horizontal
El escalado horizontal (scale out) consiste en aumentar el número de instancias de un servicio, mientras que el vertical (scale up) implica añadir más recursos a un único nodo. Para los microservicios stateless en Go, el escalado horizontal es preferible por varias razones: no requiere tiempo de inactividad, permite aumentar el rendimiento de forma lineal y proporciona tolerancia a fallos gracias a la redundancia de réplicas.
El escalado vertical choca con los límites físicos del nodo y frecuentemente genera un único punto de fallo. Kubernetes, con su HPA (Horizontal Pod Autoscaler), ofrece un mecanismo declarativo para gestionar automáticamente el número de réplicas en función de métricas: CPU, memoria o indicadores de negocio personalizados.
Características de Go como lenguaje para servicios escalables
Go fue diseñado desde el principio para sistemas de alta carga. Varias características clave lo convierten en una excelente opción para microservicios con escalado horizontal en Kubernetes:
- Goroutines y planificador M:N. Miles de tareas concurrentes caben en una pequeña cantidad de memoria. Una goroutine arranca con ~2 KB de pila frente a ~1 MB de un hilo del sistema operativo.
- Footprint mínimo. Un binario compilado estáticamente pesa pocos megabytes; un contenedor basado en
scratchodistrolessarranca en segundos. - Stateless por defecto. La ausencia de estado global mutable (con una arquitectura correcta) simplifica la adición de réplicas sin necesidad de sincronización.
net/httpintegrado y arranque rápido. Un servicio Go está listo para recibir tráfico casi de inmediato tras el inicio del pod, algo crítico en escenarios de escalado agresivo.
Preparación del servicio Go para el escalado
Graceful Shutdown
Al reducir el número de réplicas, Kubernetes envía al pod la señal SIGTERM. El servicio Go debe completar las solicitudes en curso, cerrar las conexiones a la base de datos y liberar recursos antes de que expire el terminationGracePeriodSeconds.
package main\n\nimport (\n \"context\"\n \"log\"\n \"net/http\"\n \"os\"\n \"os/signal\"\n \"syscall\"\n \"time\"\n)\n\nfunc main() {\n mux := http.NewServeMux()\n mux.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n w.WriteHeader(http.StatusOK)\n })\n\n srv := &http.Server{\n Addr: \":8080\",\n Handler: mux,\n }\n\n go func() {\n if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n log.Fatalf(\"listen: %v\", err)\n }\n }()\n\n quit := make(chan os.Signal, 1)\n signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)\n <-quit\n\n ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n defer cancel()\n if err := srv.Shutdown(ctx); err != nil {\n log.Fatalf(\"Server forced to shutdown: %v\", err)\n }\n log.Println(\"Server exited cleanly\")\n}\nReadiness y Liveness Probes
La readiness probe indica a Kubernetes que el pod está listo para recibir tráfico. Sin un endpoint correctamente configurado, las nuevas réplicas recibirán solicitudes antes de haberse inicializado por completo, lo que genera errores durante el escalado. La liveness probe permite reiniciar un pod bloqueado.
readinessProbe:\n httpGet:\n path: /ready\n port: 8080\n initialDelaySeconds: 5\n periodSeconds: 5\n failureThreshold: 3\nlivenessProbe:\n httpGet:\n path: /health\n port: 8080\n initialDelaySeconds: 10\n periodSeconds: 10\nGestión correcta de conexiones
Con el escalado horizontal, el número de conexiones a PostgreSQL o Redis crece proporcionalmente al número de réplicas. Utilice pools con límites razonables (SetMaxOpenConns, SetMaxIdleConns para database/sql) y PgBouncer / Redis Cluster a nivel de infraestructura para no agotar los límites del gestor de bases de datos.
HPA basado en CPU y Memoria: configuración básica
HPA es el controlador integrado de Kubernetes que, de forma periódica (por defecto cada 15 segundos), consulta el Metrics Server y ajusta el número de réplicas del Deployment.
apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: go-api-hpa\n namespace: production\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: go-api\n minReplicas: 3\n maxReplicas: 20\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 60\n - type: Resource\n resource:\n name: memory\n target:\n type: Utilization\n averageUtilization: 75\n behavior:\n scaleDown:\n stabilizationWindowSeconds: 120\n policies:\n - type: Pods\n value: 2\n periodSeconds: 60\n scaleUp:\n stabilizationWindowSeconds: 0\n policies:\n - type: Percent\n value: 100\n periodSeconds: 30\nPuntos conflictivos:
- Sin
requestscorrectamente definidos, el HPA no podrá calcular la utilización y la métrica aparecerá comounknown. - Go hace un uso intensivo del GC, lo que genera picos de CPU. Un valor objetivo del 60–70% deja margen para la recolección de basura.
- El escalado por memoria es arriesgado en Go: el runtime retiene la memoria liberada para reutilizarla (MADV_FREE). Use
GOGCyGOMEMLIMITpara controlar el comportamiento del GC.
Métricas personalizadas para HPA: Prometheus y Prometheus Adapter
Exportación de métricas desde el servicio Go
Utilice el cliente oficial github.com/prometheus/client_golang para registrar y publicar métricas.
package metrics\n\nimport (\n \"github.com/prometheus/client_golang/prometheus\"\n \"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n RequestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{\n Name: \"go_api_requests_in_flight\",\n Help: \"Current number of HTTP requests being processed\",\n })\n\n QueueDepth = promauto.NewGaugeVec(prometheus.GaugeOpts{\n Name: \"go_worker_queue_depth\",\n Help: \"Number of tasks waiting in the processing queue\",\n }, []string{\"queue_name\"})\n\n RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{\n Name: \"go_api_request_duration_seconds\",\n Help: \"HTTP request duration\",\n Buckets: prometheus.DefBuckets,\n }, []string{\"method\", \"path\", \"status\"})\n)\nConfiguración de Prometheus Adapter
Prometheus Adapter implementa la Custom Metrics API de Kubernetes, permitiendo que el HPA utilice métricas arbitrarias de Prometheus. Se instala mediante Helm:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts\nhelm install prometheus-adapter prometheus-community/prometheus-adapter \\\n --namespace monitoring \\\n --set prometheus.url=http://prometheus.monitoring.svc \\\n --set prometheus.port=9090\nConfiguración de reglas en values.yaml:
rules:\n custom:\n - seriesQuery: 'go_worker_queue_depth{namespace!=\"\",pod!=\"\"}'\n resources:\n overrides:\n namespace: {resource: \"namespace\"}\n pod: {resource: \"pod\"}\n name:\n matches: \"go_worker_queue_depth\"\n as: \"worker_queue_depth\"\n metricsQuery: 'avg(go_worker_queue_depth{<<.LabelMatchers>>}) by (<<.GroupBy>>)'\nEscalado por métricas de negocio: la longitud de la cola como disparador
Imaginemos un worker en Go que procesa tareas de una cola interna. La longitud de la cola es un indicador directo de carga: si hay más tareas, hay que añadir más workers.
apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: go-worker-hpa\n namespace: production\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: go-worker\n minReplicas: 2\n maxReplicas: 30\n metrics:\n - type: Pods\n pods:\n metric:\n name: worker_queue_depth\n target:\n type: AverageValue\n averageValue: \"10\"\nLa lógica es la siguiente: el HPA mantendrá el valor promedio de worker_queue_depth en 10 tareas por réplica. Si la cola crece hasta 100 tareas con 2 réplicas (50 por réplica), el controlador escalará el deployment a 10 réplicas.
KEDA como alternativa: escalado por Redis y Kafka
KEDA (Kubernetes Event-Driven Autoscaling) amplía las capacidades del HPA añadiendo escaladores predefinidos para decenas de fuentes de eventos. Para workers en Go que consumen mensajes de una Redis List o Kafka, KEDA es la solución más conveniente.
ScaledObject para una cola Redis
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n name: go-worker-redis-scaler\n namespace: production\nspec:\n scaleTargetRef:\n name: go-worker\n minReplicaCount: 1\n maxReplicaCount: 50\n pollingInterval: 10\n cooldownPeriod: 60\n triggers:\n - type: redis\n metadata:\n address: redis.production.svc:6379\n listName: task_queue\n listLength: \"5\"\n enableTLS: \"false\"\n authenticationRef:\n name: redis-auth\nScaledObject para Kafka
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n name: go-worker-kafka-scaler\n namespace: production\nspec:\n scaleTargetRef:\n name: go-kafka-worker\n minReplicaCount: 2\n maxReplicaCount: 40\n triggers:\n - type: kafka\n metadata:\n bootstrapServers: kafka.production.svc:9092\n consumerGroup: go-worker-group\n topic: events\n lagThreshold: \"20\"\n offsetResetPolicy: latest\nKEDA puede escalar el deployment a cero réplicas cuando la cola está vacía, lo cual es útil para workers batch que operan según un calendario.
Gestión de recursos: requests/limits, VPA y Cluster Autoscaler
Unos requests y limits correctamente configurados son la base del buen funcionamiento del HPA y del planificador de Kubernetes.
resources:\n requests:\n cpu: \"250m\"\n memory: \"128Mi\"\n limits:\n cpu: \"1000m\"\n memory: \"512Mi\"\nPara servicios Go se recomienda:
- Establecer
GOMEMLIMITal 80–90% delimits.memorypara que el GC libere memoria de forma más agresiva antes de que el contenedor sea eliminado por OOMKiller. - No configurar los límites de CPU demasiado bajos: el throttling de CPU genera latencia indistinguible de una sobrecarga del servicio.
- El VPA (Vertical Pod Autoscaler) en modo
Offes útil para obtener recomendaciones sobre requests/limits sin aplicarlas automáticamente.
El Cluster Autoscaler añade nodos al clúster automáticamente cuando los pods no pueden ser planificados por falta de recursos. Asegúrese de que el PodDisruptionBudget esté configurado para evitar la expulsión simultánea de demasiadas réplicas durante el scale-down de nodos.
apiVersion: policy/v1\nkind: PodDisruptionBudget\nmetadata:\n name: go-api-pdb\n namespace: production\nspec:\n minAvailable: 2\n selector:\n matchLabels:\n app: go-api\nPruebas de escalado: tests de carga con k6
Antes de pasar a producción es necesario verificar el comportamiento del HPA bajo carga. k6 es una herramienta ligera de pruebas de carga escrita en Go, con una API de escenarios en JavaScript.
import http from 'k6/http';\nimport { sleep, check } from 'k6';\n\nexport const options = {\n stages: [\n { duration: '2m', target: 50 }, // ramp-up\n { duration: '5m', target: 200 }, // carga sostenida\n { duration: '2m', target: 500 }, // estrés\n { duration: '2m', target: 0 }, // ramp-down\n ],\n thresholds: {\n http_req_duration: ['p(95)<500'],\n http_req_failed: ['rate<0.01'],\n },\n};\n\nexport default function () {\n const res = http.get('http://go-api.production.svc/api/v1/items');\n check(res, { 'status 200': (r) => r.status === 200 });\n sleep(0.1);\n}\nDurante la prueba, observe el HPA con el comando kubectl get hpa -n production -w y monitorice el tiempo de reacción: desde el pico de carga hasta la aparición de nuevas réplicas suelen pasar entre 1 y 3 minutos (tiempo de scrape de Prometheus + ciclo del HPA + descarga de la imagen + inicialización del pod).
Problemas comunes y cómo resolverlos
Cold Start
A pesar del rápido arranque del binario Go, un nuevo pod consume tiempo en descargar la imagen, inicializar conexiones a la base de datos y calentar cachés. Durante este período, la readiness probe no pasa y el pod no recibe tráfico. Soluciones: usar imágenes pre-descargadas (DaemonSet o caché de imágenes), reducir initialDelaySeconds en la readiness probe y calentar el pool de conexiones en segundo plano antes de activar readiness.
Thundering Herd
Ante un aumento brusco de carga, todas las nuevas réplicas intentan conectarse simultáneamente a la base de datos, Redis o APIs externas, provocando una tormenta de conexiones. Utilice exponential backoff con jitter durante la inicialización de conexiones y limite maxReplicaCount combinándolo con rate limiting a nivel de servicio.
// Jitter al reconectar\nfunc retryWithJitter(attempt int) time.Duration {\n base := time.Duration(attempt) * 100 * time.Millisecond\n jitter := time.Duration(rand.Int63n(int64(base)))\n return base + jitter\n}\nFlapping (escalado inestable)
El HPA añade y elimina réplicas continuamente: señal de umbrales mal elegidos o de una ventana de estabilización demasiado corta. Solución: aumentar stabilizationWindowSeconds para scale-down (se recomiendan 120–300 segundos), elevar el utilization objetivo y usar behavior.scaleDown.policies para limitar la velocidad de reducción de réplicas.
Métrica unknown en HPA
Si kubectl describe hpa muestra unknown para CPU/memoria, lo más probable es que no estén definidos los requests en el manifiesto del pod o que no esté ejecutándose el Metrics Server. Para métricas personalizadas, verifique la disponibilidad de la Custom Metrics API: kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1.
Conclusión y recomendaciones
El escalado horizontal de microservicios Go en Kubernetes no consiste simplemente en aplicar un manifiesto de HPA. Es una práctica integral que incluye la preparación del propio servicio (graceful shutdown, endpoints de probe, gestión de conexiones), la configuración correcta de recursos y la elección de la fuente de métricas adecuada.
- Comience con HPA por CPU, fijando un utilization objetivo del 60–70% teniendo en cuenta los picos del GC.
- Añada métricas personalizadas a través de Prometheus Adapter para escalar por RPS o latencia.
- Para workers event-driven basados en Redis o Kafka, utilice KEDA: es significativamente más sencillo de configurar y admite escalado a cero.
- Pruebe siempre el comportamiento bajo carga con k6 o herramientas similares antes de desplegar en producción.
- Configure PodDisruptionBudget y
terminationGracePeriodSecondspara un scale-down seguro. - Use
GOMEMLIMITpara controlar la presión del GC y prevenir OOM.
La combinación de Go —con tipado estático y arranque ultrarrápido—, Kubernetes declarativo y un HPA flexible con métricas personalizadas proporciona una infraestructura capaz de gestionar picos de carga imprevisibles sin intervención manual: exactamente lo que necesitan los microservicios modernos de alta demanda en 2026.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →