12k
All articles

Substituindo seus cron jobs por timers do systemd

Troque tarefas cron por timers do systemd: crie unidades service e timer, valide horários OnCalendar, recupere execuções perdidas e consulte logs e falhas.

OpenReplay Team
OpenReplay Team
Substituindo seus cron jobs por timers do systemd

Para substituir um cron job por um timer do systemd, você escreve dois arquivos de unidade: um .service, que executa o script, e um .timer, que define quando ele roda. Depois, basta habilitar o timer com systemctl enable --now.

Se você já abriu o /var/log/backup.log às 9 da manhã e não conseguiu dizer se a execução da noite anterior funcionou, já sabe por que tanta gente está abandonando o cron. Este guia migra uma linha real de crontab para um timer:

30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Cada seção substitui uma parte dessa linha. O agendamento vira OnCalendar=, o redirecionamento vira o journal e a pergunta “será que rodou?” passa a ser respondida pelo systemctl status. Para saber mais sobre o próprio cron, veja como automatizar cron jobs. Os comandos funcionam em qualquer distribuição atual baseada em systemd.

Principais pontos

  • Um timer do systemd é uma unidade .timer cuja seção [Timer] define quando um .service correspondente será executado. O service define o que roda, e o timer é a unidade que você habilita.
  • O service acionado por um timer não precisa de seção [Install]. O próprio timer se vincula à inicialização por meio de WantedBy=timers.target.
  • O systemd-analyze calendar verifica se uma expressão OnCalendar= é interpretada corretamente e exibe o próximo horário em que ela será disparada.
  • Persistent=true executa um job perdido enquanto a máquina estava desligada assim que o timer é ativado novamente.
  • Os timers do systemd não têm um equivalente nativo ao MAILTO do cron. Para alertas de falha, use OnFailure=.

O que o cron não consegue fazer

O cron inicia comandos de acordo com um agendamento e faz pouca coisa além disso. As limitações abaixo são os motivos mais comuns para migrar um job:

  • Nenhum log além da saída enviada por e-mail. Ou você redireciona a saída para um arquivo manualmente, ou depende do MAILTO. Jobs de timers gravam no journal automaticamente (veja Lendo os logs).
  • Nenhuma dependência entre jobs. Jobs de timers são unidades comuns, então podem depender de outras unidades do systemd.
  • Nada é executado se a máquina estava desligada no horário agendado. Persistent=true recupera a execução perdida (veja a seção sobre Persistent).
  • Nenhuma forma fácil de verificar o resultado da noite anterior. O systemctl status mostra a última execução e seu código de saída.

Montando um timer do systemd de ponta a ponta

Um timer funcional consiste em dois arquivos em /etc/systemd/system/ e mais um comando: sudo systemctl enable --now backup.timer. Comece pelo script.

#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail

SRC=/srv/app/data
DEST=/var/backups/app

tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete

O set -euo pipefail faz o script encerrar com código diferente de zero assim que qualquer comando falha, e é esse código de saída que o systemd registra. Torne o script executável e crie o diretório de destino. Se os bits de permissão não forem familiares para você, o guia de permissões de arquivos no Linux explica como funcionam.

sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app

Em seguida, o service, /etc/systemd/system/backup.service:

[Unit]
Description=Nightly tarball of /srv/app/data

