Resolução de Problemas com Systemd: Compreendendo Dependências de Serviços e Diretivas de Ordenação

Corrija problemas de dependência e ordenação do systemd usando corretamente Requires, Wants, After, Before e comandos de diagnóstico.

Resolução de Problemas com Systemd: Compreendendo Dependências de Serviços e Diretivas de Ordenação

A maioria dos bugs confusos de dependência do systemd vem da confusão entre duas ideias separadas: requisito e ordem. Requires= e Wants= respondem "qual outra unidade deve ser puxada?" After= e Before= respondem "quando esta unidade deve iniciar em relação a outra?" Se você lembrar apenas de uma coisa deste guia, lembre-se que After=postgresql.service não inicia o PostgreSQL para você. Ele apenas diz que, se ambas as unidades estiverem sendo iniciadas, sua unidade deve esperar até que o trabalho de inicialização do PostgreSQL tenha sido executado.

Essa distinção é a fonte de muitos incidentes de "funciona quando inicio manualmente, falha após a reinicialização". Um aplicativo web inicia antes do socket do banco de dados aceitar conexões. Um worker inicia antes de /mnt/jobs ser montado. Um serviço aguarda network.target e ainda falha porque um endereço IP ainda não foi atribuído. Estes não são problemas exóticos do systemd. São suposições comuns de inicialização que precisam ser explicitadas.

Diretivas de Dependência Principais: Requires e Wants

O systemd usa duas diretivas principais para definir dependências diretas entre unidades: Requires e Wants. Essas diretivas são colocadas na seção [Unit] de um arquivo de unidade (por exemplo, um arquivo .service).

Requires=

A diretiva Requires= estabelece uma dependência forte. Se a unidade A Requires= a unidade B, o systemd inicia B quando A é iniciada. Se B não puder ser iniciada, o trabalho de inicialização de A também falha. Se B for explicitamente parada enquanto A está em execução, A normalmente também é parada, pois a relação de requisito não é mais válida.

Exemplo:

Considere um serviço de aplicativo web (myapp.service) que depende criticamente de um serviço de banco de dados (mariadb.service). O arquivo de unidade myapp.service pode incluir:

[Unit]
Description=My Web Application
Requires=mariadb.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

Neste cenário, se mariadb.service falhar ao iniciar ou for parado manualmente, o systemd também parará myapp.service. Se você tentar iniciar myapp.service e mariadb.service não estiver em execução, o systemd tentará iniciar mariadb.service primeiro. Se mariadb.service falhar, myapp.service não será iniciado.

Wants=

A diretiva Wants= define uma dependência mais fraca e opcional. Se a unidade A Wants= a unidade B, o systemd tentará iniciar a unidade B ao iniciar a unidade A, mas a unidade A ainda será ativada mesmo se a unidade B falhar ao iniciar ou não estiver em execução. Isso é útil para serviços que se beneficiam de outro serviço, mas podem funcionar de forma independente, talvez com recursos reduzidos ou um aviso.

Exemplo:

Suponha que um agente de monitoramento (monitoring-agent.service) possa ser executado sem um serviço de log específico (app-logger.service), mas idealmente o teria disponível. O arquivo de unidade monitoring-agent.service pode ser assim:

[Unit]
Description=Monitoring Agent
Wants=app-logger.service

[Service]
ExecStart=/usr/bin/monitoring-agent

[Install]
WantedBy=multi-user.target

Aqui, o systemd tentará iniciar app-logger.service quando monitoring-agent.service for ativado. No entanto, se app-logger.service falhar ao iniciar, monitoring-agent.service ainda prosseguirá para iniciar com sucesso.

Requires= vs. Wants=

  • Requires=: Dependência forte. Se a unidade exigida falhar, a unidade dependente falha ou para.
  • Wants=: Dependência fraca. A unidade dependente tenta iniciar a unidade desejada, mas prossegue mesmo se falhar.

É importante notar que Requires= implica Wants=. Se uma unidade requer outra, ela também implicitamente a deseja.

Diretivas de Ordenação: After e Before

Enquanto Requires e Wants definem o que precisa estar em execução, After e Before definem quando as unidades devem ser iniciadas em relação umas às outras. Essas diretivas controlam a sequência de operações durante o processo de inicialização do sistema ou quando as unidades são ativadas sob demanda. Elas são frequentemente usadas em conjunto com diretivas de dependência.

After=

A diretiva After= especifica que o trabalho de inicialização da unidade atual deve ser ordenado após os trabalhos de inicialização das unidades listadas. Por si só, ela não puxa essas unidades para a transação. Também não prova que a dependência está logicamente pronta para sua aplicação; ela usa apenas a visão do systemd do estado de ativação da unidade.

Exemplo:

Um serviço dependente de rede (custom-network-app.service) deve iniciar somente após a rede estar totalmente configurada. Isso é normalmente tratado garantindo que ele inicie após o alvo de rede (network.target).

