Rust para Microsserviços: Guia Prático | Rust Brasil

Guia de microsserviços em Rust: gRPC com Tonic, message queues, service mesh e observabilidade distribuída.

Introdução

Microsserviços em produção exigem uma combinação rara de qualidades: alta performance para lidar com milhares de requisições por segundo, baixo consumo de memória para reduzir custos de infraestrutura, latência previsível para cumprir SLAs, e confiabilidade para operar 24/7 sem memory leaks ou crashes inesperados. Rust oferece todas essas qualidades de forma nativa, tornando-se uma escolha cada vez mais popular para serviços que precisam ser robustos em produção.

Enquanto Go dominou a primeira onda de microsserviços cloud-native, Rust está se estabelecendo em cenários onde Go não é suficiente: serviços com requisitos de latência p99 agressivos, orçamento de memória limitado (serverless, edge computing), processamento de dados em tempo real e componentes de infraestrutura crítica. Neste artigo, vamos construir um microsserviço completo com gRPC, health checks, graceful shutdown e deploy otimizado.

Ecossistema para Microsserviços

Comunicação

BibliotecaProtocoloDescrição
TonicgRPCImplementação gRPC completa e de alta performance
AxumHTTP/RESTFramework web com middleware Tower
reqwestHTTP ClientCliente HTTP assíncrono
lapinAMQPCliente RabbitMQ
rdkafkaKafkaCliente Apache Kafka (bindings librdkafka)
redisRedisCliente Redis com suporte a pub/sub e streams

Observabilidade

BibliotecaFunção
tracingLogging estruturado e distributed tracing
metricsMétricas (counters, gauges, histograms)
opentelemetryOpenTelemetry (traces, metrics, logs)
tower-httpMiddleware para HTTP (logging, compression, CORS)

Resiliência

BibliotecaFunção
towerMiddleware composicional (retry, timeout, rate limit)
backoffRetry com backoff exponencial
circuit-breakerPadrão circuit breaker

Exemplo Prático: Microsserviço de Pedidos com gRPC

Vamos construir um serviço de gerenciamento de pedidos que se comunica via gRPC, inclui health checks, métricas e graceful shutdown.

Definição do Protocolo (proto/pedidos.proto)

syntax = "proto3";

package pedidos;

service ServicoPedidos {
    // Criar um novo pedido
    rpc CriarPedido(NovoPedidoRequest) returns (PedidoResponse);

    // Buscar pedido por ID
    rpc BuscarPedido(BuscarPedidoRequest) returns (PedidoResponse);

    // Listar pedidos de um cliente
    rpc ListarPedidos(ListarPedidosRequest) returns (ListarPedidosResponse);

    // Atualizar status do pedido
    rpc AtualizarStatus(AtualizarStatusRequest) returns (PedidoResponse);

    // Stream de atualizações de status
    rpc ObservarPedido(BuscarPedidoRequest) returns (stream StatusUpdate);
}

message NovoPedidoRequest {
    string cliente_id = 1;
    repeated ItemPedido itens = 2;
    string endereco_entrega = 3;
}

message ItemPedido {
    string produto_id = 1;
    string nome = 2;
    uint32 quantidade = 3;
    double preco_unitario = 4;
}

message BuscarPedidoRequest {
    string pedido_id = 1;
}

message ListarPedidosRequest {
    string cliente_id = 1;
    uint32 limite = 2;
    uint32 offset = 3;
}

message PedidoResponse {
    string id = 1;
    string cliente_id = 2;
    repeated ItemPedido itens = 3;
    double total = 4;
    string status = 5;
    string endereco_entrega = 6;
    string criado_em = 7;
    string atualizado_em = 8;
}

message ListarPedidosResponse {
    repeated PedidoResponse pedidos = 1;
    uint32 total = 2;
}

message AtualizarStatusRequest {
    string pedido_id = 1;
    string novo_status = 2;
}

message StatusUpdate {
    string pedido_id = 1;
    string status_anterior = 2;
    string status_novo = 3;
    string timestamp = 4;
}

Cargo.toml

[package]
name = "servico-pedidos"
version = "0.1.0"
edition = "2021"