[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh

Type=oneshot em systemd.service(5) é adequado para scripts que rodam até o fim. O systemd só considera a unidade iniciada depois que o script termina. A menos que você adicione RemainAfterExit=, a unidade volta imediatamente para o estado inativo e nunca aparece como “active”. Substitua deploy por uma conta existente sem privilégios de root que consiga ler SRC e gravar em DEST. Sem User=, o job é executado como root. O service não tem seção [Install] nem qualquer referência ao timer. A página sobre timers da Arch Wiki explica o motivo: o timer é a unidade que você habilita, e ele ativa o service de mesmo nome.

Agora o timer, /etc/systemd/system/backup.timer:

[Unit]
Description=Run backup.service daily at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

O timers.target é um target especial que o systemd usa durante a inicialização para ativar todos os timers que devem estar em execução. Recarregue o systemd, execute o job uma vez manualmente e depois agende-o:

sudo systemctl daemon-reload
sudo systemctl start backup.service     # test run now; returns when the script exits
systemctl status backup.service         # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer

Como todo job agendado é um service comum, você pode executá-lo sob demanda com systemctl start backup.service e depurá-lo sem precisar esperar até 02:30. Sozinho, o enable apenas cria os links simbólicos. A opção --now em systemctl(1) também inicia o timer. O list-timers aceita um padrão de nome de unidade, e --all inclui os timers inativos. Entre uma execução e outra, o service aparece como inativo. Isso é normal.

OnCalendar vs. a linha de cinco campos do cron

Execute systemd-analyze calendar em qualquer expressão OnCalendar= antes de colocá-la em um arquivo de timer. O comando confirma que a expressão é interpretada corretamente e exibe o próximo horário de disparo. A gramática descrita em systemd.time(7) é DayOfWeek Year-Month-Day Hour:Minute:Second. Qualquer campo de data ou hora pode ser * para corresponder a qualquer valor, uma lista de valores separados por vírgula ou um intervalo escrito com ... Você pode omitir totalmente o dia da semana. Se o mantiver, use os nomes dos dias em inglês, como Mon ou Monday, e não *.

Os agendamentos mais comuns do cron correspondem às expressões OnCalendar= mostradas abaixo. A cheatsheet de cron no Linux aborda o lado do cron desta tabela:

AgendamentocronOnCalendar
Diariamente às 02:3030 2 * * **-*-* 02:30:00
Dias úteis às 22:3030 22 * * 1-5Mon..Fri 22:30
Domingos às 03:000 3 * * 0Sun *-*-* 03:00:00
Dia 1º do mês, à meia-noite0 0 1 * **-*-01 00:00:00
A cada 15 minutos*/15 * * * **:00/15

A linha de cron 30 22 * * 1-5 se torna OnCalendar=Mon..Fri 22:30, porque os números de 1 a 5 dos dias da semana no cron correspondem a segunda a sexta-feira. O sufixo /15 significa “repetir a cada 15 unidades”. Valide uma expressão com o systemd-analyze calendar:

systemd-analyze calendar "Mon..Fri 22:30" --iterations=3

Exemplo de saída (as datas são apenas marcadores):

  Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
    Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
       (in UTC): Mon YYYY-MM-DD 20:30:00 UTC
       From now: 5h 12min left
   Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
   Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST

A linha normalizada mostra a forma abreviada expandida para a gramática completa. A linha (in UTC) só aparece quando o horário local não é UTC. Versões anteriores ao systemd 253 identificam as execuções adicionais como Iter. #2 em vez de Iteration #2. O --iterations= exige o systemd 242 ou posterior.

Uma ressalva sobre precisão: o AccuracySec de um timer tem como padrão um minuto, então um job agendado para 02:30:00 não tem garantia de iniciar exatamente nesse segundo. Se você precisar da maior precisão que o systemd oferece, adicione AccuracySec=1us à seção [Timer].

Persistent=true: o recurso pensado para notebooks

Com Persistent=true, um timer que perdeu a execução agendada porque a máquina estava desligada inicia seu service assim que for ativado novamente. Normalmente, isso significa na próxima inicialização. O cron puro não tem nada equivalente. Se o notebook estiver desligado às 02:30, o cron simplesmente pula o job. A ferramenta separada anacron existe justamente para recuperar jobs perdidos (em sistemas da família RHEL, ela vem junto com o cronie), mas só faz agendamentos em dias inteiros, não em horários específicos. O systemd.timer(5) registra quando cada timer persistente foi disparado pela última vez. O Persistent= não tem efeito em timers sem OnCalendar=, e a execução de recuperação ainda respeita qualquer atraso definido em RandomizedDelaySec=.

Timers monotônicos, como OnBootSec= e OnUnitActiveSec=, se comportam de outra forma. Eles param de contar enquanto o sistema está suspenso, a menos que WakeSystem= esteja definido. Use OnCalendar= com Persistent=true para jobs vinculados a um horário do dia.

Lendo os logs e o status da última execução

Para saber se o job da noite anterior foi bem-sucedido, execute systemctl status backup.service. O comando mostra quando o service foi executado pela última vez, se teve sucesso ou falhou e o código de saída do processo. O journalctl(1) mostra tudo o que o script imprimiu:

systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"

O --since aceita yesterday, today, timestamps absolutos e deslocamentos relativos prefixados com -. Observe que yesterday significa a meia-noite do início do dia anterior, então a saída também inclui as entradas de hoje.

Vale a pena provocar uma falha proposital em um job de timer do systemd pelo menos uma vez. Edite o script para que ele contenha SRC=/srv/app/missing e execute-o:

sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20

O tar falha, o set -e faz o script encerrar com código diferente de zero e a unidade termina no estado failed, com esse código de saída registrado no status. O journal contém a própria mensagem de erro do tar. Restaure o SRC quando terminar.

Quando o cron ainda é uma boa opção?

O cron continua sendo uma boa opção, e mais simples, para um único job sem dependências. Um job ocupa uma única linha no crontab, o cron está disponível em praticamente qualquer lugar, e um job noturno isolado e sem dependências funciona perfeitamente com ele. Os timers exigem dois arquivos e alguns comandos systemctl. Além disso, não têm um equivalente nativo ao MAILTO. Para receber uma notificação quando um job falhar, aponte a diretiva OnFailure= do service para uma unidade que envie o alerta.

Conclusão

Migrar do cron para os timers do systemd traz três vantagens que o cron puro não oferece: um job que você pode executar e inspecionar sob demanda, uma saída que vai direto para o journal sem redirecionamentos e execuções de recuperação após períodos fora do ar. Escolha o job do crontab que mais lhe causa preocupação. Escreva o service e o timer correspondentes, valide o agendamento com systemd-analyze calendar, teste com systemctl start e remova a linha do crontab assim que o systemctl status mostrar uma execução agendada bem-sucedida.

Perguntas frequentes

Posso executar um timer do systemd como usuário comum, sem root?

Sim. Coloque os arquivos .service e .timer em ~/.config/systemd/user/ e gerencie-os com systemctl --user, por exemplo, systemctl --user enable --now backup.timer. Por padrão, a instância do systemd do usuário é encerrada quando a última sessão de login desse usuário é fechada, e os timers também deixam de disparar. Execute sudo loginctl enable-linger seguido do nome de usuário para iniciar a instância do usuário durante o boot e mantê-la em execução sem uma sessão ativa.

Qual é a diferença entre AccuracySec e RandomizedDelaySec?

O AccuracySec dá ao systemd margem para agrupar eventos de timers e despertar a CPU com menos frequência. O timer pode disparar em qualquer momento entre o horário agendado e o fim do intervalo definido em AccuracySec, que por padrão é de 1 minuto. O RandomizedDelaySec funciona no sentido oposto ao agrupamento. Ele adia cada execução por um tempo aleatório de até o valor definido (0 por padrão), de modo que timers com o mesmo agendamento se distribuam em vez de dispararem todos juntos. Se você definir ambos, o atraso aleatório é aplicado antes de o systemd alinhar o evento com os demais.

Como agendo um job pontual com o systemd sem escrever arquivos de unidade?

Use o systemd-run com uma opção de timer, por exemplo, sudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.sh. Isso cria um par transitório de timer e service. Apenas o timer é iniciado imediatamente, e o service roda quando o timer expira. Flags como --on-active= e --on-boot= criam timers transitórios monotônicos. Unidades transitórias não sobrevivem a uma reinicialização, então jobs recorrentes devem continuar em arquivos de unidade.

O arquivo do timer precisa ter o mesmo nome do service que ele aciona?

Não, mas esse é o comportamento padrão. Um timer ativa o service com o mesmo nome, sem o sufixo, então backup.timer inicia backup.service. Para acionar uma unidade com outro nome, defina Unit= na seção [Timer], por exemplo, Unit=nightly-backup.service. O valor não pode ser outra unidade .timer. Ainda assim, o systemd.timer(5) recomenda dar ao timer e à unidade que ele inicia o mesmo nome base.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.