SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Systemd não inicia: entenda o código de saída

Leia systemctl status primeiro: veja o que 203/EXEC e 226/NAMESPACE significam e por que uma unidade inicia sem erro e termina um segundo depois.

Por que uma unidade systemd não inicia

Uma unidade systemd que não inicia informa o motivo num campo. Execute systemctl status <unit> e procure code= e status= na linha que indica a falha. Um código de estado na casa dos 200 significa que o systemd nunca chegou a executar o seu programa: falhou ao preparar o ambiente solicitado pelo ficheiro da unidade. Um código inferior a 200 significa que o programa foi executado e terminou por conta própria. Nesse caso, o ficheiro da unidade está provavelmente correto e o problema está na aplicação.

Esta divisão define o caminho de diagnóstico. O restante segue essa divisão, pela ordem em que os números aparecem.

Quais três comandos respondem à pergunta, pela ordem

systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.service

systemctl status dá o veredito. Leia primeiro a linha Loaded:, porque ela identifica o ficheiro que o systemd efetivamente analisou e indica se a unidade está ativa, mascarada ou se nem sequer foi encontrada. Depois, leia a linha Active: e o par code= e status= abaixo dela.

journalctl -u myapp.service -b --no-pager fornece os detalhes. -u filtra para essa unidade, -b limita a saída ao boot atual para não ler uma falha da semana passada, e --no-pager imprime diretamente no terminal, permitindo encaminhar a saída para grep. status mostra apenas as últimas linhas do log e encurta as linhas longas. O journal mostra tudo o que o programa escreveu antes de terminar, que normalmente é o erro real. Adicione -n 100 para obter mais histórico, ou execute-o com -f num segundo terminal enquanto reinicia a unidade.

systemd-analyze verify carrega um ficheiro de unidade sem o executar. Emite avisos sobre secções e diretivas desconhecidas e sinaliza comandos em ExecStart= que não consegue executar. Isto deteta as duas classes discretas de erro: uma chave escrita incorretamente, que o systemd ignora durante o carregamento com um aviso que a maioria das pessoas nunca lê, e um caminho que não existe.

Depois de editar qualquer ficheiro de unidade, execute sudo systemctl daemon-reload. Até fazer isso, o systemd continua a usar a cópia que carregou anteriormente, e systemctl status adiciona um aviso de que o ficheiro no disco foi alterado. Uma correção que “não fez nada” é muitas vezes uma correção que o systemd ainda não leu.

Há mais dois comandos úteis. systemctl cat myapp.service imprime a unidade efetiva, ou seja, o ficheiro principal mais todos os drop-ins em /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory imprime esses valores tal como o systemd os analisou, que é exatamente o que será executado.

O que significa status=203/EXEC?

203/EXEC significa que o systemd concluiu a configuração, chamou execve() e o kernel recusou a execução. O seu programa não executou nenhuma linha do próprio código. Quatro causas abrangem quase todos os casos.

  1. O caminho em ExecStart= está errado ou não é absoluto. Verifique-o com ls -l e compare-o com a cadeia exata no ficheiro da unidade.
  2. O ficheiro não tem o bit de execução. sudo chmod +x /opt/myapp/run.sh corrige o problema. Um ficheiro extraído de um arquivo ou copiado de outra máquina perde frequentemente esse bit.
  3. A linha shebang está inválida. O kernel lê a primeira linha de um script e executa o interpretador indicado nela. Por isso, #!/usr/bin/env python3 falha quando o PATH do serviço não contém python3. Um ficheiro guardado com terminações de linha do Windows pede um interpretador chamado /bin/bash\r, que não existe.
  4. O ficheiro não é algo que esta máquina consiga executar: tem a arquitetura errada ou é um ficheiro de texto sem shebang.

Reproduza o problema manualmente, como o utilizador do serviço, antes de alterar qualquer coisa.

sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -A

file identifica a arquitetura e informa "with CRLF line terminators" quando as terminações de linha são o problema. cat -A mostra a mesma informação como um ^M no final. Remova-as com sed -i 's/\r$//' /opt/myapp/run.sh.

