Comprendere le Dipendenze di Systemd: Prevenire e Risolvere i Conflitti tra Unità

Scopri come funzionano dipendenze, ordinamento, target e conflitti di systemd per garantire l'avvio affidabile dei servizi e semplificare il debug degli errori.

Comprendere le Dipendenze di Systemd: Prevenire e Risolvere i Conflitti tra Unità

Le dipendenze di systemd sono facili da capire a metà. Un file di unità contiene Requires=postgresql.service, ma l'applicazione si avvia comunque troppo presto, e tutti si chiedono perché systemd abbia ignorato la dipendenza. Non ha ignorato nulla. Requires= e After= rispondono a domande diverse.

Questa distinzione è il cuore della maggior parte dei problemi con le dipendenze di systemd. Una direttiva controlla se un'altra unità viene inclusa nella stessa transazione. Una direttiva diversa controlla l'ordinamento. Altre direttive collegano il comportamento di spegnimento, legano la durata di vita di un'unità a un'altra o rendono due unità mutuamente esclusive. Una volta separati questi concetti, i conflitti tra unità diventano molto meno misteriosi.

Le Basi: Direttive per le Dipendenze delle Unità di Systemd

Systemd utilizza direttive specifiche all'interno dei file di unità (tipicamente situati in /etc/systemd/system/ o /lib/systemd/system/) per stabilire quando un'unità deve avviarsi, fermarsi o attendere un'altra. Comprendere queste direttive è il primo passo per gestire correttamente le dipendenze.

Direttive Principali per le Dipendenze

Queste direttive controllano le relazioni tra le unità. Di per sé, non sempre controllano l'ordine di avvio:

  • Requires=:
    • Stabilisce una dipendenza forte. Se l'unità richiesta non si avvia, anche l'unità corrente fallirà.
    • Non implica PartOf= e non significa automaticamente "avvia dopo questa unità".
  • Wants=:
    • Una dipendenza debole. Se l'unità desiderata fallisce, l'unità corrente tenterà comunque di avviarsi. Viene utilizzata per dipendenze opzionali.
  • BindsTo=:
    • Simile a Requires=, ma più forte per quanto riguarda l'arresto. Se l'unità legata si ferma (per qualsiasi motivo), anche l'unità corrente viene fermata.
  • PartOf=:
    • Indica che l'unità corrente è una parte subordinata di un'altra unità (ad esempio, una specifica attivazione tramite socket relativa a un servizio principale). Se l'unità superiore si ferma, anche l'unità subordinata si ferma.
  • Conflicts=:
    • Dice che due unità non dovrebbero essere attive contemporaneamente. Avviarne una fa sì che systemd fermi l'altra come parte della stessa transazione.

Direttive Principali per la Sincronizzazione dell'Avvio

Queste direttive stabiliscono quando l'unità dipendente deve avviarsi rispetto all'unità richiesta:

  • After=:
    • Specifica che il job di avvio dell'unità corrente è ordinato dopo il job di avvio dell'unità elencata. Di per sé non include quell'unità.
  • Before=:
    • Specifica che l'unità corrente deve avviarsi prima dell'unità elencata.

Buona Pratica: Per il tipico ordinamento di avvio dei servizi, Wants= combinato con After= è il pattern più comune e sicuro. Requires= dovrebbe essere riservato alle dipendenze in cui il fallimento della dipendenza deve causare il fallimento del servizio dipendente.

Esempio: Definire le Dipendenze in un File di Servizio

Considera un servizio applicativo personalizzato, myapp.service, che deve comunicare con un database gestito da PostgreSQL (postgresql.service).

# /etc/systemd/system/myapp.service
[Unit]
Description=My Custom Application

# Assicura che PostgreSQL sia in esecuzione prima di tentare di avviarmi
Requires=postgresql.service
After=postgresql.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

