SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Por qué systemd no reinicia tu servicio

Restart= solo supervisa el proceso principal: un hijo muerto puede pasar inadvertido en el mismo cgroup. Revisa Type=, los límites y el journal.

La respuesta breve: las políticas de reinicio de systemd supervisan un proceso

Las políticas de reinicio de systemd supervisan un proceso por unidad: el proceso principal. Restart= lee el estado de salida de ese proceso y nada más. El grupo de control de una unidad puede contener veinte procesos; uno de ellos puede terminar y la unidad permanece active (running) porque el proceso principal sigue activo. Para systemd no ha fallado nada, así que no se reinicia nada.

systemd sí conoce los demás procesos. Los termina cuando se detiene la unidad, contabiliza su memoria dentro de los límites de la unidad, les aplica la cuota de CPU de la unidad y los muestra en systemctl status. Simplemente nunca lee su estado de salida. La lógica de reinicio y el cgroup son dos cosas distintas, y gran parte de esta guía trata sobre la diferencia entre ambas.

Qué contiene el cgroup y qué lee la lógica de reinicio

Un cgroup (grupo de control) es un objeto del kernel que contiene un conjunto de procesos. Cada unidad de servicio tiene uno, cuyo nombre coincide con el de la unidad. Un proceso no puede salir de él. Los procesos hijos heredan el cgroup de su proceso padre, y un proceso sin privilegios no puede trasladarse a otro. Por eso systemd puede limpiar un daemon que crea procesos dos veces, algo que los antiguos scripts de init nunca podían hacer de forma fiable.

Observe ambos datos uno junto al otro:

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

systemd-cgls muestra todos los procesos de la unidad. MainPID es el único número que lee la política de reinicio. Cuando esos dos datos no coinciden con su modelo mental, esa discrepancia es el error. MainPID=0 es peor que un PID incorrecto: significa que systemd no está siguiendo ningún proceso, por lo que ningún valor de Restart= puede activarse.

Existe una excepción real a la regla del proceso principal. Si el killer de falta de memoria del kernel termina cualquier proceso dentro del cgroup de la unidad, systemd lo detecta porque supervisa el archivo memory.events del cgroup. OOMPolicy= decide qué ocurre después, y su valor predeterminado es stop: se detiene toda la unidad, el resultado se registra como oom-kill y eso cuenta como un fallo, por lo que se activa Restart=on-failure. El journal lo muestra claramente.

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

Por tanto, si un proceso hijo termina por falta de memoria, la unidad sí se detiene, mientras que si el mismo proceso hijo termina por un fallo de segmentación, no lo hace. Si establece límites de memoria en una unidad, lea cómo se aplican MemoryMax y CPUQuota al cgroup de una unidad antes de ajustar la política de reinicio, porque ambas funciones interactúan aquí y en ningún otro punto.

Cómo Type= selecciona el proceso principal

Type= en la sección [Service] no sólo controla el orden de arranque. Es la regla que determina qué PID (identificador de proceso) se convierte en MainPID. En otras palabras, determina lo que Restart= puede ver.

  • Type=simple es el valor predeterminado. El proceso que systemd bifurca desde ExecStart= es el proceso principal. systemd marca la unidad como iniciada de inmediato, antes de saber si exec ha funcionado. Un error tipográfico en la ruta del binario produce un trabajo de arranque que termina correctamente y después Main process exited, code=exited, status=203/EXEC un momento más tarde.
  • Type=exec se comporta como simple, pero el trabajo de arranque espera hasta que exec haya terminado correctamente. Así, el error tipográfico anterior se convierte en un fallo de arranque real. Requiere systemd 240 o posterior, versión que incluyen todas las distribuciones compatibles. Prefiéralo a simple.
  • Type=forking espera que el proceso de ExecStart= bifurque un daemon en segundo plano y después termine. systemd espera a que termine el proceso padre y luego busca el daemon real. Indique PIDFile=. Sin uno, GuessMainPID= (habilitado de forma predeterminada) sólo funciona cuando queda exactamente un proceso en el cgroup. Si quedan dos, MainPID permanece en 0.
  • Type=notify significa que el servicio llama a sd_notify(3) y envía READY=1 cuando puede atender tráfico. También puede enviar MAINPID= para indicar a systemd que supervise otro proceso. NotifyAccess= tiene main de forma predeterminada, por lo que se ignora una notificación enviada por un proceso hijo y el journal muestra el PID desde el que se envió.
  • Type=oneshot no tiene un proceso principal persistente. La unidad pasa a estar inactiva en cuanto termina ExecStart=, a menos que configure RemainAfterExit=yes. Aquí se rechazan Restart=always y Restart=on-success, con el mensaje Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Se aceptan los demás valores, incluido on-failure.

