Learning Platform
Глоссарий Troubleshooting
Урок 17.03 · 22 мин
Начальный
systemdtimerschedulingOnCalendarcron alternative

Зачем альтернатива cron

cron — proven, простой, надёжный. Но у него есть фундаментальные ограничения:

  • Пропущенные запуски не догоняются. Если VM была off в 06:00, daily job не запустится. Cron не имеет понятия «последний запуск».
  • Логи разбросаны. cron сам пишет в syslog, скрипт пишет куда сам redirect-ит, status невозможно получить.
  • Нет встроенного supervisor. cron запустил — забыл. Если процесс висит — никто не убьёт.
  • Нет dependencies. Нельзя «запусти ETL после того как Postgres готов».
  • Нет встроенной jitter. 100 VM с одинаковым 0 6 * * * ударят по DB одновременно.
  • Минимальное окружение. См. урок 02-cron-gotchas — там целый список граблей.

systemd timer — это альтернатива, решающая эти проблемы. Идея: вместо одного запутанного crontab, у тебя пара .service + .timer:

  • .service описывает, что выполнить (как обычный сервис из урока 04 модуля 15).
  • .timer описывает, когда запускать.

Анатомия: пара .service + .timer

Минимальный пример. Файл orders-etl.service:

[Unit]
Description=Daily orders ETL job
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=oneshot
User=etl
WorkingDirectory=/opt/orders-etl
ExecStart=/opt/orders-etl/venv/bin/python -m orders_etl
EnvironmentFile=/etc/orders-etl/env
StandardOutput=journal
StandardError=journal

Type=oneshot — выполнился и вышел. Это batch-job, не daemon. Нет Restart=on-failure — это не сервис, который должен работать постоянно.

Файл orders-etl.timer:

[Unit]
Description=Daily orders ETL timer

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
RandomizedDelaySec=5min
Unit=orders-etl.service

[Install]
WantedBy=timers.target

OnCalendar= — расписание. *-*-* 06:00:00 = «любой год, любой месяц, любое число, 06:00:00 локального времени». Эквивалент cron 0 6 * * *.

Запуск:

# Положить оба файла в /etc/systemd/system/
$ sudo cp orders-etl.service orders-etl.timer /etc/systemd/system/

# Перечитать
$ sudo systemctl daemon-reload

# Enable + start TIMER (не service)
$ sudo systemctl enable --now orders-etl.timer

enable --now для timer-а: установит autostart при boot и запустит таймер сейчас (чтобы он начал тикать). Сервис сам запустится в 06:00.

OnCalendar=: формат расписания

OnCalendar= — это более выразительный, чем cron, формат:

DAYS YYYY-MM-DD HH:MM:SS

Все части опциональны:

OnCalendar — примеры расписаний

Гораздо более выразительно и читаемо, чем cron.

*-*-* 06:00:00каждый день в 06:00
OnCalendar=dailyежедневно в полночь
hourly / minutelyкаждый час / минуту
Mon-Fri 09:00будни в 09:00
*:0/15каждые 15 минут
*-*-01 02:30:001-го числа в 02:30
*-01,07-01 00:00дважды в год
*-*-* 06:00:00 UTCв UTC, не local

Проверить, во сколько следующее срабатывание:

$ systemd-analyze calendar "Mon-Fri *-*-* 09:00:00"
Original form: Mon-Fri *-*-* 09:00:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Mon 2026-05-18 09:00:00 UTC
From now: 4 days left

Это аналог cronitor.io или calendar-parser для cron-выражений, но встроенный.

Persistent=true — главный killer feature

Это самое важное преимущество timers над cron:

[Timer]
OnCalendar=daily
Persistent=true

Persistent=true означает: если timer должен был сработать, пока система была off, при следующем boot он сработает сразу.

Сценарий: laptop с DE-задачами, выключаешь на ночь. cron-job в 0 6 * * * пропустится — VM не работала в 06:00, cron его не запомнит. systemd timer с Persistent=true сработает сразу, как только laptop включится в 09:00.

Для production server это тоже важно: при maintenance window (планируемая перезагрузка) или unexpected reboot timer догонит пропущенные запуски.

Под капотом: systemd хранит timestamp последнего запуска в /var/lib/systemd/timers/. При boot сравнивает с OnCalendar= — если время уже прошло, запускает.

RandomizedDelaySec=: jitter для thundering herd

[Timer]
OnCalendar=*-*-* 06:00:00
RandomizedDelaySec=10min

Без jitter все 100 VM с одинаковым расписанием стрелят в один и тот же момент -> DB перегружается, S3 throttles, kubernetes API rate-limits.

RandomizedDelaySec=10min — добавляет случайную задержку от 0 до 10 минут на каждый запуск. На 100 VM с этим — нагрузка равномерно размазана между 06:00 и 06:10.

