Scale-To-Zero postgresql databases

created: mercoledì, ott 1, 2025

Puntiamo sempre a costruire servizi il più possibile efficienti in termini di risorse. Questo vale sia per i servizi che offriamo esternamente sia, altrettanto importante, per la nostra infrastruttura interna. Molti dei nostri strumenti interni, come sistemi di fatturazione e monitoraggio, si basano su database PostgreSQL. Pur essendo essenziali, questi database spesso rimangono inattivi per lunghi periodi, consumando RAM e cicli CPU senza motivo.

Quindi ci siamo chiesti: possiamo applicare la nostra filosofia scale-to-zero anche ai nostri database interni? La risposta è sì. Abbiamo sviluppato un sistema per mettere a disposizione istanze PostgreSQL che sono attive solo quando effettivamente utilizzate. Questo design è incredibilmente efficiente in termini di risorse, ma comporta alcuni compromessi, che andremo a esplorare.

Ecco una panoramica schematica di ciò che abbiamo costruito e di come abbiamo raggiunto questa scalabilità dinamica.

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 dell’Attivazione Socket

Il cuore di questa configurazione è l’attivazione socket di systemd. Invece di tenere un container PostgreSQL attivo 24/7, lasciamo che il sistema di init systemd ascolti sulla porta del database. Quando un’applicazione prova a connettersi, systemd intercetta la richiesta, avvia il container del database on-demand e poi gli passa la connessione. Quando il database non viene più utilizzato, viene chiuso automaticamente.

Questo approccio combina la potenza degli strumenti Linux standard e consolidati: systemd per la gestione dei servizi e l’attivazione socket, e Docker Compose per definire l’ambiente containerizzato del database. È semplice, robusto e non richiede software personalizzato.

Le Nostre Scelte Tecnologiche: Perché Container e Docker Compose?

Abbiamo fatto due scelte tecnologiche specifiche per questa configurazione: eseguire PostgreSQL in un container e gestirlo con Docker Compose.

Analizziamo i file di configurazione che rendono tutto ciò possibile.

I Componenti

Utilizziamo una combinazione di un file docker-compose.yml per definire il database e tre unità systemd per gestire il ciclo di vita scale-to-zero.

1. La Definizione del Database: Docker Compose

Questo è un file docker-compose.yml standard. Definisce un container PostgreSQL 18, mappa una porta interna su quella dell’host e monta un volume per mantenere i dati del database persistenti su disco locale. Questo garantisce che, anche quando il container si ferma, i dati rimangano al sicuro. Tutte le impostazioni documentate nella immagine ufficiale di PostgreSQL su Docker Hub possono essere utilizzate qui, permettendo ulteriori personalizzazioni come la creazione di utenti o database specifici all’avvio.

/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. Il Listener: systemd Socket

Questa unità .socket dice a systemd di ascoltare sulla porta 24532 su tutte le interfacce di rete. Quando arriva una connessione TCP, systemd attiva il servizio pg1-proxy.service. Questo è il punto di ingresso per tutte le connessioni al database.

/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. Il Proxy e il Timer di Inattività: systemd Service

Qui risiede la logica on-demand. Quando attivato dal socket, questo servizio avvia prima il servizio database effettivo (Requires=pg1-postgres.service). Il comando ExecStartPre è un piccolo ma critico ciclo shell che verifica ripetutamente se la porta interna di PostgreSQL è aperta. Senza questo controllo si potrebbe verificare una race condition in cui il proxy parte e inoltra la connessione del client prima che il container PostgreSQL abbia terminato l’inizializzazione. Ciò causerebbe un immediato errore “Connection Refused” per il client. Questo script pre-start assicura che il passaggio avvenga senza problemi e che il client si connetta solo quando il database è completamente pronto.

Il processo principale è systemd-socket-proxyd, uno strumento integrato che inoltra la connessione in ingresso alla porta interna dove il container PostgreSQL ascolta (127.0.0.1:14532). La parte cruciale è --exit-idle-time=3min. Questo indica al proxy di uscire automaticamente se è inattivo da tre minuti.

/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. Il Gestore del Container: systemd Service

Questo servizio gestisce il ciclo di vita di Docker Compose. Viene avviato dal servizio proxy. La direttiva chiave è StopWhenUnneeded=true. Questo collega il suo ciclo di vita a quello del servizio proxy. Quando pg1-proxy.service si ferma (perché il timer di inattività è scaduto), systemd vede che questo servizio non è più necessario e lo arresta automaticamente eseguendo docker-compose down. Il container viene spento, liberando tutte le sue risorse.

/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

Il Compromesso: Cold Starts

Questa configurazione è estremamente efficiente, ma comporta una considerazione importante: la latenza del “cold start”. La primissima connessione al database dopo un periodo di inattività sarà ritardata. Il client deve aspettare che systemd esegua docker-compose up e che il container PostgreSQL si inizializzi. Nella nostra esperienza, questo richiede circa un secondo per un database piccolo, ma aumenta con la dimensione dello storage.

Per molti sistemi interni — CI/CD, job batch o dashboard amministrative con uso sporadico — questo ritardo è un compromesso perfettamente accettabile rispetto al significativo risparmio di risorse. Per applicazioni di produzione ad alto traffico e sensibili alla latenza, una soluzione tradizionale di database sempre attivo rimane la scelta migliore.

Abilitare il Servizio

Per mettere online un nuovo database, basta abilitare le unità systemd.

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

Una volta abilitato, il database è pronto per accettare connessioni, ma non consumerà risorse fino all’arrivo della prima richiesta. Questo è un ulteriore piccolo passo nella nostra missione di eliminare gli sprechi, dimostrando che anche infrastrutture essenziali come un database relazionale possono essere gestite in modo snello e on-demand.