Conviene memorizar dos errores de Type=forking, porque cada uno deja una unidad que parece averiada sin una causa visible:

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.

El primero significa que el daemon escribe su archivo PID en otra ubicación o lo escribe después de que systemd lo haya buscado. El segundo significa que el archivo PID identifica un proceso fuera del cgroup de la unidad. systemd se niega a adoptarlo porque, de lo contrario, un archivo PID modificable permitiría hacer que systemd enviara señales a cualquier proceso del sistema.

Por qué un script envoltorio oculta la terminación de sus procesos secundarios

Esta es la estructura que plantea la pregunta del título.

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

La unidad es Type=simple, por lo que el proceso principal es el shell. wait sin argumentos no devuelve el control hasta que todos los procesos secundarios terminan. Mate el proceso de trabajo y el shell seguirá esperando al proceso web. Por tanto, el shell no termina, MainPID tampoco termina y Restart= nunca se consulta. El cgroup contiene ahora un proceso menos, systemctl status muestra el árbol reducido y la unidad sigue en estado active (running). systemd no supervisa los cambios de ese árbol.

Una segunda versión del mismo error es más silenciosa:

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

El proceso principal es el shell, no myapp. En systemctl stop, systemd envía SIGTERM al proceso principal, y un shell que espera a un proceso secundario en primer plano no reenvía la señal. La detención tarda entonces todo TimeoutStopSec, 90 segundos de forma predeterminada, y termina así:

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

La solución es exec. Escriba exec /usr/local/bin/myapp y el shell será sustituido por el programa. Así, MainPID será el programa y las señales llegarán a él. Mejor aún, elimine el shell y use Environment= o EnvironmentFile= en la unidad. Tenga en cuenta que este error queda oculto cuando la cadena -c contiene un solo comando, porque bash y dash optimizan ambos ese caso y lo convierten en un exec directo. Añada un segundo comando a la cadena y el shell seguirá ejecutándose delante del programa.

Reprodúzcalo en un VPS de prueba en dos minutos

Guarde el envoltorio anterior como /usr/local/bin/two-children.sh, hágalo ejecutable con chmod +x y sustituya las dos rutas de programa por sleep 3600. Asigne una unidad al script con Type=simple y Restart=on-failure. Después, ejecute systemctl daemon-reload e inícielo. Ejecute systemd-cgls --unit two-children.service y anote los tres PID: el shell y sus dos procesos secundarios. Mate uno de los procesos secundarios con sudo kill <pid>. Compruebe de nuevo la unidad. El árbol tiene un proceso menos, el estado sigue siendo active (running) y el journal no muestra nada nuevo. Ahora ejecute sudo kill -9 <shell pid>. La unidad falla, el proceso secundario superviviente se limpia porque KillMode=control-group es el valor predeterminado y el journal muestra Scheduled restart job, restart counter is at 1.

El vocabulario completo de Restart= y cuándo on-failure es mejor que always

Restart= acepta uno de siete valores. La diferencia entre ellos está en qué se considera una salida limpia. systemd considera limpia la salida con el código 0, cualquier código incluido en SuccessExitStatus= y las señales SIGHUP, SIGINT, SIGTERM y SIGPIPE. Todo lo demás, incluidos SIGKILL y SIGSEGV, es una salida no limpia.

  • no es el valor predeterminado. La unidad nunca se reinicia por sí sola. Por eso, una unidad sin una línea Restart= falla en el primer fallo y permanece detenida.
  • on-success reinicia la unidad sólo después de una salida limpia.
  • on-failure reinicia la unidad tras un código de salida distinto de cero, una señal no limpia, un tiempo de espera agotado durante el arranque o la detención, o la expiración del watchdog.
  • on-abnormal reinicia la unidad tras una señal no limpia, un tiempo de espera agotado o la expiración del watchdog, pero nunca tras un código de salida distinto de cero normal.
  • on-abort reinicia la unidad sólo tras una señal no limpia, es decir, tras un fallo.
  • on-watchdog reinicia la unidad sólo cuando expira WatchdogSec=.
  • always reinicia la unidad después de todos los casos anteriores, incluida una salida limpia con estado 0.

