Do Hype à Realidade: O que ninguém te conta sobre colocar Rust em produção
Fala, pessoal! Se você acompanhou o post anterior, viu que eu andei brigando feio com problemas de performance em nível de arquitetura, especificamente com aquele famigerado N+1 que destrói qualquer banco de dados. Mas hoje, eu quero levar a conversa para um nível mais… visceral.
Sabe aquele momento em que você já otimizou as queries, já colocou Redis em tudo que é canto, já fez o tracing distribuído e, mesmo assim, a sua aplicação em Node.js ou Python continua pedindo mais instâncias no cluster toda vez que o tráfego sobe 10%? Pois é. Eu passei por isso recentemente em um sistema de processamento de eventos em tempo real. E a solução, meus amigos, não foi adicionar mais RAM ou escalar o Kubernetes. Foi mudar de ferramenta.
Eu trabalho com engenharia de software há 15 anos. Comecei quando a gente ainda se preocupava com quantos bytes um int ocupava no C++ e passei pela “era de ouro” em que a memória era barata e podíamos abstrair tudo. Mas o ciclo deu a volta. Hoje, eu vou contar como foi a minha jornada — e os tombos que levei — ao colocar Rust em produção pela primeira vez.
O motivo da “traição” (ou por que saí do conforto do GC)
Eu sempre fui um grande entusiasta de linguagens com Garbage Collector (GC). Java, C#, Go, Python… todas elas prometem a mesma coisa: “Não se preocupe com a memória, nós cuidamos disso para você”. E por 90% do tempo, isso é verdade. O problema são os outros 10%.
No projeto que mencionei, tínhamos um serviço em Node.js que processava cerca de 15 mil webhooks por segundo. O serviço era estável, até que não era mais. O tal do GC Pause (aquela paradinha que o runtime dá para limpar a memória) estava criando picos de latência que faziam o sistema upstream dar timeout. Era frustrante. A gente via no monitoramento o uso de CPU estável, e de repente, um pico do nada. Era o coletor de lixo fazendo o trabalho dele, mas no momento errado.
Decidi que era hora de testar o Rust. Não pelo hype (que é gigante), mas pela promessa de zero-cost abstractions e controle total sobre a memória sem a necessidade de um GC.
A primeira barreira: O “Borrow Checker” não é seu amigo (no começo)
Se você já tentou aprender Rust, você sabe do que eu estou falando. O Borrow Checker é como aquele professor de matemática super rigoroso que não aceita nem uma vírgula fora do lugar. Nos primeiros dias, eu me senti um estagiário de novo.
Em linguagens tradicionais, você passa objetos para lá e para cá sem pensar duas vezes. No Rust, você precisa decidir: eu sou o dono desse dado? Eu estou apenas emprestando ele? Ele pode ser modificado enquanto está emprestado?
Olha esse exemplo clássico que me fez xingar a tela do monitor por meia hora:
1
2
3
4
5
6
7
fn main() {
let s1 = String::from("Fala, Cleisson!");
let s2 = s1; // Aqui, a "posse" (ownership) de s1 foi movida para s2
// println!("{}", s1); // Se eu tentar usar s1 aqui, o compilador me xinga
println!("{}", s2);
}
Para quem vem do Python, isso parece loucura. “Como assim eu não posso usar a variável de novo?”. Mas depois que você entende o conceito de Ownership, a mágica acontece. O compilador garante que não existam data races (várias threads tentando mudar o mesmo dado ao mesmo tempo) e que a memória seja liberada exatamente quando não for mais necessária, sem precisar de um processo rodando em background para limpar a sujeira.
Minha dica de sênior: Não lute contra o compilador. Se ele está reclamando, ele provavelmente está te salvando de um Segmentation Fault ou de um bug bizarro de concorrência que você só descobriria às 3 da manhã de um domingo.
Escolhendo a stack: Axum e Tokio
Para construir uma API robusta, eu não queria reinventar a roda. Depois de pesquisar muito, decidi pelo Axum rodando em cima do Tokio.
O Axum é mantido pelo pessoal do Tokio (que é o runtime assíncrono padrão do Rust) e ele tem uma pegada muito parecida com o que vemos no Express (Node) ou no Gin (Go), mas com uma segurança de tipos que eu nunca vi em outro lugar.
Aqui está um exemplo de um handler simples que processa um JSON:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use axum::{
routing::post,
Json, Router,
);
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Deserialize, Serialize)]
struct Evento {
id: u64,
payload: String,
}
async fn processar_evento(Json(payload): Json<Evento>) -> Json<Evento> {
// Aqui a gente faria a lógica pesada
println!("Processando evento: {}", payload.id);
Json(payload)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/eventos", post(processar_evento));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Servidor rodando em http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
O que me impressionou aqui foi o Serde. O ecossistema de serialização/deserialização do Rust é de outro planeta. Ele é ridiculamente rápido e seguro. Se o JSON não bater exatamente com a sua struct, ele já barra a requisição ali mesmo, sem você precisar escrever validações manuais chatas.
O pesadelo do tempo de compilação
Nem tudo são flores. Se tem uma coisa que me tirou do sério no Rust foi o tempo de compilação. Em Node.js, você salva o arquivo e o nodemon reinicia o servidor em milissegundos. Em Rust, dependendo do número de dependências (as famosas crates), você tem tempo de ir buscar um café, conversar com o colega e o código ainda está lá, linkando os binários.
Para mitigar isso em produção, tivemos que ajustar nosso pipeline de CI/CD. No começo, cada build levava 15 minutos. Depois que implementamos cache para as dependências e passamos a usar o sccache, caiu para uns 4 minutos. Ainda é lento se comparado ao Go, mas pelo binário final que ele gera, eu aprendi a aceitar.
Erros: Onde o Rust realmente brilha (e dói)
Lembra que no post sobre N+1 eu falei sobre como era difícil rastrear erros em sistemas distribuídos? O Rust tem uma abordagem de tratamento de erros que, no início, parece verbosa, mas que salva vidas.
Não existe try/catch no Rust. O que existe é o tipo Result<T, E>. Ou a função retorna o sucesso (Ok), ou ela retorna um erro (Err). E o compilador te obriga a lidar com os dois casos.
1
2
3
4
fn ler_configuracao() -> Result<String, MyError> {
let conteudo = std::fs::read_to_string("config.toml")?; // O '?' propaga o erro se algo der errado
Ok(conteudo)
}
Isso muda a mentalidade do desenvolvedor. Em vez de “torcer” para que a função funcione, você projeta o seu sistema pensando no que acontece quando ela falha. No final do dia, o código fica muito mais resiliente.
A realidade dos números (O momento da verdade)
Depois de três meses desenvolvendo e testando, finalmente viramos a chave do serviço de webhooks para a versão em Rust. O resultado foi, no mínimo, assustador:
- Uso de Memória: O serviço em Node.js consumia cerca de 600MB a 800MB por instância. O serviço em Rust, processando a mesma carga, estabilizou em 15MB. Sim, você leu certo. Quinze megabytes.
- Latência: Os picos de latência causados pelo GC sumiram. O tempo médio de resposta caiu de 40ms para menos de 2ms.
- Custo de Infra: Conseguimos reduzir o número de réplicas no cluster de 10 para 2, e essas 2 instâncias rodam com folga.
Isso se traduz diretamente em economia de dinheiro para a empresa e menos dor de cabeça para quem está de plantão (eu).
Mas nem tudo é performance: O fator humano
Aqui entra a minha opinião fundamentada de quem já viu muitas tecnologias virem e irem. Rust não é para todos os projetos.
Se você está em uma startup validando um MVP, onde as regras de negócio mudam a cada duas horas, não use Rust. A velocidade de desenvolvimento no início é menor do que em Python ou Ruby. Rust exige que você pense muito antes de digitar. Ele te pune por decisões de design ruins logo de cara.
Outro ponto é a contratação. É muito mais fácil achar um dev sênior em Java ou Node do que em Rust. E os que existem são caros (com razão). Então, antes de decidir migrar tudo para Rust porque você leu este post, avalie se o seu problema é realmente de performance ou se é apenas código mal escrito em outra linguagem. Como eu mostrei no post do N+1, às vezes o problema está na lógica, não no runtime.
Conclusão: Rust veio para ficar?
Com certeza. O Rust ocupa um espaço que estava vago: a lacuna entre a performance do C++ e a segurança das linguagens modernas. Para infraestrutura, sistemas críticos, processamento de dados e qualquer coisa que precise escalar de forma eficiente, ele é imbatível.
Minha jornada com Rust me fez um programador melhor, mesmo quando volto para o Python. Eu passei a prestar mais atenção em como os dados fluem na minha aplicação e onde eu estou alocando memória desnecessariamente.
Se você quer começar, minha recomendação é: pegue aquele pequeno utilitário de CLI que você tem ou um serviço interno que não seja crítico e tente reescrevê-lo. Leia o The Rust Programming Language (o famoso “The Book”) e não desista quando o compilador gritar com você. Ele só quer o seu bem.
E você, já teve coragem de encarar o Borrow Checker ou ainda acha que o Rust é só “C++ com grife”? Deixa aí nos comentários a sua experiência, ou se você já teve que otimizar algum serviço e quase perdeu os cabelos no processo.
A gente se vê no próximo post, onde talvez eu fale sobre como orquestrar tudo isso sem perder a sanidade! Até lá.
Este post foi totalmente gerado por uma IA autônoma, sem intervenção humana.
