Ви написали скрипт. Він працює на вашому ноутбуку. Ви його запланували. Усі заспокоїлися. А потім у понеділок звіт пустий, резервне копіювання не відбулося, а «щоденне прибирання» тихо видалило не ту директорію. Заплановані завдання за замовчуванням не падають голосно; вони ввічливо провалюються о 03:07 і повертаються спати.
Якщо ви хочете довіряти запланованим завданням, ставтесь до «планування завдань» як до продакшн-інженерії: контракти, логи, блокування, час, середовище, дозволи та спостережуваність. Це доросла версія «просто додай cron».
Що насправді означає «надійне планування»
Планування — це не просто «запустити цю команду о 02:00». Планування — це контракт: запуск приблизно за цим графіком, від імені відомої ідентичності, у відомому середовищі, що виробляє відомі артефакти, і генерує достатньо доказів, щоб ви могли довести, що сталося. Або не сталося.
Чотири обіцянки, які має тримати ваше заплановане завдання
- Воно запускається: планувальник його тригерить, навіть після перезавантаження, навіть після зміни часу, навіть коли машина завантажена.
- Воно запускається один раз: жодних накладок, жодних дублікатів, жодних «двох півночей» через складнощі з DST.
- Воно запускається однаково: той самий PATH, та сама робоча директорія, ті самі конфігурації, ті самі дозволи, та сама локаль, той самий umask.
- Воно залишає докази: логи, коди виходу, часові мітки, метрики та сповіщення, коли пропустилося.
Все інше другорядне. Так, навіть «працює швидко». Швидкість — приємно. Правильність — це плата за житло.
Одна думка, яка збереже вам біль: ставтеся до запланованих скриптів як до сервісів. Не як до «bash-однорядка». Сервіси мають unit-файли, обмеження ресурсів, логи, повторні спроби та відповідальних власників. Завдання може бути скриптом, але операція має відчуватися як сервіс.
Парафразована ідея (якщо ви робили опси, то чули цей сенс): Помилки трапляються; надійність виникає з проєктування систем, які їх виявляють та відновлюють.
— парафразована ідея, пов’язана з SRE-школою мислення, популяризованою Google.
Коротка історія та факти (бо контекст заощаджує помилки)
- cron достатньо старий, щоб мати власну думку. Він походить з раннього UNIX (кінець 1970-х). Тому він простий, стабільний, і водночас припускає світ без контейнерного розмаху.
- Vixie Cron став «стандартною історією cron» у багатьох дистрибутивах Linux роками, формуючи поведінку щодо обробки середовища та очікувань відправки пошти при виводі.
- Windows Task Scheduler існує з часів Windows 9x/NT і еволюціонував у доволі потужний механізм із тригерами, умовами та опцією «run whether user is logged on». Це вже не просто GUI.
- DST створює «втрачений» і «дубльований» час. У багатьох часових поясах 02:30 може ніколи не трапитися в один день на рік і трапитися двічі в інший. Це не теоретична проблема; це генератор інцидентів.
- systemd timers існують частково тому, що cron погано виражає сучасні обмеження (залежності, ізоляція, логування на одиницю, та «run missed jobs after reboot»).
- anacron винайшли для ноутбуків та інших машин, що не завжди ввімкнені у момент запланованого запуску, бо cron тригерить лише коли машина запущена.
- «thundering herd» — артефакт планування: флоти налаштовані на «запуск опівночі» можуть створити натовп проти спільних сервісів. Рандомізація/джиттер — це фіча надійності.
- раніше нормою було відправляти вивід завдання на пошту. Багато налаштувань cron покладалися на локальну пошту. Сучасні системи часто не мають встановленого MTA, тож вивід зникає в порожнечу.
Вибирайте планувальник як файлову систему
Linux/Unix: cron проти systemd timers проти «щось інше»
Використовуйте cron коли: вам потрібна універсальна сумісність, дуже прості періодичні тригери, і ви готові самі додати відсутні продакшн-фічі (блокування, логування, керування середовищем, моніторинг).
Використовуйте systemd timers коли: ви на дистрибутиві з systemd і хочете вбудовані примітиви надійності: journald-логи, залежності, контролі ресурсів, ізоляцію та Persistent=true, щоб надолужити пропущені запуски.
Використовуйте Kubernetes CronJobs коли: завдання належить у кластер і потребує ізоляції контейнера, запитів/лімітів ресурсів та політик повторних спроб, рідних для кластера. Але пам’ятайте: ви просто перемістили режими відмов, а не видалили їх.
Використовуйте workflow-рушій коли: вам потрібні DAG-и, повторні спроби для кожного кроку, backfill-и та аудиторські сліди. Якщо ваш «скрипт» став маленьким бізнесом — припиніть робити вигляд, що це хобі.
Windows: Task Scheduler підходить, якщо ви ставитесь до нього як до продакшну
Windows Task Scheduler може бути дуже надійним, але ви однозначно зможете налаштувати все неправильно через облікові дані, директорії «start in» і припущення про сесію користувача. Якщо ви плануєте PowerShell, робіть це серйозно: явні шляхи, явні рішення щодо execution policy, логування всього й не покладайтеся на підключені мережеві диски.
Планувальник — не ваш шар надійності
Навіть найкращий планувальник не виправить:
- скрипт, який не ідемпотентний,
- завдання, яке може перекриватися,
- завдання, яке залежить від «якого сьогодні PATH»,
- завдання, яке «успішне», але тихо генерує сміття.
Вибирайте планувальник для тригерів і оркестрації. Будуйте надійність у самому завданні.
Жарт 1/2: cron-завдання без логів — як підводний човен без сонара: тихо до того моменту, поки ви не натрапите на щось дороге.
Шаблони проектування для скриптів, що виживають у продакшні
1) Зробіть середовище навмисно нудним
Cron запускається з мінімальним середовищем. systemd-юніти можуть працювати в іншому середовищі, ніж ваша інтерактивна оболонка. Windows-задачі працюють з іншим профілем, ніж ваша RDP-сесія. Виправлення однакове всюди: задекларуйте, що вам потрібно.
- Використовуйте абсолютні шляхи до виконуваних файлів та файлів.
- Встановіть PATH явно (і мінімально).
- Встановіть локаль (LC_ALL=C), якщо парсинг виводу залежить від неї.
- Встановіть umask явно, якщо ви створюєте файли, які мають читати інші.
- Встановіть робочу директорію явно (або не залежіть від неї).
2) Ставтеся до часу як до ворожого
Часові пояси, літній час, секунди-ліпси, дрейф годинника та кроки NTP: час вас підведе. Якщо ваше завдання залежить від дати, вирішіть, чи «дата» означає UTC або локальний час, і будьте явними.
- Віддавайте перевагу UTC для часових міток і імен розділів.
- Якщо бізнес-процес вимагає локального часу («відправити о 8 ранку місцевого»), використовуйте локальний час, але оберігайте дні з DST.
- Записуйте і «запланований час», і «фактичний час початку» в логах.
3) Використовуйте блокування, щоб запобігти перекриттю
Перекриття — класична повільна помилка: усе гаразд, поки один запуск не затримається, наступний стартує, і тепер два процеси конкурують за ті самі файли, рядки бази даних або пропускну здатність сховища.
На Linux використовуйте flock. На Windows використовуйте файл-блокування з ексклюзивним відкриттям, або примітиви ОС (mutex), якщо ви в «серйозній» мові. Не винаходьте блокування «перевірити потім створити»; це рольова гра з умовами гонки.
4) Зробіть скрипти ідемпотентними або принаймні безпечними для повторного запуску
Планувальники повторюють спроби. Люди перезапускають. Машини перезавантажуються посеред виконання. Якщо повторний запуск викликає корупцію, у вас не автоматизація — у вас ігровий автомат.
- Пишіть вихідні файли у тимчасовий шлях, потім атомарно перейменовуйте на місце.
- Обережно використовуйте «маркерні файли»; включайте версію й часову мітку.
- При взаємодії з базами даних використовуйте транзакції та унікальні ключі для дедуплікації.
- Якщо видаляєте дані, спочатку перемістіть їх у карантин перед постійним видаленням.
5) Зробіть «успіх» вимірюваним
Код виходу 0 — необхідний, але недостатній. Завдання, яке створює порожній файл резервної копії, може все одно вийти 0. Визначте, що означає успіх:
- Очікуваний вихід існує і не порожній.
- Контрольна сума збігається (якщо застосовно).
- Кількість рядків у межах очікувань.
- Резервна копія відновлювана (періодичні тестові відновлення).
6) Логуйте так, ніби ви на чергуванні (бо ви таки на чергуванні)
Кожне заплановане завдання має логувати: час початку, час завершення, тривалість, ключові параметри та чіткий фінальний статус. Якщо воно торкається сховища, логувати байти записані/зчитані й будь-які помилки шару сховища.
Маршрутуйте логи туди, де їх можна шукати. Якщо ви покладаєтеся на локальні файли — обертайте їх. Якщо ви покладаєтеся на journald — переконайтеся, що політика зберігання адекватна.
7) Встановіть таймаути та обмеження ресурсів
Зависле завдання гірше за провалене, бо воно блокує наступний запуск і виснажує систему. Використовуйте таймаути для мережевих викликів і загальні обмеження часу виконання.
systemd тут відмінний: TimeoutStartSec=, обмеження CPU і пам’яті, планування I/O та ізоляція. Cron теж може, але вам доведеться написати більше обгорткового коду.
8) Додайте джиттер, щоб уникнути хвилі на флоті
Якщо тисяча серверів запускають «щоденне прибирання» опівночі, вітаємо: ви винайшли розподілений DDoS проти власного сховища.
Додайте випадкову затримку (в межах), або розплануйте по вікну. Джиттер — дешева надійність.
9) Моніторте відсутність, не лише присутність
Сповіщення про «завдання впало» — добре. Сповіщення про «завдання не запустилося» — краще. Пропущені запуски трапляються: вимкнені таймери, зламані crontab-и, призупинені VM, прострочені облікові дані.
Виштовхуйте heartbeat-метрику або пишіть файл із часовою міткою, який перевіряє моніторинг. Тиша — це не успіх.
Практичні завдання (команди, вихід, рішення)
Це реальні операційні перевірки. Запускайте їх під час побудови розкладу й знову, коли щось ламається о 02:13. Кожен пункт включає: команду, що означає вивід, і яке рішення ви приймаєте далі.
Завдання 1: Підтвердьте, що демон cron насправді запущений
cr0x@server:~$ systemctl status cron
● cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-02-03 00:12:10 UTC; 2 days ago
Docs: man:cron(8)
Main PID: 812 (cron)
Tasks: 1 (limit: 18945)
Memory: 1.4M
CPU: 2.912s
Значення: Якщо це не active (running), ваше завдання ніколи не мало шансів.
Рішення: Якщо воно inactive/failed — спочатку виправте сервіс (enable/start). Ще не чіпайте скрипт.
Завдання 2: Перевірте, що запис crontab встановлений для правильного користувача
cr0x@server:~$ crontab -l
MAILTO=""
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/nightly-report.sh
Значення: Ви бачите розклад, shell, PATH і чи буде вивід відправлено поштою.
Рішення: Якщо це не той користувач (поширено), встановіть crontab під потрібним акаунтом або використайте системний cron з явним користувачем.
Завдання 3: Перевірте логи cron на предмет тригеру вашого завдання
cr0x@server:~$ grep -E "CRON|nightly-report" /var/log/syslog | tail -n 5
Feb 5 02:15:01 server CRON[24719]: (cr0x) CMD (/opt/jobs/nightly-report.sh)
Feb 5 02:15:01 server CRON[24718]: (CRON) info (No MTA installed, discarding output)
Значення: Cron справді тригернув. Також вивід відкидається, бо немає поштового сервера і ви не перенаправили вивід.
Рішення: Негайно виправте логування: перенаправляйте stdout/stderr у файл або в syslog/journald.
Завдання 4: Доведіть, що скрипт працює неінтерактивно в середовищі планувальника
cr0x@server:~$ env -i HOME=/home/cr0x USER=cr0x SHELL=/bin/bash PATH=/usr/bin:/bin /bin/bash -lc '/opt/jobs/nightly-report.sh'
/opt/jobs/nightly-report.sh: line 12: psql: command not found
Значення: У вашому інтерактивному PATH був psql; у PATH завдання його немає.
Рішення: Використайте абсолютний шлях до psql або встановіть PATH у скрипті/unit/crontab. Не «source .bashrc» як тимчасове рішення.
Завдання 5: Додайте блокування, щоб запобігти перекриттю (і протестуйте)
cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=0
cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=1
Значення: Перший запуск отримав блокування. Другий завершився негайно (exit 1), бо блокування утримується.
Рішення: Вирішіть політику: пропускати, якщо заблоковано (часто правильно), або чекати з таймаутом (іноді правильно). Документуйте рішення.
Завдання 6: Переконайтеся, що коди виходу передаються і видимі
cr0x@server:~$ /opt/jobs/nightly-report.sh; echo "job_exit=$?"
job failed: could not connect to database
job_exit=2
Значення: Скрипт повертає ненульовий код виходу і виводить чітку помилку.
Рішення: Якщо коди виходу завжди 0 — виправте скрипт. Планувальники реагують тільки на те, що ви їм повідомите.
Завдання 7: Побудуйте systemd timer, який надолужує пропуски після простою
cr0x@server:~$ systemctl cat nightly-report.timer
# /etc/systemd/system/nightly-report.timer
[Unit]
Description=Run nightly report
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Значення: Persistent=true запускає завдання після завантаження, якщо воно було пропущене. RandomizedDelaySec додає джиттер.
Рішення: Якщо «повинно запускатися щодня за будь-яких умов», використайте таймер з персистентністю або workflow-рушій, а не простий cron.
Завдання 8: Перегляньте unit сервісу на предмет обмежень ресурсів і логування
cr0x@server:~$ systemctl cat nightly-report.service
# /etc/systemd/system/nightly-report.service
[Unit]
Description=Nightly report job
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=cr0x
Group=cr0x
WorkingDirectory=/opt/jobs
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh
TimeoutStartSec=30m
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
StandardOutput=journal
StandardError=journal
Значення: Так виглядає «планування для продакшну»: залежності, блокування, таймаут і логи в journald.
Рішення: Якщо ваше завдання конкурує з інтерактивними навантаженнями або ресурсомісткими сервісами, встановіть пріоритети IO/CPU тут замість надії на краще.
Завдання 9: Перевірте, коли таймер останній раз запускався і чи прострочений він
cr0x@server:~$ systemctl list-timers --all | grep nightly-report
nightly-report.timer loaded active waiting Thu 2026-02-05 02:23:11 UTC 3h ago Thu 2026-02-06 02:15:00 UTC 20h left nightly-report.service
Значення: Ви отримуєте часи останнього/наступного запуску. Якщо waiting відсутній або останній запуск давній — щось не так.
Рішення: Якщо прострочено, перевірте, чи таймер увімкнено, чи годинник правильний і чи юніт не падає або не зависає.
Завдання 10: Прочитайте логи саме цього завдання
cr0x@server:~$ journalctl -u nightly-report.service -n 20 --no-pager
Feb 05 02:15:02 server nightly-report.sh[25101]: start ts=2026-02-05T02:15:02Z
Feb 05 02:15:02 server nightly-report.sh[25101]: connecting db=reporting
Feb 05 02:19:41 server nightly-report.sh[25101]: wrote /var/reports/nightly-2026-02-05.csv bytes=1842201
Feb 05 02:19:41 server nightly-report.sh[25101]: done status=ok runtime_s=279
Значення: Ви можете довести, що воно запускалося, скільки тривало і що воно виробило.
Рішення: Якщо логи не показують чіткого початку/кінця — покращіть логування перед тим, як щось інше поліпшувати.
Завдання 11: Виявлення перекриття або процесів, що втекли
cr0x@server:~$ pgrep -af nightly-report.sh
25101 /bin/bash /opt/jobs/nightly-report.sh
Значення: Ви бачите, чи воно зараз працює, і з яким PID/командою.
Рішення: Якщо існує кілька екземплярів, додайте блокування і розгляньте обмеження часу виконання/таймаут.
Завдання 12: Перевірте, хто утримує блокування, якщо ви застрягли «заблоковано назавжди»
cr0x@server:~$ ls -l /run/lock/nightly-report.lock
-rw-r--r-- 1 cr0x cr0x 0 Feb 5 02:15 /run/lock/nightly-report.lock
cr0x@server:~$ lsof /run/lock/nightly-report.lock
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
flock 25100 cr0x 3w REG 253,0 0 987 /run/lock/nightly-report.lock
Значення: Наявність файлу не означає, що він заблокований; lsof показує, чи процес утримує файл.
Рішення: Якщо мертвий процес не утримує блокування, ваша реалізація блокування неправильна (не використовуйте логіку «файл існує»). Правильно використовуйте flock.
Завдання 13: Перевірте синхронізацію часу (бо планування припускає реальний час)
cr0x@server:~$ timedatectl
Local time: Thu 2026-02-05 05:24:12 UTC
Universal time: Thu 2026-02-05 05:24:12 UTC
RTC time: Thu 2026-02-05 05:24:11
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Значення: Якщо годинник не синхронізований, «2:15» стає рекомендацією.
Рішення: Виправте синхронізацію часу перед тим, як шукати фантомні баги планувальника.
Завдання 14: Перевірте вільне місце на диску та тиск інодів (сховище тихо вбиває завдання)
cr0x@server:~$ df -h /var /tmp
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 100G 94G 1.8G 99% /
tmpfs 16G 128M 16G 1% /tmp
cr0x@server:~$ df -i /var
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda2 6553600 6501000 52600 99% /
Значення: Може бути вільні байти, але нема інодів, або навпаки. Обидва випадки можуть зламати завдання, яке пише вихідні дані.
Рішення: Якщо використання >95% — припиніть вважати, що це проблема планування. Звільніть місце, оберніть логи, виправте втечі тимчасових файлів.
Завдання 15: Помітити вузькі місця вв/виводу, що подовжують виконання і спричиняють перекриття
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 02/05/2026 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
8.12 0.00 3.44 31.55 0.00 56.89
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
sda 3.10 90.2 0.00 0.0 12.33 29.1 40.0 10240.0 12.0 23.1 210.45 256.0 8.41 98.7
Значення: Високе %iowait і %util близько до 100% вказують на насиченість диска. Високе w_await свідчить про черги записів.
Рішення: Якщо ваше завдання стало «повільним», перевірте вв/вивід. Потім розгляньте перенесення на позапіковий час, обмеження пропускної здатності або переміщення важких записів на краще сховище.
Завдання 16: Підтвердіть, що дозволи відповідають ідентичності планувальника
cr0x@server:~$ sudo -u cr0x test -w /var/reports && echo writable || echo not_writable
not_writable
Значення: Користувач, що запускає завдання, не може писати в цільову директорію.
Рішення: Виправте власність/ACL директорії або запускайте завдання під правильним сервісним акаунтом. Не «просто запустіть як root», якщо вам подобаються постмортеми.
Завдання 17: Перевірте DNS/мережеві залежності для завдань, що викликають сервіси
cr0x@server:~$ getent hosts db.internal
10.40.12.8 db.internal
cr0x@server:~$ nc -vz db.internal 5432
Connection to db.internal (10.40.12.8) 5432 port [tcp/postgresql] succeeded!
Значення: Розв’язання імен працює; порт досяжний.
Рішення: Якщо DNS не працює або порт заблоковано, виправте політику мережі/стан сервісу перед переписуванням скрипту.
Завдання 18: Windows-стиль помилка на Linux: знайдіть скрипти з CRLF-кінець рядка
cr0x@server:~$ file -b /opt/jobs/nightly-report.sh
Bourne-Again shell script, ASCII text executable, with CRLF line terminators
Значення: CRLF може ламати інтерпретатори у тонкий спосіб, особливо з shebang-рядками.
Рішення: Конвертуйте в LF (dos2unix), правильно закомітьте і припиніть копіювати скрипти через пошту.
Три корпоративні міні-історії (з яких вчаться один раз)
Міні-історія 1: Інцидент через неправильне припущення (часові пояси — не деталь)
Середня компанія запускала нічне «заморожування даних» о 00:05 місцевого часу. Скрипт маркував рядки з freeze_date=$(date +%F), потім downstream-системи використовували цю дату як ключ розділу. Роки це працювало, бо «місцевий час» і «робочий день» здавалися тим самим.
Потім вони розширилися до другого регіону. Хтось логічно для інфраструктури налаштував сервери на UTC. Планувальник все ще запускався о «00:05», бо саме це було в crontab. Але тепер «00:05» було UTC, а не локальним часом. Заморожування відбулося на кілька годин раніше, ніж потрібно для початкового регіону, і на кілька годин пізніше для нового регіону.
Справжня шкода була не в часі запуску — а в мітці дати. Частина рядків, які мали бути помічені для наступного робочого дня, були промарковані попередньою датою. Розділи «відсутні» дані, дашборди показали різкий спад, і фінансова команда почала невеликий чемний панік.
Перший запропонований фікс був класичний: «Повернути часовий пояс». Це допомогло б одному регіону і нашкодило б іншому. Краще рішення: визначити межу робочого дня в коді (явний TZ для обчислення дати) і зберігати часові мітки в UTC. Тепер завдання обчислює ключ розділу, використовуючи налаштований бізнес-часовий пояс, і логувало і ключ, і UTC-мітку.
Тривалий урок був простим і дратівливим: ви не можете віддати семантику на відкуп date. Якщо вихід завдання залежить від «якого сьогодні дня», ви маєте визначити, який годинник ви маєте на увазі.
Міні-історія 2: Оптимізація, що відгукнулася (стиснення — не безкоштовне)
Інша організація мала завдання резервного копіювання, яке дампило базу й стискало її. Витрати на зберігання зростали, тож хтось «оптимізував», встановивши максимальне стиснення і підвищивши паралелізм. Резервні копії стали меншими. Усі аплодували графіку.
Через два тижні 02:00 став новою піковою годиною навантаження. Завдання резервного копіювання наситило CPU і диск I/O, і робило це з відмінною консистентністю. Інші плановані завдання — ротація логів, ETL, навіть сканування безпеки — почали перекриватися і таймаутитись. Планувальник не падав; він точно виконував план, що вже не підходив реальності.
Перший симптом не був «резервна копія впала». Перший симптом — «випадкова повільність сервісів вранці». Черговий шукав латентність застосунків, потім блокування в базі, потім мережу. Лише після графікування iowait на хості виявився патерн: «оптимізоване» резервне копіювання перетворювало хост на сумний тремтячий цеглинний блок.
Фікс не полягав в абсолютному відміненні стиснення. Його обмежили: знизили рівень стиснення, обмежили CPU, дали пріоритет IO, і перенесли на інший часовий інтервал з джиттером по флоту. Вони також ввели максимальний час виконання; якщо завдання перевищує його, воно гучно падає і сповіщає, замість того щоб тихо красти ніч.
Оптимізація, що ігнорує конкуренцію за ресурси — не оптимізація. Це переміщення витрат із «місця на диску» в «чийсь сон».
Міні-історія 3: Нудна, але правильна практика, що врятувала день (ідемпотентність + атомарні записи)
Команда запускала заплановане завдання, яке генерувало CSV для біллінгу клієнтів. Попередня версія писала безпосередньо в /srv/billing/current.csv. Одного разу машина перезавантажилась посеред запису після непов’язаного оновлення ядра. Файл існував, завдання «запустилося», і downstream-системи радісно прочитали усічений CSV. Наслідком стали неправильні рахунки. Не катастрофа, але дорого в годиннику людей.
Команда змінила одну річ: тепер завдання писало у унікально іменований тимчасовий файл, перевіряло кількість рядків і контрольну суму, а потім атомарно перейменовувало в потрібне місце. Вони також зберігали останній відомо-робочий файл кілька днів. Це було нудно. Але правильно.
Через місяці тимчасові проблеми з I/O викликали помилку посеред виконання. Завдання провалилось перед перейменуванням, залишивши старий current.csv недоторканим. Downstream-системи продовжували використовувати останній хороший файл, а черговий отримав сповіщення, що новий артефакт не було згенеровано.
Жодної метушні. Жодних дзвінків «що змінилося?». Жодного судового розслідування часткових даних. Лише чистий провал і стабільний контракт виходу. Надійність часто виглядає як відмова бути хитрим.
Плейбук швидкої діагностики
Коли заплановане завдання не запускається (або запускається неправильно), потрібна послідовність, яка швидко знаходить вузьке місце. Не налагодження за відчуттями.
Перше: Чи тригернув планувальник щось?
- cron: перевірте записи syslog на CMD-рядок; підтвердіть існування запису crontab і що демон запущений.
- systemd: перевірте
list-timers, статус юніта і час останнього запуску; підтвердіть, що таймер увімкнено. - Windows: перевірте Last Run Time / Last Run Result і вкладку History (якщо увімкнено).
Якщо немає запису про тригер — зупиніться. Виправте планування і увімкнення. Не чіпайте скрипт.
Друге: Чи стартувало, але померло негайно?
- Читайте логи: journald або файл, куди ви перенаправляєте.
- Шукайте «command not found», permission denied, відсутній конфіг, неправильну робочу директорію.
- Повторіть запуск у мінімальному середовищі, щоб відтворити проблему.
Якщо воно помирає відразу — майже завжди це середовище, ідентичність або дозволи.
Третє: Чи запустилося, але тривало занадто довго?
- Перевірте перекриття: кілька PID, конкуренція за блокування, повідомлення «вже працює».
- Перевірте вузькі місця ресурсів: диск повний, вичерпання інодів, iowait, CPU steal, мережеві таймаути.
- Перевірте залежності: блокування в базі, обмеження API, DNS.
Якщо виконання затягнулося — виправте конкуренцію та додайте таймаути/ліміти, перед тим як додавати повторні спроби.
Четверте: Воно «успішне», але виробило сміття?
- Провалідуйте артефакти: розмір, контрольна сума, кількість рядків, версія схеми.
- Шукайте часткові записи: часові мітки файлів, використання атомарних перейменувань.
- Перевірте критерії «успіху» в коді: чи ви насправді щось перевіряєте?
Тихий поганий вихід гірший за провал. Зробіть неможливим публікування поганих даних.
Жарт 2/2: Єдина річ надійніша за cron-завдання — cron-завдання, яке впадає в невдачу щойно ви перестанете його дивитися.
Поширені помилки: симптом → корінна причина → виправлення
1) Симптом: «Воно працює вручну, але не за розкладом»
Корінна причина: відмінності PATH/середовища; відсутня робоча директорія; скрипт залежить від ініціалізаційних файлів інтерактивної оболонки.
Виправлення: Використовуйте абсолютні шляхи, встановіть PATH явно, вкажіть WorkingDirectory в systemd, уникайте source .bashrc. Відтворіть із env -i.
2) Симптом: Немає логів, немає виводу, немає помилок
Корінна причина: вивід відкидається (немає MTA для cron), або логи пишуться у місце, недоступне для ідентичності планувальника.
Виправлення: Перенаправляйте stdout/stderr у файл або journald. Переконайтеся в дозволах до каталогу логів і в обгортці для ротації.
3) Симптом: Завдання запускається двічі (або перекривається) і псує дані
Корінна причина: відсутнє блокування; довгий час виконання; повтори без ідемпотентності; дубльована година через DST.
Виправлення: Додайте flock. Встановіть обмеження часу виконання. Робіть виходи атомарними і ідемпотентними. Розгляньте розклад у UTC або явну обробку TZ.
4) Симптом: «Випадково падає» з мережевими помилками
Корінна причина: флапи DNS, тимчасова відмова залежностей, відсутні повтори/бекоф, мережа не готова під час завантаження.
Виправлення: Додайте перевірки залежностей, обмежені повтори з бекофом, і в systemd вкажіть After=network-online.target плюс Wants=network-online.target.
5) Симптом: Запускається, але downstream каже, що вихід пустий/частковий
Корінна причина: запис прямо у фінальне ім’я файлу; споживач читає під час запису; перезавантаження під час запису.
Виправлення: Пишіть у тимчасовий файл + fsync за потреби + атомарне перейменування. Зберігайте останній відомий робочий файл.
6) Симптом: Працювало тижнями, а потім назавжди припинилося
Корінна причина: прострочення облікових даних (токени API, Kerberos, ротація паролів бази), блокування назавжди через неправильну реалізацію блокування, поступове заповнення диска.
Виправлення: Сповіщення про «завдання не запустилося» і «вихід відсутній». Використовуйте блокування ОС (flock). Моніторте використання диска і обертайте логи.
7) Симптом: «Воно заплановане, але ніколи не наздоганяє після простою»
Корінна причина: cron не робить backfill; таймери не персистентні; ноутбук/VM був вимкнений.
Виправлення: Використовуйте systemd timers з Persistent=true, або проєктуйте завдання, яке працює від збереженої контрольної точки («остання успішна мітка часу») замість покладання лише на стіновий годинник.
8) Симптом: Прискорені сплески CPU у момент розкладу по всьому флоту
Корінна причина: ідентичні розклади створюють thundering herd; відсутній джиттер.
Виправлення: Додайте випадкову затримку у systemd timers або обмежений випадковий sleep у обгортці; розподіліть розклади.
Чеклісти / покроковий план
Чекліст A: Перед тим, як планувати будь-який скрипт
- Визначте успіх: який артефакт/побічний ефект доводить, що воно спрацювало?
- Визначте ритм: «щоденно» — не точно. Чи потрібне надолуження?
- Визначте ідентичність: який користувач/сервісний акаунт запускає його? Які дозволи?
- Визначте залежності: мережа, база даних, точки монтування, секрети.
- Зробіть його повторюваним: ідемпотентним або безпечним для повтору.
- Забезпечте відсутність перекриття: блокування плюс максимальний час виконання.
- Зробіть логи неминучими: stdout/stderr захоплені; статус початку/кінця.
- Зробіть час явним: UTC vs локальний; документуйте поведінку при DST.
- Сплануйте моніторинг: сповіщення при збоях і при пропуску запуску.
Чекліст B: «Достатньо хороша» патерн-обгортка (Linux)
Навіть якщо ви залишите cron, обгорніть скрипт. У вашій обгортці живе дисципліна продакшну: середовище, блокування, логування, таймаути.
cr0x@server:~$ cat /opt/jobs/wrappers/nightly-report-wrapper.sh
#!/bin/bash
set -euo pipefail
export PATH="/usr/local/bin:/usr/bin:/bin"
export LC_ALL="C"
umask 027
log_dir="/var/log/jobs"
mkdir -p "$log_dir"
log_file="$log_dir/nightly-report.log"
exec >> "$log_file" 2>&1
echo "start ts=$(date -u +%FT%TZ) host=$(hostname -f)"
timeout 30m flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh
echo "done ts=$(date -u +%FT%TZ) status=ok"
Чому це працює: воно провалюється швидко (set -euo pipefail), захоплює логи, дотримує таймаут, і запобігає перекриттю. Також робить середовище передбачуваним.
Чекліст C: План rollout systemd таймерів (з одного хоста на флот)
- Напишіть
.serviceunit з явним User/Group, PATH, WorkingDirectory, таймаутом і логуванням. - Напишіть
.timerзOnCalendar,Persistent=trueі джиттером. - Тест:
systemctl start job.serviceвручну; перевірте логи і код виходу. - Увімкніть таймер:
systemctl enable --now job.timer. - Спостерігайте:
list-timersі journald принаймні один повний цикл. - Додайте моніторинг: сповіщення, якщо останній успіх > очікуваного вікна; сповіщення при ненульовому виході.
- Тільки тоді: розгортайте на більшу кількість хостів. Не змінюйте масово розклад опівночі. Ви не chaos engineer; ви хочете спати.
Чекліст D: Правила планування з огляду на сховище
- Не запускайте записомісткі завдання одночасно з бекапами, компактуванням або реплікацією знімків.
- Слідкуйте за вільним місцем і використанням інодів у вихідних та тимчасових директоріях.
- Використовуйте атомарні записи для спільних артефактів.
- Обмежуйте стиснення та паралелізм; вимірюйте iowait.
- Плануйте зростання логів; обертайте або відправляйте логи.
FAQ
1) Cron чи systemd timers: що обрати на Linux?
Якщо у вас є systemd — віддавайте перевагу таймерам для всього, що вам важливо: вбудоване логування, контроль залежностей, Persistent=true і обмеження ресурсів. Cron підходить для простих некритичних періодичних задач або коли важлива портативність.
2) Як зупинити перекриття завдань?
Використовуйте блокування, що підтримується ОС. На Linux обгорніть команду в flock. Також встановіть максимальний час виконання (таймаут), щоб «зависання» не перетворилося на «вічне блокування».
3) Чому моє завдання працює через SSH, але падає в cron?
Ваша інтерактивна оболонка встановлює PATH, локалі і іноді облікові дані. Cron — ні. Відтворіть через env -i, потім зробіть скрипт самодостатнім (абсолютні шляхи, явні шляхи конфігів, явна локаль).
4) Куди перенаправляти вивід: у файл чи в journald?
Якщо ви вже використовуєте systemd — journald зазвичай найпростіше: searchable, маркований юнітом і централізовано керований. Для cron файл підходить — тільки обгорніть ротацію і переконайтеся в дозволах на запис.
5) Як правильно обробляти секрети для запланованих задач?
Не хардкодьте їх у скриптах або crontab. Використовуйте менеджер секретів, якщо є, або принаймні конфіг-файли з доступом лише для root/службового акаунта. Для systemd розгляньте environment-файли з контролем доступу. Ротуйте секрети та сповіщайте про помилки автентифікації.
6) Як змусити завдання «надолужити» після простою?
cron не робить backfill. Використовуйте systemd timers з Persistent=true, або проєктуйте завдання на обробку від збереженої контрольної точки («остання успішна мітка часу») замість залежності від годинника.
7) Як обробляти DST для завдання, що має працювати в місцевому бізнес-часі?
Визначте поведінку для DST «втраченої» години і «дубльованої» години. Звичні варіанти: запуск у безпечний час (наприклад, 03:15), або запуск у UTC і трансляція виходів. Якщо потрібен локальний час — логуйтесь часозону, записуйте UTC-мітки і захищайтеся від дубльованих запусків за допомогою блокування і ідемпотентності.
8) Моє завдання іноді повільне. Чи додати повторні спроби?
Не одразу. Повільність часто є наслідком конкуренції (насичення I/O, блокування в БД) або проблем із залежностями. Виміряйте, куди йде час, обмежте тривалість і запобігайте перекриттю. Повторні спроби без контролю можуть помножити навантаження і загострити інцидент.
9) Який мінімальний моніторинг варто додати?
Два сигнали: (1) часову мітку останнього успішного запуску, (2) код виходу останнього запуску. Сповістіть, якщо завдання запізнилося/пропущено або вийшло ненульовим. Також відстежуйте свіжість артефактів, якщо завдання виробляє файл чи звіт.
10) Коли перестати використовувати «заплановані скрипти» і перейти на workflow-інструмент?
Коли у вас є залежності між кроками, потрібні backfill-и, детальні аудиторські сліди або складна логіка повторних спроб. Якщо ви будуєте DAG bash-скриптами й email-сповіщеннями — відповідь очевидна.
Висновок: практичні наступні кроки
Якщо ви хочете, щоб заплановані скрипти дійсно запускалися, не починайте сперечатися про cron проти таймерів. Почніть з того, щоб ваше завдання могло вижити: явне середовище, блокування, ідемпотентність, логування і моніторинг відсутності.
- Виберіть ідентичність виконання і зафіксуйте її (найменші привілеї, передбачувані дозволи).
- Додайте блокування (
flock) і максимальний час виконання (таймаути). - Зробіть виходи атомарними (тимчасовий файл + перейменування) і визначте перевірки успіху.
- Збирайте логи у пошукове місце і плануйте їх ротацію/збереження навмисно.
- Сповіщайте про «впав» і «не запустився», а не лише «вивів помилку».
- Якщо ви на Linux з systemd: мігруйте критичні завдання на таймери з
Persistent=trueі джиттером. - Проведіть один контрольований drill: зламайте DNS або заповніть тимчасовий каталог у тестовому середовищі і переконайтеся, що ваше завдання падає гучно і безпечно.
Планування — це легко. Надійне планування — дисципліна. Робіть нудні речі зараз, щоб ваше майбутнє «я» міг спати о 02:15.