DevGuideTK
← Trilha Backend

Escolha da linguagem

O que muda de verdade entre Node, Python, Go, Java e C# em modelo de execução, ecossistema e operação.

A diferença mais prática entre as cinco linguagens em produção não está na sintaxe: está em como cada runtime percebe os limites de CPU e memória do container. Um runtime que acha que tem mais CPU ou memória do que tem é freado pelo sistema operacional, ou encerrado por falta de memória.

O que o runtime enxerga dentro do container

Os programas abaixo mostram, em cada linguagem, quantas CPUs e quanta memória o runtime acredita ter.

import os from "node:os";
import v8 from "node:v8";

const mb = (bytes: number) => Math.round(bytes / 1024 / 1024);

console.log("CPUs do host:          ", os.cpus().length);
console.log("paralelismo disponível:", os.availableParallelism());
console.log("limite de memória (MB):", mb(process.constrainedMemory()));
console.log("teto do heap V8 (MB):  ", mb(v8.getHeapStatistics().heap_size_limit));

O resultado, num container com 1 CPU e 512 MB

Rodamos os cinco programas com docker run --cpus=1 --memory=512m, numa máquina com 8 CPUs:

CPUs que o runtime usaMemória que o runtime usaObservação
Node 24availableParallelism: 1Heap do V8 limitado a 259 MBReconhece o limite do container; o heap fica em cerca de metade da memória
Python 3.13cpu_count e process_cpu_count: 8Sem limite próprioNão enxerga a cota de CPU; só lendo o cgroup (cpu.max: 100000 100000, ou seja, 1 CPU)
Go 1.25GOMAXPROCS: 2Sem limite para o coletor de lixoConsidera o limite, mas com mínimo de 2; com go.mod em 1.24, usava 8
Java 21availableProcessors: 1Heap máximo de 123 MBReconhece o container, mas por padrão usa só 25% da memória para o heap
C# .NET 9ProcessorCount: 1384 MB disponíveis para o coletorReconhece o container e usa 75% da memória por padrão

Cada linha tem uma consequência prática:

  • Python: frameworks que calculam o número de processos ou threads a partir de os.cpu_count() criam 8 numa máquina de 1 CPU. Defina o número de processos explicitamente.
  • Go: com 2 threads numa cota de 1 CPU, o processo é freado. No nosso teste de carga, um servidor Go atendeu 7.000 pedidos por segundo com p99 de 84 ms sem ajuste, e 55.000 com p99 de 2 ms com GOMAXPROCS=1. Sem GOMEMLIMIT, o coletor de lixo não sabe que existe um limite, e o processo pode ser encerrado por falta de memória antes de coletar.
  • Java: com 512 MB no container, o heap fica em 123 MB, e a aplicação pode falhar por falta de memória com três quartos do container livres. Com -XX:MaxRAMPercentage=75, o heap subiu para 371 MB no mesmo teste.

Configurar cada runtime para o container

LinguagemConfiguração recomendadaPara quê
Node--max-old-space-size só se precisar mudar o padrão; UV_THREADPOOL_SIZE se houver muito trabalho de arquivo, DNS ou criptografiaO padrão já respeita o container; o pool de threads interno tem 4 threads
PythonNúmero de processos explícito (--workers no Uvicorn ou Gunicorn), igual ao número de CPUs do limiteNão confiar em os.cpu_count()
GoGOMAXPROCS igual ao limite de CPU (ou a biblioteca automaxprocs), e GOMEMLIMIT em cerca de 80 a 90% do limite de memóriaEvitar freio de CPU e encerramento por falta de memória
Java-XX:MaxRAMPercentage=75 (ou outro valor medido)Usar a memória que o container oferece
C#Geralmente nada; DOTNET_GCHeapHardLimitPercent para mudar a fração de memóriaO padrão já respeita o container

No Kubernetes, o limite de CPU pode ser fracionado, como 500m (meia CPU). Runtimes que arredondam para cima, como o Go com mínimo de 2, ficam ainda mais distantes do limite real. Uma alternativa comum é definir só a requisição de CPU, sem limite rígido, para evitar o freio, e controlar o uso pela escala automática.

Medir com honestidade

Se a escolha da linguagem ou do framework depender de desempenho, meça com a sua carga, evitando as armadilhas que distorceram o nosso próprio teste:

