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.
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
.timercuja seção[Timer]define quando um.servicecorrespondente 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 deWantedBy=timers.target. - O
systemd-analyze calendarverifica se uma expressãoOnCalendar=é interpretada corretamente e exibe o próximo horário em que ela será disparada. Persistent=trueexecuta 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
MAILTOdo cron. Para alertas de falha, useOnFailure=.
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=truerecupera a execução perdida (veja a seção sobre Persistent). - Nenhuma forma fácil de verificar o resultado da noite anterior. O
systemctl statusmostra 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:
| Agendamento | cron | OnCalendar |
|---|---|---|
| Diariamente às 02:30 | 30 2 * * * | *-*-* 02:30:00 |
| Dias úteis às 22:30 | 30 22 * * 1-5 | Mon..Fri 22:30 |
| Domingos às 03:00 | 0 3 * * 0 | Sun *-*-* 03:00:00 |
| Dia 1º do mês, à meia-noite | 0 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.