Scale-To-Zero postgresql databases

created: mercoledì, ott 1, 2025

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

Ci siamo quindi chiesti: possiamo applicare la nostra filosofia scale-to-zero anche ai nostri database? La risposta è sì. Abbiamo sviluppato un sistema per fornire istanze PostgreSQL che vengono eseguite solo quando sono effettivamente utilizzate. Questo design è incredibilmente efficiente in termini di risorse, ma presenta alcuni compromessi, che esploreremo.

Ecco una panoramica schematica di ciò che abbiamo costruito e di come abbiamo ottenuto 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 via Socket

Il cuore di questo sistema è l’attivazione tramite socket di systemd. Invece di avere un container PostgreSQL sempre attivo 24/7, lasciamo che il sistema init systemd ascolti sulla porta del database. Quando un’applicazione tenta di connettersi, systemd intercetta la richiesta, avvia il container del database on-demand e poi passa la connessione. Una volta che il database non è più utilizzato, viene spento automaticamente.

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

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

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

Analizziamo i file di configurazione che rendono tutto questo 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 standard docker-compose.yml. Definisce un container PostgreSQL 18, mappa una porta interna all’host e monta un volume per persistere i dati del database sul disco locale. Questo garantisce che, anche se il container si ferma, i dati rimangono al sicuro. Tutte le configurazioni descritte nell’immagine ufficiale PostgreSQL su Docker Hub possono essere usate 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 attiverà 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 Timer di inattività: systemd Service

Qui risiede la logica on-demand. Attivato dal socket, questo servizio prima avvia il servizio database vero e proprio (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, potrebbe verificarsi una condizione di gara in cui il proxy parte e inoltra la connessione del client prima che il container PostgreSQL abbia finito di inizializzarsi. Questo causerebbe un errore immediato “Connection Refused” per il client. Questo script prima dell’avvio assicura che il passaggio sia fluido 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 arrivo alla porta interna su cui ascolta il container PostgreSQL (127.0.0.1:14532). La parte cruciale è --exit-idle-time=3min. Questo dice al proxy di uscire automaticamente se è inattivo per 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 vita di Docker Compose. Viene avviato dal servizio proxy. La direttiva chiave è StopWhenUnneeded=true. Questo lega il suo ciclo di vita a quello del servizio proxy. Quando pg1-proxy.service si arresta (perché il timer di inattività è scaduto), systemd vede che questo servizio non è più necessario e lo ferma 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

Questo setup è 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, batch jobs o dashboard di amministrazione usati di rado — questo ritardo è un compromesso accettabile per il notevole risparmio di risorse. Per applicazioni di produzione ad alto traffico e sensibili alla latenza, un database tradizionale sempre attivo resta 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 ad accettare connessioni, ma non consumerà risorse finché non arriva la prima. È 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.