ArmadilhaComo evitar
O runtime não respeita o limite de CPU do containerConfira o que ele enxerga (os programas acima) e ajuste antes de medir
Sem aquecimento em runtimes com JITRode a carga por pelo menos 30 segundos antes de medir, e meça por tempo suficiente
Servidor de exemplo em vez do de produçãoUse o mesmo servidor e framework de produção
Rota que não faz nadaInclua o que a rota real faz: consulta ao banco, serialização, validação
Gerador de carga na mesma máquina, disputando CPUSepare o gerador de carga, ou limite os dois
Olhar só a médiaOlhe os percentis altos (p99) e os erros

Uma configuração mínima para comparar, com os números apresentados neste tópico:

# O servidor com 1 CPU e 512 MB, numa rede compartilhada com o gerador de carga
docker network create teste
docker run -d --name api --network teste --cpus=1 --memory=512m minha-api

# Aquecimento e depois a medição, com percentis
docker run --rm --network teste alpine sh -c \
  "apk add -q wrk && wrk -t2 -c50 -d30s http://api:3000/ >/dev/null && \
   wrk -t2 -c50 -d15s --latency http://api:3000/"

# Memória durante a carga
docker stats --no-stream api

Lista de revisão

Escolha

  • A escolha considerou a equipe, as bibliotecas e o custo de operação, e não só desempenho?
  • O modelo de concorrência da linguagem está claro para a equipe, com as armadilhas dele?
  • Se há uma segunda linguagem, existe um motivo claro e um nicho definido?

Container

  • O runtime enxerga o número certo de CPUs, ou foi configurado para isso?
  • O limite de memória do runtime (heap, GOMEMLIMIT) está ajustado ao limite do container?
  • O número de processos ou threads de trabalho foi definido explicitamente onde o runtime não detecta o limite?

Medição

  • Comparações de desempenho usaram a carga real, com aquecimento e percentis altos?
  • O servidor e o framework medidos são os mesmos de produção?

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.

  • Medir os cinco runtimes num container

    Quero comparar na prática Node, Python, Go, Java e C# num container, medindo imagem, partida, memória e pedidos por segundo.
    
    Vamos em etapas:
    1. Escreva comigo o mesmo servidor mínimo nas cinco linguagens, com o servidor HTTP que cada uma usa em produção.
    2. Monte um Dockerfile em estágios para cada uma e compare o tamanho das imagens.
    3. Meça o tempo até o primeiro pedido e a memória parada, com cada container limitado a 1 CPU e 512 MB.
    4. Meça pedidos por segundo e percentis com wrk, com aquecimento.
    5. Confira o que cada runtime enxerga de CPU e memória, e ajuste os que estiverem errados.
    6. Acrescente uma consulta a um PostgreSQL em cada rota e meça de novo.
    
    Em cada etapa, peça que eu preveja os resultados, e explique as diferenças entre a previsão e a medição.
  • Configurar meu runtime para containers

    Quero configurar corretamente o runtime da minha aplicação para rodar em containers.
    
    Pergunte qual linguagem e versão eu uso, onde a aplicação roda e quais são os limites de CPU e memória dos containers.
    
    Explique, para o meu caso:
    1. O que o runtime enxerga por padrão dentro do container, e como conferir.
    2. Quais configurações ajustar para CPU e memória, com os valores recomendados.
    3. Como definir o número de processos, threads ou trabalhadores.
    4. Como isso interage com limites de CPU fracionados no Kubernetes.
    5. Como confirmar, depois do ajuste, que o processo não está sendo freado nem chegando perto do limite de memória.
    
    Termine com as linhas exatas de configuração para o meu Dockerfile ou manifesto de implantação.
  • Encontrar código que briga com o modelo da linguagem

    Vou colar abaixo código de um serviço. Revise procurando trechos que brigam com o modelo de execução e concorrência da linguagem.
    
    Pergunte qual linguagem, versão e framework são usados, se não estiver claro.
    
    Procure, conforme a linguagem:
    - Node: cálculos pesados ou operações síncronas de arquivo e criptografia no caminho dos pedidos.
    - Python: chamadas síncronas dentro de funções async, e trabalho de CPU que precisaria de outro processo.
    - Go: goroutines que podem nunca terminar, e canais sem quem leia.
    - Java: blocos synchronized longos, bibliotecas que bloqueiam threads de plataforma, threads criadas sem limite.
    - C#: .Result, .Wait() e async void.
    
    Para cada problema, explique o efeito sob carga e mostre a correção. Não reescreva o que já está correto.
    
    Código:
    [cole aqui]