Scale-To-Zero postgresql databases

created: miércoles, oct. 1, 2025

Siempre buscamos construir servicios lo más eficientes posible en cuanto a recursos. Esto es cierto para los servicios que ofrecemos externamente y, igualmente importante, para nuestra propia infraestructura interna. Muchas de nuestras herramientas internas, como los sistemas de facturación y monitoreo, dependen de bases de datos PostgreSQL. Aunque son esenciales, estas bases de datos a menudo permanecen inactivas por largos periodos, consumiendo RAM y ciclos de CPU sin motivo.

Así que nos preguntamos: ¿podemos aplicar nuestra filosofía scale-to-zero a nuestras propias bases de datos? La respuesta es sí. Hemos desarrollado un sistema para aprovisionar instancias de PostgreSQL que sólo se ejecutan cuando están activamente en uso. Este diseño es increíblemente eficiente en recursos, pero sí conlleva algunos compromisos, que exploraremos.

Aquí hay un esquema general de lo que construimos y cómo logramos esta escalabilidad dinámica.

flowchart LR
  subgraph Machine
    A[systemd.socket]
    B[systemd-socket-proxyd]
    D[(local disk)]
    A -- port 15432 --> B

    subgraph Docker-Compose
      C[postgres container]
    end
    B -- port 5432 --> C
    C -- data-dir --> D
  end

  X[Internet] -- port 25432 --> A

La Magia de la Activación por Socket

El núcleo de esta configuración es la activación por socket de systemd. En lugar de tener un contenedor PostgreSQL corriendo 24/7, dejamos que el sistema init systemd escuche en el puerto de la base de datos. Cuando una aplicación intenta conectarse, systemd intercepta la solicitud, inicia el contenedor de la base de datos bajo demanda y luego entrega la conexión. Una vez que la base de datos ya no está en uso, se apaga automáticamente.

Este enfoque combina el poder de herramientas estándar y probadas en Linux: systemd para la gestión de servicios y activación por socket, y Docker Compose para definir nuestro entorno de base de datos contenedorizado. Es simple, robusto y no requiere software personalizado.

Nuestras Elecciones Tecnológicas: ¿Por qué Contenedores y Docker Compose?

Hicimos dos elecciones tecnológicas específicas para esta configuración: ejecutar PostgreSQL en un contenedor y gestionarlo con Docker Compose.

Desglosemos los archivos de configuración que hacen esto posible.

Los Componentes

Usamos una combinación de un archivo docker-compose.yml para definir la base de datos y tres unidades de systemd para manejar el ciclo de vida scale-to-zero.

1. La Definición de la Base de Datos: Docker Compose

Este es un archivo docker-compose.yml estándar. Define un contenedor PostgreSQL 18, mapea un puerto interno al host y monta un volumen para persistir los datos de la base de datos en el disco local. Esto asegura que aunque el contenedor se detenga, los datos permanezcan seguros. Todos los ajustes documentados en la imagen oficial de PostgreSQL en Docker Hub se pueden usar aquí, permitiendo personalizaciones adicionales, como crear usuarios o bases de datos específicas al iniciar.

/root/pg/pg1/docker-compose.yml

 1version: "3"
 2services:
 3  database:
 4    image: 'postgres:18'
 5    ports:
 6      - 127.0.0.1:14532:5432
 7    volumes:
 8      - /root/pg/pg1/data:/var/lib/postgresql
 9    environment:
10      POSTGRES_PASSWORD: SuperSecretAdminPassword

2. El Escuchador: systemd Socket

Esta unidad .socket le indica a systemd que escuche en el puerto 24532 en todas las interfaces de red. Cuando llega una conexión TCP, systemd activará pg1-proxy.service. Este es el punto de entrada para todas las conexiones a la base de datos.

/etc/systemd/system/pg1-proxy.socket

 1[Unit]
 2Description=Socket for pg1 pg proxy (24532->127.0.0.1:14532)
 3
 4[Socket]
 5ListenStream=0.0.0.0:24532
 6ReusePort=true
 7NoDelay=true
 8Backlog=128
 9
