Risoluzione dei problemi di Systemd: Comprensione delle dipendenze dei servizi e delle direttive di ordinamento

Risolvi i problemi di dipendenza e ordinamento di systemd utilizzando correttamente Requires, Wants, After, Before e i comandi diagnostici.

Risoluzione dei problemi di Systemd: Comprensione delle dipendenze dei servizi e delle direttive di ordinamento

La maggior parte dei bug di dipendenza confusi di systemd deriva dalla confusione tra due concetti separati: requisito e ordine. Requires= e Wants= rispondono alla domanda "quale altra unità dovrebbe essere inclusa?" After= e Before= rispondono alla domanda "quando dovrebbe avviarsi questa unità rispetto a un'altra?" Se ricordi solo una cosa da questa guida, ricorda che After=postgresql.service non avvia PostgreSQL per te. Dice solo che se entrambe le unità vengono avviate, la tua unità dovrebbe attendere fino a quando il job di avvio di PostgreSQL non è stato eseguito.

Questa distinzione è la fonte di molti incidenti del tipo "funziona quando lo avvio manualmente, fallisce dopo il riavvio". Un'app web si avvia prima che il socket del database sia pronto ad accettare connessioni. Un worker si avvia prima che /mnt/jobs sia montato. Un servizio attende network.target e fallisce comunque perché un indirizzo IP non è stato ancora assegnato. Questi non sono problemi esotici di systemd. Sono ipotesi di avvio ordinarie che devono essere rese esplicite.

Direttive di dipendenza principali: Requires e Wants

Systemd utilizza due direttive principali per definire dipendenze dirette tra unità: Requires e Wants. Queste direttive sono inserite nella sezione [Unit] di un file di unità (ad esempio, un file .service).

Requires=

La direttiva Requires= stabilisce una dipendenza forte. Se l'unità A Requires= l'unità B, systemd avvia B quando A viene avviata. Se B non può essere avviata, anche il job di avvio di A fallisce. Se B viene fermata esplicitamente mentre A è in esecuzione, A viene normalmente fermata perché la relazione richiesta non è più valida.

Esempio:

Considera un servizio di applicazione web (myapp.service) che dipende criticamente da un servizio di database (mariadb.service). Il file di unità myapp.service potrebbe includere:

[Unit]
Description=My Web Application
Requires=mariadb.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

In questo scenario, se mariadb.service non si avvia o viene fermato manualmente, systemd fermerà anche myapp.service. Se provi ad avviare myapp.service e mariadb.service non è in esecuzione, systemd tenterà di avviare prima mariadb.service. Se mariadb.service fallisce, myapp.service non si avvierà.

Wants=

La direttiva Wants= definisce una dipendenza più debole e opzionale. Se l'unità A Wants= l'unità B, systemd proverà ad avviare l'unità B quando avvia l'unità A, ma l'unità A si attiverà comunque anche se l'unità B non si avvia o non è in esecuzione. Questo è utile per servizi che beneficiano di un altro servizio ma possono funzionare in modo indipendente, magari con funzionalità ridotte o un avviso.

Esempio:

Supponiamo che un agente di monitoraggio (monitoring-agent.service) possa funzionare senza un servizio di logging specifico (app-logger.service) ma idealmente lo avrebbe disponibile. Il file di unità monitoring-agent.service potrebbe assomigliare a questo:

[Unit]
Description=Monitoring Agent
Wants=app-logger.service

[Service]
ExecStart=/usr/bin/monitoring-agent

[Install]
WantedBy=multi-user.target

Qui, systemd tenterà di avviare app-logger.service quando monitoring-agent.service viene attivato. Tuttavia, se app-logger.service non si avvia, monitoring-agent.service procederà comunque ad avviarsi con successo.

Requires= vs. Wants=

  • Requires=: Dipendenza forte. Se l'unità richiesta fallisce, l'unità dipendente fallisce o si ferma.
  • Wants=: Dipendenza debole. L'unità dipendente tenta di avviare l'unità desiderata ma procede anche se fallisce.

