Dependências e condições no systemd explicadas
Entenda Requires, Wants, After, Before, ExecStartPre e Condition, e descubra por que uma unidade falha no boot mesmo funcionando ao iniciar manualmente.
Requires não significa After
As dependências e as condições do systemd são quatro mecanismos separados que a maioria dos ficheiros de unidade usa como se fossem um só. Requires= e Wants= determinam que outras unidades são incluídas. After= e Before= determinam a ordem de arranque das unidades. ExecStartPre= executa uma verificação que pode fazer a unidade falhar. As famílias Condition e Assert determinam se a unidade é executada. Cada mecanismo é independente dos restantes. Por isso, uma unidade pode exigir outra unidade e ainda assim arrancar exatamente ao mesmo tempo que ela.
Essa última frase está na origem de quase todos os relatos do tipo "funciona quando o inicio manualmente, mas falha no arranque".
[Unit]
Description=Inventory API
Requires=postgresql.service
[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-apiRequires=postgresql.service inclui o PostgreSQL na mesma transação de arranque. Não espera por ele. O systemd inicia ambos os trabalhos em paralelo. Assim, pg_isready é executado enquanto o PostgreSQL ainda está a abrir o diretório de dados. O comando termina com o código 2 porque ainda não existe nada a escutar. A unidade falha antes de ExecStart ser sequer alcançado. Executar sudo systemctl start inventory-api uma hora mais tarde funciona porque, nessa altura, o PostgreSQL já está ativo. Nada no ficheiro da unidade foi alterado. Por isso, o ficheiro parece correto.
A correção consiste numa linha.
[Unit]
Requires=postgresql.service
After=postgresql.serviceHá um detalhe importante no mesmo local. Uma dependência Requires= que falha só impede o arranque da sua unidade quando também define After= nessa dependência. Sem a ordenação, o systemd já iniciou a sua unidade quando a outra falha. Nesse momento, já não há nada para cancelar. Definir apenas Requires= não fornece a proteção que muitos administradores esperam. Escreva After= junto de cada Requires= e de cada Wants=, salvo quando tiver uma razão específica para não o fazer.
O que Requires, Wants, Requisite e BindsTo garantem
Todas estas definições configuram dependências. Nenhuma delas define a ordem de arranque.
Wants=: inclui a outra unidade. Se ela falhar ou não existir, esta unidade arranca na mesma. É isto quesystemctl enablecria, como um link simbólico dentro de um diretório.wants/.Requires=: inclui a outra unidade. Se ela falhar e também tiver sido ordenada comAfter=, esta unidade não arranca. Se a outra unidade for parada explicitamente mais tarde, esta unidade também para.Requisite=: não inclui a outra unidade. Se ela não estiver ativa, esta unidade falha imediatamente.BindsTo=: funciona comoRequires=, mas esta unidade também para sempre que a outra unidade parar por qualquer motivo, incluindo o desaparecimento de hardware.PartOf=: a paragem e o reinício propagam-se da outra unidade para esta unidade. O arranque não se propaga.Conflicts=: ao arrancar esta unidade, a outra para.
Para um daemon que comunica com outro daemon, Wants= e After= são normalmente o par correto. Requires= associa os ciclos de vida: se parar a base de dados para manutenção, a aplicação também para e não volta a arrancar quando a base de dados regressar. Wants= e After= fornecem a ordem de arranque sem essa associação, e uma política de reinício trata do caso em que a dependência desaparece mais tarde.
Também recebe dependências que nunca escreveu. Com DefaultDependencies=yes, que é o valor predefinido, um serviço normal recebe Requires=sysinit.target, After=sysinit.target basic.target e Conflicts=shutdown.target automaticamente. É por isso que um serviço com uma secção [Unit] quase vazia ainda arranca numa fase tardia do boot e continua a ser parado corretamente durante o encerramento.
After e Before apenas ordenam a transação
After= e Before= servem exclusivamente para ordenar. Não impõem qualquer requisito. After=redis.service numa unit que nada mais inclui Redis na transação não tem efeito: se redis.service não fizer parte da transação, não há nada por que esperar e a sua unit inicia imediatamente.
Vale a pena repetir isto, porque essa é exatamente a forma do erro network-online.target mais abaixo. A ordenação espera apenas por units que já estejam a ser iniciadas na mesma transação.
O par é simétrico. After=b.service escrito em a.service significa o mesmo que Before=a.service escrito em b.service. Use apenas uma das formas e coloque-a na unit que controla. A ordenação é invertida automaticamente durante o encerramento, portanto After=b.service também significa que a sua unit é parada antes de b.service.
After= aguarda "started", e Type= define o que isso significa
After= aguarda até que a outra unidade termine de iniciar. O significado de "terminar de iniciar" é definido inteiramente por Type= dessa unidade.
Type=simple: assim que o systemd cria o processo com fork. O programa pode ainda não ter analisado a configuração, muito menos aberto um socket.Type=exec: assim queexecve()for concluído com sucesso. É ligeiramente mais forte. Ainda assim, não diz nada sobre prontidão.Type=forking: quando o processo pai original termina.Type=oneshot: quando o processo termina. Aqui, "iniciado" significa realmente que o trabalho terminou.Type=notify: quando o serviço enviaREADY=1através do seu socket de notificação. Este é o único tipo que comunica prontidão real.
Assim, After= num daemon Type=simple é uma promessa fraca, e essa é a segunda metade da condição de corrida no primeiro exemplo. Se a unidade da qual depende for fornecida como Type=simple, ordenar o arranque depois dela não significa que esteja a aceitar ligações. Existem duas respostas corretas. Ordene o arranque depois da unidade do socket, para que o kernel coloque as ligações recebidas em fila enquanto o daemon ainda está a iniciar. Ou faça o seu próprio serviço tentar novamente e deixe a política de reinício tratar do problema. O tipo usado por uma unidade pode ser consultado em systemctl cat, e vale a pena ler a configuração Type= e o que cada valor informa ao systemd antes de depender da ordenação.
ExecStartPre é uma barreira que pode fazer a unidade falhar
ExecStartPre= é executado antes de ExecStart=. Se terminar com um código diferente de zero, a ativação é abortada e a unidade passa para failed. ExecStart= nunca é executado. Este é o mecanismo por trás de grande parte das unidades que falham sem uma mensagem do programa propriamente dito, porque o programa nunca foi iniciado.
Alguns factos importantes:
- Não é um shell. Não há pipes, redirecionamentos, curingas nem
&&. O primeiro token tem de ser um caminho absoluto. Envolva a linha em/bin/sh -c '...'quando precisar de sintaxe de shell. - Um prefixo
-torna uma saída diferente de zero não fatal:ExecStartPre=-/usr/bin/optional-check. - Cada
ExecStartPre=tem de terminar antes de o seguinte ser executado. Não pode iniciar um processo de execução prolongada. - Todas as linhas
ExecStartPre=partilhamTimeoutStartSec=comExecStart=. Uma verificação prévia que fica em loop à espera de uma base de dados consome o tempo limite de arranque, e a unidade falha então comResult: timeoutdepois destart operation timed out. Terminating.aparecer no journal.
A linha da falha identifica o processo de controlo, não o processo principal:
inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.Leia cuidadosamente esse nome simbólico. O systemd mapeia códigos de saída pequenos através de uma tabela fixa, por isso 2 imprime sempre INVALIDARGUMENT, independentemente do significado que o programa lhe atribuiu. status=203/EXEC é o elemento que contém informação real: o systemd não conseguiu executar o binário, porque o caminho está errado ou o ficheiro não é executável.
Não use ExecStartPre= para criar diretórios. RuntimeDirectory=, StateDirectory=, LogsDirectory= e CacheDirectory= criam-nos com o proprietário e o modo corretos, e RuntimeDirectory= é removido quando o serviço para. Também funcionam corretamente com DynamicUser=, ao contrário de um mkdir escrito manualmente.
Condition falha silenciosamente. Assert falha de forma explícita.
As famílias Condition e Assert executam os mesmos testes. A diferença está apenas no que acontece quando um teste falha.
Um Condition...= que falhe faz a unidade ser ignorada. O job de arranque é reportado como bem-sucedido. A unidade permanece inactive (dead), nada é marcado como falhado, nenhum alerta é acionado e o journal regista uma linha:
Condition check resulted in Inventory API being skipped.No systemd 250 e posteriores, systemctl status mostra o motivo diretamente:
Active: inactive (dead)
Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min agoA linha indentada abaixo identifica a diretiva exata que falhou, por exemplo ConditionPathExists=/etc/inventory/api.conf was not met.
Um Assert...= que falhe faz a unidade falhar. O journal indica Assertion failed for Inventory API. e a unidade termina em failed (Result: assert), o que é suficientemente explícito para ser detetado pelo monitoramento.
Escolha entre os dois perguntando o que significa um teste falhado. Condition significa “esta unidade não se aplica a esta máquina”. Assert significa “isto tem de ser verdadeiro e, se não for, alguém deve ser avisado”. A maioria das unidades deve usar Condition. Use Assert apenas quando não fazer nada silenciosamente for pior do que ter uma unidade falhada.
A família Condition tem duas armadilhas.
Primeiro, uma condição falhada não faz falhar as unidades que dependem dela. Se a.service tiver Requires=b.service e b.service for ignorada por causa de uma condição, o job de arranque de b.service continua a ser considerado concluído. Assim, a.service arranca normalmente num estado em que b não está em execução. Uma condição protege apenas a unidade onde foi definida.
Segundo, as condições são avaliadas sempre que a unidade arranca, no momento em que o job é executado. Uma unidade acionada por um temporizador do systemd numa VPS pode ser ignorada cem vezes seguidas sem nunca parecer falhada. É a mesma classe de operação silenciosa sem efeito que um job do cron que é executado mas não faz nada. A forma de encontrar o problema é a mesma: leia o journal da unidade em vez de confiar no seu estado de saída.
As condições que vale a pena conhecer num servidor:
ConditionPathExists=/etc/inventory/api.confe a sua negação,ConditionPathExists=!/etc/inventory/api.conf.ConditionFileNotEmpty=eConditionDirectoryNotEmpty=, para um ficheiro de configuração ou diretório de dados que um pacote criou mas deixou vazio.ConditionVirtualization=, para que uma unidade que precisa de uma interface real do kernel possa incluirConditionVirtualization=!container. Verifique o que o seu sistema reporta comsystemd-detect-virt.ConditionHost=corresponde ao hostname ou ao machine ID. É assim que um único ficheiro de unidade partilhado pode comportar-se de forma diferente em dois servidores.ConditionKernelCommandLine=eConditionKernelVersion=, para unidades associadas a um parâmetro de arranque ou a uma versão mínima do kernel.
Uma atribuição vazia limpa a lista. É assim que um drop-in remove uma condição fornecida por um pacote:
[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.confPor que network.target não significa que a rede está ativa
network.target é um ponto de sincronização, não um estado. Durante o boot, ordenar uma unidade depois desse target significa que o software de gestão da rede foi iniciado. Isso não significa que uma interface tenha um endereço ou que exista uma rota para a internet. O target existe sobretudo para o sentido inverso: uma unidade ordenada After=network.target é parada antes de a rede ser desativada durante o shutdown.
network-online.target é o target que aguarda. Ele é suportado por um serviço wait-online pertencente ao gestor de rede utilizado:
systemd-networkd-wait-online.servicequando systemd-networkd gere as interfaces, que é o caso normal num servidor Ubuntu configurado através do netplan.NetworkManager-wait-online.servicecom NetworkManager.
Configurações antigas com ifupdown obtêm o mesmo efeito através de networking.service. Seja qual for o target disponível, utilizá-lo corretamente requer duas linhas, não uma.
[Unit]
Wants=network-online.target
After=network-online.targetnetwork-online.target não faz parte da transação de boot predefinida e nenhum outro componente o inclui automaticamente. Se escrever apenas After=, estará a ordenar a unidade em relação a um target que nunca foi colocado na fila. Essa ordenação não faz absolutamente nada. É o no-op descrito anteriormente, na sua forma mais dispendiosa. A linha Wants= é que inclui o target na transação, para que a linha After= tenha algo por que esperar.
A segunda coisa a saber é que o estado "online" é definido pela implementação de wait-online, não pelo systemd. systemd-networkd-wait-online retorna quando as interfaces que gere atingem um estado configurado. Não verifica se o DNS resolve nomes e não verifica se algum host remoto está acessível.
Essa definição provoca uma falha comum em VPS. Uma máquina com uma segunda interface para uma rede privada, declarada no netplan mas sem endereço atribuído, faz com que o serviço wait-online aguarde até desistir:
systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.O boot demora mais dois minutos porque o timeout predefinido é de 120 segundos. Existem duas correções. Marque a interface não utilizada como optional: true no ficheiro do netplan, para que o networkd deixe de esperar por ela. Ou adicione um drop-in ao serviço wait-online que identifique a interface pretendida com --interface=, ou que passe --any para retornar assim que uma interface esteja ativa.
Melhor ainda, evite depender do target. Muitos serviços são ordenados depois de network-online.target apenas porque fazem bind a um endereço específico e falham durante o boot com uma linha como esta:
nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)O kernel recusa o bind porque esse endereço ainda não está ativo. Definir net.ipv4.ip_nonlocal_bind=1 permite que um processo faça bind a um endereço que a máquina ainda não possui, e uma política de reinício resolve o restante. Atrasar todo o boot à espera da rede é uma ferramenta pesada para um problema que normalmente envolve apenas um socket.
Como ler as dependências reais do systemd num sistema em execução
Nunca se baseie apenas no ficheiro da unidade. Drop-ins, links simbólicos .wants/ e dependências predefinidas implícitas também adicionam relações que o ficheiro não mostra.
systemctl cat inventory-api.serviceIsto apresenta o ficheiro da unidade e todos os drop-ins, pela ordem em que são aplicados, com o caminho de origem acima de cada bloco. Execute primeiro este comando. Um override de cinco linhas em /etc/systemd/system/inventory-api.service.d/ sobrepõe-se ao ficheiro fornecido pelo pacote e, de outro modo, fica invisível.
systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResultIsto apresenta os valores resolvidos, depois da aplicação dos drop-ins e da adição das dependências implícitas pelo systemd. ConditionResult=no responde diretamente à pergunta "a unidade comunicou sucesso e não fez nada".
systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.serviceA forma simples percorre Requires= e Wants= para baixo. --reverse mostra quais unidades ativam a sua unidade. É assim que encontra o target que a inicia no boot. --after e --before mostram a ordenação. É esse o par que deve consultar quando a questão é saber se alguma coisa esperou efetivamente.
journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.serviceO segundo comando intercala duas unidades com marcas temporais em milissegundos. Assim pode provar uma condição de corrida na ordenação em vez de fazer suposições. A falha de pg_isready ocorre antes de o PostgreSQL registar database system is ready to accept connections, e o intervalo entre ambos aparece diretamente na saída.
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.serviceverify carrega a unidade da forma como o systemd a carregaria e comunica diretivas desconhecidas, dependências de unidades inexistentes, ciclos de ordenação e sintaxe que não consegue analisar. Não altera nada no sistema. critical-chain apresenta a cadeia de ordenação que atrasou a unidade, com a hora em que cada etapa ficou ativa. Funciona apenas para uma unidade que tenha arrancado durante o boot atual.
Depois de editar qualquer ficheiro de unidade, execute sudo systemctl daemon-reload. Para alterar uma unidade fornecida por um pacote, use sudo systemctl edit inventory-api.service. Este comando cria um drop-in por si. Editar o ficheiro do fornecedor em /usr/lib/systemd/system/ funciona até à próxima atualização do pacote o substituir. O mesmo mecanismo de drop-in permite associar limites de memória e CPU a um serviço sem alterar um ficheiro pertencente ao pacote.
Ciclos de ordenação e a linha que deixam no journal
Adicione a ordenação nos dois sentidos e o systemd quebra o ciclo ao eliminar um dos jobs:
systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/startO systemd escolhe qual job eliminar, e pode não escolher aquele que você escolheria. O resultado é um serviço que aparece como ausente depois de alguns reboots e presente depois de outros. Isso é difícil de depurar externamente. A maioria dos ciclos surge quando as unidades definem DefaultDependencies=no e, mesmo assim, ordenam-se em relação a basic.target, ou quando você adiciona Before= a uma unidade que já tinha After= apontando de volta para a unidade atual. systemd-analyze verify encontra esses ciclos sem reboot.
A unidade corrigida
[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf
[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.targetCada linha tem uma função. Wants= inclui ambas as dependências na transação, sem associar o tempo de vida desta unidade ao delas. After= trata da espera e tem de repetir os dois nomes, porque dependência e ordenação são definições separadas. ConditionPathExists= significa que uma máquina que tem o pacote, mas não a configuração, ignora a unidade silenciosamente em vez de gerar um alerta. Esse é o comportamento correto para um serviço orientado por configuração. Type=notify significa que tudo o que estiver ordenado depois desta unidade espera pela disponibilidade real, e não por um processo que apenas fez fork. Restart=on-failure trata do caso em que a base de dados deixa de estar disponível muito depois do boot, porque a ordenação só se aplica ao primeiro arranque. O grau de agressividade dessa nova tentativa é controlado por as definições Restart= e RestartSec=.
Verifique a unidade antes de confiar nela:
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p ResultUma unidade saudável apresenta ConditionResult=yes com ActiveState=active, e Result=success confirma que nada falhou na última execução. ConditionResult=no juntamente com ActiveState=inactive significa que a unidade foi ignorada, e a linha do journal que identifica a condição indica qual teste falhou.
FAQ
Requires= espera que a outra unidade seja iniciada?
Não. Requires= e After= são definições separadas. Requires= inclui a outra unidade na mesma transação e, em seguida, o systemd inicia ambos os jobs em paralelo. Para esperar, adicione After=, indicando a mesma unidade. Há uma segunda razão para o adicionar: uma dependência Requires= que falhe só impede o arranque da sua unidade quando After= também está definido, porque, sem ordenação, a sua unidade já terá sido iniciada quando a outra falhar.
Devo ordenar depois de network.target ou network-online.target?
No arranque, network.target significa apenas que o software de gestão da rede foi iniciado. Não garante que existam endereços ou rotas. Use network-online.target quando o serviço precisar de um endereço funcional no arranque e escreva Wants=network-online.target e After=network-online.target, porque o target não faz parte da transação de arranque predefinida e After=, por si só, espera por uma unidade que não foi colocada na fila. Se o serviço falhar apenas porque associa um IP específico, net.ipv4.ip_nonlocal_bind=1 com Restart=on-failure é uma opção mais leve do que atrasar o arranque.
Por que razão a minha unidade comunica sucesso, mas nunca é executada?
Um teste Condition...= que falhe ignora a unidade e comunica o job de arranque como bem-sucedido. Por isso, nada é marcado como falha. Execute systemctl show <unit> -p ConditionResult; ConditionResult=no confirma o resultado. Em seguida, consulte journalctl -b -u <unit> pela linha Condition check resulted in <description> being skipped. No systemd 250 e posteriores, systemctl status <unit> também indica a diretiva exata que não foi satisfeita.
Qual é a diferença entre Condition e Assert?
Ambos executam testes idênticos. Quando Condition falha, a unidade é ignorada silenciosamente e o job continua a ser bem-sucedido. Quando Assert falha, a unidade falha, regista Assertion failed for <description>. e fica em failed (Result: assert). Use Condition para indicar que "esta unidade não se aplica a esta máquina". Este é o caso da maioria das situações reais. Use Assert apenas quando uma pré-condição em falta tiver de ficar visível para quem monitoriza as unidades com falha.
Por que razão ExecStartPre falha com status=203/EXEC?
203/EXEC significa que o systemd não conseguiu executar o comando. As causas habituais são um caminho que não é absoluto, um binário inexistente nessa máquina, um ficheiro sem o bit de execução ou um script cuja linha #! aponta para um interpretador inexistente. Os outros códigos pequenos do systemd vêm de uma tabela fixa. Assim, status=2/INVALIDARGUMENT significa apenas que o comando terminou com 2 e não fornece informação sobre os argumentos. Tenha em atenção que ExecStartPre= não é executado através de uma shell. Por isso, pipes e globs precisam de /bin/sh -c '...'.