[dependencies]
tonic = "0.12"
prost = "0.13"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
chrono = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
anyhow = "1"
dashmap = "6"

[build-dependencies]
tonic-build = "0.12"

Build Script (build.rs)

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/pedidos.proto")?;
    Ok(())
}

Implementação do Serviço

use chrono::Utc;
use dashmap::DashMap;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, Stream, StreamExt};
use tonic::{Request, Response, Status};
use uuid::Uuid;

// Módulo gerado pelo tonic-build
pub mod pedidos {
    tonic::include_proto!("pedidos");
}

use pedidos::servico_pedidos_server::ServicoPedidos;
use pedidos::*;

// === Modelo Interno ===

#[derive(Debug, Clone)]
struct Pedido {
    id: String,
    cliente_id: String,
    itens: Vec<ItemPedido>,
    total: f64,
    status: String,
    endereco_entrega: String,
    criado_em: String,
    atualizado_em: String,
}

impl From<&Pedido> for PedidoResponse {
    fn from(p: &Pedido) -> Self {
        PedidoResponse {
            id: p.id.clone(),
            cliente_id: p.cliente_id.clone(),
            itens: p.itens.clone(),
            total: p.total,
            status: p.status.clone(),
            endereco_entrega: p.endereco_entrega.clone(),
            criado_em: p.criado_em.clone(),
            atualizado_em: p.atualizado_em.clone(),
        }
    }
}

// === Implementação do Serviço ===

pub struct ServicoPedidosImpl {
    pedidos: Arc<DashMap<String, Pedido>>,
    notificacoes: broadcast::Sender<StatusUpdate>,
}

impl ServicoPedidosImpl {
    pub fn new() -> Self {
        let (tx, _) = broadcast::channel(100);
        Self {
            pedidos: Arc::new(DashMap::new()),
            notificacoes: tx,
        }
    }
}

#[tonic::async_trait]
impl ServicoPedidos for ServicoPedidosImpl {
    async fn criar_pedido(
        &self,
        request: Request<NovoPedidoRequest>,
    ) -> Result<Response<PedidoResponse>, Status> {
        let req = request.into_inner();

        // Validações
        if req.itens.is_empty() {
            return Err(Status::invalid_argument(
                "Pedido deve ter pelo menos um item",
            ));
        }
        if req.cliente_id.is_empty() {
            return Err(Status::invalid_argument(
                "ID do cliente é obrigatório",
            ));
        }

        let total: f64 = req
            .itens
            .iter()
            .map(|i| i.preco_unitario * i.quantidade as f64)
            .sum();

        let agora = Utc::now().to_rfc3339();
        let pedido = Pedido {
            id: Uuid::new_v4().to_string(),
            cliente_id: req.cliente_id,
            itens: req.itens,
            total,
            status: "criado".to_string(),
            endereco_entrega: req.endereco_entrega,
            criado_em: agora.clone(),
            atualizado_em: agora,
        };

        tracing::info!(
            pedido_id = %pedido.id,
            cliente_id = %pedido.cliente_id,
            total = %pedido.total,
            "Novo pedido criado"
        );

        let resposta = PedidoResponse::from(&pedido);
        self.pedidos.insert(pedido.id.clone(), pedido);

        Ok(Response::new(resposta))
    }

    async fn buscar_pedido(
        &self,
        request: Request<BuscarPedidoRequest>,
    ) -> Result<Response<PedidoResponse>, Status> {
        let pedido_id = &request.into_inner().pedido_id;

        let pedido = self
            .pedidos
            .get(pedido_id)
            .ok_or_else(|| Status::not_found("Pedido não encontrado"))?;

        Ok(Response::new(PedidoResponse::from(pedido.value())))
    }

    async fn listar_pedidos(
        &self,
        request: Request<ListarPedidosRequest>,
    ) -> Result<Response<ListarPedidosResponse>, Status> {
        let req = request.into_inner();
        let limite = if req.limite == 0 { 20 } else { req.limite } as usize;
        let offset = req.offset as usize;

        let todos: Vec<PedidoResponse> = self
            .pedidos
            .iter()
            .filter(|entry| entry.value().cliente_id == req.cliente_id)
            .map(|entry| PedidoResponse::from(entry.value()))
            .collect();

        let total = todos.len() as u32;
        let pedidos: Vec<PedidoResponse> = todos
            .into_iter()
            .skip(offset)
            .take(limite)
            .collect();

        Ok(Response::new(ListarPedidosResponse { pedidos, total }))
    }