Há uma ressalva importante sobre o intervalo: 200 ou superior é uma convenção, não uma garantia. O seu próprio programa pode terminar com o código 203, e o systemd não consegue distinguir os dois casos. systemd-analyze exit-status 203 mostra o nome e a classe de qualquer código, o que ajuda a consultar a tabela. No entanto, se a sua aplicação escolher códigos de saída superiores a 199, altere-os.

Por que recebo 217/USER ou 216/GROUP?

217/USER significa que a conta indicada em User= não existe no momento em que o serviço é iniciado. 216/GROUP indica a mesma falha para Group= ou SupplementaryGroups=. Confirme cada caso com um comando.

getent passwd appuser
getent group appgroup

Cada comando imprime uma linha ou não imprime nada e retorna um código diferente de zero. Nenhuma saída significa que o nome não é conhecido pelo sistema. Por isso, o systemd não consegue mudar para essa conta e interrompe o processo antes de executar o serviço. A correção é criar a conta, não definir User=root. Executar cada serviço com uma conta de sistema dedicada e privilégios mínimos é precisamente o objetivo dessa diretiva.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser

DynamicUser=yes contorna o problema fazendo o systemd alocar uma conta temporária a cada inicialização. Esta opção é adequada para um serviço que não mantém estado. Qualquer serviço que grave ficheiros também precisa de StateDirectory=, porque o ID de utilizador muda entre as inicializações e os ficheiros num caminho comum acabam pertencendo a uma conta que já não existe.

O que é 226/NAMESPACE?

226/NAMESPACE vem das diretivas de isolamento. Quando uma unidade define ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= ou algo semelhante, o systemd cria um namespace de montagem privado para esse serviço antes de executar o programa. Um namespace é uma vista privada do sistema de ficheiros para um processo. Se alguma montagem desse plano falhar, o arranque termina com o erro 226 e o programa nunca é executado.

A causa habitual é um caminho em ReadWritePaths= que não existe. ProtectSystem=strict monta todo o sistema de ficheiros como somente leitura, e ReadWritePaths= reabre os caminhos indicados para permitir a escrita. O systemd não consegue reabrir um diretório que não existe. Há duas correções adequadas. Permita que o systemd crie o diretório com StateDirectory=. Essa opção cria /var/lib/<name> em cada arranque e atribui-o ao utilizador do serviço. Em alternativa, acrescente - ao início do caminho. Isso instrui o systemd a ignorar essa entrada quando a origem não existe. A correção errada é remover o isolamento. Isso troca um problema de cinco minutos por um problema permanente.

