DevGuideTK
← Trilha Backend

Gerenciamento de memória

Pilha, heap e coletor de lixo, e de onde vêm os vazamentos de memória em linguagens gerenciadas.

O vazamento mais comum em backend é o cache sem limite. Os exemplos mostram um cache em memória que guarda no máximo um número fixo de itens. Em Node, Python, Go e Java, ele descarta o item usado há mais tempo (LRU). Em C#, o exemplo usa o MemoryCache da plataforma, que limita o total de outra forma, descrita abaixo.

Conferimos o comportamento dos cinco com um limite de dois itens: guardar A e B, ler A e guardar C. Nos quatro primeiros, B foi descartado e A ficou, como se espera de um LRU.

Um cache em memória com limite

// O Map do JavaScript mantém a ordem de inserção: o primeiro item é o mais antigo
export class CacheLimitado<K, V> {
  private readonly itens = new Map<K, V>();
  private readonly limite: number;

  constructor(limite: number) {
    this.limite = limite;
  }

  obter(chave: K): V | undefined {
    const valor = this.itens.get(chave);
    if (valor !== undefined) {
      this.itens.delete(chave); // move para o fim: usado agora
      this.itens.set(chave, valor);
    }
    return valor;
  }

  guardar(chave: K, valor: V): void {
    this.itens.delete(chave);
    this.itens.set(chave, valor);
    if (this.itens.size > this.limite) {
      const maisAntigo = this.itens.keys().next().value as K;
      this.itens.delete(maisAntigo);
    }
  }
}

const perfis = new CacheLimitado<number, Perfil>(10_000);

O que muda de uma linguagem para outra

Estrutura usadaSeguro entre threadsAlternativa pronta
NodeMap, que guarda a ordem de inserçãoNão precisa: uma threadlru-cache
PythonOrderedDict; lru_cache para funçõesNão, sem trava própriacachetools.LRUCache, TTLCache
Gocontainer/list com um mapaSim, com sync.Mutexhashicorp/golang-lru
JavaLinkedHashMap com ordem de acessoSim, com synchronizedMapCaffeine
C#MemoryCache com SizeLimitSimO HybridCache, ou um LRU próprio com LinkedList e Dictionary

Três detalhes que costumam passar despercebidos:

  • lru_cache sem maxsize, ou com maxsize=None, é um cache sem limite. A anotação parece segura e não é. O mesmo vale para functools.cache, que nunca descarta nada.
  • MemoryCache sem SizeLimit não limita nada. Com SizeLimit definido, todo item precisa informar um Size: no nosso teste, um Set sem Size lançou InvalidOperationException.
  • MemoryCache cheio recusa o item novo. No mesmo teste, com o limite de dois itens ocupado por A e B, o C não foi guardado, e A e B continuaram. A memória fica limitada, que é o que evita o vazamento, mas os itens novos e mais procurados podem ficar de fora até a compactação, que remove itens vencidos e de menor prioridade em segundo plano. Use expiração (SlidingExpiration ou AbsoluteExpirationRelativeToNow) junto com o limite, para que o cache se renove.

Limitar pela quantidade de itens só funciona se os itens têm tamanho parecido. Se alguns forem muito maiores que outros, limite pelo tamanho estimado em bytes, como o MemoryCache e o lru-cache permitem.

Configurar o runtime: os testes deste tópico

Os resultados do nível Conceito vieram destes comandos, num container de 512 MB:

# Go: 300 MB em uso e 3 GB de lixo temporário
docker run --memory=512m --memory-swap=512m minha-imagem-go
# → morto pelo sistema operacional (código 137, OOMKilled)

docker run --memory=512m --memory-swap=512m -e GOMEMLIMIT=450MiB minha-imagem-go
# → terminou normalmente, com 30 coletas

# Java: 250 MB em uso
docker run --memory=512m eclipse-temurin:21-jdk java Memoria.java
# → java.lang.OutOfMemoryError: Java heap space (heap padrão de 123 MB)

docker run --memory=512m eclipse-temurin:21-jdk java -XX:MaxRAMPercentage=75 Memoria.java
# → funcionou, com heap máximo de 371 MB

--memory-swap igual a --memory desliga o uso de disco como memória extra, que esconderia o problema atrás de uma lentidão enorme. Em produção, os orquestradores costumam se comportar da mesma forma.