    async fn atualizar_status(
        &self,
        request: Request<AtualizarStatusRequest>,
    ) -> Result<Response<PedidoResponse>, Status> {
        let req = request.into_inner();

        // Validar transição de status
        let status_validos = [
            "criado", "confirmado", "preparando",
            "enviado", "entregue", "cancelado",
        ];
        if !status_validos.contains(&req.novo_status.as_str()) {
            return Err(Status::invalid_argument(format!(
                "Status inválido: {}. Válidos: {:?}",
                req.novo_status, status_validos
            )));
        }

        let mut pedido = self
            .pedidos
            .get_mut(&req.pedido_id)
            .ok_or_else(|| Status::not_found("Pedido não encontrado"))?;

        let status_anterior = pedido.status.clone();
        pedido.status = req.novo_status.clone();
        pedido.atualizado_em = Utc::now().to_rfc3339();

        tracing::info!(
            pedido_id = %req.pedido_id,
            de = %status_anterior,
            para = %req.novo_status,
            "Status atualizado"
        );

        // Notificar observadores
        let _ = self.notificacoes.send(StatusUpdate {
            pedido_id: req.pedido_id,
            status_anterior,
            status_novo: req.novo_status,
            timestamp: Utc::now().to_rfc3339(),
        });

        Ok(Response::new(PedidoResponse::from(pedido.value())))
    }

    type ObservarPedidoStream =
        Pin<Box<dyn Stream<Item = Result<StatusUpdate, Status>> + Send>>;

    async fn observar_pedido(
        &self,
        request: Request<BuscarPedidoRequest>,
    ) -> Result<Response<Self::ObservarPedidoStream>, Status> {
        let pedido_id = request.into_inner().pedido_id;

        // Verificar se pedido existe
        if !self.pedidos.contains_key(&pedido_id) {
            return Err(Status::not_found("Pedido não encontrado"));
        }

        let rx = self.notificacoes.subscribe();
        let stream = BroadcastStream::new(rx)
            .filter_map(move |result| {
                match result {
                    Ok(update) if update.pedido_id == pedido_id => {
                        Some(Ok(update))
                    }
                    Err(_) => Some(Err(Status::internal("Erro no stream"))),
                    _ => None,
                }
            });

        Ok(Response::new(Box::pin(stream)))
    }
}

Main com Health Check e Graceful Shutdown

use pedidos::servico_pedidos_server::ServicoPedidosServer;
use tokio::signal;
use tonic::transport::Server;

mod servico; // módulo com a implementação acima

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Logging estruturado em JSON para produção
    tracing_subscriber::fmt()
        .json()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info".into()),
        )
        .init();

    let addr = "0.0.0.0:50051".parse()?;
    let servico = servico::ServicoPedidosImpl::new();

    tracing::info!(%addr, "Iniciando serviço de pedidos");

    // Health check para Kubernetes/Docker
    let (mut health_reporter, health_service) =
        tonic_health::server::health_reporter();
    health_reporter
        .set_serving::<ServicoPedidosServer<servico::ServicoPedidosImpl>>()
        .await;

    // Reflection para ferramentas como grpcurl
    let reflection_service = tonic_reflection::server::Builder::configure()
        .register_encoded_file_descriptor_set(
            tonic::include_file_descriptor_set!("pedidos_descriptor")
        )
        .build_v1()?;

    Server::builder()
        .add_service(health_service)
        .add_service(reflection_service)
        .add_service(ServicoPedidosServer::new(servico))
        .serve_with_shutdown(addr, async {
            shutdown_signal().await;
            tracing::info!("Sinal de shutdown recebido, finalizando...");
        })
        .await?;

    tracing::info!("Serviço encerrado com sucesso");
    Ok(())
}

/// Aguarda sinais de shutdown (SIGTERM, SIGINT).
async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("Falha ao registrar handler Ctrl+C");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("Falha ao registrar handler SIGTERM")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }
}