È importante notare che Requires= implica Wants=. Se un'unità ne richiede un'altra, la vuole anche implicitamente.

Direttive di ordinamento: After e Before

Mentre Requires e Wants definiscono cosa deve essere in esecuzione, After e Before definiscono quando le unità dovrebbero essere avviate l'una rispetto all'altra. Queste direttive controllano la sequenza delle operazioni durante il processo di avvio del sistema o quando le unità vengono attivate su richiesta. Sono spesso utilizzate insieme alle direttive di dipendenza.

After=

La direttiva After= specifica che il job di avvio dell'unità corrente dovrebbe essere ordinato dopo i job di avvio delle unità elencate. Di per sé, non include quelle unità nella transazione. Inoltre, non prova che la dipendenza sia logicamente pronta per la tua applicazione; utilizza solo la visione di systemd dello stato di attivazione dell'unità.

Esempio:

Un servizio dipendente dalla rete (custom-network-app.service) dovrebbe avviarsi solo dopo che la rete è completamente configurata. Questo viene tipicamente gestito assicurandosi che si avvii dopo il target di rete (network.target).

[Unit]
Description=Custom Network Application
Requires=network.target
After=network.target

[Service]
ExecStart=/usr/bin/custom-network-app

[Install]
WantedBy=multi-user.target

In questa configurazione, systemd ordinerà custom-network-app.service dopo network.target se entrambi fanno parte della stessa transazione. Per servizi che necessitano di un indirizzo, DNS o una route verso un altro host, network-online.target è spesso più vicino all'intento, ma solo se il servizio wait-online della distribuzione è abilitato e configurato correttamente.

Before=

La direttiva Before= specifica che l'unità corrente dovrebbe essere avviata prima delle unità elencate in Before=. Questo è utile per servizi che devono essere fermati dopo altri durante lo spegnimento, o avviati prima di determinati servizi per fornire loro un ambiente.

Esempio:

Immagina uno scenario in cui un server di posta (postfix.service) deve essere in esecuzione prima di qualsiasi servizio rivolto all'utente che potrebbe inviare email. Potresti usare Before= per assicurarti che postfix.service si avvii presto.

[Unit]
Description=Postfix Mail Transfer Agent
# ... altre direttive come Conflicts=
Before=user-session.target

[Service]
ExecStart=/usr/lib/postfix/master

[Install]
WantedBy=multi-user.target

Questa configurazione tenta di avviare postfix.service prima che qualsiasi cosa faccia parte di user-session.target inizi il suo avvio. Allo stesso modo, durante lo spegnimento, postfix.service sarebbe tra gli ultimi ad essere fermati se ha un corrispondente After=user-session.target.

After= vs. Before=

  • After=: Garantisce che le unità elencate siano attive prima che l'unità corrente si avvii.
  • Before=: Garantisce che l'unità corrente si avvii prima delle unità elencate.

After= e Before= sono complementari. Se l'unità A dice Before=B, l'ordinamento è A prima, poi B. Se l'unità B dice After=A, il risultato è lo stesso. Di solito devi solo esprimere la relazione in un file di unità. Quando modifichi il tuo servizio, di solito è più chiaro dire cosa il tuo servizio deve venire dopo, perché mantiene il ragionamento locale.

Combinare le direttive per configurazioni robuste

Negli scenari del mondo reale, combinerai spesso queste direttive per creare grafi di dipendenza complessi. multi-user.target è un target comune che significa che il sistema è pronto per operazioni multi-utente. Molti servizi sono configurati per essere WantedBy=multi-user.target e After=multi-user.target (o più precisamente, After=basic.target e After=getty.target ecc. da cui multi-user.target dipende).

Un pattern comune:

Un servizio che richiede un database e dovrebbe avviarsi dopo che la rete è configurata potrebbe assomigliare a questo:

