Resolución de problemas en Systemd: Comprendiendo las dependencias de servicios y directivas de ordenamiento
Soluciona problemas de dependencia y ordenamiento en systemd utilizando correctamente Requires, Wants, After, Before y comandos de diagnóstico.
Resolución de problemas en Systemd: Comprendiendo las dependencias de servicios y directivas de ordenamiento
La mayoría de los errores confusos de dependencia en systemd provienen de mezclar dos conceptos separados: requisito y orden. Requires= y Wants= responden a "¿qué otra unidad debería ser incluida?" After= y Before= responden a "¿cuándo debería iniciar esta unidad en relación con otra?" Si solo recuerdas una cosa de esta guía, recuerda que After=postgresql.service no inicia PostgreSQL por ti. Solo indica que si ambas unidades se están iniciando, tu unidad debe esperar hasta que el trabajo de inicio de PostgreSQL se haya ejecutado.
Esa distinción es la fuente de muchos incidentes de "funciona cuando lo inicio manualmente, falla después del reinicio". Una aplicación web se inicia antes de que el socket de la base de datos esté aceptando conexiones. Un worker se inicia antes de que /mnt/jobs esté montado. Un servicio espera por network.target y aún falla porque no se ha asignado una dirección IP. Estos no son problemas exóticos de systemd. Son suposiciones de inicio ordinarias que deben hacerse explícitas.
Directivas de dependencia principales: Requires y Wants
Systemd utiliza dos directivas principales para definir dependencias directas entre unidades: Requires y Wants. Estas directivas se colocan dentro de la sección [Unit] de un archivo de unidad (por ejemplo, un archivo .service).
Requires=
La directiva Requires= establece una dependencia fuerte. Si la unidad A Requires= la unidad B, systemd inicia B cuando se inicia A. Si B no puede iniciarse, el trabajo de inicio de A también falla. Si B se detiene explícitamente mientras A está en ejecución, A normalmente también se detiene porque la relación requerida ya no se mantiene.
Ejemplo:
Considera un servicio de aplicación web (myapp.service) que depende críticamente de un servicio de base de datos (mariadb.service). El archivo de unidad myapp.service podría incluir:
[Unit]
Description=Mi Aplicación Web
Requires=mariadb.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
En este escenario, si mariadb.service falla al iniciar o se detiene manualmente, systemd también detendrá myapp.service. Si intentas iniciar myapp.service y mariadb.service no está en ejecución, systemd intentará iniciar mariadb.service primero. Si mariadb.service falla, myapp.service no se iniciará.
Wants=
La directiva Wants= define una dependencia más débil y opcional. Si la unidad A Wants= la unidad B, systemd intentará iniciar la unidad B al iniciar la unidad A, pero la unidad A aún se activará incluso si la unidad B falla al iniciar o no está en ejecución. Esto es útil para servicios que se benefician de otro servicio pero pueden funcionar de forma independiente, quizás con funciones reducidas o una advertencia.
Ejemplo:
Supongamos que un agente de monitoreo (monitoring-agent.service) puede ejecutarse sin un servicio de registro específico (app-logger.service) pero idealmente lo tendría disponible. El archivo de unidad monitoring-agent.service podría verse así:
[Unit]
Description=Agente de Monitoreo
Wants=app-logger.service
[Service]
ExecStart=/usr/bin/monitoring-agent
[Install]
WantedBy=multi-user.target
Aquí, systemd intentará iniciar app-logger.service cuando se active monitoring-agent.service. Sin embargo, si app-logger.service falla al iniciar, monitoring-agent.service aún procederá a iniciarse correctamente.
Requires= vs. Wants=
Requires=: Dependencia fuerte. Si la unidad requerida falla, la unidad dependiente falla o se detiene.Wants=: Dependencia débil. La unidad dependiente intenta iniciar la unidad deseada pero continúa incluso si falla.
Es importante notar que Requires= implica Wants=. Si una unidad requiere otra, también implícitamente la desea.
Directivas de ordenamiento: After y Before
Mientras que Requires y Wants definen qué necesita estar en ejecución, After y Before definen cuándo deben iniciarse las unidades entre sí. Estas directivas controlan la secuencia de operaciones durante el proceso de arranque del sistema o cuando las unidades se activan bajo demanda. A menudo se usan junto con las directivas de dependencia.
After=
La directiva After= especifica que el trabajo de inicio de la unidad actual debe ordenarse después de los trabajos de inicio de las unidades listadas. Por sí misma, no incluye esas unidades en la transacción. Tampoco prueba que la dependencia esté lógicamente lista para tu aplicación; solo usa la vista de systemd del estado de activación de la unidad.
Ejemplo:
Un servicio dependiente de red (custom-network-app.service) debería iniciarse solo después de que la red esté completamente configurada. Esto se maneja típicamente asegurando que se inicie después del objetivo de red (network.target).
[Unit]
Description=Aplicación de Red Personalizada
Requires=network.target
After=network.target
[Service]
ExecStart=/usr/bin/custom-network-app
[Install]
WantedBy=multi-user.target
En esta configuración, systemd ordenará custom-network-app.service después de network.target si ambos son parte de la misma transacción. Para servicios que necesitan una dirección, DNS o una ruta a otro host, network-online.target a menudo está más cerca de la intención, pero solo si el servicio wait-online de la distribución está habilitado y correctamente configurado.
Before=
La directiva Before= especifica que la unidad actual debe iniciarse antes de las unidades listadas en Before=. Esto es útil para servicios que necesitan detenerse después de otros durante el apagado, o iniciarse antes de ciertos servicios para proporcionar un entorno para ellos.
Ejemplo:
Imagina un escenario donde un servidor de correo (postfix.service) necesita estar en ejecución antes de cualquier servicio orientado al usuario que pueda enviar correos electrónicos. Podrías usar Before= para asegurar que postfix.service se inicie temprano.
[Unit]
Description=Agente de Transferencia de Correo Postfix
# ... otras directivas como Conflicts=
Before=user-session.target
[Service]
ExecStart=/usr/lib/postfix/master
[Install]
WantedBy=multi-user.target
Esta configuración intenta iniciar postfix.service antes de que cualquier cosa que sea parte de user-session.target comience su inicio. De manera similar, durante el apagado, postfix.service estaría entre los últimos en detenerse si tiene un After=user-session.target correspondiente.
After= vs. Before=
After=: Garantiza que las unidades listadas estén activas antes de que la unidad actual se inicie.Before=: Garantiza que la unidad actual se inicie antes de las unidades listadas.
After= y Before= son complementarios. Si la unidad A dice Before=B, el orden es A primero, luego B. Si la unidad B dice After=A, el resultado es el mismo. Generalmente solo necesitas expresar la relación en un archivo de unidad. Al editar tu propio servicio, suele ser más claro decir qué necesita venir después tu servicio, porque eso mantiene el razonamiento local.
Combinando directivas para configuraciones robustas
En escenarios del mundo real, a menudo combinarás estas directivas para crear gráficos de dependencia complejos. El multi-user.target es un objetivo común que significa que el sistema está listo para operaciones multiusuario. Muchos servicios están configurados para ser WantedBy=multi-user.target y After=multi-user.target (o más precisamente, After=basic.target y After=getty.target etc. de los que depende multi-user.target).
Un patrón común:
Un servicio que requiere una base de datos y debe iniciarse después de que la red esté configurada podría verse así:
[Unit]
Description=Mi Servicio de Aplicación
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
Explicación del patrón:
Requires=mariadb.service: Garantiza quemariadb.servicedebe estar en ejecución para quemy_app.servicefuncione. Simariadb.servicefalla,my_app.servicese detendrá.Wants=other-optional-service.service: Intenta iniciarother-optional-service.serviceperomy_app.servicecontinuará incluso si falla.After=network.target mariadb.service: Ordenamy_app.servicedespués del objetivo de red y el trabajo de inicio de MariaDB. Si la aplicación necesita conectarse a través de TCP a una base de datos en otro host, usa la configuración network-online apropiada en lugar de asumir quenetwork.targetsignifica "la red es utilizable".WantedBy=multi-user.target: Cuando está habilitado (systemctl enable my_app.service), esta directiva agrega un enlace simbólico para quemy_app.servicese inicie cuando el sistema alcance el estadomulti-user.target.
Consideraciones avanzadas y mejores prácticas
WantedByvs.RequiredBy: Similar aWantsvs.Requires,WantedByes un ordenamiento débil yRequiredByes un ordenamiento fuerte. La mayoría de los servicios usanWantedBy=multi-user.target.Conflicts=: Esta directiva especifica unidades que no deberían ejecutarse concurrentemente con la unidad actual. Si la unidad actual se inicia, las unidades en conflicto se detendrán, y viceversa.- Dependencias transitivas: Las dependencias son transitivas. Si A requiere B, y B requiere C, entonces A indirectamente requiere C. Systemd maneja estas cadenas automáticamente.
- Directivas
Condition*=: UsaConditionPathExists=,ConditionFileNotEmpty=,ConditionVirtualization=, etc., para hacer que la activación de la unidad sea condicional según el estado del sistema, mejorando aún más la robustez. - Usa
systemctl list-dependencies <unidad>: Este comando es invaluable para visualizar el árbol de dependencias de una unidad, incluyendo dependencias directas e indirectas. - Usa
systemctl status <unidad>: Siempre verifica el estado de tu servicio después de hacer cambios de configuración. A menudo mostrará razones de fallo, incluyendo problemas de dependencia. - Evita dependencias circulares: Aunque systemd intenta resolverlas, las dependencias circulares directas (
A Requires B,B Requires A) pueden llevar a bucles de inicio o fallos. Diseña cuidadosamente tus dependencias para evitar esto.
Un pase práctico de depuración
Cuando aparece un error de dependencia, comienza mirando la transacción que systemd realmente construyó:
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 te dice si systemd falló al iniciar la unidad, la mató después, o la considera activa aunque la aplicación no esté saludable. journalctl -b te mantiene dentro del arranque actual, lo que importa porque los problemas de dependencia a menudo son solo de arranque. systemctl show es directo pero útil: muestra las propiedades finales de la unidad fusionada después de que se hayan aplicado drop-ins, archivos del proveedor y dependencias generadas.
Si no estás seguro de dónde vino una dependencia, inspecciona la unidad completa:
systemctl cat my_app.service
Esto muestra la unidad empaquetada y cualquier archivo de anulación bajo /etc/systemd/system/my_app.service.d/. He visto servicios en producción donde la unidad base era correcta pero una anulación antigua aún contenía After=mysql.service de una migración anterior. El servicio estaba esperando una unidad que ya no existía, y los registros hacían parecer que la aplicación estaba rota.
Para preguntas de tiempo de arranque, usa:
systemd-analyze critical-chain my_app.service
systemd-analyze blame
critical-chain es mejor que mirar marcas de tiempo porque muestra qué unidades retrasaron el camino hacia tu servicio. blame puede ser engañoso si lo tratas como una clasificación de servicios "malos", pero es útil cuando una dependencia toma mucho más tiempo de lo esperado.
Patrones que se mantienen en producción
Para una dependencia de base de datos local, este es un punto de partida razonable:
[Unit]
Description=Servicio API
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure
[Install]
WantedBy=multi-user.target
Eso dice que PostgreSQL debe iniciarse con la API, y el inicio de la API debe ordenarse después de PostgreSQL. No garantiza que cada migración se haya completado o que la base de datos acepte las credenciales exactas que usa tu aplicación. Si eso importa, agrega una verificación de preparación a nivel de aplicación. Systemd puede ordenar procesos, pero no puede entender el estado de tu esquema a menos que enseñes a tu servicio a verificarlo.
Para una ruta montada, prefiere RequiresMountsFor= sobre nombrar manualmente las unidades de montaje:
[Unit]
RequiresMountsFor=/srv/uploads
[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads
Systemd derivará las unidades de montaje necesarias para la ruta. Esto es más fácil de mantener que recordar que /srv/uploads se asigna a srv-uploads.mount.
Para ayudantes opcionales, usa Wants=:
[Unit]
Wants=metrics-agent.service
After=metrics-agent.service
Si el agente de métricas falla, el servicio principal aún puede iniciarse. Eso es generalmente lo que quieres para sidecars de registro, exportadores opcionales y ayudantes de notificación locales. No uses Requires= solo porque dos servicios están relacionados. Úsalo solo cuando el servicio dependiente realmente no pueda hacer trabajo útil sin la otra unidad.
Para servicios estrechamente acoplados que deben detenerse juntos, mira BindsTo= y PartOf=. BindsTo= es más fuerte que Requires= y es útil cuando un servicio debe desaparecer si la unidad vinculada desaparece, como un servicio vinculado a una unidad de dispositivo específica. PartOf= es a menudo útil para grupos: reiniciar o detener la unidad padre puede propagarse a las unidades hijas. Estas no son directivas de primera elección, pero resuelven problemas que Requires= no puede expresar limpiamente.
Trampas comunes
No agregues After=multi-user.target a un servicio normal de larga duración que está habilitado con WantedBy=multi-user.target. Eso a menudo crea un ordenamiento extraño y rara vez dice lo que el autor pretendía. La mayoría de los servicios son incluidos por multi-user.target; no necesitan iniciarse después de que ya se haya alcanzado.
No asumas que network.target significa "internet es accesible". Es un punto de sincronización para la gestión de red, no una prueba de conectividad. Si tu aplicación se comunica con una API remota, agrega lógica de reintento dentro de la aplicación de todos modos. El ordenamiento de red en el arranque reduce el ruido, pero no puede protegerte de fallos de DNS, cambios de enrutamiento o una dependencia remota caída.
No escondas largos sleeps en ExecStartPre=/bin/sleep 30 a menos que no tengas una mejor opción. Los sleeps hacen que los arranques sean más lentos cuando la dependencia está lista rápidamente y aún fallan cuando la dependencia toma más tiempo del esperado. Un pequeño bucle de preparación que verifique el socket, archivo o API real suele ser más claro.
Cuando cambies directivas de dependencia, ejecuta systemctl daemon-reload, reinicia el servicio y verifica el journal del mismo arranque. La solución más rápida suele ser la que prueba la suposición de ordenamiento exacta que estaba equivocada.