Questo esempio è intenzionalmente rigoroso. Se PostgreSQL non può avviarsi, anche myapp.service deve fallire. Per un servizio che può funzionare in modalità degradata senza database, usa Wants=postgresql.service con lo stesso ordinamento After=postgresql.service. L'applicazione chiede comunque a systemd di avviare prima PostgreSQL, ma è autorizzata a continuare se PostgreSQL non è disponibile.

Non c'è vergogna nello scegliere la relazione più debole quando corrisponde alla realtà. Un esportatore di metriche, un log shipper o un cache warmer possono essere utili quando la loro dipendenza è presente, ma non dovrebbero bloccare l'avvio del sistema principale. Le dipendenze forti sono meglio riservate ai casi in cui avviare senza l'altra unità sarebbe sbagliato o pericoloso.

Per le applicazioni di rete, fai più attenzione. network.target spesso significa che lo stack di rete di base è presente, non che DHCP sia completato o che DNS sia utilizzabile. Se la tua applicazione ha davvero bisogno di una rete configurata all'avvio, usa:

[Unit]
Wants=network-online.target
After=network-online.target

Poi conferma che la tua distribuzione abbia il servizio wait-online corrispondente abilitato, come systemd-networkd-wait-online.service o l'unità wait-online di NetworkManager. Senza questo, network-online.target potrebbe non attendere ciò che pensi attenda.

Diagnosticare i Problemi di Dipendenza

Quando un servizio non si avvia, systemd di solito fornisce informazioni sufficienti nei log, ma le catene di dipendenze possono oscurare la causa principale. Ecco strumenti e comandi essenziali per la risoluzione dei problemi.

1. Controllare lo Stato dell'Unità e i Log

Il punto di partenza fondamentale è controllare lo stato del servizio e rivedere i suoi log immediatamente dopo un tentativo di avvio fallito.

# Controlla lo stato generale, che spesso menziona fallimenti di dipendenza
systemctl status myapp.service

# Visualizza i log dettagliati specificamente relativi all'unità
journalctl -u myapp.service --since "5 minutes ago"

2. Analizzare l'Albero delle Dipendenze

Systemd fornisce potenti strumenti di visualizzazione per vedere esattamente cosa sta aspettando cosa.

systemctl list-dependencies

Questo comando mostra le unità richieste o desiderate dall'unità specificata, attraversando l'intera catena di dipendenze.

Per vedere cosa myapp.service richiede per avviarsi:

# Dipendenze in avanti (cosa deve avviarsi prima di me)
systemctl list-dependencies --after myapp.service

# Dipendenze inverse (cosa dipende da me)
systemctl list-dependencies --before myapp.service

Usa --plain quando la formattazione ad albero è d'intralcio e aggiungi --all quando hai bisogno di vedere anche le unità inattive:

systemctl list-dependencies --plain --all myapp.service

systemd-analyze dot

Per domande più ampie sulle dipendenze, genera un grafico e ispezionalo visivamente:

systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg

Su un server senza Graphviz installato, l'output di testo è comunque utile perché puoi cercare i nomi delle unità e vedere quali archi conosce systemd. Per problemi di temporizzazione all'avvio, systemd-analyze critical-chain myapp.service è spesso più facile da leggere di un grafico completo.

3. Rilevare Conflitti e Problemi di Ordinamento

I conflitti di dipendenza spesso si manifestano come servizi che falliscono perché avviati troppo presto o che si fermano inaspettatamente.

Dipendenze Circolari: Questo è il conflitto più pericoloso, in cui l'Unità A richiede B e l'Unità B richiede A. Systemd tenta di risolverlo, ma spesso risulta in una o entrambe le unità che rimangono in uno stato failed o activating indefinitamente.

Per trovare potenziali problemi che interessano l'intero sistema, puoi cercare nei log messaggi di errore specifici relativi all'ordinamento:

journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"

Esegui anche systemd-analyze verify sulle unità personalizzate prima di presumere che il comportamento a runtime sia il problema:

systemd-analyze verify /etc/systemd/system/myapp.service