on-failure es el valor predeterminado adecuado para un daemon de ejecución prolongada. Recupera el servicio después de un fallo y deja sin reiniciar una exit 0 deliberada. always corresponde a un programa que termina correctamente por motivos ajenos a su control, como un cliente de túnel que devuelve 0 cuando se desconecta el extremo remoto. El coste de always es que oculta errores: un servicio que arranca, lee un archivo de configuración dañado, registra el error y termina con código 0 entrará en un bucle infinito. El único indicio será que aumenta el contador de reinicios.

SuccessExitStatus= cambia la distinción entre salidas limpias y no limpias. Borg termina con 1 para las advertencias y con 2 para los errores. Por eso, una unidad de copia de seguridad sin SuccessExitStatus=1 se marca como fallida cada vez que omite un archivo ilegible. RestartPreventExitStatus= enumera los códigos que impiden un reinicio incluso con always. Es la forma correcta de indicar que el programa no debe volver a iniciarse. RestartForceExitStatus= hace lo contrario. Un trabajo de copia de seguridad debe ejecutarse en una unidad Type=oneshot iniciada por un temporizador, no en un bucle de reinicios. La pareja de servicio y temporizador que ejecuta un trabajo según un calendario es el modelo que debe copiar.

Una advertencia sobre las pruebas. Si termina el servicio con kill <pid> sin más argumentos, envía SIGTERM, que está en la lista de salidas limpias. Por tanto, Restart=on-failure no hace nada correctamente y puede concluir que la configuración está dañada. Use kill -9 <pid> o systemctl kill -s SIGKILL myapp.service en su lugar. Recuerde también que ningún valor de Restart= se ejecuta después de systemctl stop ni cuando la unidad se detiene porque desaparece una dependencia BindsTo= o PartOf=. Una tarea de detención no es un fallo.

RestartSec y el valor predeterminado de 100 milisegundos

RestartSec= es la pausa entre la detención de la unidad y el siguiente intento de inicio por parte de systemd. El valor predeterminado es de 100 milisegundos. Compruebe qué valor cargó realmente la unidad:

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

Una unidad que no lo haya definido muestra RestartUSec=100ms. Este valor predeterminado es adecuado para un servicio que falla una vez y vuelve a iniciarse. No lo es para un servicio que no puede iniciarse en absoluto, porque se producen cinco reinicios en menos de medio segundo. Eso activa precisamente el límite de frecuencia descrito a continuación. Para cualquier servicio que dependa de una base de datos, un punto de montaje o una ruta de red, establezca RestartSec=5s o un valor superior.

Desde agosto de 2026, systemd 254 y las versiones posteriores también ofrecen RestartSteps= y RestartMaxDelaySec=. Estas opciones aumentan el retraso desde RestartSec= hasta un límite máximo a lo largo de ese número de intentos. Ubuntu 24.04 incluye systemd 255 y las admite. Debian 12 incluye systemd 252 y no las admite. Los retrasos crecientes son la opción adecuada cuando la dependencia puede permanecer fuera de servicio durante mucho tiempo.

Qué significa realmente «start request repeated too quickly»

Este es el estado que muchos interpretan como que systemd se rinde de forma arbitraria. Es un contador. La regla es la siguiente: si una unidad se inicia más de StartLimitBurst= veces dentro de StartLimitIntervalSec=, systemd se niega a iniciarla de nuevo y la marca como fallida. Los valores predeterminados son 5 inicios en 10 segundos.

El journal muestra la secuencia:

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.

y systemctl start muestra directamente la solución:

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 reinicia el contador y elimina el estado fallido. Ninguna otra acción lo hace, por lo que un systemctl start normal seguirá siendo rechazado hasta que ejecute ese comando. Los inicios manuales también cuentan para el límite, así que varias ejecuciones impacientes de systemctl restart mientras edita un archivo de configuración pueden activarlo sin que se produzca ningún fallo.

