Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Cómo detectar pérdidas ocultas de nube en tu código

By Joshua FoxMay 19, 20256 min read

Esta página también está disponible en English, Deutsch, Français, Italiano, 日本語 y Português.

Un enfoque sistemático para optimizar costos en la nube

La optimización de costos en la nube responde al principio de Pareto (80/20): un pequeño porcentaje de tus recursos suele concentrar la mayor parte del gasto. Empieza por usar herramientas como DoiT Cloud Intelligence™ para identificar tus áreas de mayor costo.

Paso 2: Conseguir victorias administrativas rápidas

Antes de meterte de lleno a optimizar el código, resuelve primero los ajustes más simples de administración en la nube:

  • Right-sizing de instancias subutilizadas
  • Eliminación de recursos huérfanos
  • Implementación de tiers de almacenamiento adecuados
  • Ajuste de los parámetros de autoscaling

Salen más baratos que ponerse a tocar el código.

Paso 3: Buscar señales de ineficiencia a nivel de código

Una vez completadas las optimizaciones administrativas, examina los recursos de mayor costo en busca de posibles ineficiencias provocadas por el código.

Por la "ilusión de eficiencia", estas señales suelen ser sutiles. Los casos reales que se analizan más adelante en este artículo muestran indicios habituales a los que conviene prestar atención, que normalmente se detectan con las herramientas estándar de monitoreo en la nube en las consolas de GCP, AWS y Azure.

Paso 4: Investigar: perfilar y analizar

Pasa del nivel de la nube al nivel del código con herramientas de profiling de ejecución que se puedan correr en la nube.

  • Para bases de datos: analizadores de consultas y performance insights de la Cloud Console
  • Para aplicaciones: profilers específicos de cada lenguaje y analizadores de memoria
  • Para data pipelines: grafos de ejecución y métricas de distribución

Flamegraph, from Google Cloud Profiler

Flame Graph de Google Cloud Profiler

Implementarlo puede ser sencillo, como en SQL —donde las herramientas vienen incluidas en la nube—, o complicado, como en el profiling de memoria de Python en aplicaciones distribuidas que corren en entornos administrados.

Paso 5: Implementar, medir, validar

Corrige los problemas detectados, vuelve a desplegar y mide las mejoras técnicas en las Cloud Consoles de AWS, GCP y Azure; después revisa los reportes de costos de Cloud Intelligence™ para validar el ahorro.

Casos reales y soluciones

Caso 1: Microservicio Java con fugas de memoria

Lo que parecía eficiente: un microservicio Java en Lambda que mantenía entre el 70 y el 100 % de uso de memoria, en apariencia aprovechando al máximo la asignación de recursos.

La realidad: la aplicación tenía fugas de memoria: un objeto global mantenía cadenas de referencias que retenían objetos entre invocaciones. Las caídas ocasionales y los reemplazos de instancias eran lo bastante esporádicos como para pasar desapercibidos para el equipo de SRE.

La pista: el monitoreo reveló un patrón de uso de memoria en forma de sierra a lo largo del tiempo. Investigar las caídas de ese patrón llevó a logs de CloudWatch que mostraban crashes periódicos.

Investigación: se activó CodeGuru Profiler, que evidenció un uso de memoria creciente con el tiempo. Análisis offline con un profiler de JVM identificaron una retención inesperada de objetos.

Solución: se modificó el código para liberar las referencias de objetos al final de cada solicitud web.

Resultado: uso estable de memoria, menos reemplazos de instancias y menores costos de recursos.

Caso 2: Pipeline de procesamiento de datos en Java con estructuras de datos ineficientes

Lo que parecía eficiente: un pipeline de Dataflow con contenedores Java personalizados que procesaba millones de registros al día con un uso de CPU consistentemente alto.

La realidad: el código usaba estructuras de datos ineficientes, incluidos maps con concatenación innecesaria de strings por objeto dentro de bucles ajustados, lo que generaba una sobrecarga excesiva de garbage collection.

La pista: el alto uso de recursos sugería la necesidad de una investigación más profunda.

Investigación: se sumó GCP Cloud Profiler al contenedor. Esto evidenció un escalado superlineal de tiempo y memoria a medida que crecían los datasets.

Solución:

  • Se reemplazaron los maps por objetos personalizados que solo guardaban la información necesaria.
  • Se implementó un join de strings adecuado en lugar de concatenación repetida.

Resultado: 50 % menos uso de memoria y 70 % menos tiempo de procesamiento, lo que permitió usar máquinas worker más pequeñas y menos instancias.

Caso 3: Estructuras de datos en memoria que provocaban VMs sobredimensionadas

Lo que parecía eficiente: las VMs con mucha memoria parecían rentables frente a soluciones con escalado horizontal, y las estructuras de datos en memoria de Python ofrecían ventajas algorítmicas de velocidad respecto a las consultas a base de datos.