Può segnalare cicli di ordinamento, direttive sconosciute e riferimenti a unità non validi in anticipo. Non dimostrerà che il tuo progetto è corretto, ma può salvarti dal inseguire un errore di battitura come se fosse un problema di dipendenza complesso.

Risolvere i Problemi Comuni di Dipendenza

Una volta identificati, i problemi di dipendenza possono essere risolti modificando le direttive nei file di unità pertinenti.

Scenario 1: Il Servizio si Avvia Prima che il Suo Prerequisito sia Pronto

Sintomo: I log della tua applicazione mostrano errori di connessione al database, ma postgresql.service appare active in systemctl status.

Diagnosi: Potrebbe mancare After=postgresql.service al servizio, oppure PostgreSQL potrebbe essere attivo prima che il database specifico, il socket, le credenziali o lo schema di cui la tua applicazione ha bisogno siano pronti. Systemd può ordinare le unità, ma non può capire automaticamente ogni condizione di prontezza a livello di applicazione.

Soluzione: Inizia con la semplice relazione tra unità:

[Unit]
Requires=postgresql.service
After=postgresql.service

Se l'applicazione è ancora in competizione con il database, risolvi il problema di prontezza al livello giusto. Alcuni servizi supportano Type=notify e segnalano la prontezza solo dopo l'inizializzazione. Alcune applicazioni necessitano di logica di ripetizione perché le dipendenze possono riavviarsi in qualsiasi momento, non solo durante l'avvio. Per un controllo locale ristretto, un comando ExecStartPre= può essere ragionevole:

