Microservicio HTTP en Rust con hyper y tokio: timeouts, límites y cierre ordenado

rust Microservicio HTTP en Rust con hyper y tokio: timeouts, límites y cierre ordenado

Microservicio HTTP en Rust con hyper y tokio: timeouts, límites y cierre ordenado

En este artículo verás un ejemplo práctico para levantar un servidor HTTP eficiente en Rust usando tokio, hyper y tower/tower-http. Cubriremos:

  • Middleware de tracing (logs estructurados)
  • Timeouts por petición
  • Límites de concurrencia y tamaño de cuerpo
  • Cierre ordenado (graceful shutdown)

El enfoque es práctico: código mínimo pero listo para producción como base sobre la que añadir autenticación, TLS o métricas.

Cargo.toml (dependencias)

[package]
name = "rust-microservice"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.34", features = ["full"] }
hyper = { version = "0.14", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.3", features = ["trace", "limit"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
bytes = "1.4"

Versiones aproximadas: ajusta según el momento en que lo uses. Asegúrate de que las versiones de tower y tower-http sean compatibles.

main.rs — servidor con middleware

use std::convert::Infallible;
use std::net::SocketAddr;
use std::time::Duration;

use bytes::Bytes;
use hyper::{Body, Request, Response, Server};
use hyper::service::make_service_fn;
use tokio::time::sleep;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower::limit::ConcurrencyLimitLayer;
use tower::timeout::TimeoutLayer;
use tracing::{info};

#[tokio::main]
async fn main() -> Result<(), Box> {
    // Inicializa tracing desde RUST_LOG, p.ej: RUST_LOG=info cargo run
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    // Opciones
    let addr: SocketAddr = "0.0.0.0:3000".parse()?;
    const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
    const MAX_CONCURRENCY: usize = 100; // límite de handlers concurrentes
    const MAX_REQUEST_BODY: u64 = 1024 * 1024; // 1 MiB

    // Crea el stack de middleware con tower
    let middleware = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(TimeoutLayer::new(REQUEST_TIMEOUT))
        .layer(ConcurrencyLimitLayer::new(MAX_CONCURRENCY))
        .layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY));

    // make_service crea un servicio por conexión
    let make_svc = make_service_fn(move |_conn| {
        let svc = middleware.service_fn(handle_request);
        async move { Ok::<_, Infallible>(svc) }
    });

    let server = Server::bind(&addr).serve(make_svc);
    info!(%addr, "Servidor escuchando");

    // Graceful shutdown: responde a Ctrl+C y espera que conexiones terminen
    let graceful = server.with_graceful_shutdown(shutdown_signal());

    if let Err(e) = graceful.await {
        eprintln!("server error: {}", e);
    }

    info!("Servidor finalizado");
    Ok(())
}

async fn handle_request(req: Request) -> Result, Infallible> {
    // Ejemplo simple: limita el tamaño total del body con tower_http::limit
    // Aquí podrías parsear JSON, consultar BD, etc. Simulamos trabajo async.

    // Registra método y ruta (TraceLayer ya hace logging estructurado)
    info!(method = ?req.method(), uri = %req.uri(), "handling request");

    // Si quieres acceder al body en bytes:
    let whole = hyper::body::to_bytes(req.into_body()).await.unwrap_or_else(|_| Bytes::new());

    // Simula trabajo que podría tardar: si el cliente supera el timeout, la petición será cancelada
    sleep(Duration::from_millis(50)).await;

    let body = format!("received {} bytes\n", whole.len());

    Ok(Response::new(Body::from(body)))
}

async fn shutdown_signal() {
    // Escucha Ctrl+C (Windows y Unix)
    tokio::signal::ctrl_c()
        .await
        .expect("failed to install Ctrl+C handler");
    tracing::info!("received shutdown signal");
}


Explicación rápida de las piezas clave

  • TraceLayer (tower-http): logging estructurado por request/response. Útil para correlación y observabilidad.
  • TimeoutLayer: cancela la ejecución de la ruta si excede el tiempo configurado. Importante para evitar hilos bloqueados o peticiones que consumen conexiones indefinidamente.
  • ConcurrencyLimitLayer: controla el número máximo de handlers concurrentes. Evita que spikes de tráfico agoten los recursos.
  • RequestBodyLimitLayer: rechaza peticiones con cuerpos demasiado grandes (protección contra DoS por tamaño).
  • graceful_shutdown: permite cerrar el servidor de forma ordenada cuando recibes señal (Ctrl+C), dejando terminar las peticiones en curso dentro de un tiempo.

Cómo probar

  1. Compila y arranca: RUST_LOG=info cargo run
  2. Pide la ruta: curl -v http://localhost:3000/
  3. Prueba límite de tamaño: curl -X POST --data-binary "$(head -c 2000000 < /dev/zero)" http://localhost:3000/ (debería ser rechazado por RequestBodyLimitLayer si supera 1 MiB)
  4. Prueba timeout simulando trabajo largo: añade un sleep mayor a REQUEST_TIMEOUT en handle_request y verás cómo la petición se cancela y la capa de timeout responde.

Notas prácticas y recomendaciones

  • Timeouts deben ser conservadores: si tu handler hace I/O externo, considera timeouts específicos por llamada (p. ej. cliente HTTP con su propio timeout) además del timeout global por request.
  • El límite de concurrencia debe ajustarse según memoria y características de CPU del host. Mide en staging con carga representativa.
  • Para TLS en producción, termina TLS en un proxy (nginx/caddy) o usa rustls y configura bien las ciphers. Evita reimplementar TLS inseguro.
  • Usa métricas (Prometheus) y tracing correlacionado para entender latencias, timeouts y rechazos por límite.

Siguiente paso práctico: añade métricas de histograma para latencias y un endpoint /health que verifique la conectividad a dependencias críticas. En producción, recuerda endurecer el entorno: pon límites OS (ulimit), controla tiempo máximo de cierre y prueba ataques de tipo slowloris para ajustar keep-alive y timeouts.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión