SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

Perché systemd non ha riavviato il tuo servizio

Restart= controlla solo il processo principale: un figlio può morire nello stesso cgroup senza attivare il riavvio. Scopri Type=, limiti e journal.

La risposta breve: le policy di riavvio di systemd monitorano un solo processo

Le policy di riavvio di systemd monitorano un processo per unit: il processo principale. Restart= legge lo stato di uscita solo di quel processo. Un control group dell'unit può contenere venti processi; uno di questi può terminare, mentre l'unit resta active (running) perché il processo principale è ancora attivo. Per systemd non si è verificato alcun errore, quindi non viene riavviato nulla.

systemd conosce anche gli altri processi. Li termina quando l'unit si arresta, conta il loro consumo di memoria nei limiti dell'unit, applica loro la quota CPU dell'unit e li mostra in systemctl status. Tuttavia, non legge mai il loro stato di uscita. La logica di riavvio e il cgroup sono due elementi distinti e gran parte di questa guida riguarda la differenza tra i due.

Cosa contiene il cgroup e cosa legge la logica di riavvio

Un cgroup (control group) è un oggetto del kernel che gestisce un insieme di processi. Ogni unità di servizio ne riceve uno, con il nome dell'unità. Un processo non può uscirne. I processi figli ereditano il cgroup del processo padre e un processo senza privilegi non può spostarsi altrove. Per questo systemd può eliminare un demone che esegue due fork, cosa che i vecchi script init non potevano fare in modo affidabile.

Osserva i due aspetti affiancati:

systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.service

systemd-cgls elenca ogni processo dell'unità. MainPID è l'unico valore che la policy di riavvio legge. Quando questi due elementi non corrispondono al modello mentale, la differenza è il problema. MainPID=0 è peggio di un PID errato: significa che systemd non sta monitorando nulla, quindi nessun valore Restart= può mai attivarsi.

Esiste un'eccezione reale alla regola del processo principale. Se l'out-of-memory killer del kernel termina un processo all'interno del cgroup dell'unità, systemd lo rileva perché monitora il file memory.events del cgroup. OOMPolicy= determina cosa accade dopo e il valore predefinito è stop: l'intera unità viene arrestata, il risultato viene registrato come oom-kill e questo viene considerato un errore, quindi Restart=on-failure si attiva. Il journal lo indica chiaramente.

myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.

Quindi, se un processo figlio viene terminato per esaurimento della memoria, l'unità viene arrestata; se lo stesso processo figlio termina a causa di un segmentation fault, invece, l'unità non viene arrestata. Se imposti limiti di memoria per un'unità, leggi come MemoryMax e CPUQuota si applicano al cgroup dell'unità prima di configurare la policy di riavvio, perché queste due funzionalità interagiscono proprio in questo punto e in nessun altro.

Come Type=seleziona il processo principale

Type= nella sezione [Service] non riguarda soltanto l'ordine di avvio. È la regola che determina quale PID (process ID) diventa MainPID, cioè determina anche ciò che Restart= può vedere.

  • Type=simple è il valore predefinito. Il processo che systemd crea tramite fork da ExecStart= è il processo principale. systemd contrassegna l'unità come avviata immediatamente, prima di sapere se exec è stato eseguito correttamente. Un errore di battitura nel percorso del binario produce un job di avvio che termina correttamente, seguito da Main process exited, code=exited, status=203/EXEC un momento dopo.
  • Type=exec si comporta come simple, ma il job di avvio attende che exec sia terminato correttamente. In questo modo l'errore descritto sopra diventa un errore di avvio effettivo. Richiede systemd 240 o versioni successive, disponibili in ogni distribuzione supportata. Preferitelo a simple.
  • Type=forking prevede che il processo avviato da ExecStart= crei un daemon in background e termini. systemd attende la terminazione del processo padre, quindi cerca il daemon effettivo. Specificare PIDFile=. Senza questo parametro, GuessMainPID= (attivo per impostazione predefinita) funziona soltanto quando nel cgroup rimane esattamente un processo. Se ne rimangono due, MainPID resta 0.
  • Type=notify indica che il servizio chiama sd_notify(3) e invia READY=1 quando è pronto a gestire il traffico. Può inoltre inviare MAINPID= per indicare a systemd un altro processo da monitorare. Per impostazione predefinita, NotifyAccess= è main, quindi una notifica inviata da un processo figlio viene ignorata e il journal indica il PID da cui proviene.
  • Type=oneshot non ha un processo principale persistente. L'unità diventa inattiva non appena termina ExecStart=, a meno che non si imposti RemainAfterExit=yes. In questo caso Restart=always e Restart=on-success vengono rifiutati, con il messaggio Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Gli altri valori, incluso on-failure, sono accettati.