La parte que induce a error: start-limit-hit nunca indica por qué fallaba el servicio. Sólo indica que falló varias veces y con rapidez. La causa real está en las líneas anteriores del journal.

Ambos ajustes pertenecen a la sección [Unit]. Encontrará ejemplos que los colocan en [Service], una ubicación que las versiones antiguas de systemd aceptaban. Ahí empieza la confusión. Escríbalos en [Unit] y pregunte después a systemd qué cargó mediante systemctl show, porque el valor cargado es el único que cuenta.

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

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

Esto da a la unidad cinco intentos dentro de un intervalo de cinco minutos antes de rendirse. StartLimitIntervalSec=0 desactiva completamente el límite, y debe saber qué implica: un servicio que nunca puede iniciarse volverá a intentarlo para siempre y escribirá una entrada en el journal cada vez. Los valores predeterminados para todo el sistema están en /etc/systemd/system.conf como DefaultStartLimitIntervalSec= y DefaultStartLimitBurst=.

Hay un ajuste relacionado que requiere una advertencia. StartLimitAction= decide qué ocurre cuando se alcanza el límite y acepta valores como reboot, reboot-force y poweroff. El valor predeterminado es none, que marca la unidad como fallida y deja la máquina sin cambios. En un VPS remoto, poweroff significa que el sistema permanecerá apagado hasta que abra la consola del proveedor.

Solución uno: un proceso por unidad

Esta es la respuesta en casi todos los casos. Si deben ejecutarse dos programas, escriba dos unidades. Cada una tendrá un proceso principal real, un estado de salida real y su propia política de reinicio. También tendrá registros, límites de recursos y contadores de reinicio independientes, que es lo que necesita a las tres de la mañana.

Exprese la relación entre las unidades en los archivos de unidad, no en un script de shell.

  • After= sólo ordena el inicio. No indica nada sobre los errores.
  • Requires= inicia la otra unidad junto con esta y detiene esta unidad si la otra se detiene explícitamente.
  • BindsTo= es Requires= más el caso que le interesa: esta unidad se detiene cuando la otra se detiene por cualquier motivo, incluido un fallo. Combínelo con After=, o el orden no estará definido.
  • PartOf= propaga la detención y el reinicio hacia las dependencias, de modo que systemctl restart myapp.target alcanza todas las unidades que sean PartOf= de ella.
  • Upholds= (systemd 249 y versiones posteriores, es decir, Ubuntu 22.04 y posteriores) mantiene en ejecución la unidad indicada: si se detiene, systemd la inicia de nuevo. Está sujeta al mismo límite de frecuencia de inicio que cualquier otra unidad.

Un worker que nunca debe ejecutarse sin su servidor API y que systemd mantiene activo mientras la API esté disponible:

# /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

El worker no tiene una sección [Install] y nunca se habilita manualmente. La unidad de la API lo incorpora mediante Upholds=, por lo que systemctl enable --now myapp-api.service es el único comando que debe ejecutar. Recargue la configuración y compruebe qué configuración ha creado systemd para el conjunto:

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

systemd-analyze verify no muestra ninguna salida cuando el archivo es válido. Cualquier salida indica un problema, normalmente una clave que systemd no reconoce en la sección donde la escribió o una dependencia de una unidad que no existe.

Corrección dos: Type=notify, para que systemd conozca algo más que un PID

Si el programa usa el protocolo de notificaciones de systemd, utilícelo. Con Type=notify, el servicio indica a systemd cuándo está listo. Esto hace que el orden de inicio sea real y no dependa de suposiciones. También puede enviar MAINPID= para indicar a systemd cuál es el proceso relevante, en lugar de señalar un proceso lanzador.

WatchdogSec= es la opción que justifica el esfuerzo. Configúrela y el servicio deberá enviar WATCHDOG=1 mediante sd_notify(3) al menos con esa frecuencia. Cuando dejan de llegar los mensajes, systemd termina el servicio con SIGABRT y lo marca como fallido. Por tanto, Restart=on-failure o Restart=on-watchdog puede iniciarlo de nuevo. Esta es la única forma integrada de reiniciar un proceso que sigue activo pero está bloqueado. Ninguna política basada en el estado de salida puede detectar ese caso.

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