В cron это сделать сложнее — обычно через sleep $((RANDOM % 600)) в начале скрипта.

OnBootSec, OnUnitActiveSec — relative timing

Кроме absolute OnCalendar=, есть relative:

[Timer]
# Запустить через 5 минут после boot
OnBootSec=5min

# Запустить через 1 час после ПРОШЛОГО запуска связанного service
OnUnitActiveSec=1h

OnUnitActiveSec=1h — это «через 1 час после прошлой активации сервиса». Если сервис long-running и upgraded — recalculation. Полезно для health-checks или monitoring.

Можно комбинировать: timer срабатывает на первое наступившее условие.

Запуск, статус, debugging

# Активировать
$ sudo systemctl enable --now orders-etl.timer

# Статус timer (НЕ service)
$ systemctl status orders-etl.timer
 orders-etl.timer - Daily orders ETL timer
     Loaded: loaded (/etc/systemd/system/orders-etl.timer; enabled)
     Active: active (waiting) since Mon 2026-05-13 08:42:11 UTC; 4h 23min ago
    Trigger: Wed 2026-05-14 06:00:00 UTC; 13h left
   Triggers: orders-etl.service

# Что сейчас запланировано:
$ systemctl list-timers
NEXT                        LEFT          LAST                        PASSED   UNIT
Wed 2026-05-14 06:00:00 UTC 13h left      Tue 2026-05-13 06:00:00 UTC 6h 23min orders-etl.timer
Wed 2026-05-14 09:00:00 UTC 16h left      Tue 2026-05-13 09:00:00 UTC 3h 23min weekly-report.timer
...

# Логи последнего запуска сервиса:
$ journalctl -u orders-etl.service --since today

# Логи самого таймера (когда срабатывает):
$ journalctl -u orders-etl.timer

# Запустить вручную (для теста):
$ sudo systemctl start orders-etl.service

systemctl status orders-etl.timer показывает: when next, when last, какой service triggered. Это намного больше информации, чем cron даёт.

systemctl list-timers — обзор всех timer-ов на системе. Очень полезная команда для DE-аудита: «что у нас запланировано?».

Сравнение: cron vs systemd timer

cron vs systemd timer

Каждый имеет свою нишу.

Feature
cron
systemd timer
Простота
Очень простой, одна строка
Два файла, больше boilerplate
Persistent (missed runs)
Нет
Persistent=true
Jitter
Только вручную (sleep $RANDOM)
RandomizedDelaySec=
Dependencies
Нет
After=, Requires= в .service
Логи
Email или ручной redirect
systemd journal, structured, journalctl -u
Resource limits
Нет встроенно
MemoryMax, CPUQuota в .service
Security sandbox
Нет
PrivateTmp, ProtectSystem, и т.д.
Status (running, last, next)
Нет — нужно парсить логи
systemctl status / list-timers
UI/UX для admin
crontab -e (один пользователь)
systemctl status, journalctl, ясные unit-файлы
Распространённость
Везде (UNIX since 1975)
Только systemd-дистрибутивы (2015+)
Где лучше
Quick personal/dev tasks, legacy, embedded
Production DE-jobs, ETL, batch workloads

Когда выбирать timer:

  • Production DE-задачи (Airflow scheduler health, daily reports).
  • Нужны dependencies («ETL стартует после Postgres ready»).
  • Важен Persistent (нет thundering herd при batch reboot).
  • Хочешь structured logs через journal.

Когда выбирать cron:

  • Простой однострочник, нет dependencies.
  • Personal/dev машина.
  • Стандартный legacy паттерн, который все знают.
  • Embedded system без systemd.

В production-DE команде обычно используют оба: cron для simple sysadmin задач (log rotation, simple cleanup), timer для serious DE pipeline шагов.

Kubernetes CronJob — те же идеи в кластерном окружении

DE-сценарий: hourly aggregation с dependencies

Требования: каждый час запускать Python-скрипт, который агрегирует данные из Postgres в S3. Зависит от того, что Postgres готов. Не должен запускаться, если предыдущий ещё работает (lock).

hourly-agg.service:

[Unit]
Description=Hourly aggregation to S3
After=network-online.target postgresql.service
Wants=network-online.target postgresql.service
ConditionPathExists=/etc/hourly-agg/env

[Service]
Type=oneshot
User=etl
WorkingDirectory=/opt/hourly-agg
EnvironmentFile=/etc/hourly-agg/env
ExecStart=/opt/hourly-agg/venv/bin/python -m hourly_agg

# Resource limits
MemoryMax=4G
CPUQuota=200%
TasksMax=128

# Защита от concurrent — systemd сам не запустит второй экземпляр oneshot,
# пока первый работает. Это эквивалент flock без flock.

# Логи в journal
StandardOutput=journal
StandardError=journal

