Entendendo Dependências do Systemd: Prevenindo e Corrigindo Conflitos de Unidades

Aprenda como dependências, ordenação, targets e conflitos do systemd funcionam para que serviços iniciem de forma confiável e falhas sejam mais fáceis de depurar.

Entendendo Dependências do Systemd: Prevenindo e Corrigindo Conflitos de Unidades

As dependências do systemd são fáceis de acertar pela metade. Um arquivo de unidade contém Requires=postgresql.service, a aplicação ainda inicia muito cedo, e todos se perguntam por que o systemd ignorou a dependência. Ele não ignorou nada. Requires= e After= respondem a perguntas diferentes.

Essa distinção é o cerne da maioria dos problemas de dependência do systemd. Uma diretiva controla se outra unidade é puxada para a mesma transação. Uma diretiva diferente controla a ordenação. Outras diretivas vinculam o comportamento de desligamento, ligam o tempo de vida de uma unidade ao de outra ou tornam duas unidades mutuamente exclusivas. Depois que você separa essas ideias, os conflitos de unidade se tornam muito menos misteriosos.

A Base: Diretivas de Dependência de Unidade do Systemd

O systemd usa diretivas específicas dentro de arquivos de unidade (normalmente localizados em /etc/systemd/system/ ou /lib/systemd/system/) para ditar quando uma unidade deve iniciar, parar ou aguardar por outra. Entender essas diretivas é o primeiro passo para gerenciar dependências corretamente.

Diretivas de Dependência Principais

Essas diretivas controlam os relacionamentos entre unidades. Elas não controlam, por si só, a ordem de inicialização:

  • Requires=:
    • Estabelece uma dependência forte. Se a unidade requerida falhar ao iniciar, a unidade atual também falhará.
    • Não implica PartOf=, e não significa automaticamente "iniciar após esta unidade".
  • Wants=:
    • Uma dependência fraca. Se a unidade desejada falhar, a unidade atual ainda tentará iniciar. Isso é usado para dependências opcionais.
  • BindsTo=:
    • Semelhante a Requires=, mas mais forte em relação à parada. Se a unidade vinculada parar (por qualquer motivo), a unidade atual também é parada.
  • PartOf=:
    • Indica que a unidade atual é uma parte subordinada de outra unidade (por exemplo, uma ativação de socket específica relacionada a um serviço principal). Se a unidade superior parar, a unidade subordinada também para.
  • Conflicts=:
    • Diz que duas unidades não devem estar ativas ao mesmo tempo. Iniciar uma faz com que o systemd pare a outra como parte da mesma transação.

Diretivas Principais de Sincronização de Inicialização

Essas diretivas ditam quando a unidade dependente deve iniciar em relação à unidade requerida:

  • After=:
    • Especifica que o job de início da unidade atual é ordenado após o job de início da unidade listada. Não puxa essa unidade por si só.
  • Before=:
    • Especifica que a unidade atual deve iniciar antes da unidade listada.

Melhor Prática: Para ordenação típica de inicialização de serviços, Wants= combinado com After= é o padrão mais comum e seguro. Requires= deve ser reservado para dependências onde a falha da dependência deve causar a falha do serviço dependente.

Exemplo: Definindo Dependências em um Arquivo de Serviço

Considere um serviço de aplicação personalizada, myapp.service, que deve se comunicar com um banco de dados gerenciado pelo PostgreSQL (postgresql.service).

# /etc/systemd/system/myapp.service
[Unit]
Description=Minha Aplicação Personalizada

# Garantir que o PostgreSQL esteja em execução antes de tentar me iniciar
Requires=postgresql.service
After=postgresql.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

Esse exemplo é intencionalmente estrito. Se o PostgreSQL não puder iniciar, myapp.service também deve falhar. Para um serviço que pode ser executado em modo degradado sem o banco de dados, use Wants=postgresql.service com a mesma ordenação After=postgresql.service. A aplicação ainda pede ao systemd para iniciar o PostgreSQL primeiro, mas é permitido continuar se o PostgreSQL estiver indisponível.

Não há vergonha em escolher o relacionamento mais fraco quando ele corresponde à realidade. Um exportador de métricas, um shipper de logs ou um aquecedor de cache podem ser úteis quando sua dependência está presente, mas não devem bloquear a inicialização do sistema principal. Dependências rígidas são melhor reservadas para casos onde iniciar sem a outra unidade seria errado ou perigoso.

Para aplicações em rede, seja mais cuidadoso. network.target geralmente significa que a pilha de rede básica está presente, não que o DHCP foi concluído ou que o DNS está utilizável. Se sua aplicação realmente precisa de rede configurada na inicialização, use:

[Unit]
Wants=network-online.target
After=network-online.target

Em seguida, confirme se sua distribuição tem o serviço wait-online correspondente habilitado, como systemd-networkd-wait-online.service ou a unidade wait-online do NetworkManager. Sem isso, network-online.target pode não esperar pelo que você pensa que espera.

Diagnosticando Problemas de Dependência