Un disparo del watchdog aparece en el journal como myapp.service: Watchdog timeout (limit 30s)!, seguido de la terminación del proceso. Si, en cambio, la unidad permanece en activating (start) hasta que se agota TimeoutStartSec, READY=1 nunca llegó. Puede que el programa no use el protocolo o que NotifyAccess=main rechace una notificación enviada por un proceso hijo. El journal informa de este último caso con ambos PID.

Para el software que expone un endpoint HTTP de comprobación de estado, pero no admite sd_notify, las opciones correctas son una unidad de temporizador pequeña que consulte el endpoint y ejecute systemctl restart, o dejar que un runtime de contenedores realice las comprobaciones. Para eso existen las comprobaciones de estado de Compose y su comportamiento de reinicio.

Solución tres: un supervisor dentro de la unidad, sólo cuando no hay alternativa

Algunos programas se distribuyen realmente como un conjunto de procesos controlados por un lanzador que no se puede dividir. En ese caso, se ejecuta un supervisor dentro de la unidad y se acepta la consecuencia: systemd supervisa al supervisor, el supervisor supervisa todo lo demás y la política de reinicio queda repartida en dos archivos.

La forma más habitual de este patrón es un runtime de contenedores. Una unidad de docker compose o podman sigue exactamente este modelo: la política de reinicio de cada contenedor se define en el archivo de Compose y la unidad de systemd sólo mantiene activo el runtime. Si ese es su caso, la unidad que inicia una pila de Compose durante el arranque muestra la versión funcional e incluye el motivo por el que Type=oneshot con RemainAfterExit=yes suele ser lo correcto en ese contexto.

El cgroup sigue siendo útil. Todo lo que inicia el supervisor permanece dentro del cgroup de la unidad, por lo que MemoryMax=, CPUQuota= y la limpieza al detenerse siguen abarcando todo el árbol de procesos. Sólo se delega la decisión de reinicio.

Independientemente del supervisor que elija, no configure Restart=always en la unidad externa y una política de reinicio agresiva dentro de ella sin analizar las consecuencias. Dos capas de lógica de reinicio, cada una con su propio retroceso, producen un servicio que entra en ciclos de reinicio durante minutos y un journal que no explica el motivo.

ExitType=cgroup no significa «reiniciar cuando muera cualquier proceso»

ExitType= (systemd 250 y versiones posteriores, por lo que Ubuntu 24.04 y Debian 12 lo incluyen) es la opción que aparece al buscar este problema, pero hace lo contrario de lo que su nombre sugiere. El valor predeterminado, ExitType=main, significa que el servicio se considera detenido cuando termina el proceso principal. ExitType=cgroup significa que el servicio se considera en ejecución hasta que termina el último proceso del cgroup.

Por tanto, ExitType=cgroup hace que una unidad sea menos sensible a la terminación de un proceso, no más. Es la opción adecuada para un programa que crea su proceso de trabajo real y termina el proceso padre sin escribir un archivo PID, situación en la que Type=forking no puede encontrar el daemon. Es la opción incorrecta para el fallo descrito aquí.

No existe ningún valor Restart= que signifique «reiniciar la unidad cuando muera cualquier proceso del cgroup». Si necesita ese comportamiento, debe usar un proceso por unidad. Si no puede dividir el programa y controla el script envoltorio, la opción más cercana es wait -n, que retorna en cuanto termina el primer proceso hijo:

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

Ahora, la terminación de cualquier proceso hijo hace que el envoltorio termine con un estado distinto de cero, por lo que Restart=on-failure actúa. Es una solución de compromiso, no una corrección. Seguirá teniendo un único contador de reinicios para dos programas, un único flujo de registros y ninguna forma de reiniciar por separado la parte que falla.

Cómo inspeccionar lo que ocurrió realmente

Cuatro comandos, en este orden.

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 muestra el estado, el PID principal y el árbol de cgroups en una sola pantalla. Una unidad en buen estado muestra Active: active (running) y una línea Main PID: que identifica el proceso esperado. Si el árbol inferior incluye procesos que no reconoce o falta alguno que sí debería estar, ya tiene la respuesta.

systemd-cgls --unit muestra el mismo árbol sin truncarlo. Esto empieza a ser importante cuando una unidad mantiene más de unos pocos procesos.

