Systemd e PostgreSQL: avvio alla bisogna

systemd ha tante caratteristiche interessanti, una delle quali è l’attivazione tramite socket, vale a dire che si può configurare systemd si mette in attesa su una certa porta TCP (o una pipe, o altro tipo di socket) al posto di un altro programma. Quando arriva una richiesta di connessione, systemd l’accetta e attiva il servizio configurato, poi questo deve comunicare con systemd, prendere la richiesta in arrivo e servirla.

Perché è interessante? Perché si possono configurare molti servizi su una sola macchina, ma attivarli solo quando veramente sono richiesti. Oppure, durante l’avvio del computer, si possono attivare vari servizi in contemporanea, con l’attivazione tramite socket, e proseguire come se già fossero attivi.

Come si configura questa attivazione? Con una unit di tipo «socket» che indica su quale socket ascoltare, e una unit di tipo «service» (con lo stesso nome) che indica il servizio da attivare quando arriva una connessione sul socket. La prima unit va installata sul target corretto (in genere il multi-user). Il servizio può essere un programma o un container.

Vediamo un esempio: cerchiamo di configurare l’avvio di PostgreSQL in questo modo.

Configurazione predefinita di Debian

Per prima cosa vediamo quali sono le unit fornite con i pacchetti standard di Debian per PostgreSQL per controllare che ci sia anche quella da usare come modello per generare la vera unit (è quella identificata da una chicciolina nel nome):

$ find /usr/lib/systemd /var/lib/systemd/ /etc/systemd/ -name \*postgre\*
/usr/lib/systemd/system/postgresql@.service
/usr/lib/systemd/system/postgresql.service
/usr/lib/systemd/system-generators/postgresql-generator
/var/lib/systemd/deb-systemd-helper-enabled/multi-user.target.wants/postgresql.service
/var/lib/systemd/deb-systemd-helper-enabled/postgresql.service.dsh-also

creiamo un cluster con PostgreSQL 16 che chiamiamo linuxday

$ sudo pg_createcluster 16 linuxday
Creating new PostgreSQL cluster 16/linuxday ...
[...]
$ pg_lsclusters Ver Cluster  Port Status Owner    Data directory                  Log file
16  linuxday 5434 down   postgres /var/lib/postgresql/16/linuxday /var/log/postgresql/postgresql-16-linuxday.log

e notiamo che systemd continua a non sapere nulla di PostgreSQL

$ systemctl list-units | grep postg
$

Proviamo ad avviare il cluster con i comandi di PostgreSQL e ricontrolliamo systemd

$ sudo pg_ctlcluster 16 linuxday start
$ systemctl list-units | grep postg
postgresql@16-linuxday.service                                                                  loaded active     running   PostgreSQL Cluster 16-linuxday
system-postgresql.slice                                                                         loaded active     active    Slice /system/postgresql

adesso si vede che systemd è a conoscenza del cluster, che è ora attivo. A dire il vero ci sono due diverse unit, una di tipo service, che corrisponde ad un file sul file system, ed una di tipo slice. Quest’ultima è una unit generata internamente da un’applicazione o da systemd stesso, che non ha un file che la descrive.

Si possono usare i normali comandi di systemd per vedere lo stato del server, come ad esempio:

$ systemctl status postgresql@16-linuxday.service
● postgresql@16-linuxday.service - PostgreSQL Cluster 16-linuxday
Loaded: loaded (/lib/systemd/system/postgresql@.service; enabled-runtime; preset: enabled)
Active: active (running) since Sat 2023-10-14 09:35:38 CEST; 2min 26s ago
Process: 15725 ExecStart=/usr/bin/pg_ctlcluster --skip-systemctl-redirect 16-linuxday start (code=exited, status=0/SUCCESS)
Main PID: 15730 (postgres)
Tasks: 6 (limit: 19055)
Memory: 20.3M
CPU: 220ms
CGroup: /system.slice/system-postgresql.slice/postgresql@16-linuxday.service
├─15730 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/linuxday -c config_file=/etc/postgresql/16/linuxday/postgresql.conf
├─15731 "postgres: 16/linuxday: checkpointer "
├─15732 "postgres: 16/linuxday: background writer "
├─15734 "postgres: 16/linuxday: walwriter "
├─15735 "postgres: 16/linuxday: autovacuum launcher "
└─15736 "postgres: 16/linuxday: logical replication launcher "

