O mito do cold start: otimizando a JVM para eventos

SchemaBridge Team · 2026-01-20 · Serverless, Performance, JVM, Java

Por que a fama de cold start do Java vem do FaaS, não da JVM. Pools de threads, aquecimento do JIT e o modelo de workers de longa duração.

O imposto de latência da nuvem moderna

O "serverless" deveria nos salvar. Prometia escala infinita e zero gerenciamento. Mas, quando o assunto é latência percebida pelo usuário, funções serverless genéricas (FaaS) têm um segredo sujo: o cold start.

Quando uma requisição chega a uma Lambda que não roda há algum tempo, o provedor precisa subir um contêiner, inicializar o runtime e carregar o seu código. Em aplicações complexas, isso pode levar segundos. No mundo das aplicações web modernas, 2 segundos são uma eternidade. É a diferença entre uma experiência "ágil" e uma "quebrada".

Durante anos, a sabedoria do setor foi: "Java é pesado demais para serverless. Use Go ou Node.js."

Na SchemaBridge, questionamos essa sabedoria. Contamos com a robustez da JVM (Java Virtual Machine) e contornamos o problema do cold start não abandonando o Java, mas simplesmente não o executando como uma função de curta duração.

A JVM: um animal de carga, não um velocista

A JVM foi projetada para processos de servidor de longa duração, não para funções efêmeras. Ela passa os primeiros momentos "aquecendo" — carregando classes, interpretando bytecode e otimizando caminhos quentes (compilação JIT).

Se você trata uma aplicação Java como uma "função" que morre após 100 ms, está lutando contra a física do runtime. Você paga o custo de inicialização em cada requisição sem colher os benefícios da otimização JIT.

Na prática: contextos de aplicação de longa duração

A SchemaBridge não executa as etapas do seu workflow como funções isoladas e efêmeras. Em vez disso, as etapas rodam em workers JVM de longa duração implantados no Kubernetes.

1. Pool de threads: cada worker mantém um pool fixo de threads de trabalho aquecidas consumindo de Valkey Streams. Quando um evento chega, uma thread já existente o assume. Não há processo de sistema operacional a criar nem JVM a inicializar por evento.

2. Otimização JIT: como os workers são processos de longa duração, o compilador C2 da JVM tem tempo de otimizar os caminhos quentes para código de máquina nativo. Um worker fica mais rápido à medida que aquece — e, para ser justo na comparação, um worker recém-implantado começa frio e paga esse aquecimento uma vez, não a cada evento.

3. Pool de conexões: gerenciar conexões de banco de dados é caro. Em um modelo FaaS, as conexões só vivem enquanto dura um ambiente de execução aquecido; cada scale-up ou ambiente reciclado precisa estabelecê-las de novo. Nossos workers de longa duração mantêm conexões em pool com PostgreSQL e Valkey e reutilizam clientes HTTP para serviços externos, de modo que nenhuma etapa paga para estabelecê-las do zero.

O que um worker aquecido não elimina é o custo de ser um motor de workflows durável. Cada etapa atravessa um salto de stream, reivindica seu estado no PostgreSQL e busca sua entrada antes que a sua lógica rode. Esse é o preço de sobreviver a uma queda no meio de uma etapa, e coloca a latência de cada etapa na faixa das idas e voltas de rede e de banco de dados — não de microssegundos.

Onde o GraalVM se encaixa

O GraalVM já faz parte da plataforma, mas não como forma de iniciar o Java mais rápido: ele é o motor poliglota que executa os trechos de JavaScript e Python que os autores escrevem dentro dos workflows, em um sandbox que os impede de criar processos e de fazer E/S de arquivos ou de rede.

O GraalVM Native Image — compilar um serviço Java antecipadamente (ahead-of-time) em um binário independente — é um recurso diferente, e nós não o usamos. Segundo relatos amplamente difundidos, ele reduz substancialmente o tempo de inicialização e o uso de memória, ao custo de complexidade de build e de restrições à reflexão e ao carregamento dinâmico de classes. Para uma frota de workers de longa duração, o ganho na inicialização importa menos do que para funções de curta duração — e é exatamente por isso que escolhemos workers de longa duração.

Cenário ilustrativo: um pipeline sensível à latência

Considere uma equipe hipotética de analytics processando um fluxo constante de eventos com uma função Java em FaaS.

O problema

Cold starts periódicos geram picos de latência na casa dos segundos sempre que a plataforma escala ou recicla uma função ociosa, e a latência de cauda é dominada pelas inicializações, e não pelo código da própria equipe.

A abordagem com workers de longa duração

Mover esse processamento para workers aquecidos e de longa duração mudaria o formato da curva de latência:

1. Threads aquecidas: os eventos rodam em threads que já existem, em um processo aquecido pelo JIT.

2. Cauda mais estável: sem provisionamento de contêiner por evento, os picos causados pelas inicializações desaparecem. A latência restante é dominada pelo próprio trabalho e pelos saltos duráveis que cada etapa faz.

3. O trade-off: você paga por workers que estão rodando cheguem eventos ou não, e um orçamento rígido de tempo real, medido em milissegundos de um dígito, continua sendo a tarefa errada para um motor de workflows durável — isso pertence ao caminho de requisição da sua própria aplicação.

Comparação: FaaS efêmero vs. workers de longa duração

| Recurso | FaaS padrão (Lambda) | Workers aquecidos da SchemaBridge |

| :--- | :--- | :--- |

| Cold start | Inicialização de processo/runtime ao escalar | Sem inicialização de processo por evento |

| Runtime | Inicialização no scale-up / após ociosidade | Processo de longa duração |

| Otimização | Limitada (os ambientes são reciclados) | Compilação JIT completa |

| Conexões | Perdidas quando os ambientes são reciclados | Em pool e reutilizadas |

| Overhead por etapa | Invocação de função | Salto de stream + reivindicação de estado durável |

Checklist de especialista para desempenho em Java

1. Reutilize recursos: nunca crie um cliente de banco de dados dentro da sua função handler. Crie-o uma vez (estático/global) e reutilize-o.

2. Ajuste o heap: garanta que o heap da JVM esteja dimensionado corretamente para o seu contêiner, evitando coleta de lixo agressiva.

3. Ajuste o GC ao limite de CPU: um contêiner com limite fracionário de CPU pode não se beneficiar de um coletor paralelo; verifique qual GC a JVM realmente seleciona para os seus limites.

4. Evite reflexão: bibliotecas fortemente baseadas em reflexão (como alguns parsers JSON mais antigos) demoram a aquecer. Prefira geração em tempo de compilação sempre que possível.

5. Monitore as pausas do GC: use ferramentas para garantir que a coleta de lixo não esteja introduzindo picos de latência no seu workflow.

// Crie clientes caros uma vez por processo, não uma vez por evento.
private static final HttpClient CLIENT = HttpClient.newHttpClient();

Conclusão: o runtime importa

"Java significa cold starts lentos" é uma afirmação sobre como o Java é implantado, não sobre o Java. Ao entender a física subjacente do runtime — e escolher uma arquitetura que a respeite —, você pode manter a produtividade do ecossistema Java sem pagar uma inicialização a cada evento. Só seja honesto sobre o que sobra: um motor durável troca um pouco de latência pela garantia de que uma etapa sobrevive a uma queda.

Parte da nossa série Building the Bridge. Veja também: Compliance e auditoria: evidência como efeito colateral e Construindo conectores genéricos.

Explorar