[Unit]
Description=My Application Service
Requires=mariadb.service
Wants=other-optional-service.service
After=network.target mariadb.service

[Service]
ExecStart=/usr/local/bin/my_app

[Install]
WantedBy=multi-user.target

Spiegazione del pattern:

  1. Requires=mariadb.service: Garantisce che mariadb.service debba essere in esecuzione affinché my_app.service funzioni. Se mariadb.service fallisce, my_app.service si fermerà.
  2. Wants=other-optional-service.service: Tenta di avviare other-optional-service.service ma my_app.service procederà anche se fallisce.
  3. After=network.target mariadb.service: Ordina my_app.service dopo il target di rete e il job di avvio di MariaDB. Se l'app deve connettersi tramite TCP a un database su un altro host, utilizza la configurazione network-online appropriata invece di assumere che network.target significhi "la rete è utilizzabile".
  4. WantedBy=multi-user.target: Quando abilitato (systemctl enable my_app.service), questa direttiva aggiunge un collegamento simbolico in modo che my_app.service venga avviato quando il sistema raggiunge lo stato multi-user.target.

Considerazioni avanzate e migliori pratiche

  • WantedBy vs. RequiredBy: Simile a Wants vs. Requires, WantedBy è un ordinamento debole e RequiredBy è un ordinamento forte. La maggior parte dei servizi utilizza WantedBy=multi-user.target.
  • Conflicts=: Questa direttiva specifica unità che non dovrebbero essere in esecuzione contemporaneamente all'unità corrente. Se l'unità corrente viene avviata, le unità in conflitto verranno fermate e viceversa.
  • Dipendenze transitive: Le dipendenze sono transitive. Se A richiede B e B richiede C, allora A richiede indirettamente C. Systemd gestisce automaticamente queste catene.
  • Direttive Condition*=: Utilizza ConditionPathExists=, ConditionFileNotEmpty=, ConditionVirtualization=, ecc., per rendere l'attivazione dell'unità condizionale in base allo stato del sistema, migliorando ulteriormente la robustezza.
  • Usa systemctl list-dependencies <unità>: Questo comando è prezioso per visualizzare l'albero delle dipendenze di un'unità, incluse le dipendenze dirette e indirette.
  • Usa systemctl status <unità>: Controlla sempre lo stato del tuo servizio dopo aver apportato modifiche alla configurazione. Spesso mostrerà le ragioni del fallimento, inclusi i problemi di dipendenza.
  • Evita dipendenze circolari: Sebbene systemd cerchi di risolverle, le dipendenze circolari dirette (A Requires B, B Requires A) possono portare a loop di avvio o fallimenti. Progetta attentamente le tue dipendenze per evitarlo.

Un passaggio pratico di debug

Quando si presenta un bug di dipendenza, inizia esaminando la transazione che systemd ha effettivamente costruito:

systemctl status my_app.service
journalctl -b -u my_app.service --no-pager
systemctl list-dependencies my_app.service
systemctl show my_app.service -p Wants -p Requires -p After -p Before -p BindsTo -p PartOf

systemctl status ti dice se systemd non è riuscito ad avviare l'unità, l'ha uccisa in seguito o la considera attiva anche se l'applicazione non è sana. journalctl -b ti mantiene all'interno del boot corrente, il che è importante perché i problemi di dipendenza sono spesso solo di boot. systemctl show è diretto ma utile: mostra le proprietà finali dell'unità unite dopo l'applicazione di drop-in, file del fornitore e dipendenze generate.

Se non sei sicuro da dove provenga una dipendenza, ispeziona l'unità completa:

systemctl cat my_app.service

Questo mostra l'unità fornita e qualsiasi file di override sotto /etc/systemd/system/my_app.service.d/. Ho visto servizi di produzione in cui l'unità di base era corretta ma un vecchio override conteneva ancora After=mysql.service da una migrazione precedente. Il servizio stava aspettando un'unità che non esisteva più e i log facevano sembrare che l'applicazione fosse rotta.

