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
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.
Abbiamo fatto due scelte tecnologiche specifiche per questa configurazione: eseguire PostgreSQL in un container e gestirlo con Docker Compose.
Disaccoppiamento dall’OS Host: Eseguendo PostgreSQL dentro un container Docker, disaccoppiamo la versione del database da quella del sistema operativo host. Questo ci dà la flessibilità di usare versioni diverse di PostgreSQL per diversi servizi interni sullo stesso host senza conflitti o problemi di dipendenza. Possiamo aggiornare un database per un servizio senza impattare gli altri.
Compatibilità con systemd: Abbiamo scelto Docker Compose perché i suoi comandi di lifecycle si integrano perfettamente con il modo in cui systemd gestisce i servizi. La direttiva ExecStart di systemd si aspetta un comando che rimanga in esecuzione in primo piano finché il servizio non viene fermato. docker-compose up fa proprio questo. Una semantica più classica come docker create seguito da docker start è più difficile da gestire, poiché systemd richiederebbe uno script più complesso per gestire il ciclo di vita. docker-compose down fornisce un comando unico e pulito per la direttiva ExecStop, garantendo che l’intero ambiente venga smontato con grazia.
Analizziamo i file di configurazione che rendono tutto ciò possibile.
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.
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
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
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
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
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.
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.