[Unit]
Description=Custom Network Application
Requires=network.target
After=network.target

[Service]
ExecStart=/usr/bin/custom-network-app

[Install]
WantedBy=multi-user.target

Nesta configuração, o systemd ordenará custom-network-app.service após network.target se ambos fizerem parte da mesma transação. Para serviços que precisam de um endereço, DNS ou uma rota para outro host, network-online.target geralmente está mais próximo da intenção, mas somente se o serviço wait-online da distribuição estiver habilitado e configurado corretamente.

Before=

A diretiva Before= especifica que a unidade atual deve ser iniciada antes das unidades listadas em Before=. Isso é útil para serviços que precisam ser parados depois de outros durante o desligamento, ou iniciados antes de certos serviços para fornecer um ambiente para eles.

Exemplo:

Imagine um cenário onde um servidor de correio (postfix.service) precisa estar em execução antes de qualquer serviço voltado ao usuário que possa enviar e-mails. Você pode usar Before= para garantir que postfix.service inicie cedo.

[Unit]
Description=Postfix Mail Transfer Agent
# ... other directives like Conflicts=
Before=user-session.target

[Service]
ExecStart=/usr/lib/postfix/master

[Install]
WantedBy=multi-user.target

Esta configuração tenta iniciar postfix.service antes de qualquer coisa que faça parte de user-session.target começar sua inicialização. Da mesma forma, durante o desligamento, postfix.service estaria entre os últimos a ser parado se tiver um After=user-session.target correspondente.

After= vs. Before=

  • After=: Garante que as unidades listadas estão ativas antes da unidade atual iniciar.
  • Before=: Garante que a unidade atual inicia antes das unidades listadas.

After= e Before= são complementares. Se a unidade A diz Before=B, a ordenação é A primeiro, depois B. Se a unidade B diz After=A, o resultado é o mesmo. Você geralmente só precisa expressar a relação em um arquivo de unidade. Ao editar seu próprio serviço, geralmente é mais claro dizer o que seu serviço precisa vir depois, porque isso mantém o raciocínio local.

Combinando Diretivas para Configurações Robusta

Em cenários do mundo real, você frequentemente combinará essas diretivas para criar gráficos de dependência complexos. O multi-user.target é um alvo comum que significa que o sistema está pronto para operações multiusuário. Muitos serviços são configurados para serem WantedBy=multi-user.target e After=multi-user.target (ou mais precisamente, After=basic.target e After=getty.target etc. dos quais multi-user.target depende).

Um Padrão Comum:

Um serviço que requer um banco de dados e deve iniciar após a rede ser configurada pode ser assim:

[Unit]
Description=My Application Service
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

Explicação do padrão:

  1. Requires=mariadb.service: Garante que mariadb.service deve estar em execução para my_app.service funcionar. Se mariadb.service falhar, my_app.service será parado.
  2. Wants=other-optional-service.service: Tenta iniciar other-optional-service.service, mas my_app.service prosseguirá mesmo se falhar.
  3. After=network.target mariadb.service: Ordena my_app.service após o alvo de rede e o trabalho de inicialização do MariaDB. Se o aplicativo precisar se conectar via TCP a um banco de dados em outro host, use a configuração network-online apropriada em vez de assumir que network.target significa "a rede está utilizável".
  4. WantedBy=multi-user.target: Quando habilitado (systemctl enable my_app.service), esta diretiva adiciona um link simbólico para que my_app.service seja iniciado quando o sistema atingir o estado multi-user.target.

Considerações Avançadas e Melhores Práticas

  • WantedBy vs. RequiredBy: Semelhante a Wants vs. Requires, WantedBy é uma ordenação fraca e RequiredBy é uma ordenação forte. A maioria dos serviços usa WantedBy=multi-user.target.
  • Conflicts=: Esta diretiva especifica unidades que não devem estar em execução simultaneamente com a unidade atual. Se a unidade atual for iniciada, as unidades conflitantes serão paradas e vice-versa.
  • Dependências Transitivas: As dependências são transitivas. Se A requer B, e B requer C, então A indiretamente requer C. O systemd lida com essas cadeias automaticamente.
  • Diretivas Condition*=: Use ConditionPathExists=, ConditionFileNotEmpty=, ConditionVirtualization=, etc., para tornar a ativação da unidade condicional com base no estado do sistema, aumentando ainda mais a robustez.
  • Use systemctl list-dependencies <unidade>: Este comando é inestimável para visualizar a árvore de dependências de uma unidade, incluindo dependências diretas e indiretas.
  • Use systemctl status <unidade>: Sempre verifique o status do seu serviço após fazer alterações de configuração. Ele frequentemente mostrará razões para falha, incluindo problemas de dependência.
  • Evite Dependências Circulares: Embora o systemd tente resolvê-las, dependências circulares diretas (A Requires B, B Requires A) podem levar a loops de inicialização ou falhas. Projete cuidadosamente suas dependências para evitar isso.