# Sandbox
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/hourly-agg /tmp/hourly-agg-cache

hourly-agg.timer:

[Unit]
Description=Hourly aggregation timer

[Timer]
# Каждый час в :05 (5 минут после начала часа, чтобы данные за прошлый час уж точно были в DB):
OnCalendar=*-*-* *:05:00 UTC
Persistent=true
RandomizedDelaySec=2min
Unit=hourly-agg.service

[Install]
WantedBy=timers.target

Persistent=true гарантирует, что если VM была off час, при boot догонит. RandomizedDelaySec=2min — jitter (хорошо если VM много).

After=postgresql.service в .service — гарантирует, что postgres готов. ConditionPathExists=/etc/hourly-agg/env — не запускать без конфига.

$ sudo systemctl daemon-reload
$ sudo systemctl enable --now hourly-agg.timer

$ systemctl list-timers --no-legend | grep hourly-agg
Wed 2026-05-13 13:05:00 UTC 22min left ... hourly-agg.timer hourly-agg.service

В 13:05 (с jitter до 13:07) запустится. Логи: journalctl -u hourly-agg --since today.

Гибридный паттерн: cron триггерит systemd unit

Иногда удобно: расписание в cron (старый, знакомый), а сама задача — systemd-юнит:

# crontab
0 6 * * * sudo systemctl start orders-etl.service

Это даёт: cron-простоту расписания + systemd resource limits, logging, dependencies. Sudoers нужно настроить, чтобы cron-user мог systemctl start нужного сервиса без пароля.

Это редкий паттерн — обычно лучше или чистый cron, или чистый timer.

migration: переписать cron-job в timer

Был cron:

0 6 * * * etl  /opt/etl/run.sh >> /var/log/etl.log 2>&1

Стало пара unit-ов. Шаги:

  1. Создать etl.service:

    [Unit]
    Description=Daily ETL
    
    [Service]
    Type=oneshot
    User=etl
    ExecStart=/opt/etl/run.sh
    StandardOutput=journal
    StandardError=journal
  2. Создать etl.timer:

    [Unit]
    Description=Daily ETL timer
    
    [Timer]
    OnCalendar=*-*-* 06:00:00
    Persistent=true
    Unit=etl.service
    
    [Install]
    WantedBy=timers.target
  3. Деплой:

    $ sudo cp etl.service etl.timer /etc/systemd/system/
    $ sudo systemctl daemon-reload
    $ sudo systemctl enable --now etl.timer
    $ sudo crontab -u etl -e   # удалить старую cron строчку

Готово. Логи теперь через journalctl -u etl, статус через systemctl status etl.timer.

Попробуй сам

  1. Список всех timer на системе:
    systemctl list-timers
  2. Когда следующий запуск apt-daily?
    systemctl list-timers apt-daily.timer
  3. Проверить calendar-выражение:
    systemd-analyze calendar "Mon-Fri *-*-* 09:00:00"
  4. Создай тестовый timer (требует root):
    # /etc/systemd/system/test-echo.service
    # [Service]
    # ExecStart=/bin/echo "Hello from timer"
    
    # /etc/systemd/system/test-echo.timer  
    # [Timer]
    # OnUnitActiveSec=1min
    # OnBootSec=1min
    # [Install]
    # WantedBy=timers.target
    
    sudo systemctl daemon-reload
    sudo systemctl enable --now test-echo.timer
    # Через 1 мин:
    journalctl -u test-echo.service

macOS-различия

На macOS нет systemd -> нет systemd timer. Аналог — launchd с StartCalendarInterval:

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>6</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

Persistent runs (missed) тоже есть: RunAtLoad=true + StartCalendarInterval примерно эквивалентны systemd Persistent=true. Синтаксис заметно другой.

Главное

  • systemd timer = пара .service (что) + .timer (когда).
  • OnCalendar= — расписание. Более выразительно cron: *-*-* 06:00:00, Mon-Fri 09:00, *-*-01 02:30.
  • Persistent=true — killer feature: при boot догоняет пропущенные запуски.
  • RandomizedDelaySec= — jitter против thundering herd. В cron нужно вручную через sleep $RANDOM.
  • Dependencies через After=/Requires= в .service — «запусти после Postgres».
  • Логи в journal автоматически. journalctl -u SVC --since today — структурированно.
  • systemctl list-timers — обзор всех timer на системе. Когда last, когда next.
  • systemd-analyze calendar EXPR — проверить расписание, когда следующее срабатывание.
  • Когда timer: production DE-jobs, dependencies, Persistent, structured logs.
  • Когда cron: одностроки, dev/personal, legacy, embedded без systemd.
  • Гибрид (cron -> systemctl start SVC) возможен, но редко лучшее решение.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Что главное преимущество `Persistent=true` в systemd timer перед обычным cron?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4