Якщо вас колись будили словами «нічне завдання не виконалося», ви вже знаєте проблему: планування — це нудно, поки воно не стає єдиною річчю, яка комусь важлива. Cron простий, всюдисущий і… дивно вправний у тому, щоб тихо відмовляти.
У Debian 13 systemd timers — це варіант для дорослих: краща видимість, адекватна обробка залежностей, відлов пропущених запусків і модель помилок, яку реально автоматизувати. У них також є кілька гострих країв, які легко поранять, якщо хапатися неправильно.
Рішення: коли залишатися на cron, а коли переходити
Будьмо відвертими: cron підходить для дивовижної кількості випадків. Якщо у вас є один хост, кілька скриптів і вас влаштовує критерій успіху «воно виконалося коли-небудь», cron і досі виправдовує себе.
Але продакшен-системи не ламаються в «щасливому шляху». Вони ламаються під час перезавантажень, деплоїв, інцидентів DNS, заповнених дисків, завислих NFS-монтувань та випадкових змін середовища. Ось де простота cron перетворюється на операційний податок.
Використовуйте cron коли
- Потрібен лише розклад і нічого іншого.
- У вас стабільні хости, мінімальні залежності та ви активно моніторите результати.
- Ви підтримуєте спадкові завдання, і міграція додасть ризик за малою винагородою.
- Ви працюєте в контейнері, де systemd не є PID 1, і не хочете боротися з цим.
Використовуйте systemd timers коли
- Вам важливі пропущені запуски після перезавантаження або простою (
Persistent=true— рядок, що рятує кар’єру). - Ви хочете логи в журналі з консистентними метаданими та простим фільтруванням.
- Потрібно впорядкування залежностей (після network-online, після монтувань, після бази даних).
- Потрібен контроль ресурсів: обмеження CPU/IO, таймаути, ізоляція.
- Ви хочете моніторинг, який не зводиться до «нам надіслали лист від cron?»
- Ви хочете запобігти одночасним запускам без створення крихкої купи lockfile-ів.
Орієнтовна порада: у Debian 13 нові заплановані завдання повинні за замовчуванням переходити на systemd timers, якщо немає явної причини інакше. Cron стає винятком, а не правилом.
Цитата, що досі доречна в операціях: перефразована ідея
— John Ousterhout: «складність — ворог надійності». Таймери — не про складність; вони про те, щоб покласти складність у потрібне місце (менеджеру сервісів), а не розкидати її по скриптах.
Цікаві факти та трохи історії (бо це важливо)
Розуміння походження допомагає передбачати режими відмов. Планування завжди було менше про «запустити о 2:00» і більше про «відпрацювати, коли світ палає, і при цьому поводитися передбачувано».
- Cron з’явився наприкінці 1970-х, спочатку створений для Unix, щоб виконувати періодичні завдання з мінімальними накладними витратами. Він припускає, що хост увімкнений і годинник у порядку.
- Класичний cron використовує crontab-и для кожного користувача і глобальний системний crontab; таке розділення зручне, але фрагментує власність і аудит.
- Anacron з’явився, щоб вирішити пропущені завдання на машинах, що часто вимикаються (наприклад, ноутбуки). systemd timers фактично поглинають ідею «наздогнати після простою».
- Systemd timers походять від моделі „units всюди“: запланований запуск — це лише тригер для service unit. Через це ви отримуєте залежності та консистентне логування.
- Екосистема cron в Debian історично покладалася на email для сповіщень. Але в сучасних середовищах часто відсутня локальна MTA-конфігурація, тож помилки стають мовчазними.
- systemd інтегрується з cgroups. Це означає, що ви можете стримувати неконтрольовані завдання або захистити інші сервіси — те, чого cron ніколи не намагався робити.
- Systemd timers підтримують рандомізацію, щоб уникнути «thundering herd», коли тисячі хостів одночасно звертаються до одного API опівночі.
- Синхронізація часу стала критичною, коли розподілені системи стали нормою. І cron, і timers страждають від поганого часу, але systemd дає зрозуміліші докази і інструменти впорядкування.
- Логування перемістилося з файлів до журналів в багатьох деплойментах Debian. Таймери отримують від цього прямий виграш, бо stdout/stderr захоплюються без хитрощів.
Жарт #1: Cron як той колега, що «точно відправив лист» — ви дізнаєтеся, що цього не сталося, тільки коли хтось поскаржиться.
Ментальна модель: що робить cron і що робить systemd
Cron: мінімальний планувальник
Cron читає записи crontab, прокидається раз на хвилину і перевіряє, чи треба щось запускати. Він виконує команди з обмеженим середовищем, під ідентифікацією користувача, з опцією надсилати вивід поштою. Оце й усе.
Його сильні сторони — також його слабкі сторони:
- Перевага: передбачувана простота. Слабкість: вам доведеться реалізувати все інше самостійно (блокування, повтори, таймаути, залежності, логування, сповіщення).
- Перевага: широко зрозумілий. Слабкість: «зрозумілий» часто означає «припущений», а припущення псуються.
systemd timers: тригер плюс контракт сервісу
Timer unit планує активацію service unit. Service unit визначає, як працює завдання: користувач, середовище, робоча директорія, таймаути, контроль ресурсів і що вважається успіхом/помилкою. Це розділення — магія.
Результат — менше племінного знання. Файл unit від systemd самодокументований у способі, якого не має випадковий shell-фрагмент у crontab.
Типові оперативні відмінності, що мають значення
- Спостережуваність: таймери пишуть у journald за замовчуванням; cron часто ніде не пише, якщо ви явно не перенаправили вивід.
- Пропущені запуски: таймери можуть наздоганяти; cron — ні, якщо ви не додали anacron або власну логіку.
- Залежності: таймери можуть запускатися після монтувань/мережі; cron лише «сподівається».
- Перекриття: systemd може обмежувати конкурентність; cron із задоволенням почне «стампедо», якщо не заблокувати.
- Семантика помилок: systemd записує коди виходу, поведінку рестарту та ліміти частоти; cron здебільшого знизить плечима.
Ще одне: таймери не «більш надійні» через якусь магію. Вони надійніші, бо роблять завдання явною одиницею виконання з явною поведінкою. Надійність — це здебільшого зробити неявне болісно явним.
Практичні завдання (команди, виводи, рішення)
Ось команди, які я реально виконую під час міграції або налагодження запланованих завдань. Кожна з них показує що дивитись і яке рішення приймати.
Завдання 1: Визначити реалізації cron і системні cron-завдання
cr0x@server:~$ dpkg -l | egrep 'cron|anacron|systemd'
ii cron 3.0pl1-... amd64 process scheduling daemon
ii systemd 257-... amd64 system and service manager
Що це значить: У вас встановлений класичний cron. Якщо ви також бачите пакети anacron, можливо, у вас вже частково реалізована поведінка «наздогнати пропущене» для деяких щоденних/тижневих завдань.
Рішення: Не видаляйте cron одразу. Спочатку проінвентаризуйте завдання і зрозумійте, що від нього залежить.
Завдання 2: Інвентаризація системних cron-записів (те, що всі забувають)
cr0x@server:~$ ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
/etc/cron.d:
total 20
-rw-r--r-- 1 root root 201 Jan 10 10:12 sysstat
-rw-r--r-- 1 root root 349 Jan 10 10:12 logrotate
...
/etc/cron.daily:
total 16
-rwxr-xr-x 1 root root 539 Jan 10 10:12 apt-compat
-rwxr-xr-x 1 root root 377 Jan 10 10:12 man-db
Що це значить: Пакети Debian часто встановлюють завдання сюди. Деякі з них вже мають еквіваленти systemd у новіших релізах; інші поки покладаються на cron.
Рішення: Для пакетних сервісів краще використовувати дефолти дистрибутива, якщо немає сильної причини змінювати. Міграція скриптів, які постачає постачальник пакету, рідко вигідна.
Завдання 3: Інвентаризація crontab-ів користувачів (де живе дивне)
cr0x@server:~$ sudo ls -1 /var/spool/cron/crontabs
postgres
www-data
backup
Що це значить: Це per-user crontabs. Вони часто містять бізнес-критичні завдання без документації.
Рішення: Ставтеся до кожного crontab як до production-коду. Експортуйте і перегляньте рядок за рядком перед будь-якими змінами.
Завдання 4: Злити конкретний crontab і перевірити пастки середовища
cr0x@server:~$ sudo crontab -u backup -l
MAILTO=ops-alerts
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/backup-nightly.sh
Що це значить: Середовище cron тут явне (добре), але MAILTO припускає, що доставка пошти працює (часто це не так у сучасних кластерах).
Рішення: Під час міграції включайте середовище у unit systemd і замініть «пошта як моніторинг» на явне сповіщення.
Завдання 5: Перевірити наявні таймери (що вже планує systemd)
cr0x@server:~$ systemctl list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2025-12-29 02:15:00 UTC 3h 12min left Sun 2025-12-28 02:15:02 UTC 21h ago backup-nightly.timer backup-nightly.service
Mon 2025-12-29 00:00:00 UTC 1h - - - logrotate.timer logrotate.service
Що це значить: Таймери показують NEXT/LAST запуск. Це одразу відповідає на питання «воно запустилося?» без grep-ування файлів.
Рішення: Якщо завдання важливе, воно має бути видимим тут (або в оркестраторові), а не схованим у приватному crontab.
Завдання 6: Переглянути розклад таймера і чи він наздоганяє
cr0x@server:~$ systemctl cat backup-nightly.timer
# /etc/systemd/system/backup-nightly.timer
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Що це значить: Persistent=true означає, що systemd виконає завдання якомога швидше після завантаження, якщо воно пропустилося. RandomizedDelaySec розподіляє навантаження.
Рішення: Для бекапів, ротації логів і звітів Persistent=true зазвичай правильний. Для завдань, що мають точно сповістити когось о 02:15, це не підходить.
Завдання 7: Переглянути пов’язаний service unit (саме тут живе надійність)
cr0x@server:~$ systemctl cat backup-nightly.service
# /etc/systemd/system/backup-nightly.service
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
Що це значить: Це явно: ідентичність, робоча директорія, таймаут. Cron-завдання часто забувають про це і покладаються на вдачу.
Рішення: Якщо ваш скрипт залежить від монтувань, мережі або певних директорій — оголосіть це тут, а не ховайте логіку в крихкому скрипті.
Завдання 8: Перевірити парсинг розкладу (зловити «31 лютого» до продакшену)
cr0x@server:~$ systemd-analyze calendar "*-*-* 02:15:00"
Original form: *-*-* 02:15:00
Normalized form: *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
(in UTC): Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
Що це значить: systemd повідомляє, що він думає ви мали на увазі, і коли наступного разу спрацює. Це ваш перший захист від непорозумінь у розкладі.
Рішення: Якщо нормалізована форма відрізняється від очікуваної — зупиніться і виправте. Не «побачимо пізніше».
Завдання 9: Перевірити, чи таймер справді спрацював і що сталося
cr0x@server:~$ journalctl -u backup-nightly.service -n 20 --no-pager
Dec 28 02:15:02 server systemd[1]: Starting backup-nightly.service - Nightly backup job...
Dec 28 02:15:03 server backup-nightly.sh[1142]: snapshot created: tank/backups@2025-12-28
Dec 28 03:01:29 server backup-nightly.sh[1142]: upload complete
Dec 28 03:01:29 server systemd[1]: backup-nightly.service: Deactivated successfully.
Dec 28 03:01:29 server systemd[1]: Finished backup-nightly.service - Nightly backup job.
Що це значить: Ви отримуєте рядки початку/закінчення та вивід скрипта, прив’язаний до імені unit. Це значно чистіше, ніж «куди пішло перенаправлення?»
Рішення: Якщо вивід занадто галасливий — виправте логування в скрипті. Не викидайте видимість, перенаправляючи все в /dev/null.
Завдання 10: Довести, чи останній запуск провалився (і як)
cr0x@server:~$ systemctl status backup-nightly.service --no-pager
● backup-nightly.service - Nightly backup job
Loaded: loaded (/etc/systemd/system/backup-nightly.service; static)
Active: inactive (dead) since Sun 2025-12-28 03:01:29 UTC; 21h ago
Duration: 46min 26.113s
Process: 1142 ExecStart=/opt/jobs/backup-nightly.sh (code=exited, status=0/SUCCESS)
Що це значить: Ви маєте код виходу, тривалість виконання та точну команду, що була викликана.
Рішення: Якщо статус не 0/SUCCESS, не здогадуйтеся. Витягніть код виходу й обробіть його: повторити, сповістити або швидко припинити.
Завдання 11: Виявити перекриття запусків (тихий вбивця бекапів та ETL)
cr0x@server:~$ systemctl show backup-nightly.service -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ActiveEnterTimestamp -p ActiveExitTimestamp
ExecMainStartTimestamp=Sun 2025-12-28 02:15:03 UTC
ExecMainExitTimestamp=Sun 2025-12-28 03:01:29 UTC
ActiveEnterTimestamp=Sun 2025-12-28 02:15:02 UTC
ActiveExitTimestamp=Sun 2025-12-28 03:01:29 UTC
Що це значить: Ви можете витягнути структуровані часові мітки, щоб бачити, чи тривалість завдання наближається до інтервалу розкладу.
Рішення: Якщо час виконання регулярно близький до інтервалу, додайте захист від конкуренції і розгляньте розширення розкладу.
Завдання 12: Перевірити проблеми з синхронізацією часу, що роблять розклад брехливим
cr0x@server:~$ timedatectl
Local time: Mon 2025-12-29 00:03:11 UTC
Universal time: Mon 2025-12-29 00:03:11 UTC
RTC time: Mon 2025-12-29 00:03:11
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Що це значить: Якщо синхронізація годинника порушена, і cron, і таймери стають хаотичними. «Воно запустилося о 2:00» перестає щось означати.
Рішення: Виправте час спочатку. Налагодження планування на хості зі «втікачим» годинником — як налагодження сховища на сервері з вільними SATA-кабелями.
Завдання 13: Перевірити залежність від монтування (поширено для бекапів, звітів, інгесту)
cr0x@server:~$ systemctl status mnt-backups.mount --no-pager
● mnt-backups.mount - /mnt/backups
Loaded: loaded (/proc/self/mountinfo; generated)
Active: active (mounted) since Sun 2025-12-28 00:00:41 UTC; 1 day 0h ago
Where: /mnt/backups
What: /dev/mapper/vg0-backups
Що це значить: Цільовий монтувальний пункт існує і активний. Якщо його немає, ваше завдання може записати на кореневий файловий розділ випадково.
Рішення: Додайте явне упорядкування монтувань, використовуючи RequiresMountsFor=/mnt/backups у unit-файлі сервісу.
Завдання 14: Підтвердити середовище, яке бачить ваше завдання під systemd
cr0x@server:~$ systemctl show backup-nightly.service -p Environment -p User -p Group -p WorkingDirectory
Environment=
User=backup
Group=backup
WorkingDirectory=/opt/jobs
Що це значить: Ніяких неявних змінних середовища не встановлено. Якщо скрипту потрібен AWS_REGION або інша правка PATH, ви маєте її вказати.
Рішення: Помістіть потрібні змінні в EnvironmentFile= з жорсткими правами або використовуйте повні шляхи у скриптах. Я віддаю перевагу явним повним шляхам для утиліт, чия поведінка не повинна змінюватися.
Завдання 15: Лімітування запусків і «спалахи стартів» (чому ваші повтори «нічого не зробили»)
cr0x@server:~$ systemctl show backup-nightly.service -p StartLimitIntervalUSec -p StartLimitBurst
StartLimitIntervalUSec=10s
StartLimitBurst=5
Що це значить: systemd припинить спроби після серії помилок у межах інтервалу. Це запобігає флапінгу, що перетворюється на самодеструктивну відмову.
Рішення: Якщо ви використовуєте повтори, свідомо встановлюйте ліміти. Інакше ваша стратегія «рестарт при помилці» може тихо перестати працювати після п’яти швидких відмов.
Як побудувати таймер + сервіс, які не зганьблять вас
Хороша міграція — це переважно вирішення питання, чим саме є ваше завдання: ідентичність, залежності, таймаути, конкурентність і вивід. Cron ніколи не змушував вас вирішувати ці речі. Systemd змусить, і в цьому сенс.
Почніть з сервісного юніту
Спочатку напишіть сервіс. Таймер має бути нудною частиною.
- Використовуйте
Type=oneshotдля скриптів, що запускаються і виходять. Не вдавайте, що це демон. - Встановіть
WorkingDirectory=, якщо скрипт очікує відносні шляхи. Краще: позбудьтеся від відносних шляхів, але реальність буває бурхливою. - Використовуйте повні шляхи в
ExecStart=і в самих скриптах для критичних інструментів. Залежність відPATH— це шлях до «працює в шеллі, не працює о 2:00». - Встановіть таймаути. Якщо завдання може зависнути — воно зависне. Дайте systemd право його вбити.
- Заявляйте залежності, як-от монтування і мережа. Якщо завданню потрібен mount — скажіть це.
Потім напишіть таймер
Таймери компактні, але мають два поля, що визначають вашу позицію щодо надійності:
Persistent=true: наздоганяє пропущені запуски після простою.RandomizedDelaySec=: уникає «стадного» навантаження. Чудово для флоту, небезпечно для завдань «точно у час».
Конкретний приклад: міграція нічного бекапу
Ось робоча базова конфігурація. Не «ідеальна». Але достатньо хороша, щоб випустити, не створивши нового хобі для on-call.
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.service > /dev/null <<'EOF'
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/mnt/backups
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
KillMode=control-group
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# Basic hardening without breaking scripts:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/mnt/backups /opt/jobs /var/log
EOF
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.timer > /dev/null <<'EOF'
[Unit]
Description=Nightly backup timer
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
AccuracySec=1m
Unit=backup-nightly.service
[Install]
WantedBy=timers.target
EOF
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl enable --now backup-nightly.timer
Created symlink '/etc/systemd/system/timers.target.wants/backup-nightly.timer' → '/etc/systemd/system/backup-nightly.timer'.
Чому такі вибори:
RequiresMountsFor=запобігає класичному інциденту «бекап записав у /mnt/backups, якого не було, в результаті заповнив /».KillMode=control-groupгарантує, що дочірні процеси не втечуть, якщо завдання приведе до таймауту.- Базове підсилення без ламання скриптів зменшує радіус ураження. Якщо скрипту потрібен більший доступ — явно його дозвольте. Нехай unit-файл буде контрактом.
AccuracySecперешкоджає systemd намагатися спрацювати точно у секунду (що зазвичай не є потрібним).
Джиттер і «чому воно запустилося о 02:23?»
Якщо ви вмикаєте RandomizedDelaySec, завдання виконається десь у вказаному вікні. Це не помилка. Це механізм флот-безпеки. Якщо фінанси хочуть звіт рівно о 02:15:00 — не рандомізуйте. Якщо завдання лягає на спільне сховище — рандомізуйте і спіть спокійно.
Жарт #2: RandomizedDelaySec — це вічлива версія «всі, припиніть торкатись сховища опівночі».
Функції надійності, які варто використовувати
systemd дає багато ручок. Використання всіх їх зробить unit-файл, до якого ніхто не захоче торкатися. Використовуйте ті ручки, які приносять надійність за рядок конфігурації.
1) Наздоганяти пропущені запуски: Persistent=true
Якщо сервер був вимкнений о 02:15, cron знизить плечима. Таймери можуть виконати запуск при завантаженні. Для бекапів, ротації логів, періодичного очищення й eventual-consistent задач persistent-таймери — явна вигода.
Не використовуйте це, коли завдання має виконуватися лише у конкретний момент стінового часу (наприклад, координовані ринкові дії). У таких випадках трактуйте розклад як контракт і обробляйте простій явно.
2) Уникнути «thundering herd»: RandomizedDelaySec
Якщо у вас більше кількох хостів, не ставте все на початок години. DNS, об’єктне сховище, база даних і спільне сховище відчують це одночасно.
Рандомізація — недорога страховка. Ви обмінюєте точність на стабільність системи в цілому.
3) Таймаути: TimeoutStartSec та інші
Кожне завдання, що звертається в мережу, може зависнути. Кожне, що торкається сховища, може зависнути. Якщо ваш скрипт не може зависнути — вам пощастило: ви ще не бачили NFS у поганий день.
Вибирайте таймаут на основі SLO. Якщо завдання зазвичай займає 10 хвилин, таймаут у 3 години — це не «безпечно», це «ви не помітите, що воно зламалося». Використовуйте реалістичні бюджети.
4) Контроль конкурентності: один запуск одночасно
За замовчуванням cron — «запускай і забувай», що стає «запустили двічі і пошкодували». У systemd ви можете спроектувати недопущення перекриттів.
Практичний підхід:
- Зробіть сервіс таким, щоб він відмовлявся стартувати, якщо інший екземпляр активний.
- Або поставте блокування в скрипті через
flock, але сприймайте цей lock як частину контракту (і логувати, коли пропускаєте запуск).
Systemd не дає одно-рядкового прапора «без перекриттів» так, як дехто бажає, але він робить перекриття видимими і керованими: ви бачите активні юніти, визначаєте таймаути і примусово забезпечуєте єдиний шлях виконання.
5) Залежності: монтування й мережа — не «мабуть там»
Найгірші відмови не в «завдання провалилося». Найгірші — «завдання зробило неправильну річ». Якщо бекап виконається без mount-а для бекпів, він може записати на корінь і при цьому повернути 0.
Використовуйте RequiresMountsFor= для будь-якого шляху, що має бути змонтований. Використовуйте After=network-online.target лише якщо дійсно потрібна мережа; інакше ви сповільнюєте завантаження і ускладнюєте порядок готовності.
6) Середовище: зробіть його явним або усуньте
Середовище cron відоме своєю мінімальністю. Systemd теж мінімальний, але по-іншому. Залежність від інтерактивної ініціалізації шеллу — шлях до «працює у мене локально». Ця фраза має викликати внутрішній сигнал тривоги.
Якщо потрібні секрети або конфігурація — використовуйте EnvironmentFile=, що читається лише сервісним користувачем, або завантажуйте їх із файлу root з жорсткими правами. Не вшивайте секрети у unit-файли.
7) Контроль ресурсів: не дайте завданням з’їсти хост
Тут systemd тихо випереджає cron на багато кілометрів. З cgroups ви можете обмежити або пріоритезувати завдання. Завдання для стиснення логів не повинно позбавляти ресурсів продуктивних сервісів.
Корисні контролі: Nice, IOSchedulingClass, і (коли готові) CPU/IO контролі. Почніть з малого: пріоритет і таймаути вже покривають більшість інцидентів із «втечею» завдань.
8) Логування: припиніть кидати вивід у порожнечу
Journald робить легкою задачу послідовно захоплювати вивід. Залишайте stdout/stderr. Нехай скрипти логують у структурованому вигляді, якщо можливо (навіть прості «key=value» рядки — золото під час інцидентів).
Потім моніторьте це. Заплановане завдання без моніторингу — запланований сюрприз.
Швидка діагностика: план дій
Ось порядок перевірок, які я роблю, коли таймер-проєкт «не запустився» або «запустився, але нічого не зробив». Мета — знайти вузьке місце за хвилини, а не у Slack-потязі до обіду.
Перше: чи думав systemd, що він спрацював?
- Перевірте
systemctl list-timers --allдля LAST і NEXT. - Перевірте
systemctl status yourjob.timerдля стану активації. - Рішення: якщо LAST відсутній або старий — це питання розкладу/включення. Якщо LAST недавній — це виконання.
Друге: чи запускався сервіс і який код виходу?
- Запустіть
systemctl status yourjob.serviceі дивіться код виходу. - Рішення: ненульовий вихід означає помилку на рівні завдання; нуль — «запустилось», і проблема, ймовірно, «виконалося неправильно» або «не зробило корисної роботи».
Третє: що кажуть логи навколо запуску?
- Використайте
journalctl -u yourjob.serviceз фільтрами часу або-n. - Рішення: якщо логи порожні — можливо, ви дивитесь не той unit, не той таймер або не той хост. Якщо логи показують зависання — дивіться залежності або таймаути.
Четверте: перевірте передумови (монтування, мережа, DNS, облікові дані)
- Перевірте монтування за допомогою
systemctl status mnt-*.mountабоfindmnt. - Перевірте синхронізацію часу через
timedatectl. - Рішення: якщо передумови нестабільні — виправляйте їх; не заклеюйте повторними спробами.
П’яте: пошукайте перекриття і зворотний тиск
- Перевірте тривалість завдання й частоту. Якщо воно довше за інтервал — отримаєте перекриття або постійне накопичення.
- Рішення: якщо є перекриття — забезпечте політику одного запуску і відрегулюйте інтервал або оптимізуйте безпечно.
Типові помилки: симптом → корінь → вирішення
Цей розділ існує тому, що ці помилки повторюються під час міграцій cron→timer. Якщо бачите симптом — не дискутуйте. Перейдіть до кореня.
1) Симптом: «Таймер ввімкнено, але завдання ніколи не запускається»
Корінь: Таймер ввімкнено, але розклад невірний, або він не встановлений у timers.target належним чином.
Вирішення: Перевірте розклад і підтвердіть увімкнення.
cr0x@server:~$ systemd-analyze calendar "Mon..Fri *-*-* 02:15:00"
Original form: Mon..Fri *-*-* 02:15:00
Normalized form: Mon..Fri *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
cr0x@server:~$ systemctl is-enabled backup-nightly.timer
enabled
2) Симптом: «Ручно працює, але під таймером падає»
Корінь: Різниця середовища: відсутній PATH, неправильна робоча директорія, відсутні облікові дані, інший користувач.
Вирішення: Нехай unit задає контракт: User=, WorkingDirectory=, повні шляхи, EnvironmentFile=.
3) Симптом: «Запустилося, код 0, але нічого не зробило / не вивело»
Корінь: Скрипт перевіряє стан і тихо виходить, або записав щось в неочікуване місце, або завдання працювало з неправильного екземпляра (неправильний конфіг).
Вирішення: Додайте явне логування. Також переконайтеся, що unit вказує правильний скрипт і конфіг. Перегляньте WorkingDirectory і абсолютні шляхи.
4) Симптом: «Воно запустилося після перезавантаження в дивний час»
Корінь: Persistent=true спричинив наздоганяння, можливо в поєднанні з RandomizedDelaySec.
Вирішення: Вирішіть, чи потрібне наздоганяння. Якщо ні — приберіть Persistent=true. Якщо так — повідомте зацікавлені сторони і моніторьте.
5) Симптом: «Два екземпляри запустилися одночасно і пошкодили стан»
Корінь: Сервіс дозволяє одночасні старти, інтервал таймера коротший за гірший сценарій тривалості, або ручний запуск перекрив плановий.
Вирішення: Додайте блокування (на рівні скрипта через flock), збільшіть інтервал і встановіть реалістичний таймаут. Розгляньте запуск ручних операцій через systemctl start, щоб вони були видимі.
6) Симптом: «Він раз упав і більше не запускається»
Корінь: Досягнуто ліміту частоти стартів через швидкі відмови, або таймер нормальний, але сервіс відразу падає і пригнічується.
Вирішення: Перевірте статус і логи; налаштуйте ліміти стартів, якщо ви навмисно повторюєте, і виправте корінну помилку.
7) Симптом: «Бекапи заповнили корінь»
Корінь: Місце призначення бекапів не було змонтоване; скрипт записав у директорію, яка існувала на корені.
Вирішення: Використовуйте RequiresMountsFor= і записуйте бекапи у шлях, який існує лише коли змонтований (або принаймні перевіряйте ідентичність монтування в скрипті).
8) Симптом: «Cron раніше надсилав помилки поштою; тепер ми нічого не бачимо»
Корінь: Поведінка cron з email була випадковим сповіщенням. Systemd логувалa в журнал, але ніхто цього не моніторить.
Вирішення: Налаштуйте моніторинг: опитуйте стан unit-ів, сповіщайте про відмови, сповіщайте про відсутність успішних запусків і, за потреби, форвардьте журнали в ваш лог-пайплайн.
Три міні-історії з корпоративного життя
Міні-історія 1: Інцидент через неправильне припущення
Команда мігрувала експорт бази даних з cron на systemd timer. Старий cron запускав як postgres. Новий сервіс працював від root через «root може все». Це припущення протрималося рівно тиждень.
Скрипт експорту записував у директорію, власником якої був postgres, і використовував локальний сокет з типовими налаштуваннями ~postgres. Під root він все ще підключався — іноді — але створював файли власності root з обмежувальними правами. Подальша задача, що працювала як postgres, почала падати при читанні експортів. Першою ознакою не була явна помилка; це був застарілий звіт і питання від керівництва, чому цифри не змінилися.
On-call перевіряв базу спочатку (зрозуміло). Потім — сховище. Через кілька годин хтось помітив змішані власності файлів та невідповідні часові мітки в директорії експорту.
Вирішення не було героїчним: встановити User=postgres, зафіксувати WorkingDirectory і перестати покладатися на неявні поведінки домашньої директорії. Також додали ReadWritePaths= на рівні unit, щоб унеможливити запис кудись ще. Інцидент закінчився простою ремаркою в постмортемі: «Ми припустили, що root безпечніший. Насправді він просто менш видимий.»
Міні-історія 2: Оптимізація, що відгукнулась бумерангом
Компанія мала флот Debian-хостів, що стискали логи і шліфували їх кожні п’ять хвилин. Хтось вирішив «оптимізувати час завантаження», прибравши залежності від network-online.target у запланованих сервісах. Швидше завантаження, менше складнощів. На папері — чисто.
На практиці таймер спрацював одразу після завантаження, до того як DNS і накладання мережевих інтерфейсів стабілізувались. Завдання не завжди відмовляло гучно. Іноді воно кешувало дані локально і виходило з кодом 0. Іноді писало у fallback-ціль. Іноді зависало на connect до сокета до стандартного таймауту (який фактично «деякий час»).
Після тижня сигнали від сховища почали надходити: кореляція між плановими перезавантаженнями і накопиченням логів призвела до лагу інгесту, що зробило дашборди хибними. «Оптимізація» виявилась не у тому, що завдання було неправильно, а в тому, що система тепер поводилася інакше під час штормів завантажень.
Нудне вирішення — повернути залежності, але лише правильні. Вони замінили широке «чекати мережу» на залежність від потрібного монтування і невелику перевірку перед запуском, що підтверджує резолв конкретного кінцевого пункту. Також встановили адекватний TimeoutStartSec і логували помилки явно. Завантаження стало трохи повільнішим. Операції — значно спокійнішими.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Команда, що працювала з фінансовими звітами, виконувала місячні звірки. Всі ненавиділи торкатися цього через старість скрипта і складність бізнес-правил. Те, що вони зробили правильно, було не хитрим: вони трактували заплановане завдання як сервіс зі SLO.
Вони мігрували на systemd timer і додали два контролі: (1) сповіщення при провалі сервісу, і (2) сповіщення, якщо таймер не виконувався успішно у вікні. Також вони закріпили середовище через EnvironmentFile і використовували повні шляхи для кожного інструмента.
Якось місяця рутинне оновлення пакету змінило поведінку утиліти парсингу (та сама назва, інші дефолти виводу). Скрипт все ще запускалося, але продукувало невідповідний вихід. Завдання завершилося з ненульовим кодом через строгі перевірки, і unit перейшов у failed.
Оскільки команда мала «нудний моніторинг», відмову помітили за кілька хвилин у робочий час. Вони відкотили пакет на тому хості, виправили парсинг і повторно запустили сервіс через systemd, тож повторний запуск був залогований і атрибутований. Жодних нічних панік і екстрених зборів — просто чиста відмова і чисте виправлення.
Чеклісти / покроковий план
Практичний план міграції, що мінімізує сюрпризи. Використовуйте його, чи мігруєте одне завдання, чи сто.
Крок 1: Інвентаризація і класифікація завдань
- Перелічіть системні директорії cron і crontab-и користувачів.
- Класифікуйте кожне завдання: критичне, важливе, приємне мати.
- Вирішіть бажану поведінку після простою: наздоганяти чи пропускати?
Крок 2: Визначте контракт виконання для кожного завдання
- Яким користувачем має виконуватись?
- Які директорії повинні існувати і бути доступними для запису?
- Які монтування мають бути присутні?
- Які мережеві умови потрібні?
- Яка гірша прийнятна тривалість виконання?
- Чи допустиме перекриття запусків?
Крок 3: Побудуйте service unit спочатку
- Почніть з
Type=oneshot. - Додайте
User=,Group=,WorkingDirectory=. - Встановіть реалістичний
TimeoutStartSec=. - Додавайте залежності монтувань і мережі лише коли потрібно.
- Додайте мінімальне підсилення:
NoNewPrivileges,PrivateTmp, і обмежені шляхи для запису.
Крок 4: Додайте timer unit
- Перевірте розклад через
systemd-analyze calendar. - Використовуйте
Persistent=trueтам, де треба наздогнати пропущені запуски. - Використовуйте
RandomizedDelaySecдля флотів, щоб зменшити піки навантаження.
Крок 5: Тестуйте як насправді
- Запустіть сервіс вручну:
systemctl start yourjob.service. - Перевірте статус і логи.
- Симулюйте режими відмов: відсутність монтування, падіння DNS, проблеми з правами.
Крок 6: Короткий паралельний запуск (обережно)
- Якщо безпечно, тримайте cron вимкненим, але збереженим; або запускайте systemd-завдання у «dry run» режимі, поки cron залишається первинним.
- Ніколи не запускайте два stateful-завдання паралельно, якщо у вас немає явного блокування і впевненості в безпечності побічних ефектів.
Крок 7: Перехід і моніторинг
- Відключіть crontab-посилання, коли таймер увімкнено і перевірено.
- Налаштуйте оповіщення про відмови і про відсутність успішних запусків.
- Перегляньте логи після перших кількох реальних виконань.
Крок 8: Знижуйте ризик з часом
- Покращуйте скрипти, щоб вони були ідемпотентними.
- Додавайте структуроване логування.
- Зробіть залежності явними і приберіть випадкові побічні залежності.
Питання й відповіді (FAQ)
1) Чи завжди systemd timers «краще» за cron?
Ні. Вони кращі, коли вам потрібні функції надійності та видимість. Cron підходить для простих, низькоімпактних завдань. Виграш — не «модерність», а операційна ясність.
2) Який еквівалент systemd для «запускати кожні 5 хвилин»?
Використовуйте OnUnitActiveSec=5min (монтонний) або вираз OnCalendar на зразок *:0/5 залежно від того, що ви маєте на увазі під «кожні 5 хвилин». Монотонні таймери запускаються відносно останньої активації; календарні таймери вирівнюються по стіновому часу.
3) Як запобігти пропущеним запускам після перезавантаження?
Використовуйте Persistent=true у таймері. Тоді переконайтеся, що ваше завдання безпечне для запуску після простою (ідемпотентне або з урахуванням стану).
4) Чому моє завдання запускається з іншим PATH, ніж у моєму шеллі?
Тому що заплановані завдання не повинні успадковувати інтерактивну конфігурацію шеллу. Виправте це, використовуючи повні шляхи у скриптах, або явно встановивши Environment=PATH=... (або EnvironmentFile) у unit-файлі.
5) Куди покласти логіку: у таймер чи в скрипт?
Планування — у таймер, семантика виконання — у сервіс. Бізнес-логіку тримайте в скрипті/додатку. Не перевинаходьте планування і повтори в шеллі, якщо systemd може зробити це з яснішою поведінкою.
6) Що замінює «cron мені надсилав результат поштою»?
Дві речі: (1) journald-логи для stdout/stderr, і (2) моніторинг, що сповіщає про падіння unit-ів і відсутність успішних запусків у вікні. Пошта — не моніторинг; це пастка ностальгії.
7) Чи можна запускати таймери від не-root користувачів?
Так. Можна використовувати системні таймери, що запускають сервіси від певного User=, або таймери на рівні користувача (потребує lingering у деяких випадках). Для серверів системні unit-и з явним User= зазвичай простіше керувати централізовано.
8) Як коректно обробляти залежності від монтувань і мережі?
Використовуйте RequiresMountsFor= для файлових шляхів і After=network-online.target/Wants=network-online.target лише коли завдання справді вимагає стабільної мережі. Уникайте надмірно широких залежностей, що сповільнюють завантаження і ховають реальні перевірки готовності.
9) А що з завданнями, які ніколи не повинні перекриватися?
Припускайте, що перекриття відбудеться, якщо ви його не запобіжите. Використовуйте механіку блокувань (зазвичай flock) і впевніться, що моніторинг відрізняє «пропущено через lock» від «успішно виконано». Також налаштуйте інтервали і таймаути так, щоб поведінка системи була передбачуваною.
10) Чи безпечно повністю відключати cron у Debian 13?
Іноді так. Але багато пакетів ще постачають задачі, що покладаються на cron. Глобальне відключення cron може призвести до тонких збоїв. Краще мігруйте свої завдання першими, а потім вирішіть, чи потрібен cron для пакетних обов’язків.
Висновок: наступні кроки, які окупаються
Якщо ви керуєте продакшен-серверами на Debian 13 і вам важливо не пропускати заплановану роботу, systemd timers — це інструмент за замовчуванням, до якого варто прагнути. Вони не усувають відмови, але роблять їх читабельними, тестованими і моніторингованими.
Практичні наступні кроки:
- Інвентаризуйте cron-завдання і позначте критичні.
- Мігруйте одне критичне завдання на timer + service з явним користувачем, робочою директорією, таймаутом і залежностями монтувань.
- Вмикайте
Persistent=trueде потрібне наздоганяння і додавайте jitter там, де важливе навантаження флоту. - Підключіть моніторинг: оповіщення про відмови сервісів і про «немає успішного запуску у вікні».
- Тільки потім починайте видаляти записи cron. Зберігайте старі визначення в системі контролю версій, а не в кошику.
Найкраща система планування — та, що робить питання «воно запустилося?» двохвилинним з нудною відповіддю. systemd допомагає досягти цього — якщо ви сприймаєте unit-файли як операційні контракти, а не як нове місце для ховання шелл-скриптів.