Entendiendo las Dependencias de Systemd: Previniendo y Solucionando Conflictos de Unidades
Aprende cómo funcionan las dependencias, el ordenamiento, los objetivos y los conflictos de systemd para que los servicios se inicien de manera confiable y los fallos sean más fáciles de depurar.
Entendiendo las Dependencias de Systemd: Previniendo y Solucionando Conflictos de Unidades
Las dependencias de systemd son fáciles de entender a medias. Un archivo de unidad tiene Requires=postgresql.service, la aplicación aún se inicia demasiado temprano, y todos se preguntan por qué systemd ignoró la dependencia. No ignoró nada. Requires= y After= responden a preguntas diferentes.
Esa distinción es el núcleo de la mayoría de los problemas de dependencias de systemd. Una directiva controla si otra unidad se incluye en la misma transacción. Una directiva diferente controla el ordenamiento. Otras directivas vinculan el comportamiento de apagado, ligan la vida útil de una unidad a otra, o hacen que dos unidades sean mutuamente excluyentes. Una vez que separas esas ideas, los conflictos de unidades se vuelven mucho menos misteriosos.
La Base: Directivas de Dependencia de Unidades de Systemd
Systemd utiliza directivas específicas dentro de los archivos de unidad (típicamente ubicados en /etc/systemd/system/ o /lib/systemd/system/) para dictar cuándo una unidad debe iniciarse, detenerse o esperar a otra. Comprender estas directivas es el primer paso para gestionar las dependencias correctamente.
Directivas de Dependencia Principales
Estas directivas controlan las relaciones entre unidades. Por sí mismas, no siempre controlan el orden de inicio:
Requires=:- Establece una dependencia fuerte. Si la unidad requerida falla al iniciar, la unidad actual también fallará.
- No implica
PartOf=, y no significa automáticamente "iniciar después de esta unidad".
Wants=:- Una dependencia débil. Si la unidad deseada falla, la unidad actual aún intentará iniciarse. Se utiliza para dependencias opcionales.
BindsTo=:- Similar a
Requires=, pero más fuerte en cuanto a la detención. Si la unidad vinculada se detiene (por cualquier motivo), la unidad actual también se detiene.
- Similar a
PartOf=:- Indica que la unidad actual es una parte subordinada de otra unidad (por ejemplo, una activación de socket específica relacionada con un servicio principal). Si la unidad superior se detiene, la unidad subordinada también se detiene.
Conflicts=:- Dice que dos unidades no deben estar activas al mismo tiempo. Iniciar una hace que systemd detenga la otra como parte de la misma transacción.
Directivas Principales de Sincronización de Inicio
Estas directivas dictan cuándo debe iniciarse la unidad dependiente en relación con la unidad requerida:
After=:- Especifica que el trabajo de inicio de la unidad actual se ordena después del trabajo de inicio de la unidad listada. No incluye esa unidad por sí misma.
Before=:- Especifica que la unidad actual debe iniciarse antes de la unidad listada.
Mejor Práctica: Para el ordenamiento típico de inicio de servicios,
Wants=combinado conAfter=es el patrón más común y seguro.Requires=debe reservarse para dependencias donde el fallo de la dependencia debe causar que el servicio dependiente falle.
Ejemplo: Definiendo Dependencias en un Archivo de Servicio
Considera un servicio de aplicación personalizada, myapp.service, que debe comunicarse con una base de datos gestionada por PostgreSQL (postgresql.service).
# /etc/systemd/system/myapp.service
[Unit]
Description=Mi Aplicación Personalizada
# Asegurar que PostgreSQL esté ejecutándose antes de intentar iniciarme
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
Ese ejemplo es intencionalmente estricto. Si PostgreSQL no puede iniciarse, myapp.service también debe fallar. Para un servicio que puede ejecutarse en modo degradado sin la base de datos, usa Wants=postgresql.service con el mismo ordenamiento After=postgresql.service. La aplicación aún le pide a systemd que inicie PostgreSQL primero, pero se le permite continuar si PostgreSQL no está disponible.
No hay vergüenza en elegir la relación más débil cuando coincide con la realidad. Un exportador de métricas, un transportador de registros o un calentador de caché pueden ser útiles cuando su dependencia está presente, pero no deben bloquear el arranque del sistema principal. Las dependencias duras se reservan mejor para casos donde iniciar sin la otra unidad sería incorrecto o peligroso.
Para aplicaciones en red, ten más cuidado. network.target a menudo significa que la pila de red básica está presente, no que DHCP se haya completado o que DNS sea utilizable. Si tu aplicación realmente necesita una red configurada al inicio, usa:
[Unit]
Wants=network-online.target
After=network-online.target
Luego confirma que tu distribución tenga el servicio wait-online correspondiente habilitado, como systemd-networkd-wait-online.service o la unidad wait-online de NetworkManager. Sin eso, network-online.target puede no esperar lo que crees que espera.
Diagnosticando Problemas de Dependencia
Cuando un servicio falla al iniciar, systemd generalmente proporciona suficiente información en los registros, pero las cadenas de dependencia pueden ocultar la causa raíz. Aquí hay herramientas y comandos esenciales para la resolución de problemas.
1. Verificando el Estado de la Unidad y los Registros
El punto de partida fundamental es verificar el estado del servicio y revisar sus registros inmediatamente después de un intento de inicio fallido.
# Verificar el estado general, que a menudo menciona fallos de dependencia
systemctl status myapp.service
# Ver registros detallados específicamente relacionados con la unidad
journalctl -u myapp.service --since "hace 5 minutos"
2. Analizando el Árbol de Dependencias
Systemd proporciona potentes herramientas de visualización para ver exactamente qué está esperando a qué.
systemctl list-dependencies
Este comando muestra las unidades que son requeridas o deseadas por la unidad especificada, recorriendo toda la cadena de dependencias.
Para ver qué necesita myapp.service para iniciar:
# Dependencias hacia adelante (qué debe iniciar antes que yo)
systemctl list-dependencies --after myapp.service
# Dependencias inversas (qué depende de mí)
systemctl list-dependencies --before myapp.service
Usa --plain cuando el formato del árbol se interponga, y añade --all cuando necesites ver también unidades inactivas:
systemctl list-dependencies --plain --all myapp.service
systemd-analyze dot
Para preguntas de dependencia más grandes, genera un gráfico e inspecciónalo visualmente:
systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg
En un servidor sin Graphviz instalado, la salida de texto sigue siendo útil porque puedes buscar los nombres de las unidades y ver qué bordes conoce systemd. Para problemas de tiempo de arranque, systemd-analyze critical-chain myapp.service suele ser más fácil de leer que un gráfico completo.
3. Detectando Conflictos y Problemas de Ordenamiento
Los conflictos de dependencia a menudo se manifiestan como servicios que fallan porque se iniciaron demasiado temprano o se detienen inesperadamente.
Dependencias Circulares: Este es el conflicto más peligroso, donde la Unidad A requiere B, y la Unidad B requiere A. Systemd intenta resolver esto, pero a menudo resulta en que una o ambas unidades permanezcan en un estado failed o activating indefinidamente.
Para encontrar posibles problemas que abarquen todo el sistema, puedes buscar en los registros mensajes de fallo específicos relacionados con el ordenamiento:
journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"
También ejecuta systemd-analyze verify en unidades personalizadas antes de asumir que el comportamiento en tiempo de ejecución es el problema:
systemd-analyze verify /etc/systemd/system/myapp.service
Puede marcar ciclos de ordenamiento, directivas desconocidas y referencias de unidad inválidas temprano. No probará que tu diseño sea correcto, pero puede ahorrarte perseguir un error tipográfico como si fuera un problema de dependencia complejo.
Solucionando Problemas Comunes de Dependencia
Una vez identificados, los problemas de dependencia se pueden resolver ajustando las directivas en los archivos de unidad relevantes.
Escenario 1: El Servicio se Inicia Antes de que su Requisito Previo Esté Listo
Síntoma: Los registros de tu aplicación muestran errores de conexión a la base de datos, pero postgresql.service aparece active en systemctl status.
Diagnóstico: El servicio puede carecer de After=postgresql.service, o PostgreSQL puede estar activo antes de que la base de datos, socket, credenciales o esquema específico que tu aplicación necesita esté listo. Systemd puede ordenar unidades, pero no puede entender automáticamente todas las condiciones de preparación a nivel de aplicación.
Solución: Comienza con la relación de unidad simple:
[Unit]
Requires=postgresql.service
After=postgresql.service
Si la aplicación aún compite con la base de datos, soluciona el problema de preparación en la capa correcta. Algunos servicios admiten Type=notify y solo informan que están listos después de la inicialización. Algunas aplicaciones necesitan lógica de reintento porque las dependencias pueden reiniciarse en cualquier momento, no solo durante el arranque. Para una verificación local estrecha, un comando ExecStartPre= puede ser razonable:
[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp
Usa ese tipo de verificación previa con moderación. Si se convierte en un script de shell con sleeps y bucles, la aplicación probablemente necesita un comportamiento de reintento adecuado en su lugar.
Evita sleep 30 como solución de dependencia. Puede ocultar la competencia en una máquina de desarrollo tranquila y fallar nuevamente en almacenamiento más lento, una VM ocupada o un host esperando DNS. Una directiva de ordenamiento real, notificación de preparación, activación de socket o bucle de reintento de aplicación te da una razón por la que el servicio está listo en lugar de una esperanza de que haya pasado suficiente tiempo.
Escenario 2: Orden de Inicio/Parada Conflictivo
Síntoma: Detener el sistema hace que procesos críticos se cuelguen o fallen abruptamente.
Diagnóstico: Esto a menudo indica un mal uso de BindsTo= o una interacción compleja entre las directivas Before= y After= en servicios hermanos.
Solución: Revisa los servicios que son hermanos (por ejemplo, servicios iniciados por el mismo objetivo). Asegúrate de que si el Servicio A debe ejecutarse mientras el Servicio B se está ejecutando, uses BindsTo= o Requires=. Si el Servicio A debe terminar sus tareas antes de que el Servicio B comience la limpieza, verifica que el orden After= sea correcto.
Recuerda que el orden de apagado es el inverso del orden de inicio. Si app.service tiene After=database.service, entonces al apagar, systemd detiene app.service antes que database.service. Eso suele ser lo que quieres: la aplicación deja de aceptar trabajo antes de que la base de datos desaparezca. Muchos errores de apagado provienen de la falta de ordenamiento de inicio, no de una configuración separada solo de apagado.
Escenario 3: Eliminando Dependencias Innecesarias
Síntoma: El arranque del sistema es lento porque se están incluyendo servicios innecesarios en la cadena de inicio.
Diagnóstico: Puede que hayas usado Requires= cuando solo se necesitaba una conexión opcional.
Solución: Cambia Requires= a Wants=. Si el servicio no necesita absolutamente la dependencia para funcionar, Wants= permite que el sistema continúe incluso si la dependencia falla o está enmascarada.
# Antes (Demasiado Estricto)
Requires=optional_logging.service
# Después (Mejor)
Wants=optional_logging.service
After=optional_logging.service
Esto es especialmente útil para agentes de monitoreo, exportadores de métricas, ayudantes sidecar y cachés locales opcionales. Si el servicio principal puede hacer trabajo útil sin ellos, Requires= convierte una interrupción parcial en una interrupción total. Usa dependencias estrictas para cosas que son realmente necesarias: una base de datos local para una aplicación que no puede iniciar sin ella, un punto de montaje que contiene los datos de la aplicación, o una unidad de socket que es la única ruta de activación compatible.
Escenario 4: Un Montaje o Dispositivo No Está Listo
Síntoma: Un servicio falla al arrancar con No such file or directory, pero la ruta existe después de iniciar sesión. Esto sucede a menudo con servicios que leen desde /mnt/data, /srv/app, discos extraíbles, volúmenes cifrados o sistemas de archivos de red.
Diagnóstico: El servicio se está iniciando antes de que la unidad de montaje esté activa, o el montaje es opcional y falló sin detener el servicio.
Solución: Encuentra el nombre de la unidad de montaje:
systemd-escape -p --suffix=mount /mnt/data
Para /mnt/data, eso generalmente produce mnt-data.mount. Luego ordena tu servicio después de él y requiérelo si el servicio no puede ejecutarse sin los datos:
[Unit]
Requires=mnt-data.mount
After=mnt-data.mount
Si el montaje proviene de /etc/fstab, opciones como nofail, x-systemd.automount y _netdev afectan el comportamiento de arranque. No agregues un Requires= duro a un montaje opcional a menos que realmente quieras que ese fallo de montaje bloquee el servicio.
Escenario 5: Un Objetivo Incluye Demasiado
Síntoma: Habilitar un servicio parece iniciar unidades no relacionadas, o deshabilitar un servicio no evita que aparezca durante el arranque.
Diagnóstico: El servicio puede ser incluido por un objetivo a través de [Install] WantedBy=..., por Wants= de otro servicio, o por una unidad de socket, temporizador, ruta o montaje. enable crea enlaces simbólicos para la activación en el arranque; no es la única forma en que una unidad puede iniciarse.
Solución: Inspecciona tanto la unidad como las dependencias inversas:
systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp
Si una unidad de socket activa el servicio, deshabilitar solo myapp.service puede no ser suficiente. Puede que necesites deshabilitar o enmascarar myapp.socket también, dependiendo del comportamiento deseado.
Aplicando Cambios y Recargando
Cada vez que modifiques un archivo de unidad, debes indicar a systemd que recargue su configuración antes de probar los cambios.
# 1. Recargar la configuración del gestor de systemd
sudo systemctl daemon-reload
# 2. Reiniciar el servicio afectado
sudo systemctl restart myapp.service
# 3. Verificar el estado
systemctl status myapp.service
Una Pequeña Lista de Verificación Mental
Cuando un problema de dependencia se siente enredado, redúcelo a unas pocas comprobaciones simples.
Primero, pregúntate si la otra unidad debe iniciarse en absoluto. Si es así, usa Wants= o Requires=. Si el servicio actual puede ejecutarse sin ella, prefiere Wants=. Si el fallo de esa unidad significa que este servicio debe fallar, usa Requires=.
Segundo, pregúntate si el orden de inicio importa. Si es así, añade After= o Before=. No esperes que las directivas de dependencia manejen el ordenamiento por sí mismas.
Tercero, pregúntate si el acoplamiento de vida útil importa después del inicio. Si una unidad que se detiene debe detener la otra, mira BindsTo= o PartOf=. No los uses a la ligera; pueden hacer que los reinicios de rutina se extiendan más de lo esperado.
Finalmente, inspecciona lo que systemd realmente cargó:
systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts
Esa salida suele ser más útil que el archivo que crees que editaste. Los drop-ins, unidades de proveedor, unidades generadas, sockets, temporizadores y objetivos pueden cambiar el comportamiento final.