notare che il nome del file della unit è quello del generatore, con la @.

Oppure si può usare il comando di PostgreSQL:

$ pg_lsclusters
Ver Cluster  Port Status Owner    Data directory                  Log file
16  linuxday 5434 online postgres /var/lib/postgresql/16/linuxday /var/log/postgresql/postgresql-16-linuxday.log

Vediamo ora su che porte sta ascoltando PostgreSQL:

$ sudo lsof -p 15730 2>&1 | grep LISTEN
postgres 15730 postgres    6u     IPv4              81476      0t0      TCP localhost:5434 (LISTEN)
postgres 15730 postgres    7u     unix 0x00000000cca3376a      0t0    81477 /var/run/postgresql/.s.PGSQL.5434 type=STREAM (LISTEN)

La prima è una porta TCP, la 5434, la seconda è una named pipe in /var/run/postgresql.

Ora colleghiamoci al cluster come utente postgres, che è il super utente, e creiamo un utente per me:

$ sudo su - postgres -c 'psql --cluster 16/linuxday'
psql (16.0 (Debian 16.0-1.pgdg120+1))
Digita "help" per avere un aiuto.

postgres=# create user giuseppe superuser;
CREATE ROLE
postgres=# \password giuseppe
Inserisci la password per l'utente "giuseppe":
Conferma password:
postgres=#\q

E lo spegniamo usando systemctl al posto dei comandi di PostgreSQL e poi controlliamo che sia spento:

$ sudo systemctl stop postgresql@16-linuxday.service
$ pg_lsclusters
Ver Cluster  Port Status Owner    Data directory                  Log file
16  linuxday 5434 down   postgres /var/lib/postgresql/16/linuxday /var/log/postgresql/postgresql-16-linuxday.log

Quindi, possiamo accendere e spegnere il nostro cluster PostgreSQL indifferentemente con i comandi originali o con systemd. Ma con systemd possiamo indicare le dipendenze e inserire nel punto corretto la unit da avviare con l’accensione della macchina.

Per i curiosi, l’integrazione tra i due sistemi è stata fatta nel comando pg_ctlcluster, il quale se viene invocato normalmente fa passare l’azione da systemd, il quale a sua volta richiama pg_ctlcluster con un argomento speciale che dice di fare effettivamente l’azione richiesta (avvio, arresto, ricaricamento della configurazione). Lo si può vedere nella sezione [Service] della unit postgresql@.service con il comando

systemctl cat postgresql@16-linuxday.service

Attivazione via socket

L’attivazione via socket richiede che il programma da avviare collabori con systemd, difatti se il socket viene creato da systemd, che si mette in ascolto, l’applicazione non potrà a sua volta creare quel socket perché lo troverà già in uso.

PostgreSQL non è (ancora) stato modificato per interagire con systemd per l’attivazione via socket, ma systemd ha un modulo, chiamato systemd-socket-proxyd che può essere usato in queste situazioni: si fa in modo che l’attivazione via socket comunichi con il proxy, il quale collabora con systemd e poi passa la comunicazione al programma finale.

Ovviamente vale la regola che l’applicazione non può usare il socket in uso da systemd. Quindi in questo caso mettiamo in ascolto sulla porta TCP il proxy e lasciamo PostgreSQL in ascolto solo sulla pipe.

Per farlo, cambiamo il valore del parametro listen_adresses in ” (stringa vuota) nel file /etc/postgresql/16/linuxday/postgresql.conf.

Poi creiamo la unit /etc/systemd/system/postgresql-proxy.socket che indica a systemd di ascoltare sulla porta TCP 5434 e poi la unit /etc/systemd/system/postgresql-proxy.service che viene attivata da systemd alla connessione TCP, e che avvia systemd-socket-proxyd che a sua volta comunicherà con postgresl tramite pipe.

La prima unit è un semplice socket:

$ cat /etc/systemd/system/postgresql-proxy.socket
[Socket]
ListenStream=5434

[Install]
WantedBy=sockets.target

La seconda dipende da PostgreSQL, quindi quando il socket verrà richiamato, systemd attiverà prima PostgreSQL e poi il proxy:

$ cat /etc/systemd/system/postgresql-proxy.service
[Unit]
Requires=postgresql@16-linuxday.service
After=postgresql@16-linuxday.service
Requires=postgresql-proxy.socket
After=postgresql-proxy.socket