[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp

Usa questo tipo di precontrollo con parsimonia. Se si trasforma in uno script shell con sleep e loop, probabilmente l'applicazione necessita di un comportamento di ripetizione appropriato.

Evita sleep 30 come soluzione per le dipendenze. Potrebbe nascondere la competizione su una macchina di sviluppo tranquilla e fallire di nuovo su storage più lento, una VM occupata o un host in attesa di DNS. Una direttiva di ordinamento reale, una notifica di prontezza, un'attivazione tramite socket o un ciclo di ripetizione dell'applicazione ti danno un motivo per cui il servizio è pronto, invece di una speranza che sia passato abbastanza tempo.

Scenario 2: Ordine di Avvio/Arresto Conflittuale

Sintomo: L'arresto del sistema causa il blocco o il fallimento improvviso di processi critici.

Diagnosi: Questo spesso indica un uso improprio di BindsTo= o un'interazione complessa tra le direttive Before= e After= in servizi fratelli.

Soluzione: Rivedi i servizi che sono fratelli (ad esempio, servizi avviati dallo stesso target). Assicurati che se il Servizio A deve essere eseguito mentre il Servizio B è in esecuzione, usi BindsTo= o Requires=. Se il Servizio A deve completare le sue attività prima che il Servizio B inizi la pulizia, verifica che l'ordine After= sia corretto.

Ricorda che l'ordine di spegnimento è l'inverso dell'ordine di avvio. Se app.service ha After=database.service, allora allo spegnimento systemd ferma app.service prima di database.service. Di solito è quello che vuoi: l'applicazione smette di accettare lavoro prima che il database scompaia. Molti bug di spegnimento derivano da un ordinamento di avvio mancante, non da un'impostazione separata solo per lo spegnimento.

Scenario 3: Rimuovere Dipendenze Non Necessarie

Sintomo: L'avvio del sistema è lento perché servizi non necessari vengono inclusi nella catena di avvio.

Diagnosi: Potresti aver usato Requires= quando era necessaria solo una connessione opzionale.

Soluzione: Cambia Requires= in Wants=. Se il servizio non ha assolutamente bisogno della dipendenza per funzionare, Wants= permette al sistema di procedere anche se la dipendenza fallisce o è mascherata.

# Prima (Troppo Restrittivo)
Requires=optional_logging.service

# Dopo (Meglio)
Wants=optional_logging.service
After=optional_logging.service

Questo è particolarmente utile per agenti di monitoraggio, esportatori di metriche, helper sidecar e cache locali opzionali. Se il servizio principale può fare lavoro utile senza di loro, Requires= trasforma un'interruzione parziale in un'interruzione totale. Usa dipendenze strette per cose che sono veramente necessarie: un database locale per un'applicazione che non può avviarsi senza, un punto di montaggio che contiene i dati dell'applicazione o un'unità socket che è l'unico percorso di attivazione supportato.

Scenario 4: Un Mount o un Dispositivo Non è Pronto

Sintomo: Un servizio fallisce all'avvio con No such file or directory, ma il percorso esiste dopo che hai effettuato l'accesso. Questo accade spesso con servizi che leggono da /mnt/data, /srv/app, dischi rimovibili, volumi crittografati o filesystem di rete.

Diagnosi: Il servizio si avvia prima che l'unità di mount sia attiva, oppure il mount è opzionale e fallito senza fermare il servizio.

Soluzione: Trova il nome dell'unità di mount:

systemd-escape -p --suffix=mount /mnt/data

Per /mnt/data, di solito produce mnt-data.mount. Quindi ordina il tuo servizio dopo di esso e richiedilo se il servizio non può funzionare senza i dati:

[Unit]
Requires=mnt-data.mount
After=mnt-data.mount

Se il mount proviene da /etc/fstab, opzioni come nofail, x-systemd.automount e _netdev influenzano il comportamento all'avvio. Non aggiungere un Requires= stretto a un mount opzionale a meno che tu non voglia davvero che quel fallimento del mount blocchi il servizio.

Scenario 5: Un Target Include Troppo

Sintomo: Abilitare un servizio sembra avviare unità non correlate, o disabilitare un servizio non lo ferma dall'apparire durante l'avvio.

Diagnosi: Il servizio potrebbe essere incluso da un target tramite [Install] WantedBy=..., da un Wants= di un altro servizio, o da un'unità socket, timer, path o mount. enable crea collegamenti simbolici per l'attivazione all'avvio; non è l'unico modo in cui un'unità può avviarsi.

Soluzione: Ispeziona sia l'unità che le dipendenze inverse:

systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp

Se un'unità socket attiva il servizio, disabilitare solo myapp.service potrebbe non essere sufficiente. Potrebbe essere necessario disabilitare o mascherare anche myapp.socket, a seconda del comportamento desiderato.

Applicare le Modifiche e Ricaricare

Ogni volta che modifichi un file di unità, devi istruire systemd a ricaricare la sua configurazione prima di testare le modifiche.

# 1. Ricarica la configurazione del manager systemd
sudo systemctl daemon-reload

# 2. Riavvia il servizio interessato
sudo systemctl restart myapp.service

# 3. Verifica lo stato
systemctl status myapp.service

Una Piccola Lista di Controllo Mentale

Quando un problema di dipendenza sembra intricato, riducilo a pochi controlli semplici.

Primo, chiediti se l'altra unità dovrebbe essere avviata affatto. Se sì, usa Wants= o Requires=. Se il servizio corrente può funzionare senza di essa, preferisci Wants=. Se il fallimento di quell'unità significa che questo servizio deve fallire, usa Requires=.

Secondo, chiediti se l'ordine di avvio è importante. Se sì, aggiungi After= o Before=. Non aspettarti che le direttive di dipendenza gestiscano l'ordinamento da sole.

Terzo, chiediti se l'accoppiamento della durata di vita è importante dopo l'avvio. Se un'unità che si ferma deve fermare l'altra, guarda a BindsTo= o PartOf=. Non usarli con leggerezza; possono far sì che i riavvii di routine si propaghino più del previsto.

Infine, ispeziona ciò che systemd ha effettivamente caricato:

systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts

Quell'output è spesso più utile del file che pensi di aver modificato. Drop-in, unità del fornitore, unità generate, socket, timer e target possono tutti cambiare il comportamento finale.