È utile memorizzare due errori Type=forking, perché ciascuno lascia un'unità apparentemente non funzionante senza una causa visibile:

myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.

Il primo indica che il daemon scrive il proprio file PID in un'altra posizione oppure lo scrive dopo che systemd lo ha cercato. Il secondo indica che il file PID identifica un processo esterno al cgroup dell'unità. systemd rifiuta di adottarlo, perché altrimenti un file PID scrivibile potrebbe consentire di fare in modo che systemd invii segnali a qualsiasi processo presente nel sistema.

Perché uno script wrapper nasconde la terminazione dei processi figli

Ecco la struttura che porta alla domanda del titolo.

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait

L'unità è Type=simple, quindi il processo principale è la shell. wait senza argomenti restituisce il controllo solo dopo la terminazione di tutti i processi figli. Terminate il worker: la shell continua ad attendere il processo web, quindi la shell non termina, MainPID non termina e Restart= non viene mai consultato. Il cgroup contiene ora un processo in meno, systemctl status stampa l'albero più corto e l'unità è ancora active (running). systemd non controlla l'albero per rilevare queste modifiche.

Una seconda variante dello stesso errore è più silenziosa:

ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'

Il processo principale è la shell, non myapp. Su systemctl stop, systemd invia SIGTERM al processo principale, ma una shell in attesa di un processo figlio in primo piano non inoltra il segnale. L'arresto richiede quindi l'intero TimeoutStopSec, 90 secondi per impostazione predefinita, e termina così:

myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.

La correzione è exec. Scrivete exec /usr/local/bin/myapp: la shell viene sostituita dal programma, quindi MainPID è il programma e i segnali lo raggiungono. Meglio ancora, eliminate la shell e usate Environment= o EnvironmentFile= nell'unità. Questo problema non si manifesta quando la stringa -c contiene un solo comando, perché bash e dash ottimizzano entrambi questo caso eseguendo direttamente exec. Aggiungete un secondo comando alla stringa e la shell rimane in esecuzione davanti al programma.

Riproduzione su una VPS di test in due minuti

Salvate il wrapper precedente come /usr/local/bin/two-children.sh, rendetelo eseguibile con chmod +x e sostituite i due percorsi dei programmi con sleep 3600. Collegate un'unità allo script con Type=simple e Restart=on-failure, quindi eseguite systemctl daemon-reload e avviatela. Eseguite systemd-cgls --unit two-children.service e prendete nota dei tre PID: la shell e i due processi figli. Terminate un processo figlio con sudo kill <pid>. Controllate nuovamente l'unità. L'albero contiene un processo in meno, lo stato è ancora active (running) e il journal non contiene nuovi messaggi. Ora eseguite sudo kill -9 <shell pid>. L'unità entra nello stato di errore, il processo figlio rimasto viene terminato perché KillMode=control-group è il valore predefinito e il journal mostra Scheduled restart job, restart counter is at 1.

Il vocabolario completo di Restart= e quando on-failure è preferibile a always

Restart= accetta uno dei sette valori disponibili. La distinzione fondamentale riguarda ciò che viene considerato un’uscita corretta. systemd considera corretti il codice di uscita 0, tutti i codici elencati in SuccessExitStatus= e i segnali SIGHUP, SIGINT, SIGTERM e SIGPIPE. Tutto il resto, inclusi SIGKILL e SIGSEGV, è considerato un’uscita anomala.

  • no è il valore predefinito. L’unità non viene mai riavviata automaticamente. Per questo un’unità priva della direttiva Restart= si arresta al primo crash e rimane inattiva.
  • on-success riavvia l’unità solo dopo un’uscita corretta.
  • on-failure riavvia l’unità dopo un codice di uscita diverso da zero, un segnale anomalo, il superamento del timeout di avvio o arresto oppure la scadenza del watchdog.
  • on-abnormal riavvia l’unità dopo un segnale anomalo, un timeout o la scadenza del watchdog, ma mai dopo un semplice codice di uscita diverso da zero.
  • on-abort riavvia l’unità solo dopo un segnale anomalo, cioè in caso di crash.
  • on-watchdog riavvia l’unità solo quando scade WatchdogSec=.
  • always riavvia l’unità dopo ogni caso precedente, inclusa un’uscita corretta con stato 0.