Quando um serviço falha ao iniciar, o systemd geralmente fornece informações suficientes nos logs, mas as cadeias de dependência podem obscurecer a causa raiz. Aqui estão ferramentas e comandos essenciais para solução de problemas.

1. Verificando o Status da Unidade e Logs

O ponto de partida fundamental é verificar o status do serviço e revisar seus logs imediatamente após uma tentativa de inicialização com falha.

# Verificar o status geral, que geralmente menciona falhas de dependência
systemctl status myapp.service

# Visualizar logs detalhados especificamente relacionados à unidade
journalctl -u myapp.service --since "5 minutes ago"

2. Analisando a Árvore de Dependência

O systemd fornece ferramentas de visualização poderosas para ver exatamente o que está esperando o quê.

systemctl list-dependencies

Este comando mostra as unidades que são requeridas ou desejadas pela unidade especificada, percorrendo toda a cadeia de dependência.

Para ver o que myapp.service requer para iniciar:

# Dependências diretas (o que deve iniciar antes de mim)
systemctl list-dependencies --after myapp.service

# Dependências reversas (o que depende de mim)
systemctl list-dependencies --before myapp.service

Use --plain quando a formatação da árvore atrapalhar, e adicione --all quando precisar ver também unidades inativas:

systemctl list-dependencies --plain --all myapp.service

systemd-analyze dot

Para questões de dependência maiores, gere um gráfico e inspecione-o visualmente:

systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg

Em um servidor sem Graphviz instalado, a saída de texto ainda é útil porque você pode pesquisar pelos nomes das unidades e ver quais arestas o systemd conhece. Para problemas de tempo de inicialização, systemd-analyze critical-chain myapp.service geralmente é mais fácil de ler do que um gráfico completo.

3. Detectando Conflitos e Problemas de Ordenação

Conflitos de dependência geralmente se manifestam como serviços falhando porque foram iniciados muito cedo ou parando inesperadamente.

Dependências Circulares: Este é o conflito mais perigoso, onde a Unidade A requer B, e a Unidade B requer A. O systemd tenta resolver isso, mas muitas vezes resulta em uma ou ambas as unidades permanecendo em um estado failed ou activating indefinidamente.

Para encontrar possíveis problemas em todo o sistema, você pode pesquisar nos logs por mensagens de falha específicas relacionadas à ordenação:

journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"

Execute também systemd-analyze verify em unidades personalizadas antes de assumir que o comportamento em tempo de execução é o problema:

systemd-analyze verify /etc/systemd/system/myapp.service

Isso pode sinalizar ciclos de ordenação, diretivas desconhecidas e referências de unidade inválidas precocemente. Não provará que seu design está correto, mas pode poupá-lo de perseguir um erro de digitação como se fosse um problema de dependência complexo.

Corrigindo Problemas Comuns de Dependência

Uma vez identificados, os problemas de dependência podem ser resolvidos ajustando as diretivas nos arquivos de unidade relevantes.

Cenário 1: O Serviço Inicia Antes de Seu Pré-requisito Estar Pronto

Sintoma: Os logs da sua aplicação mostram erros de conexão com o banco de dados, mas postgresql.service aparece como active no systemctl status.

Diagnóstico: O serviço pode estar faltando After=postgresql.service, ou o PostgreSQL pode estar ativo antes que o banco de dados, socket, credenciais ou esquema específico que sua aplicação precisa esteja pronto. O systemd pode ordenar unidades, mas não pode entender automaticamente todas as condições de prontidão no nível da aplicação.

Correção: Comece com o relacionamento de unidade simples:

[Unit]
Requires=postgresql.service
After=postgresql.service

Se a aplicação ainda competir com o banco de dados, corrija o problema de prontidão na camada certa. Alguns serviços suportam Type=notify e só relatam prontidão após a inicialização. Algumas aplicações precisam de lógica de repetição porque as dependências podem reiniciar a qualquer momento, não apenas durante a inicialização. Para uma verificação local restrita, um comando ExecStartPre= pode ser razoável:

[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp

Use esse tipo de pré-verificação com moderação. Se crescer para um script de shell com sleeps e loops, a aplicação provavelmente precisa de um comportamento de repetição adequado.

Evite sleep 30 como uma correção de dependência. Pode esconder a condição de corrida em uma máquina de desenvolvimento silenciosa e falhar novamente em armazenamento mais lento, uma VM ocupada ou um host aguardando DNS. Uma diretiva de ordenação real, notificação de prontidão, ativação de socket ou loop de repetição da aplicação fornece um motivo pelo qual o serviço está pronto, em vez de uma esperança de que tempo suficiente passou.

Cenário 2: Ordem de Inicialização/Parada Conflitante

Sintoma: Parar o sistema faz com que processos críticos travem ou falhem abruptamente.

Diagnóstico: Isso geralmente indica uso indevido de BindsTo= ou uma interação complexa entre as diretivas Before= e After= em serviços irmãos.

Correção: Revise os serviços que são irmãos (por exemplo, serviços iniciados pelo mesmo target). Certifique-se de que, se o Serviço A deve ser executado enquanto o Serviço B está em execução, você use BindsTo= ou Requires=. Se o Serviço A deve concluir suas tarefas antes que o Serviço B inicie a limpeza, verifique se a ordem After= está correta.

Lembre-se de que a ordem de desligamento é o inverso da ordem de inicialização. Se app.service tem After=database.service, então no desligamento o systemd para app.service antes de database.service. Isso geralmente é o que você deseja: a aplicação para de aceitar trabalho antes que o banco de dados desapareça. Muitos bugs de desligamento vêm da falta de ordenação de inicialização, não de uma configuração separada apenas de desligamento.

Cenário 3: Removendo Dependências Desnecessárias

Sintoma: A inicialização do sistema está lenta porque serviços desnecessários estão sendo puxados para a cadeia de inicialização.

Diagnóstico: Você pode ter usado Requires= quando apenas uma conexão opcional era necessária.

Correção: Altere Requires= para Wants=. Se o serviço não precisa absolutamente da dependência para funcionar, Wants= permite que o sistema prossiga mesmo se a dependência falhar ou estiver mascarada.

# Antes (Muito Estrito)
Requires=optional_logging.service

# Depois (Melhor)
Wants=optional_logging.service
After=optional_logging.service

Isso é especialmente útil para agentes de monitoramento, exportadores de métricas, ajudantes sidecar e caches locais opcionais. Se o serviço principal pode fazer trabalho útil sem eles, Requires= transforma uma interrupção parcial em uma interrupção total. Use dependências estritas para coisas que são realmente necessárias: um banco de dados local para uma aplicação que não pode iniciar sem ele, um ponto de montagem que contém os dados da aplicação ou uma unidade de socket que é o único caminho de ativação suportado.

Cenário 4: Um Mount ou Dispositivo Não Está Pronto

Sintoma: Um serviço falha na inicialização com No such file or directory, mas o caminho existe depois que você faz login. Isso geralmente acontece com serviços que leem de /mnt/data, /srv/app, discos removíveis, volumes criptografados ou sistemas de arquivos de rede.

Diagnóstico: O serviço está iniciando antes que a unidade de montagem esteja ativa, ou a montagem é opcional e falhou sem parar o serviço.

Correção: Encontre o nome da unidade de montagem:

systemd-escape -p --suffix=mount /mnt/data

Para /mnt/data, isso geralmente produz mnt-data.mount. Em seguida, ordene seu serviço depois dela e a requisite se o serviço não puder ser executado sem os dados:

[Unit]
Requires=mnt-data.mount
After=mnt-data.mount

Se a montagem vier de /etc/fstab, opções como nofail, x-systemd.automount e _netdev afetam o comportamento de inicialização. Não adicione um Requires= rígido a uma montagem opcional, a menos que você realmente queira que essa falha de montagem bloqueie o serviço.

Cenário 5: Um Target Puxa Muita Coisa

Sintoma: Habilitar um serviço parece iniciar unidades não relacionadas, ou desabilitar um serviço não o impede de aparecer durante a inicialização.

Diagnóstico: O serviço pode ser puxado por um target através de [Install] WantedBy=..., por Wants= de outro serviço, ou por uma unidade de socket, timer, path ou mount. enable cria links simbólicos para ativação no momento da inicialização; não é a única maneira de uma unidade iniciar.

Correção: Inspecione as dependências diretas e reversas da unidade:

systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp

Se uma unidade de socket ativar o serviço, desabilitar apenas myapp.service pode não ser suficiente. Você pode precisar desabilitar ou mascarar myapp.socket também, dependendo do comportamento desejado.

Aplicando Alterações e Recarregando

Sempre que você modificar um arquivo de unidade, deve instruir o systemd a recarregar sua configuração antes de testar as alterações.

# 1. Recarregar a configuração do gerenciador systemd
sudo systemctl daemon-reload

# 2. Reiniciar o serviço afetado
sudo systemctl restart myapp.service

# 3. Verificar o status
systemctl status myapp.service

Uma Pequena Lista de Verificação Mental

Quando um problema de dependência parece emaranhado, reduza-o a algumas verificações simples.

Primeiro, pergunte se a outra unidade deve ser iniciada. Se sim, use Wants= ou Requires=. Se o serviço atual pode ser executado sem ela, prefira Wants=. Se a falha dessa unidade significa que este serviço deve falhar, use Requires=.

Segundo, pergunte se a ordem de inicialização importa. Se sim, adicione After= ou Before=. Não espere que as diretivas de dependência lidem com a ordenação por si mesmas.

Terceiro, pergunte se o acoplamento de tempo de vida importa após a inicialização. Se uma unidade parar deve parar a outra, veja BindsTo= ou PartOf=. Não os use casualmente; eles podem fazer com que reinicializações de rotina se propaguem mais longe do que o esperado.

Finalmente, inspecione o que o systemd realmente carregou:

systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts

Essa saída é muitas vezes mais útil do que o arquivo que você acha que editou. Drop-ins, unidades de fornecedor, unidades geradas, sockets, timers e targets podem todos alterar o comportamento final.