systemctl show proporciona datos legibles por máquina. NRestarts= es el contador de reinicios. Es la forma más rápida de distinguir un servicio que se ha reiniciado cuarenta veces de otro que permanece activo desde el arranque. Result= contiene el último motivo del fallo: exit-code, signal, timeout, oom-kill, watchdog o start-limit-hit. ExecMainStatus= es el estado de salida sin procesar del último proceso principal.

El journal contiene la secuencia. Estas son las tres líneas que debe buscar:

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 que el programa eligió devolver N, por lo que el fallo está en el programa o en su configuración. code=killed, signal=SEGV significa que se bloqueó. code=killed, signal=TERM normalmente significa que otro proceso le pidió que se detuviera. Esto no es un fallo y no activará Restart=on-failure. code=dumped significa que dejó un archivo core. coredumpctl list se lo mostrará cuando systemd-coredump esté instalado.

En más de una máquina, NRestarts es el dato que conviene recopilar periódicamente. Una unidad cuyo contador aumenta cada día está fallando cada día, aunque nadie lo haya detectado. Cuando administra más de dos o tres equipos, una forma coherente de ejecutar un comando en todos los servidores convierte esa suposición en un informe.

FAQ

¿Por qué systemctl indica que mi servicio está activo aunque el proceso haya terminado?

systemd sigue un proceso por unidad de servicio: el proceso principal, y Restart= sólo lee el estado de salida de ese proceso. Todos los demás procesos que inicia la unidad viven en el mismo cgroup. systemd los terminará cuando la unidad se detenga, pero nunca supervisa su salida. Ejecute systemctl show -p MainPID myapp.service y compare el número con systemd-cgls --unit myapp.service. Si el proceso que terminó aparece en el árbol, pero no es MainPID, systemd se comportó exactamente como está diseñado. La solución es usar un proceso por unidad y definir la relación mediante BindsTo= y Upholds= entre las unidades.

¿Qué significa "start request repeated too quickly"?

Significa que la unidad se inició más de StartLimitBurst= veces dentro de StartLimitIntervalSec=. El valor predeterminado es 5 inicios en 10 segundos, por lo que systemd dejó de intentarlo. Es un límite de frecuencia y no indica por qué fallaba el servicio. Lea las líneas anteriores del journal. Borre el estado con systemctl reset-failed myapp.service y corrija el fallo subyacente. Si el servicio espera a que algo lento esté disponible, aumente RestartSec=, porque el intervalo predeterminado de 100 milisegundos consume los cinco intentos en menos de un segundo.

¿Debo usar Restart=always o Restart=on-failure?

Use on-failure en casi todos los casos. Reinicia el servicio tras un fallo, una salida distinta de cero, un timeout o una activación del watchdog, y deja intacto un exit 0 deliberado. Use always sólo cuando el programa termine correctamente por motivos ajenos a su control, como un cliente que devuelve 0 cuando su peer se desconecta. El coste de always es que un servicio que lee una configuración incorrecta, registra un error y termina con 0 entrará en un bucle infinito. El único síntoma visible será que NRestarts aumenta en systemctl show.

¿Por qué matar manualmente mi proceso no activa un reinicio?

Porque systemd considera SIGHUP, SIGINT, SIGTERM y SIGPIPE como salidas correctas, y un kill <pid> simple envía SIGTERM. Con Restart=on-failure, una salida correcta no es un fallo, por lo que nada se reinicia y parece que la configuración está rota aunque no lo esté. Pruebe con kill -9 <pid> o systemctl kill -s SIGKILL myapp.service, que provocan una terminación incorrecta y sí activan la política. La misma regla explica por qué systemctl stop nunca entra en conflicto con la política de reinicio.

¿Dónde se configuran StartLimitIntervalSec y StartLimitBurst?

En la sección [Unit]. El material antiguo y las versiones antiguas de systemd los colocaban en [Service], por lo que los ejemplos copiados no coinciden. No suponga cuál acepta su versión. Después de systemctl daemon-reload, pregunte a systemd qué cargó mediante systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service y considere esos valores como la referencia válida. systemd-analyze verify /etc/systemd/system/myapp.service detecta las claves que systemd no reconoce y no muestra nada cuando el archivo es correcto.

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