Para conferir se um container foi morto por falta de memória:

docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' nome-do-container
# 137 true   → morto pelo sistema operacional por exceder o limite

kubectl describe pod nome-do-pod | grep -A2 "Last State"
# Reason: OOMKilled

Encontrar o que está ocupando a memória

Um exemplo com o tracemalloc, da biblioteca padrão do Python, comparando duas fotografias da memória:

import tracemalloc

tracemalloc.start()
antes = tracemalloc.take_snapshot()

processar_lote_de_pedidos()  # o trecho suspeito

depois = tracemalloc.take_snapshot()
for diferenca in depois.compare_to(antes, "lineno")[:5]:
    print(diferenca)
# app/cache.py:42: size=48.2 MiB (+48.2 MiB), count=50012 (+50012)

A saída aponta o arquivo e a linha onde a memória que cresceu foi reservada. Em Go, o equivalente é o pprof:

# Com import _ "net/http/pprof" e um servidor de depuração na porta 6060
go tool pprof -top http://localhost:6060/debug/pprof/heap

E no Node, uma captura do heap enviada por sinal, sem mudar o código:

node --heapsnapshot-signal=SIGUSR2 servidor.js
kill -USR2 <pid>   # grava um arquivo .heapsnapshot no diretório atual

O arquivo abre na aba Memory do Chrome DevTools. Duas capturas comparadas pela visão Comparison mostram quais tipos de objeto cresceram entre elas.

Lista de revisão

Código

  • Todo cache em memória tem limite de tamanho ou de itens, e de preferência expiração?
  • lru_cache tem maxsize definido? MemoryCache tem SizeLimit?
  • Nenhuma coleção global só cresce?
  • Ouvintes, temporizadores e tarefas têm cancelamento?
  • Arquivos e resultados grandes são processados em partes?

Configuração

  • O runtime conhece o limite do container: GOMEMLIMIT, MaxRAMPercentage, max-old-space-size?
  • Há folga de 20 a 40% para memória fora do heap?

Operação

  • O heap depois das coletas é monitorado ao longo do tempo?
  • Reinícios por falta de memória geram alerta?
  • A equipe sabe capturar e comparar o heap da linguagem usada?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Reproduzir e consertar um vazamento

    Quero reproduzir um vazamento de memória, encontrá-lo com as ferramentas da minha linguagem e consertá-lo.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas:
    1. Escreva comigo um servidor com um cache global sem limite, que guarda dados de cada usuário novo.
    2. Rode num container com limite de memória e gere carga até ele morrer. Observe se foi o runtime ou o sistema operacional.
    3. Capture o heap duas vezes durante a carga e compare as capturas com a ferramenta da linguagem.
    4. Encontre na comparação o objeto que cresce e o que o mantém alcançável.
    5. Troque por um cache com limite e confirme que a memória se estabiliza.
    
    Em cada etapa, me diga o que observar.
  • Ver o efeito do limite de memória no coletor

    Quero ver na prática como o limite de memória do container afeta o coletor de lixo da minha linguagem.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas:
    1. Escreva um programa que mantém uma quantidade fixa de dados em uso e cria muitos objetos temporários.
    2. Rode num container com limite de memória e swap desligado, com a configuração padrão do runtime.
    3. Repita com a configuração recomendada (GOMEMLIMIT, MaxRAMPercentage, max-old-space-size ou equivalente).
    4. Compare o resultado, o número de coletas e o tempo total.
    5. Explique qual valor eu deveria usar em produção e por quê.
  • Revisar código e configuração de memória

    Vou colar abaixo código e configuração de implantação de um serviço. Revise com foco em memória.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Há caches em memória sem limite de tamanho ou expiração?
    2. Há coleções globais que só crescem?
    3. Há ouvintes, temporizadores ou tarefas sem cancelamento?
    4. Há leitura de arquivos ou resultados grandes inteiros na memória?
    5. O runtime está configurado para o limite de memória do container, com folga?
    6. O limite de memória do container existe e é coerente com a carga?
    
    Para cada problema, descreva como ele se manifesta em produção e mostre a correção. Não reescreva o que já está correto.
    
    Código e configuração:
    [cole aqui]