Uma Passagem Prática de Depuração

Quando um bug de dependência aparecer, comece olhando para a transação que o systemd realmente construiu:

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 informa se o systemd falhou ao iniciar a unidade, a matou depois ou a considera ativa mesmo que o aplicativo esteja com problemas. journalctl -b mantém você dentro da inicialização atual, o que importa porque os problemas de dependência são frequentemente apenas de inicialização. systemctl show é direto, mas útil: mostra as propriedades finais da unidade mescladas após a aplicação de drop-ins, arquivos do fornecedor e dependências geradas.

Se você não tem certeza de onde veio uma dependência, inspecione a unidade completa:

systemctl cat my_app.service

Isso mostra a unidade empacotada e quaisquer arquivos de substituição em /etc/systemd/system/my_app.service.d/. Já vi serviços de produção onde a unidade base estava correta, mas uma substituição antiga ainda continha After=mysql.service de uma migração anterior. O serviço estava esperando em uma unidade que não existia mais, e os logs faziam parecer que o aplicativo estava quebrado.

Para questões de tempo de inicialização, use:

systemd-analyze critical-chain my_app.service
systemd-analyze blame

critical-chain é melhor do que olhar para timestamps porque mostra quais unidades atrasaram o caminho para o seu serviço. blame pode ser enganoso se você o tratar como uma classificação de serviços "ruins", mas é útil quando uma dependência leva muito mais tempo do que o esperado.

Padrões que Funcionam em Produção

Para uma dependência de banco de dados local, este é um ponto de partida razoável:

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

[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure

[Install]
WantedBy=multi-user.target

Isso diz que o PostgreSQL deve ser iniciado com a API, e a inicialização da API deve ser ordenada após o PostgreSQL. Não garante que toda migração foi concluída ou que o banco de dados aceita as credenciais exatas que seu aplicativo usa. Se isso for importante, adicione uma verificação de prontidão no nível do aplicativo. O systemd pode ordenar processos, mas não pode entender o estado do seu esquema a menos que você ensine seu serviço a verificá-lo.

Para um caminho montado, prefira RequiresMountsFor= em vez de nomear manualmente as unidades de montagem:

[Unit]
RequiresMountsFor=/srv/uploads

[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads

O systemd derivará as unidades de montagem necessárias para o caminho. Isso é mais fácil de manter do que lembrar que /srv/uploads mapeia para srv-uploads.mount.

Para ajudantes opcionais, use Wants=:

[Unit]
Wants=metrics-agent.service
After=metrics-agent.service

Se o agente de métricas falhar, o serviço principal ainda pode iniciar. Isso geralmente é o que você deseja para sidecars de log, exportadores opcionais e ajudantes de notificação local. Não use Requires= apenas porque dois serviços estão relacionados. Use-o apenas quando o serviço dependente realmente não puder fazer trabalho útil sem a outra unidade.

Para serviços fortemente acoplados que devem parar juntos, veja BindsTo= e PartOf=. BindsTo= é mais forte que Requires= e é útil quando um serviço deve desaparecer se a unidade vinculada desaparecer, como um serviço vinculado a uma unidade de dispositivo específica. PartOf= é frequentemente útil para grupos: reiniciar ou parar a unidade pai pode propagar para unidades filhas. Estas não são diretivas de primeira escolha, mas resolvem problemas que Requires= não consegue expressar claramente.

Armadilhas Comuns

Não adicione After=multi-user.target a um serviço normal de longa duração que está habilitado com WantedBy=multi-user.target. Isso frequentemente cria uma ordenação estranha e raramente diz o que o autor pretendia. A maioria dos serviços é puxada por multi-user.target; eles não precisam iniciar depois que ele já foi alcançado.

Não presuma que network.target significa "a internet está acessível". É um ponto de sincronização para gerenciamento de rede, não um teste de conectividade. Se seu aplicativo se comunica com uma API remota, adicione lógica de repetição dentro do aplicativo de qualquer maneira. A ordenação de rede no momento da inicialização reduz o ruído, mas não pode protegê-lo de falha de DNS, mudanças de roteamento ou uma dependência remota estar inativa.

Não esconda longos sleeps em ExecStartPre=/bin/sleep 30 a menos que você não tenha uma opção melhor. Sleeps tornam as inicializações mais lentas quando a dependência está pronta rapidamente e ainda falham quando a dependência leva mais tempo do que o esperado. Um pequeno loop de prontidão que verifica o socket, arquivo ou API real geralmente é mais claro.

Quando você alterar as diretivas de dependência, execute systemctl daemon-reload, reinicie o serviço e verifique o journal da mesma inicialização. A correção mais rápida geralmente é aquela que prova a suposição de ordenação exata que estava errada.