on-failure è il valore predefinito appropriato per un demone a esecuzione continua. Riavvia il servizio dopo un crash, ma lascia invariato un exit 0 deliberato. always è adatto a un programma che termina correttamente per motivi indipendenti dal servizio, ad esempio un client tunnel che restituisce 0 quando l’endpoint remoto si disconnette. always può però nascondere i bug: un servizio che si avvia, legge un file di configurazione non valido, registra l’errore e termina con codice 0 entrerà in un ciclo infinito. L’unico indizio sarà l’aumento del contatore dei riavvii.

SuccessExitStatus= modifica la distinzione tra uscite corrette e anomale. Borg restituisce 1 per gli avvisi e 2 per gli errori. Di conseguenza, un’unità di backup priva di SuccessExitStatus=1 viene contrassegnata come fallita ogni volta che salta un file non leggibile. RestartPreventExitStatus= elenca i codici che impediscono il riavvio anche con always. È il modo corretto con cui un programma può indicare che non deve essere riavviato. RestartForceExitStatus= fa l’opposto. Un’attività di backup dovrebbe essere eseguita da un’unità Type=oneshot avviata da un timer, non da un ciclo di riavvio. La coppia di servizio e timer che esegue un’attività secondo una pianificazione è il modello da adottare.

Un’avvertenza sui test. Terminare il servizio con un semplice kill <pid> invia SIGTERM, che fa parte dell’elenco delle uscite corrette. Di conseguenza Restart=on-failure non esegue correttamente alcuna azione e si può concludere erroneamente che la configurazione sia danneggiata. Usare invece kill -9 <pid> o systemctl kill -s SIGKILL myapp.service. Ricordare inoltre che nessun valore di Restart= viene applicato dopo systemctl stop oppure quando l’unità è stata arrestata perché una dipendenza BindsTo= o PartOf= non è più disponibile. Un’operazione di arresto non indica un errore.

RestartSec e il valore predefinito di 100 millisecondi

RestartSec= è l'intervallo tra l'arresto dell'unità e il nuovo avvio da parte di systemd. Il valore predefinito è 100 millisecondi. Verifica il valore effettivamente caricato dall'unità:

systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.service

Se l'unità non ha impostato questo parametro, viene restituito RestartUSec=100ms. Questo valore predefinito è adeguato per un servizio che si arresta una volta e poi torna operativo. Non è adeguato per un servizio che non riesce ad avviarsi, perché cinque riavvii avvengono in meno di mezzo secondo. Questo attiva esattamente il limite di frequenza descritto nella sezione successiva. Per i servizi che attendono un database, un mount o una route di rete, imposta RestartSec=5s o un valore superiore.

Ad agosto 2026, systemd 254 e versioni successive offrono anche RestartSteps= e RestartMaxDelaySec=, che aumentano il ritardo da RestartSec= fino a un limite massimo nell'arco del numero di tentativi specificato. Ubuntu 24.04 include systemd 255 e supporta questi parametri. Debian 12 include systemd 252 e non li supporta. I ritardi crescenti sono la soluzione corretta quando la dipendenza potrebbe rimanere indisponibile a lungo.

Cosa significa realmente “start request repeated too quickly”

Questo è lo stato che porta a pensare che systemd rinunci in modo arbitrario. Si tratta di un contatore. La regola è la seguente: se un'unità viene avviata più di StartLimitBurst= volte entro StartLimitIntervalSec=, systemd rifiuta di avviarla nuovamente e la porta nello stato failed. I valori predefiniti sono 5 avvii in 10 secondi.

Il journal mostra la sequenza:

myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.

e systemctl start fornisce già la correzione:

Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.

systemctl reset-failed myapp.service azzera il contatore e lo stato failed. Nient'altro svolge questa operazione, quindi un semplice systemctl start continua a essere rifiutato finché non lo si esegue. Anche gli avvii manuali contribuiscono al limite: alcune esecuzioni impazienti di systemctl restart mentre si modifica un file di configurazione possono quindi farlo scattare, senza che si verifichi alcun crash.

L'aspetto fuorviante è il seguente: start-limit-hit non indica mai perché il servizio non riusciva ad avviarsi. Indica soltanto che il servizio è fallito ripetutamente e in rapida successione. La causa reale si trova nelle righe precedenti del journal.

