Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Como identificar desperdícios ocultos de nuvem no seu código

By Joshua FoxMay 19, 20256 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

Uma abordagem sistemática para otimizar custos de nuvem

A otimização de custos na nuvem segue o Princípio de Pareto (80/20) — uma pequena parcela dos seus recursos costuma responder pela maior parte dos custos. Comece usando ferramentas como o DoiT Cloud Intelligence™ para identificar suas maiores áreas de gasto.

Passo 2: garanta vitórias administrativas rápidas

Antes de partir para otimizações de código, resolva primeiro os ajustes mais simples de administração da nuvem:

  • Right-sizing de instâncias subutilizadas
  • Remoção de recursos órfãos
  • Adoção de tiers de armazenamento adequados
  • Ajuste dos parâmetros de autoscaling

Essas ações saem mais barato do que mexer no código.

Passo 3: procure indicadores de ineficiência no código

Depois de concluir as otimizações administrativas, examine os recursos mais caros em busca de possíveis ineficiências causadas pelo código.

Por causa da "ilusão de eficiência", esses indicadores podem ser sutis. Os cenários reais discutidos mais adiante neste artigo ilustram sinais comuns que merecem atenção e que normalmente aparecem nas próprias ferramentas de monitoramento dos consoles de GCP, AWS e Azure.

Passo 4: investigue — faça profiling e análise

Saia do nível da nuvem e desça até o nível do código com ferramentas de profiling de execução que rodam na própria nuvem.

  • Para bancos de dados: analisadores de queries do Cloud Console e performance insights
  • Para aplicações: profilers e analisadores de memória específicos da linguagem
  • Para pipelines de dados: gráficos de execução e métricas de distribuição

Flamegraph, do Google Cloud Profiler

Flame Graph do Google Cloud Profiler

Implementar isso pode ser fácil — como no caso do SQL, em que as ferramentas já vêm prontas na nuvem — ou difícil, como no profiling de memória de aplicações Python distribuídas rodando em ambientes gerenciados.

Passo 5: implemente, meça e valide

Corrija os problemas identificados, faça o redeploy e meça as melhorias técnicas pelos Cloud Consoles da AWS, GCP e Azure; depois consulte os relatórios de custo do Cloud Intelligence™ para validar a economia.

Cenários reais e soluções

Cenário 1: microsserviço Java com vazamentos de memória

O que parecia eficiente: um microsserviço Java em Lambda mantendo entre 70% e 100% de utilização de memória, aparentemente aproveitando ao máximo a alocação de recursos.

A realidade: a aplicação sofria com vazamentos de memória, em que um objeto global mantinha cadeias de referências que retinham objetos entre invocações. Crashes ocasionais e trocas de instâncias eram raros o suficiente para passar despercebidos pelo time de SRE.

A pista: o monitoramento mostrou um padrão de uso de memória em formato de dente de serra ao longo do tempo. Investigar as quedas desse padrão levou aos logs do CloudWatch, que indicavam crashes periódicos.

Investigação: ativamos o CodeGuru Profiler, que revelou aumento contínuo do uso de memória. Análises offline com um profiler da JVM identificaram retenção inesperada de objetos.

Solução: alteramos o código para liberar as referências dos objetos ao final de cada requisição web.

Resultado: uso de memória estável, menos trocas de instâncias e custos de recursos mais baixos.

Cenário 2: pipeline de processamento de dados em Java com estruturas ineficientes

O que parecia eficiente: um pipeline no Dataflow com containers Java customizados, processando milhões de registros por dia com utilização de CPU consistentemente alta.

A realidade: o código usava estruturas de dados ineficientes, incluindo maps com concatenação desnecessária de strings por objeto dentro de loops apertados, gerando uma sobrecarga excessiva de garbage collection.

A pista: o alto consumo de recursos sugeria a necessidade de uma investigação mais aprofundada.

Investigação: adicionamos o GCP Cloud Profiler ao container, que revelou um escalonamento super-linear de tempo e uso de memória conforme os datasets cresciam.

Solução:

  • Substituímos os maps por objetos customizados contendo apenas as informações necessárias.
  • Implementamos junção adequada de strings em vez de concatenações repetidas.

Resultado: 50% menos uso de memória e redução de 70% no tempo de processamento, o que permitiu usar máquinas worker menores e em menor número.

Cenário 3: estruturas de dados em memória causando VMs superdimensionadas

O que parecia eficiente: VMs com muita memória pareciam ter um bom custo-benefício em comparação a soluções escaladas horizontalmente, e estruturas de dados em memória no Python ofereciam ganhos algorítmicos de velocidade frente a queries em banco.