10[Install]
11WantedBy=sockets.target

3. El Proxy y Temporizador de Inactividad: Servicio systemd

Aquí vive la lógica bajo demanda. Cuando se activa por el socket, este servicio primero inicia el servicio real de la base de datos (Requires=pg1-postgres.service). El comando ExecStartPre es un pequeño pero crítico bucle de shell que revisa repetidamente si el puerto interno de PostgreSQL está abierto. Sin esta revisión, podría ocurrir una condición de carrera donde el proxy comienza y reenvía la conexión del cliente antes de que el contenedor PostgreSQL termine de inicializarse. Esto resultaría en un error inmediato de “Conexión Rechazada” para el cliente. Este script previo al inicio asegura que la entrega sea suave y que el cliente sólo se conecte cuando la base de datos esté completamente lista.

El proceso principal es systemd-socket-proxyd, una herramienta integrada que reenvía la conexión entrante al puerto interno donde escucha el contenedor PostgreSQL (127.0.0.1:14532). La parte crucial es --exit-idle-time=3min. Esto indica al proxy que salga automáticamente si ha estado inactivo por tres minutos.

/etc/systemd/system/pg1-proxy.service

 1[Unit]
 2Description=Socket-activated TCP proxy to local Postgres on 14532
 3
 4Requires=pg1-postgres.service
 5After=pg1-postgres.service
 6
 7[Service]
 8Type=simple
 9Sockets=pg1-proxy.socket
10ExecStartPre=/bin/bash -c 'for i in {1..10}; do nc -z 127.0.0.1 14532 && exit 0; sleep 1; done; exit 0'
11ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=3min 127.0.0.1:14532

4. El Gestor del Contenedor: Servicio systemd

Este servicio maneja el ciclo de vida de Docker Compose. Es iniciado por el servicio proxy. La directiva clave es StopWhenUnneeded=true. Esto vincula su ciclo de vida al servicio proxy. Cuando pg1-proxy.service se detiene (porque expiró su temporizador de inactividad), systemd ve que este servicio ya no es necesario y lo detiene automáticamente ejecutando docker-compose down. El contenedor se apaga, liberando todos sus recursos.

/etc/systemd/system/pg1-postgres.service

 1[Unit]
 2Description=postgres container
 3PartOf=pg1-proxy.service
 4StopWhenUnneeded=true
 5
 6[Service]
 7WorkingDirectory=/root/pg/pg1
 8
 9Type=simple
10ExecStart=/usr/bin/docker-compose up
11ExecStop=/usr/bin/docker-compose down
12
13Restart=on-failure
14RestartSec=2s
15TimeoutStopSec=30s

El Compromiso: Arranques en Frío

Esta configuración es increíblemente eficiente, pero tiene una consideración importante: la latencia de “arranque en frío”. La primera conexión a la base de datos tras un período de inactividad se retrasará. El cliente debe esperar a que systemd ejecute docker-compose up y a que el contenedor PostgreSQL se inicialice. En nuestra experiencia, esto toma cerca de un segundo para una base de datos pequeña, pero el tiempo aumenta con el tamaño del almacenamiento.

Para muchos sistemas internos — CI/CD, trabajos batch o paneles administrativos de uso poco frecuente — esta demora es un compromiso aceptable a cambio del significativo ahorro de recursos. Para aplicaciones productivas con alto tráfico y sensibilidad a la latencia, una base de datos tradicional, siempre encendida, sigue siendo la opción correcta.

Habilitando el Servicio

Para poner en línea una nueva base de datos, sólo necesitamos habilitar las unidades systemd.

1systemctl daemon-reload
2systemctl enable pg1-proxy.service
3systemctl enable pg1-postgres.service
4systemctl enable --now pg1-proxy.socket

Una vez habilitadas, la base de datos está lista para aceptar conexiones, pero no consumirá recursos hasta que llegue la primera. Este es otro pequeño paso en nuestra misión para eliminar el desperdicio, demostrando que incluso una infraestructura esencial como una base de datos relacional puede ser ejecutada de forma ágil y bajo demanda.