Dockerfile Multi-Stage para Produção

# === Build ===
FROM rust:1.85-bookworm AS builder

# Instalar protoc para compilar .proto
RUN apt-get update && apt-get install -y protobuf-compiler && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Cache de dependências
COPY Cargo.toml Cargo.lock ./
COPY build.rs ./
COPY proto ./proto
RUN mkdir src && echo 'fn main() {}' > src/main.rs
RUN cargo build --release && rm -rf src

# Build real
COPY src ./src
RUN touch src/main.rs && cargo build --release

# === Runtime ===
FROM debian:bookworm-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl \
    && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home --shell /bin/bash app
USER app

COPY --from=builder /app/target/release/servico-pedidos /usr/local/bin/

EXPOSE 50051

# Health check via grpc-health-probe
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
    CMD grpc_health_probe -addr=:50051 || exit 1

ENV RUST_LOG=info
ENTRYPOINT ["servico-pedidos"]

Docker Compose com Serviços Dependentes

version: "3.8"

services:
  pedidos:
    build: .
    ports:
      - "50051:50051"
    environment:
      RUST_LOG: "info,servico_pedidos=debug"
      DATABASE_URL: "postgres://app:senha@postgres:5432/pedidos"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: senha
      POSTGRES_DB: pedidos
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

Padrões Essenciais para Produção

Retry com Backoff Exponencial

use std::time::Duration;
use tokio::time::sleep;

async fn com_retry<F, Fut, T, E>(
    operacao: F,
    max_tentativas: u32,
) -> Result<T, E>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = Result<T, E>>,
    E: std::fmt::Display,
{
    let mut tentativa = 0;
    loop {
        match operacao().await {
            Ok(resultado) => return Ok(resultado),
            Err(e) => {
                tentativa += 1;
                if tentativa >= max_tentativas {
                    tracing::error!(%e, "Máximo de tentativas excedido");
                    return Err(e);
                }
                let delay = Duration::from_millis(100 * 2u64.pow(tentativa));
                tracing::warn!(
                    %e, tentativa, delay_ms = %delay.as_millis(),
                    "Tentativa falhou, retrying..."
                );
                sleep(delay).await;
            }
        }
    }
}

Middleware de Métricas com Tower

use std::time::Instant;
use tower::Layer;

#[derive(Clone)]
pub struct MetricasLayer;

impl<S> Layer<S> for MetricasLayer {
    type Service = MetricasMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MetricasMiddleware { inner }
    }
}

#[derive(Clone)]
pub struct MetricasMiddleware<S> {
    inner: S,
}

// A implementação completa registraria métricas em Prometheus/OpenTelemetry
// usando a crate `metrics` para counters e histograms.

Empresas Usando Rust para Microsserviços

  • Discord: Migrou serviços críticos de Go para Rust, reduzindo latência p99 e uso de memória
  • Cloudflare: Centenas de microsserviços Rust no edge processando trilhões de requests
  • AWS: Serviços internos, incluindo componentes do S3, Lambda e IAM
  • Dropbox: Componentes de sincronização e storage
  • Figma: Servidor multiplayer para colaboração em tempo real
  • 1Password: Backend de criptografia e sincronização
  • Fly.io: Runtime de aplicações distribuídas
  • Linkerd: Data plane proxy (microsserviço de infraestrutura)

Como Começar

  1. Fundamentos de Rust: Tutorial de primeiros passos e concorrência
  2. API REST: Comece com uma API REST simples com Axum antes de ir para gRPC
  3. Servidor HTTP: Pratique com a receita de servidor HTTP
  4. Docker: Configure seu ambiente Docker para Rust
  5. Banco de dados: Integre com PostgreSQL
  6. Tratamento de erros: Essencial para produção — tutorial completo

Conclusão

Rust é uma escolha excelente para microsserviços que precisam entregar alta performance com baixo consumo de recursos. A combinação de gRPC com Tonic, observabilidade com tracing/OpenTelemetry e deploy otimizado com Docker multi-stage cria um stack de produção robusto. Se seus microsserviços precisam processar milhares de requisições por segundo com latência previsível e consumo mínimo de memória, Rust oferece o melhor custo-benefício entre performance e produtividade.


Veja Também