Per domande sui tempi di boot, usa:

systemd-analyze critical-chain my_app.service
systemd-analyze blame

critical-chain è meglio che fissare i timestamp perché mostra quali unità hanno ritardato il percorso verso il tuo servizio. blame può essere fuorviante se lo tratti come una classifica di servizi "cattivi", ma è utile quando una dipendenza richiede molto più tempo del previsto.

Pattern che reggono in produzione

Per una dipendenza da database locale, questo è un punto di partenza ragionevole:

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

[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure

[Install]
WantedBy=multi-user.target

Questo dice che PostgreSQL dovrebbe essere avviato con l'API e l'avvio dell'API dovrebbe essere ordinato dopo PostgreSQL. Non garantisce che ogni migrazione sia stata completata o che il database accetti le credenziali esatte utilizzate dalla tua app. Se questo è importante, aggiungi un controllo di prontezza a livello di applicazione. Systemd può ordinare i processi, ma non può capire lo stato del tuo schema a meno che non insegni al tuo servizio a verificarlo.

Per un percorso montato, preferisci RequiresMountsFor= rispetto a nominare manualmente le unità di mount:

[Unit]
RequiresMountsFor=/srv/uploads

[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads

Systemd deriverà le unità di mount necessarie per il percorso. Questo è più facile da mantenere che ricordare che /srv/uploads corrisponde a srv-uploads.mount.

Per helper opzionali, usa Wants=:

[Unit]
Wants=metrics-agent.service
After=metrics-agent.service

Se l'agente delle metriche fallisce, il servizio principale può comunque avviarsi. Questo è di solito ciò che desideri per sidecar di logging, esportatori opzionali e helper di notifica locali. Non usare Requires= solo perché due servizi sono correlati. Usalo solo quando il servizio dipendente non può davvero svolgere un lavoro utile senza l'altra unità.

Per servizi strettamente accoppiati che dovrebbero fermarsi insieme, guarda BindsTo= e PartOf=. BindsTo= è più forte di Requires= ed è utile quando un servizio dovrebbe scomparire se l'unità associata scompare, come un servizio legato a una specifica unità dispositivo. PartOf= è spesso utile per i gruppi: riavviare o fermare l'unità padre può propagarsi alle unità figlie. Queste non sono direttive di prima scelta, ma risolvono problemi che Requires= non può esprimere in modo pulito.

Trappole comuni

Non aggiungere After=multi-user.target a un normale servizio a lunga esecuzione che è abilitato con WantedBy=multi-user.target. Questo spesso crea un ordinamento strano e raramente dice ciò che l'autore intendeva. La maggior parte dei servizi viene inclusa da multi-user.target; non hanno bisogno di avviarsi dopo che è già stato raggiunto.

Non assumere che network.target significhi "internet è raggiungibile". È un punto di sincronizzazione per la gestione della rete, non un test di connettività. Se la tua applicazione comunica con un'API remota, aggiungi comunque la logica di retry all'interno dell'applicazione. L'ordinamento di rete al boot riduce il rumore, ma non può proteggerti da fallimenti DNS, cambiamenti di routing o una dipendenza remota non disponibile.

Non nascondere lunghi sleep in ExecStartPre=/bin/sleep 30 a meno che tu non abbia un'opzione migliore. Gli sleep rendono i boot più lenti quando la dipendenza è pronta rapidamente e falliscono comunque quando la dipendenza richiede più tempo del previsto. Un piccolo ciclo di prontezza che controlla il socket, il file o l'API effettivi è di solito più chiaro.

Quando modifichi le direttive di dipendenza, esegui systemctl daemon-reload, riavvia il servizio e controlla il journal dallo stesso boot. La correzione più veloce è di solito quella che dimostra l'esatta ipotesi di ordinamento che era sbagliata.