Entrambe le impostazioni appartengono alla sezione [Unit]. Si trovano esempi che le inseriscono in [Service], una posizione accettata dalle versioni meno recenti di systemd; è da qui che nasce la confusione. Scriverle in [Unit], quindi chiedere a systemd che cosa ha caricato con systemctl show, perché conta soltanto il valore effettivamente caricato.

[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10s

In questo modo l'unità dispone di cinque tentativi entro una finestra di cinque minuti prima di rinunciare. StartLimitIntervalSec=0 disabilita completamente il limite. È importante sapere che cosa comporta: un servizio che non può mai avviarsi tenterà ora di farlo all'infinito e scriverà una voce nel journal a ogni tentativo. I valori predefiniti a livello dell'intera macchina si trovano in /etc/systemd/system.conf, come DefaultStartLimitIntervalSec= e DefaultStartLimitBurst=.

È opportuno prestare attenzione a un'impostazione correlata. StartLimitAction= determina che cosa accade quando viene raggiunto il limite e accetta valori tra cui reboot, reboot-force e poweroff. Il valore predefinito è none: l'unità viene portata nello stato failed, senza modificare lo stato della macchina. Su un VPS remoto, poweroff significa avere un sistema che rimane spento finché non si apre la console del provider.

Una procedura per unità: un processo per ogni unità

Questa è la soluzione nella maggior parte dei casi. Se devono essere eseguiti due programmi, scrivete due unità. In questo modo ciascuna unità ha un processo principale effettivo, un vero stato di uscita e una propria politica di riavvio. Ottenete anche log, limiti delle risorse e contatori dei riavvii separati, come è necessario alle tre del mattino.

Definite la relazione tra le unità nei file delle unità, non in uno script shell.

  • After= ordina soltanto l'avvio. Non fornisce alcuna informazione sugli errori.
  • Requires= avvia l'altra unità insieme a questa e arresta questa unità se l'altra viene arrestata esplicitamente.
  • BindsTo= equivale a Requires=, ma include il caso che interessa: questa unità viene arrestata quando l'altra si arresta per qualsiasi motivo, incluso un crash. Abbinatelo a After=, altrimenti l'ordinamento non è definito.
  • PartOf= propaga verso il basso l'arresto e il riavvio, quindi systemctl restart myapp.target raggiunge ogni unità che è PartOf= a essa.
  • Upholds= (systemd 249 e versioni successive, quindi Ubuntu 22.04 e versioni successive) mantiene in esecuzione l'unità indicata: se si arresta, systemd la avvia nuovamente. È soggetta allo stesso limite sulla frequenza di avvio applicato a tutte le altre unità.

Un worker che non deve mai essere eseguito senza il relativo server API e che systemd mantiene attivo ogni volta che l'API è attiva:

# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5s

Il worker non ha una sezione [Install] e non viene mai abilitato manualmente. L'unità API lo include con Upholds=, quindi systemctl enable --now myapp-api.service è l'unico comando da eseguire. Ricaricate la configurazione e verificate come systemd ha gestito la coppia:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.service

systemd-analyze verify non stampa nulla quando il file è corretto. Qualsiasi output indica un problema, di solito una chiave che systemd non riconosce nella sezione in cui è stata scritta oppure una dipendenza da un'unità inesistente.

Correzione due: Type=notify, così systemd conosce più del solo PID

Se il programma supporta il protocollo di notifica di systemd, usalo. Con Type=notify il servizio comunica a systemd quando è pronto. In questo modo l'ordinamento delle unità è effettivo e non dipende da supposizioni. Inoltre, può inviare MAINPID= per indicare a systemd il processo rilevante, invece del processo di avvio.

WatchdogSec= è l'impostazione che giustifica l'intervento. Impostala: il servizio deve inviare WATCHDOG=1 tramite sd_notify(3) almeno con questa frequenza. Quando i messaggi si interrompono, systemd termina il servizio con SIGABRT e lo contrassegna come non riuscito. Di conseguenza, Restart=on-failure o Restart=on-watchdog lo riavvia. Questo è l'unico meccanismo integrato per riavviare un processo ancora attivo ma bloccato. Nessuna policy basata sul codice di uscita può rilevarlo.

[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5s

Nel journal, l'intervento del watchdog compare come myapp.service: Watchdog timeout (limit 30s)!, seguito dalla terminazione del processo. Se invece l'unità rimane in activating (start) fino alla scadenza di TimeoutStartSec, READY=1 non è mai arrivato. Le cause possibili sono due: il programma non supporta il protocollo oppure NotifyAccess=main rifiuta una notifica proveniente da un processo figlio. In quest'ultimo caso, il journal registra entrambi i PID.

Per il software che espone un endpoint HTTP di health check ma non supporta sd_notify, le opzioni corrette sono due: usare una piccola unità timer che interroga l'endpoint e chiama systemctl restart, oppure lasciare il controllo al runtime dei container. È questo lo scopo di health check di Compose e relativo comportamento di riavvio.

Correzione tre: un supervisore all'interno dell'unità, solo quando non c'è alternativa

Alcuni software vengono effettivamente distribuiti come un insieme di processi gestiti da un launcher che non può essere suddiviso. In questo caso si esegue un supervisore all'interno dell'unità e se ne accetta la conseguenza: systemd monitora il supervisore, il supervisore monitora tutti gli altri processi e la politica di riavvio viene quindi definita in due file.

Il caso più comune è rappresentato da un runtime per container. Un'unità docker compose o podman segue esattamente questo modello: la politica di riavvio del singolo container è definita nel file Compose, mentre l'unità systemd mantiene in esecuzione soltanto il runtime. Se questo è il tuo caso, l'unità che avvia uno stack Compose all'avvio mostra la configurazione corretta e spiega anche perché Type=oneshot con RemainAfterExit=yes è in genere la scelta appropriata.

Il cgroup continua a essere utile. Tutto ciò che il supervisore avvia rimane nel cgroup dell'unità, quindi MemoryMax=, CPUQuota= e la pulizia eseguita al momento dell'arresto coprono ancora l'intero albero dei processi. Viene delegata soltanto la decisione sul riavvio.

Qualunque supervisore si scelga, non impostare Restart=always sull'unità esterna e una politica di riavvio aggressiva al suo interno senza valutarne le conseguenze. Due livelli di logica di riavvio, ciascuno con il proprio backoff, possono mantenere il servizio in stato di flapping per diversi minuti e produrre un journal che non spiega la causa.

ExitType=cgroup non significa «riavviare quando termina un processo qualsiasi»

ExitType= (systemd 250 e versioni successive, quindi presente sia in Ubuntu 24.04 sia in Debian 12) è l'impostazione che si trova cercando una soluzione a questo problema, ma fa l'opposto di quanto suggerisce il nome. Il valore predefinito, ExitType=main, indica che il servizio viene considerato arrestato quando termina il processo principale. ExitType=cgroup indica invece che il servizio viene considerato in esecuzione finché non termina l'ultimo processo del cgroup.

Di conseguenza, ExitType=cgroup rende un'unità meno sensibile alla terminazione di un singolo processo, non più sensibile. È l'impostazione corretta per un programma che crea un worker effettivo tramite fork e termina il processo padre senza scrivere un file PID, impedendo a Type=forking di individuare il demone. È invece l'impostazione sbagliata per il problema descritto qui.

Non esiste un valore Restart= con il significato di «riavviare l'unità quando termina un processo qualsiasi del cgroup». Se è necessario questo comportamento, occorre usare un processo per unità. Se non è possibile separare il programma e si controlla lo script wrapper, l'alternativa più vicina è wait -n, che restituisce il controllo non appena termina il primo processo figlio:

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1

La terminazione di un processo figlio ora causa l'arresto del wrapper con uno stato diverso da zero, quindi Restart=on-failure interviene. È un compromesso, non una correzione. Si continua ad avere un solo contatore dei riavvii per due programmi, un unico flusso di log e nessuna possibilità di riavviare autonomamente la parte che ha avuto l'errore.

Come verificare che cosa è successo realmente

Quattro comandi, in questo ordine.

systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precise

systemctl status mostra lo stato, il PID principale e l'albero cgroup in un'unica schermata. Un'unità in condizioni normali mostra Active: active (running), con una riga Main PID: che indica il processo previsto. Se l'albero in fondo elenca processi che non riconosci oppure non include un processo che invece ti aspetti, hai già trovato la risposta.

systemd-cgls --unit stampa lo stesso albero senza troncare l'output. Questo diventa importante quando un'unità gestisce più di una manciata di processi.

systemctl show restituisce dati leggibili dalle macchine. NRestarts= è il contatore dei riavvii ed è il modo più rapido per distinguere un servizio che è stato riavviato quaranta volte da uno attivo dall'avvio del sistema. Result= contiene il motivo dell'ultimo errore: exit-code, signal, timeout, oom-kill, watchdog o start-limit-hit. ExecMainStatus= è lo stato di uscita grezzo dell'ultimo processo principale.

Il journal conserva la sequenza degli eventi. Queste sono le tre righe da cercare:

myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.

code=exited, status=N significa che il programma ha scelto di restituire N, quindi il problema riguarda il programma o la relativa configurazione. code=killed, signal=SEGV significa che il programma è andato in crash. code=killed, signal=TERM indica in genere che un altro componente ha richiesto l'arresto; non è un errore e non attiverà Restart=on-failure. code=dumped significa che il programma ha lasciato un file core, che coredumpctl list mostrerà quando systemd-coredump è installato.

Su più macchine, NRestarts è il valore che conviene raccogliere con una pianificazione. Un'unità il cui contatore aumenta ogni giorno sta fallendo ogni giorno, anche se nessuno se ne è accorto. Quando gestisci più di due o tre server, un metodo coerente per eseguire un comando su ogni server trasforma un'ipotesi in un report.

FAQ

Perché systemctl indica che il servizio è attivo anche se il processo è terminato?

systemd tiene traccia di un processo per ogni unità di servizio, il processo principale, e Restart= legge soltanto il codice di uscita di quel processo. Tutti gli altri processi avviati dall'unità appartengono allo stesso cgroup. systemd termina questi processi quando l'unità si arresta, ma non ne monitora mai l'uscita. Esegui systemctl show -p MainPID myapp.service e confronta il numero con systemd-cgls --unit myapp.service. Se il processo terminato compare nell'albero ma non è MainPID, systemd si è comportato esattamente come previsto. La correzione consiste nell'usare un processo per unità e nel definire la relazione tra le unità tramite BindsTo= e Upholds=.

Che cosa significa "start request repeated too quickly"?

Significa che l'unità è stata avviata più di StartLimitBurst= volte nell'intervallo StartLimitIntervalSec=. Per impostazione predefinita, si tratta di 5 avvii in 10 secondi, quindi systemd ha smesso di riprovare. È un limite di frequenza e non indica il motivo per cui il servizio non riusciva ad avviarsi. Leggi quindi le righe precedenti del journal. Cancella lo stato con systemctl reset-failed myapp.service, quindi correggi il problema alla base. Se il servizio attende l'avvio di una dipendenza lenta, aumenta RestartSec=, perché l'intervallo predefinito di 100 millisecondi esaurisce tutti i cinque tentativi in meno di un secondo.

Devo usare Restart=always o Restart=on-failure?

Usa on-failure nella maggior parte dei casi. Riavvia il servizio dopo un crash, un'uscita con codice diverso da zero, un timeout o l'attivazione del watchdog, ma lascia invariato un exit 0 intenzionale. Usa always solo quando il programma termina correttamente per motivi indipendenti dal suo controllo, ad esempio quando un client restituisce 0 dopo la disconnessione del peer. Il rischio di always è che un servizio che legge una configurazione non valida, registra un errore e termina con codice 0 entri in un ciclo infinito. L'unico sintomo visibile è l'aumento di NRestarts in systemctl show.

Perché terminare manualmente il processo non attiva un riavvio?

Perché systemd considera SIGHUP, SIGINT, SIGTERM e SIGPIPE come terminazioni corrette, mentre un semplice kill <pid> invia SIGTERM. Con Restart=on-failure, una terminazione corretta non è un errore. Pertanto nulla viene riavviato e la configurazione sembra non funzionare, anche se è corretta. Esegui il test con kill -9 <pid> o systemctl kill -s SIGKILL myapp.service, che provoca una terminazione non corretta e attiva la policy. La stessa regola spiega perché systemctl stop non entra mai in conflitto con la policy di riavvio.

Dove devono essere impostati StartLimitIntervalSec e StartLimitBurst?

Nella sezione [Unit]. Il materiale precedente e le versioni meno recenti di systemd li inserivano in [Service], quindi gli esempi copiati possono essere in conflitto tra loro. Non indovinare quale sezione venga riconosciuta dalla tua versione. Dopo systemctl daemon-reload, chiedi a systemd quali impostazioni ha caricato con systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service e considera quei valori come riferimento. systemd-analyze verify /etc/systemd/system/myapp.service rileva le chiavi che systemd non riconosce e non stampa nulla quando il file è corretto.

#systemd#restart#service-unit#cgroups#reliability