Implementar graceful shutdown y reinicios sin downtime
Cómo implementar graceful shutdown y reinicios sin downtime para servidores web, workers y contenedores
Visión General
Un graceful shutdown asegura que tu aplicación termine peticiones en vuelo, vacíe buffers, cierre conexiones de base de datos y libere bloqueos antes de salir. Sin él, los despliegues y eventos de escalado causan peticiones caídas, corrupción de datos y fallos en cascada. Esta receta implementa manejo de SIGTERM, drenado de conexiones y patrones de despliegue sin downtime para servidores web, workers y contenedores.
Cuándo Usar
Usa este recurso cuando:
- Despliegas frecuentemente en Kubernetes, Docker o grupos de auto-escalado
- Ejecutas workers de long-polling, WebSocket o trabajos en background
- Necesitas vaciar métricas, logs o escrituras de base de datos antes de terminar
- Quieres despliegues sin downtime con rolling updates o releases blue/green
Solución
Python
import signal
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from flask import Flask
app = Flask(__name__)
executor = ThreadPoolExecutor(max_workers=4)
shutting_down = False
@app.route("/health")
def health():
if shutting_down:
return {"status": "shutting-down"}, 503
return {"status": "ok"}
@app.route("/")
def home():
if shutting_down:
return {"error": "server is shutting down"}, 503
time.sleep(0.5) # simular trabajo
return {"message": "hello"}
def graceful_shutdown(signum, frame):
global shutting_down
print("Received SIGTERM, starting graceful shutdown...")
shutting_down = True
# Dejar de aceptar nuevo trabajo
executor.shutdown(wait=True)
# Permitir que peticiones en vuelo terminen en hasta 15 segundos
time.sleep(15)
print("Shutdown complete. Exiting.")
sys.exit(0)
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, threaded=True)
JavaScript
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(isShuttingDown ? 503 : 200);
res.end(JSON.stringify({ status: isShuttingDown ? 'shutting-down' : 'ok' }));
return;
}
// Simular trabajo async
setTimeout(() => {
res.writeHead(isShuttingDown ? 503 : 200);
res.end(JSON.stringify({ message: 'hello' }));
}, 500);
});
let isShuttingDown = false;
let connections = new Set();
server.on('connection', (conn) => {
connections.add(conn);
conn.on('close', () => connections.delete(conn));
});
function shutdown() {
console.log('Received SIGTERM, starting graceful shutdown...');
isShuttingDown = true;
server.close(() => {
console.log('HTTP server closed. Draining connections...');
});
// Forzar cierre de conexiones restantes después del timeout
setTimeout(() => {
connections.forEach((conn) => conn.destroy());
console.log('Shutdown complete.');
process.exit(0);
}, 15000);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
server.listen(8080);
Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
// Spring Boot maneja graceful shutdown nativamente desde 2.3
// application.properties:
// server.shutdown=graceful
// spring.lifecycle.timeout-per-shutdown-phase=15s
// management.endpoint.health.probes.enabled=true
// management.health.livenessState.enabled=true
// management.health.readinessState.enabled=true
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook triggered. Closing context...");
ctx.close();
System.out.println("Context closed gracefully.");
}));
}
}
// Para Java sin Spring (Jetty/Netty plano):
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Handler;
Server server = new Server(8080);
server.setHandler(handler);
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
server.stop();
server.join();
} catch (Exception e) {
e.printStackTrace();
}
}));
Explicación
El graceful shutdown es un proceso de tres fases:
- Recepción de señal: El SO o runtime de contenedor envía
SIGTERM(oSIGINTlocalmente). Tu aplicación debe interceptarla en lugar de salir inmediatamente. - Drenado: Establece una bandera de health-check a
shutting-down(devolviendo HTTP 503) para que el balanceador deje de enviar nuevo tráfico. Termina peticiones en vuelo dentro de una ventana de timeout. - Limpieza: Cierra pools de base de datos, vacía logs/métricas, libera bloqueos y sale.
Despliegues sin downtime dependen del orquestador (Kubernetes, AWS ECS) ejecutando los pods viejo y nuevo concurrentemente. El pod viejo recibe SIGTERM, drena y sale solo después de que el nuevo pase readiness checks.
Variantes
| Tecnología | Enfoque | Notas |
|---|---|---|
| Kubernetes | terminationGracePeriodSeconds + preStop hook | Default 30s; ajusta según tiempo máximo de petición |
| Docker | docker stop envía SIGTERM, luego SIGKILL después de 10s | Usa --stop-timeout para extender |
| systemd | TimeoutStopSec en unidad de servicio | Alinea con timeout de drenado de la app |
| Node.js | server.close() + tracking de conexiones | Destruye sockets persistentes después del período de gracia |
| Spring Boot | server.shutdown=graceful + readiness probe | Built-in desde 2.3; nativo para Kubernetes |
| Gunicorn | Config graceful-timeout | Workers pre-fork salen después de terminar peticiones |
Mejores Prácticas
- Siempre implementa un endpoint
/healthque devuelva 503 durante shutdown para que los balanceadores redirijan tráfico - Establece
terminationGracePeriodSeconds(K8s) ostop-timeout(Docker) para que coincida con tu timeout de drenado - Usa logging estructurado para emitir un evento
shutdown_initiatedpara observabilidad y alertas - Maneja
SIGTERM,SIGINTy señales específicas de plataforma (WindowsCTRL_CLOSE_EVENT) - Prueba graceful shutdown en CI: envía SIGTERM durante una prueba de carga y verifica cero peticiones fallidas
Errores Comunes
- Salir inmediatamente al recibir SIGTERM — mata peticiones en vuelo; siempre drena primero
- No cambiar readiness del health-check — el balanceador sigue enrutando a un pod moribundo
- Bloquear el shutdown hook — los hooks de shutdown corren en paralelo; usa un latch o executor single-threaded para secuenciar limpieza
- No cerrar el pool de conexiones de base de datos — conexiones fugadas causan que el siguiente inicio falle con “demasiadas conexiones”
- Ignorar el preStop hook — Kubernetes puede enviar SIGTERM antes de que el pod sea removido de los endpoints del servicio; un
sleep 5en preStop previene esta carrera
Preguntas Frecuentes
¿Cuál es la diferencia entre SIGTERM y SIGKILL?
SIGTERM pregunta educadamente. Tu aplicación puede interceptarla, drenar conexiones y salir limpiamente. SIGKILL no puede ser interceptado; el SO termina el proceso a la fuerza. Kubernetes envía SIGKILL después de que terminationGracePeriodSeconds expire.
¿Cuánto tiempo debería ser mi período de gracia?
Al menos tan largo como tu endpoint o timeout de job más lento. Para APIs HTTP, 10–30 segundos es típico. Para workers de batch, minutos pueden ser necesarios. Siempre agrega un pequeño buffer.
¿Puedo lograr zero-downtime sin Kubernetes?
Sí. Usa un reverse proxy (Nginx, HAProxy) o service mesh (Envoy) con health checks. Despliega nuevas instancias, caliéntalas, luego drena y remueve instancias viejas. Los despliegues blue/green y rolling son posibles con cualquier balanceador de carga.
Recursos Relacionados
Blue-Green and Canary Deployments
A practical guide to deployment strategies: blue-green, canary, rolling, and feature flags. Minimize risk and rollback time when releasing to production.
DocPost-Deployment Verification Checklist Template
A checklist template for verifying deployments: health checks, smoke tests, metric validation, and rollback readiness before declaring all-clear.
GuideCI/CD Pipeline Guide
A practical guide to building CI/CD pipelines with GitHub Actions, testing, deployment strategies, and rollback procedures.
GuideDocker for Developers — A Complete Guide
Learn Docker from the ground up: images, containers, Dockerfiles, networks, volumes, and Docker Compose for local development.
GuideKubernetes Basics for Application Developers
Learn the core Kubernetes concepts every developer needs: Pods, Services, Deployments, ConfigMaps, and basic kubectl commands.