La realidad: este enfoque generaba varias ineficiencias:

  • Los proveedores de nube imponen ratios mínimos de CPU a memoria, lo que se traduce en capacidad de CPU cara y sin usar.
  • Las asignaciones de memoria vienen en incrementos predefinidos, lo que obliga a pagar por capacidad de buffer no utilizada.
  • Los tiempos de inicialización largos obligaban a mantener varias instancias caras corriendo en paralelo por robustez.

La pista: DoiT Cloud Intelligence™ mostraba que una buena parte del gasto total provenía de VMs ultra grandes, algo que suele indicar problemas de statefulness en arquitecturas en la nube.

Investigación: un análisis profundo de los algoritmos reveló oportunidades para refactorizar y permitir que los datos se almacenaran fuera de la memoria de la aplicación.

Solución:

  • Se refactorizaron los algoritmos para trabajar con datasets parciales consultados desde la base de datos cuando se necesitaran.
  • Se implementó una base de datos NoSQL con Redis como caché en memoria.
  • Cuando hacía falta precargar todos los datos para información de referencia clave, las estructuras optimizadas en Redis ocupaban menos memoria que los objetos en la memoria de la aplicación.

Resultado: este cambio arquitectónico redujo significativamente el tamaño de las VMs, permitió escalar horizontalmente y bajó los costos de manera radical, aunque exigió un esfuerzo de ingeniería considerable.

Repaso de casos anteriores

Volvamos a dos ejemplos más del artículo que mencioné antes, "Stop Chasing Idle Servers", para ver cómo encajan en este marco.

Caso 4: Base de datos al 85 % de IOPS

Lo que parecía eficiente: la instancia de RDS aparecía totalmente utilizada, lo que sugería una asignación óptima de recursos.

La realidad: cada consulta hacía full-table scans porque faltaban dos índices críticos, lo que disparaba los requisitos de recursos.

La pista: dado que la mayoría de las consultas SQL no deberían requerir un uso alto de recursos (salvo en procesos batch muy ajustados), un patrón de alta utilización indicaba oportunidades de optimización.

Investigación: se identificaron las consultas problemáticas y los índices faltantes con AWS RDS Performance Optimizer, activado por defecto en la AWS Console. (GCP tiene su equivalente: Cloud SQL Query Insights).

Solución: se agregaron los índices que faltaban.

Resultado: latencia de consultas reducida 10x y posibilidad de bajar dos tiers el tamaño de la base de datos.

Caso 5: Job de Spark al 70 % de CPU durante cuatro horas cada noche

Lo que parecía eficiente: el cluster mantenía un uso alto de CPU, lo que sugería una asignación adecuada de recursos.

La realidad: el 80 % de los datos se concentraba en una única clave sesgada, lo que generaba tareas straggler que alargaban significativamente el tiempo de procesamiento.

La pista: el problema empezó en un momento puntual sin otra causa evidente. (Más tarde se descubrió que coincidía con la llegada de nuevos datos con la "hot key").

Investigación: el código de Spark corre en un entorno altamente distribuido, lo que dificulta usar un profiler convencional como el que emplearías en aplicaciones. Esa es una buena razón para mantener la lógica centrada en transformaciones simples y no en lógica de negocio compleja. Aun así, usar la Spark UI para analizar la distribución de tareas entre stages permitió identificar a los stragglers. El monitoreo de BigTable reveló "hot keys" en la base de datos que se estaban procesando.

Solución: se reparticionó y se aplicó salting a la clave problemática para distribuir los workloads de forma más pareja.

Resultado: el tiempo de finalización del job bajó de 4 horas a 45 minutos, y el tamaño necesario del cluster se redujo en dos tercios.

Lograr una verdadera eficiencia en la nube a veces exige ir más allá de las configuraciones del proveedor y atacar también las ineficiencias a nivel de código. Cuando el código es la causa raíz del exceso de costos en la nube, los ajustes de infraestructura por sí solos no alcanzan.

Si sumas a tu equipo de desarrollo junto a FinOps y SRE, puedes identificar y resolver estas ineficiencias ocultas siguiendo un enfoque sistemático:

  1. Empieza por las áreas de mayor gasto que muestran las analíticas de costos.
  2. Primero, resuelve las victorias rápidas a nivel de configuración en la nube.
  3. En las Cloud Consoles, busca pistas reveladoras que justifiquen una investigación más profunda.
  4. Usa herramientas de profiling adecuadas, de preferencia en la nube y offline si hace falta, para localizar las ineficiencias.
  5. Corrige el código, vuelve a desplegar y valida el ahorro.

Este enfoque colaborativo no solo reduce costos: muchas veces también mejora el rendimiento y la confiabilidad de la aplicación. Una victoria tanto para tu presupuesto como para tus usuarios.

En el equipo de Customer Reliability Engineering de DoiT acompaño a las organizaciones a lo largo de todo el recorrido de optimización. Apoyándonos en DoiT Cloud Intelligence™ y en décadas de experiencia, ayudamos a detectar oportunidades de ahorro, describir correcciones a nivel de nube, desarmar ilusiones de eficiencia y validar el impacto de las mejoras a nivel de código. Escríbenos en doit.com/services