A realidade: essa abordagem criava várias ineficiências:

  • Os provedores de nuvem impõem proporções mínimas entre CPU e memória, resultando em capacidade de CPU cara e ociosa.
  • As alocações de memória vêm em incrementos predefinidos, exigindo pagar por uma capacidade de buffer que não é usada.
  • Tempos longos de inicialização obrigavam a manter várias instâncias caras rodando ao mesmo tempo para garantir robustez.

A pista: o DoiT Cloud Intelligence™ mostrou uma fatia grande da despesa total vinda de VMs ultragrandes — em geral, um sinal de statefulness problemática em arquiteturas de nuvem.

Investigação: uma análise aprofundada dos algoritmos revelou oportunidades de refatoração para armazenar os dados fora da memória da aplicação.

Solução:

  • Refatoramos os algoritmos para trabalhar com datasets parciais consultados nos bancos sob demanda.
  • Implementamos um banco NoSQL com Redis como cache em memória.
  • Quando o pré-carregamento total era necessário para dados de referência críticos, estruturas otimizadas no Redis ocupavam menos memória do que objetos na memória da aplicação.

Resultado: a mudança de arquitetura reduziu bastante o tamanho das VMs, viabilizou o escalonamento horizontal e diminuiu os custos drasticamente — mesmo exigindo um esforço considerável de engenharia.

Revisitando casos anteriores

Vamos voltar a mais dois exemplos do artigo que mencionei antes, "Stop Chasing Idle Servers", e ver como se encaixam neste framework.

Cenário 4: banco de dados operando a 85% de IOPS

O que parecia eficiente: a instância RDS parecia estar totalmente aproveitada, sugerindo uma alocação ótima de recursos.

A realidade: todas as queries faziam full-table scans por causa de dois índices críticos que estavam faltando, o que disparava o consumo de recursos.

A pista: como a maioria das queries SQL não deveria exigir uso alto de recursos (exceto em batches bastante otimizados), um padrão de alta utilização indicava oportunidades de otimização.

Investigação: identificamos as queries problemáticas e os índices ausentes com o AWS RDS Performance Optimizer, já ativado por padrão no Console da AWS. (No GCP, o equivalente é o Cloud SQL Query Insights.)

Solução: adicionar os índices que faltavam.

Resultado: redução de 10x na latência das queries e possibilidade de diminuir o porte do banco em dois tiers.

Cenário 5: job Spark a 70% de CPU por quatro horas toda noite

O que parecia eficiente: o cluster mantinha utilização alta de CPU, sugerindo uma alocação adequada de recursos.

A realidade: 80% dos dados estavam concentrados em uma única chave enviesada, criando stragglers que prolongavam bastante o tempo de processamento.

A pista: o problema começou em um momento específico, sem outra causa aparente. (Depois descobrimos que isso coincidia com a chegada de novos dados contendo a "hot key".)

Investigação: código Spark roda em um ambiente altamente distribuído, o que dificulta usar um profiler comum como o que você usaria em aplicações. Esse é um bom motivo para manter a lógica concentrada em transformações simples, em vez de regras de negócio complexas. Mesmo assim, usar a Spark UI para analisar a distribuição de tarefas entre os stages permitiu identificar os stragglers. O monitoramento do BigTable revelou "hot keys" no banco que estava sendo processado.

Solução: reparticionamos e aplicamos "salting" na chave problemática para distribuir o workload de forma mais equilibrada.

Resultado: o tempo de conclusão do job caiu de 4 horas para 45 minutos, e o tamanho necessário do cluster foi reduzido em dois terços.

Alcançar a verdadeira eficiência na nuvem às vezes exige ir além das configurações e atacar as ineficiências também no nível do código. Quando o código é a causa raiz do excesso de custos, só ajustar a infraestrutura não resolve.

Ao aproximar seu time de desenvolvimento das equipes de FinOps e SRE, você consegue identificar e resolver essas ineficiências ocultas com uma abordagem sistemática:

  1. Comece pelas maiores áreas de despesa apontadas nos relatórios de custo.
  2. Resolva primeiro as vitórias rápidas no nível da configuração de nuvem.
  3. Nos Cloud Consoles, procure pistas reveladoras que indiquem a necessidade de uma investigação mais profunda.
  4. Use as ferramentas de profiling adequadas, de preferência na nuvem (ou offline, se necessário) para localizar as ineficiências.
  5. Corrija o código, faça o redeploy e valide a economia.

Essa abordagem colaborativa não só reduz custos, como muitas vezes também melhora a performance e a confiabilidade da aplicação — uma vitória tanto para o orçamento quanto para os usuários.

No time de Customer Reliability Engineering da DoiT, eu acompanho as organizações em toda a jornada de otimização. Unindo o DoiT Cloud Intelligence™ a décadas de experiência, ajudamos a identificar ganhos potenciais, mapear correções no nível da nuvem, expor ilusões de eficiência e validar o impacto das melhorias no código. Fale com a gente em doit.com/services