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
- Compila y arranca:
RUST_LOG=info cargo run
- Pide la ruta:
curl -v http://localhost:3000/
- 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)
- 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.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación