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));import os
from pathlib import Path
def limite_cgroup(arquivo: str) -> str:
caminho = Path("/sys/fs/cgroup") / arquivo
return caminho.read_text().strip() if caminho.exists() else "indisponível"
print("CPUs do host: ", os.cpu_count())
print("CPUs do processo: ", os.process_cpu_count())
print("cota de CPU (cgroup): ", limite_cgroup("cpu.max"))
print("limite de memória: ", limite_cgroup("memory.max"))package main
import (
"fmt"
"math"
"runtime"
"runtime/debug"
)
func main() {
fmt.Println("CPUs do host: ", runtime.NumCPU())
fmt.Println("GOMAXPROCS: ", runtime.GOMAXPROCS(0))
limite := debug.SetMemoryLimit(-1) // -1 só consulta o valor atual
if limite == math.MaxInt64 {
fmt.Println("limite de memória do GC: nenhum (defina GOMEMLIMIT)")
} else {
fmt.Println("limite de memória do GC (MB):", limite/1024/1024)
}
}public class Recursos {
public static void main(String[] args) {
var runtime = Runtime.getRuntime();
System.out.println("processadores: " + runtime.availableProcessors());
System.out.println("heap máximo (MB): " + runtime.maxMemory() / 1024 / 1024);
}
}var memoria = GC.GetGCMemoryInfo();
Console.WriteLine($"processadores: {Environment.ProcessorCount}");
Console.WriteLine($"memória disponível (MB): {memoria.TotalAvailableMemoryBytes / 1024 / 1024}");
Console.WriteLine($"GC de servidor: {System.Runtime.GCSettings.IsServerGC}");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 usa | Memória que o runtime usa | Observação | |
|---|---|---|---|
| Node 24 | availableParallelism: 1 | Heap do V8 limitado a 259 MB | Reconhece o limite do container; o heap fica em cerca de metade da memória |
| Python 3.13 | cpu_count e process_cpu_count: 8 | Sem limite próprio | Não enxerga a cota de CPU; só lendo o cgroup (cpu.max: 100000 100000, ou seja, 1 CPU) |
| Go 1.25 | GOMAXPROCS: 2 | Sem limite para o coletor de lixo | Considera o limite, mas com mínimo de 2; com go.mod em 1.24, usava 8 |
| Java 21 | availableProcessors: 1 | Heap máximo de 123 MB | Reconhece o container, mas por padrão usa só 25% da memória para o heap |
| C# .NET 9 | ProcessorCount: 1 | 384 MB disponíveis para o coletor | Reconhece 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. SemGOMEMLIMIT, 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
| Linguagem | Configuração recomendada | Para quê |
|---|---|---|
| Node | --max-old-space-size só se precisar mudar o padrão; UV_THREADPOOL_SIZE se houver muito trabalho de arquivo, DNS ou criptografia | O padrão já respeita o container; o pool de threads interno tem 4 threads |
| Python | Número de processos explícito (--workers no Uvicorn ou Gunicorn), igual ao número de CPUs do limite | Não confiar em os.cpu_count() |
| Go | GOMAXPROCS igual ao limite de CPU (ou a biblioteca automaxprocs), e GOMEMLIMIT em cerca de 80 a 90% do limite de memória | Evitar 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ória | O 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:
| Armadilha | Como evitar |
|---|---|
| O runtime não respeita o limite de CPU do container | Confira o que ele enxerga (os programas acima) e ajuste antes de medir |
| Sem aquecimento em runtimes com JIT | Rode a carga por pelo menos 30 segundos antes de medir, e meça por tempo suficiente |
| Servidor de exemplo em vez do de produção | Use o mesmo servidor e framework de produção |
| Rota que não faz nada | Inclua o que a rota real faz: consulta ao banco, serialização, validação |
| Gerador de carga na mesma máquina, disputando CPU | Separe o gerador de carga, ou limite os dois |
| Olhar só a média | Olhe 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 apiLista 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]