[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploads

Quando não conseguir determinar qual linha é responsável, remova todo o bloco de isolamento, recarregue a configuração e inicie o serviço. Se o serviço iniciar, volte a adicionar as linhas uma a uma e reinicie-o depois de cada alteração. Dois erros relacionados nesta família são 233/RUNTIME_DIRECTORY e 238/STATE_DIRECTORY. Significam que o systemd não conseguiu criar o diretório indicado em RuntimeDirectory= ou StateDirectory=, nem assumir a respetiva propriedade. Normalmente, isso acontece porque o caminho já existe e pertence a outro utilizador.

Por que 200/CHDIR aparece quando WorkingDirectory parece correto?

200/CHDIR significa que chdir() em WorkingDirectory= falhou. O diretório não existe ou o utilizador do serviço não consegue entrar nele. Para entrar num diretório, é necessária permissão de execução nesse diretório e em todos os diretórios-pai acima dele. Por isso, um /home/deploy/app perfeitamente legível fica inacessível quando /home/deploy tem o modo 700 e o serviço é executado como appuser.

sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myapp

namei -l mostra o proprietário e o modo de cada componente do caminho. É a forma mais rápida de encontrar o diretório que bloqueia o acesso aos restantes. Definir WorkingDirectory=-/srv/myapp torna a ausência do diretório não fatal. Isto é adequado para um programa que não depende do diretório inicial. É inadequado para um programa que abre ficheiros usando caminhos relativos.

Por que o serviço inicia e depois para um segundo mais tarde?

Aqui não há nenhum código da série 200 e, muitas vezes, não há texto de erro. A unidade mostra inactive (dead) logo depois de iniciar ou passa repetidamente por activating (auto-restart). O systemd criou o ambiente corretamente. O problema está na diferença entre o comportamento do programa e o que Type= declara que ele deve fazer.

Type=simple, o valor predefinido, indica que o programa permanece em primeiro plano. Se fornecer um daemon que cria um processo filho em segundo plano e termina, o systemd considera que o processo principal terminou e marca o serviço como concluído. A maioria dos daemons tem uma opção para permanecer em primeiro plano, como nginx -g 'daemon off;'.

Type=forking indica que o primeiro processo termina assim que o processo filho fica pronto. Se fornecer um programa em primeiro plano, a tarefa de arranque espera até TimeoutStartSec= se esgotar, 90 segundos por predefinição. Depois, o systemd termina o processo e regista um timeout.

Type=notify indica que o programa chama sd_notify() para anunciar que está pronto. Um programa sem esse suporte não anuncia nada. Por isso, o arranque atinge o timeout e o journal regista o resultado como uma falha de protocolo.

Escolha o tipo com base no comportamento real do programa. Diferenças entre simple, forking, oneshot e notify é a decisão que resolve toda esta classe de falhas.

Quando um serviço termina repetidamente, o systemd deixa de tentar iniciá-lo e informa que o pedido de arranque foi repetido demasiado depressa. A unidade permanece em estado failed até passar a janela do limite de frequência ou até executar sudo systemctl reset-failed myapp.service. Aumentar o limite apenas oculta o sintoma. Consulte o journal a partir da primeira falha, e não da última, e veja o que Restart=on-failure tenta realmente reiniciar antes de o alterar.

Por que a unidade está inativa sem apresentar nenhum erro?

Uma unidade pode ser ignorada em vez de iniciada. As diretivas Condition* são silenciosas por definição: quando a verificação falha, o systemd marca o job como concluído e não faz nada. Uma unidade que contenha ConditionPathExists=/etc/myapp/config.yml nunca será iniciada enquanto esse ficheiro estiver ausente e também nunca comunicará um erro.

systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i condition

ConditionResult=no confirma que a unidade foi ignorada, e o journal identifica a verificação que não foi satisfeita. Use uma diretiva Assert* quando a ausência de um pré-requisito tiver de causar uma falha explícita. Condições, asserts e ordenação de unidades explica qual verificação deve ser usada em cada local.

Existem alguns outros casos silenciosos nas proximidades. Um erro "could not be found" normalmente significa que o ficheiro está no diretório errado ou que ainda não recarregou a configuração: os ficheiros de unidade que criar devem ficar em /etc/systemd/system/. Uma unidade mascarada recusa qualquer arranque até sudo systemctl unmask myapp.service a desmascarar. E systemctl enable falha numa unidade sem uma secção [Install], por isso adicione-lhe WantedBy=multi-user.target.

E se o processo tiver sido terminado em vez de falhar?

code=killed é diferente de code=exited. Algo terminou o processo a partir do exterior. status=9/KILL aponta para o out of memory (OOM) killer, e o journal identifica o processo que este selecionou. Um limite definido por si faz o mesmo dentro do cgroup (control group). Por isso, verifique a memória livre no host com free -m e confirme se a unidade tem um MemoryMax=. MemoryMax, CPUQuota e os outros limites do cgroup explica qual limite termina um processo e qual apenas o torna mais lento.

status=15/TERM logo depois de uma tentativa de arranque normalmente significa que o systemd excedeu o tempo limite de arranque e terminou o processo. Nesse caso, volte a consultar Type=.

Dois hábitos que evitam a maioria destas falhas

Use caminhos absolutos em todo o lado. O systemd não executa a sua shell de login, por isso não existe .bashrc, nem .profile, nem um ambiente virtual ativado. $PATH para um serviço do sistema é uma lista interna curta que não inclui /opt nem os shims de um gestor de versões de linguagens. Escreva /usr/bin/python3 ou /opt/myapp/venv/bin/python por extenso. command -v myapp na sua shell mostra o caminho que deve copiar. A mesma regra aplica-se a WorkingDirectory=, EnvironmentFile= e a todos os caminhos em ReadWritePaths=.

ExecStart= não é uma shell. O systemd divide a linha em palavras e chama o próprio execve(). Pipes, redirecionamentos, globbing, &&, crases e ~ não têm significado: chegam ao seu programa como argumentos literais. ExecStart=/usr/bin/myapp --flag > /tmp/out.log passa > e /tmp/out.log para myapp, que termina com um erro de utilização que não se parece com um problema do systemd. Quando precisar de funcionalidades da shell, invoque uma shell.

ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'

Para obter apenas a saída, não precisa disso. A saída do serviço é enviada para o journal por predefinição, e StandardOutput=append:/var/log/myapp.log escreve num ficheiro sem envolver uma shell.

A expansão de variáveis é limitada da mesma forma. $MYVAR e ${MYVAR} são substituídas a partir de Environment= e EnvironmentFile=, e nada mais é expandido. $HOME não é definida para um serviço do sistema, a menos que a defina. Um EnvironmentFile= também não é um script de shell: export não pertence a esse contexto, as regras de quoting são diferentes das do bash e um ficheiro em falta é fatal, a menos que prefixe o caminho com -.

Trabalhar num servidor em produção

Leia o código, confirme a causa, altere uma coisa e reinicie. Esta ordem é mais importante do que conhecer todos os números, porque impede que acumule três alterações especulativas e perca a noção de qual delas resolveu o problema. O mesmo processo aplica-se a unidades que não escreveu. Um timer que nunca é acionado depende de um serviço que nunca arrancou. Por isso, depure primeiro o serviço: um timer do systemd e o serviço que ele aciona falha exatamente das formas descritas acima, mas o timer oculta a saída até consultar o journal.

FAQ

O que significa status=203/EXEC em systemctl status?

O systemd configurou tudo o que a unidade pediu, mas a chamada execve() falhou, por isso o programa nunca iniciou. Verifique quatro pontos, por esta ordem: o caminho em ExecStart= existe e é absoluto, o ficheiro tem o bit de execução, o shebang indica um interpretador existente no PATH do serviço e o ficheiro usa finais de linha Unix. No último caso, file informa "with CRLF line terminators", o que transforma o nome do interpretador em /bin/bash\r e faz o kernel recusar a execução.

Porque é que o meu serviço inicia e para logo a seguir?

O ficheiro da unidade promete um comportamento que o programa não tem. Com Type=simple, o systemd espera que o programa permaneça em primeiro plano, por isso um daemon que faz fork para segundo plano parece terminado assim que cria o processo filho. Com Type=forking, o systemd espera que o primeiro processo termine, por isso um programa em primeiro plano faz o trabalho de arranque ficar bloqueado até TimeoutStartSec= expirar. Ajuste Type= ao programa e, quando o programa disponibilizar uma flag para primeiro plano, use essa flag com o valor predefinido Type=simple.

Como posso ver o erro real em vez da saída de estado resumida?

systemctl status mostra apenas as últimas linhas do journal e encurta as linhas longas. Execute journalctl -u myapp.service -b --no-pager para obter tudo o que a unidade registou durante este boot, adicione -n 200 para obter uma janela maior ou encaminhe a saída para grep. Se a aplicação escrever o seu próprio ficheiro de log, leia-o também, porque o systemd captura apenas o que o programa envia para a saída standard e para o erro standard.

Porque é que a minha unidade está inativa sem apresentar uma mensagem de erro?

Na maioria dos casos, uma diretiva Condition* ignorou-a. Essas verificações são silenciosas: uma condição falhada marca o trabalho de arranque como concluído com sucesso. Execute systemctl show myapp.service -p ConditionResult e procure ConditionResult=no; depois leia a linha do journal que identifica a verificação. A outra causa comum é uma unidade mascarada, que recusa qualquer arranque até sudo systemctl unmask a desmascarar.

Preciso de executar daemon-reload depois de cada alteração ao ficheiro da unidade?

Sim, para qualquer alteração a um ficheiro de unidade ou a um drop-in. sudo systemctl daemon-reload faz o systemd reler os ficheiros do disco e, em seguida, sudo systemctl restart myapp.service aplica-os ao serviço em execução. Não precisa de executar esse comando depois de systemctl edit, que faz a recarga por si, nem depois de alterar um ficheiro de configuração pertencente à aplicação e não ao systemd.

#systemd#troubleshooting#journalctl#exit-codes#linux-fundamentals