[Service]
Type=notify
ExecStart=/usr/lib/systemd/systemd-socket-proxyd /var/run/postgresql/.s.PGSQL.5434
PrivateTmp=yes
PrivateNetwork=yes

facciamo rileggere i nostri file a systemd e li attiviamo; poi controlliamo che il socket sia in LISTEN

$ sudo systemctl daemon-reload
$ sudo systemctl enable postgresql-proxy.socket
$ sudo systemctl start postgresql-proxy.socket
$ sudo netstat -alnp | grep :543
tcp6       0      0 :::5434                 :::*                    LISTEN      1/init

Quindi, quello che abbiamo ora è che c’è qualcosa in ascolto sulla normale porta di PostgreSQL 5434, ma PostgreSQL è completamente spento.

Ora cerchiamo di collegarci a PostgreSQL con il client psql indicando l’accesso tramite TCP (tramite pipe non funzionerebbe perché è ancora spento). Per curiosità ci facciamo dire quanto tempo psql ci mette a collegarsi al database e fare una semplice query.

$ time psql --host localhost --port 5434 -c "select version();"
psql: errore: connection to server at "localhost" (127.0.0.1), port 5434 failed: FATALE:  Autenticazione Peer fallita per l'utente "giuseppe"

real	0m2,542s
user	0m0,002s
sys	0m0,006s

$ time psql --host localhost --port 5434 -c "select version();"
psql: errore: connection to server at "localhost" (127.0.0.1), port 5434 failed: FATALE:  Autenticazione Peer fallita per l'utente "giuseppe"

real	0m0,093s
user	0m0,034s
sys	0m0,004s

OK, non ha funzionato l’accesso, ma la procedura ha comunque funzionato: difatti il socket è stato usato per comunicare con systemd, il quale ha attivato il proxy, il quale aveva come dipendenza PostgreSQL; e poi la richiesta è arrivata a PostgreSQL che però l’ha rifiutata.

La seconda esecuzione mostra tempi di risposta decisamente minori perché il cluster era già attivo. D’altronde abbiamo attivato un cluster di postgresql in due secondi e mezzo su un portatile del 2013!

Veniamo all’errore. Controlliamo il log di PostgreSQL per vederne il dettaglio. Il file è /var/log/postgresql/postgresql-16-linuxday.log e l’errore è:

023-10-14 18:07:33.669 CEST [31102] giuseppe@giuseppe LOG:  il nome utente fornito (giuseppe) e il nome utente autenticato (root) non combaciano
2023-10-14 18:07:33.669 CEST [31102] giuseppe@giuseppe FATALE:  Autenticazione Peer fallita per l'utente "giuseppe"
2023-10-14 18:07:33.669 CEST [31102] giuseppe@giuseppe DETTAGLI:  Connection matched file "/etc/postgresql/16/linuxday/pg_hba.conf" line 123: "local   all             all                                     peer"

La parte importante è la prima riga: è stato fatto l’accesso fornendo il nome utente giuseppe, ma la named pipe è stata scritta dall’utent root (perché il nostro proxy è attivo come utente root) e l’autenticazione era di tipo peer. È giusto: la configurazione predefinita di PostgreSQL, quando si accede tramite pipe, è proprio quella che prevedere di fidarsi dell’utente che apre la pipe controllandone uid e gid.

Allora cambiamo l’autenticazione da peer a scram-sha-256 nel file /etc/postgresql/16/linuxday/pg_hba.conf alla riga 123 indicata nel messaggio d’errore. Anzi, ne aggiungiamo una, subito prima, che valga solo per l’utente giuseppe:

# TYPE  DATABASE  USER      ADDRESS    METHOD
local   all       giuseppe             scram-sha-256

facciamo rileggere il file pg_hba.conf a PostgreSQL e poi riproviamo:

$ sudo pg_ctlcluster 16 linuxday reload
$ time psql --host localhost --port 5434 -d postgres -c "select version();"
Inserisci la password per l'utente giuseppe:
                        version
PostgreSQL 16.0 (Debian 16.0-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
(1 riga)

real	0m1,807s
user	0m0,045s
sys	0m0,004s

FUNZIONA!

Adesso, molto emozionati, ci rendiamo conto che siamo a metà strada: si potrà fare in modo che systemd spenga PostgreSQL dopo un po’ che non è usato?

Pare che questo non sia ancora possibile se si utilizza il proxy: https://github.com/systemd/systemd/issues/2106