Когда cron запускает одно и то же задание чаще, чем оно успевает завершиться, появляется классический источник проблем: пересечения. Отсюда конкурирующие записи в базу, двойные письма из рассылки, накладка бэкапов, долгая блокировка кешей. Хорошая новость: это исправляется одной-двумя строками с корректным flock или lockfile.
Зачем нужен запуск «в один экземпляр»
Системы с регулярной автоматизацией (импорт данных, рассылки, сбор метрик, ротация логов) должны гарантировать, что в любой момент времени работает не более одного экземпляра задачи. В противном случае вы получаете:
- гонки в БД и повреждение данных;
- дубликаты и повторные отправки;
- долгие «хвосты» и лавинообразный рост времени выполнения;
- непредсказуемую нагрузку на CPU/IO, просадки SLA.
Правильная координация запуска проще, чем последующая чистка последствий пересечений.
Инструменты: flock, lockfile, и что выбрать
flock — утилита из util-linux, реализует консультативные файловые блокировки ядра. Ключевые плюсы: атомарность на уровне ядра, автоматическое снятие блокировки при завершении процесса (нормально или аварийно), удобный синтаксис через -c или файловые дескрипторы.
lockfile — семейство утилит (например, из procmail или liblockfile-progs), создающих «файл-замок» по протоколу: если файл существует, считается, что ресурс занят. Можно задавать срок годности (TTL) и политику ретраев. Это хорошо, когда flock недоступен или нужна TTL-семантика на сам файл.
В подавляющем большинстве сценариев cron на Linux разумнее начинать с flock. Он проще, безопаснее и обычно уже установлен. На проектах, где cron запускается на виртуальном хостинге или на собственном VDS, приёмы одинаковые: выбирайте единый путь для локов и придерживайтесь описанных политик.

Быстрый старт с flock: одна строка в crontab
Два типовых режима: «не ждать» (пропуск, если занято) и «подождать N секунд» (мягкая очередь).
1) Не ждать: пропустить, если уже идёт
* * * * * flock -n /run/lock/cache-warm.lock -c "/usr/local/bin/cache-warm"
Ключ -n даёт неблокирующий режим: если лок не взять мгновенно, flock завершится с ненулевым кодом, cron просто перейдёт к следующему заданию. Это безопасный дефолт для частых задач (ежеминутные обновления кэша, сбор статистики), где допустим пропуск одного запуска.
2) Подождать: небольшая очередь
*/5 * * * * flock -w 120 /run/lock/db-backup.lock -c "/usr/local/bin/db-backup"
-w 120 означает: ожидать лок до 120 секунд. Если предыдущая копия почти закончилась, новый запуск войдёт в «мягкую очередь» и уступит. Важно не ставить время ожидания больше реального интервала cron, иначе очереди начнут накапливаться.
Коды возврата и логирование
В crontab удобно логировать только «успех» или наоборот фиксировать только «конкурентные пропуски»:
* * * * * flock -n /run/lock/job.lock -c "/usr/local/bin/job" >/var/log/job.log 2>&1
* * * * * flock -n /run/lock/job.lock -c "/usr/local/bin/job" || logger -t job "skipped: already running"
Так вы сразу увидите факты пропуска и сможете оценить частоту коллизий.
Расширенный приём: файловый дескриптор и trap
Форма с дескриптором даёт гибкость: можно удерживать лок пока скрипт работает, добавлять ловушки сигналов, печатать PID и делать произвольную логику вокруг.
#!/usr/bin/env bash
set -euo pipefail
lock="/run/lock/report.lock"
exec 9>"$lock"
# Неблокирующая попытка; пропускаем, если занято
if ! flock -n 9; then
echo "$(date -Is) already running" >&2
exit 0
fi
# По желанию: писать текущий PID рядом (для отладки)
echo $$ >"$lock.pid"
trap 'rm -f "$lock.pid"' EXIT INT TERM
# Тяжёлая работа
/usr/local/bin/generate-report
Важно: для flock удалять сам файл-лок не обязательно. Блокировка привязана к открытию файла через дескриптор, а не к самому факту существования файла. Даже если файл останется, без открытого дескриптора лок снят.
Политики: пропуск, ожидание, жёсткий дедлайн
- Пропуск (
-n): снижает накопление работ, сохраняет равномерную нагрузку, но отдельные окна могут быть без свежих данных. - Ожидание (
-w N): полезно для критичных задач (бэкап раз в час), где пропуск нежелателен, но можно немного подождать. - Жёсткий дедлайн: суммируйте время ожидания и лимит времени самого задания (через таймаут оболочки или в коде), чтобы один запуск не «забивал» всю линию.
lockfile: TTL и контроль «застойных» локов
Когда нужен именно «файл-замок» с контролем возраста, используйте lockfile. Пример: не пытаться ждать вообще, но если лок старше N секунд (процесс умер, сигналы не поймались) — признать его устаревшим и перехватить.
# Немедленная попытка; если лок слишком старый, он будет перехвачен
lockfile -r 0 -l 3600 /run/lock/import.lock || exit 0
trap 'rm -f /run/lock/import.lock' EXIT INT TERM
/usr/local/bin/import-data
Опции в классической реализации:
-r 0— не ретраить, действовать как-nвflock.-l 3600— считать лок устаревшим, если старше 3600 секунд, и заменить его новым.
Минус подхода с lockfile — нужна явная уборка файла (trap). Плюс — явная TTL-семантика для «починки» застоявшихся локов без ручного вмешательства.
Где хранить локи и как их называть
- Путь: современная практика —
/run/lock(tmpfs, очищается при перезагрузке). Старый путь —/var/lock. - Права: у кого запускается cron, тот и владелец. Избегайте «world-writable» директорий для локов.
- Имена: неймспейсы по проектам:
/run/lock/myapp/report.lock. Это облегчает отладку и массовую чистку.
Отладка: кто держит лок
Если «зависло», проверьте PID (если пишете его рядом) и посмотрите, какой процесс держит файл открытым. Для отладки подходят утилиты системного уровня.
cat /run/lock/report.lock.pid
ps -fp "$(cat /run/lock/report.lock.pid)"
# Или по имени лок-файла
fuser -v /run/lock/report.lock || true
С flock критично понимать: блокировка висит на открытом файловом дескрипторе. Как только процесс завершается, лок снимается. Поэтому «вечные» блокировки чаще связаны не с flock, а с тем, что задача реально не завершилась (ожидает I/O, повисла на внешнем сервисе).

Очереди против пропусков: правила выбора
- Метрики и кеши: пропуск безопасен, если следующий запуск всё догонит.
- Бэкапы и критичные регламенты: допускайте ожидание (но строго ограниченное) и лимитируйте время самой работы.
- Импорт транзакционных данных: безопаснее не допускать параллелизма вовсе, тщательно логировать и сигнализировать о пропусках.
Тонкая настройка: если работа длится 90 секунд, а интервал — 60 секунд, либо уменьшайте частоту, либо делайте «умный пропуск» (не ждать), иначе очередь никогда не рассосётся. Для построения резервного копирования посмотрите наш разбор инструментов и хранения в объектном хранилище в материале «Бэкапы в S3: restic и borg» по ссылке гайд по резервным копиям в S3.
Типовые паттерны безопасного выполнения
Общий враппер с задержкой и дедлайном
#!/usr/bin/env bash
set -euo pipefail
lock="/run/lock/cleanup.lock"
exec 9>"$lock"
# Ждать максимум 30 секунд
if ! flock -w 30 9; then
echo "$(date -Is) skip: still running" >&2
exit 0
fi
# Жёсткий лимит 10 минут через timeout(1)
/usr/bin/timeout 600 /usr/local/bin/cleanup-orphans
Так вы ограничиваете и время ожидания локов, и время самой работы.
PID-сайдкар для дебага
Писать PID рядом с локом — удобно для диагностики, особенно если команда порождает дочерние процессы. Но помните: источником истины остаётся сам flock; PID-файл может устареть.
Альтернативы: mkdir-подлок и run-one
mkdir как атомарная операция на локальном ФС иногда используют для примитивного лока:
if mkdir /run/lock/myjob.lock.d 2>/dev/null; then
trap 'rmdir /run/lock/myjob.lock.d' EXIT INT TERM
/usr/local/bin/job
else
echo "already running"
fi
Плюсы: минимум зависимостей. Минусы: нет ожидания и нет автоматического удаления при падении процесса без ловушки. В сравнении с flock это компромисс.
run-one из набора moreutils предоставляет синтаксис «запуск в один экземпляр» для произвольной команды. Под капотом — аналогичная идея с локами. Если уже используете moreutils, это быстрый вариант, но на чистом сервере проще и прозрачнее flock.
NFS, контейнеры и распределённые сценарии
Файловые локи рассчитаны на локальный узел. На NFS поведение зависит от версии и настроек, гарантий может не хватать. В контейнерах всё ок при общем неймспейсе PID/IPC и локальном томе; для межузловой синхронизации лучше выбирать другой примитив (БД, редис-локи, очередь задач). Для cron на одном хосте flock — оптимальный выбор.
Интеграция с сервис-менеджерами
Даже если вы переводите расписания на таймеры сервис-менеджера, приём остаётся тем же: вызывайте основную логику под flock в ExecStart или как префикс в командной строке. Подробности — в материале «Таймеры systemd для автоматизации» по ссылке systemd timers: практический гид и обзоре «Cron, crontab и переход на systemd timers» по ссылке cron vs systemd timers.
Практические чек-листы
- Для частых задач начните с
flock -nи проверьте частоту пропусков. - Для редких/критичных добавьте
-wс разумным пределом и общий таймаут выполнения. - Храните локи в
/run/lock, имена — по проекту и задаче. - Логируйте пропуски и длительности, чтобы не «летать вслепую».
- Если нужна TTL-семантика файла, используйте
lockfile -lи чистку черезtrap. - Не накапливайте очереди: синхронизируйте интервал cron с реальной длительностью.
Частые ошибки и как их избежать
- Неверный путь лок-файла: нет прав записи — лок не создаётся, защита мнимая. Исправление: используйте директории, доступные пользователю cron.
- Ноль внимания к кодам выхода: пропуски незаметны. Исправление: журналируйте возвраты.
- Ожидание дольше интервала: «вечная» очередь. Исправление: уменьшите частоту или примените пропуск.
- Слепая чистка локов: удаление действующего лока. Исправление: проверяйте PID/возраст, используйте TTL с умом.
Итоги
Для cron-задач на одном хосте flock закрывает 99% кейсов single-instance: простая интеграция, безопасная автоматическая разблокировка и понятная политика ожидания/пропуска. lockfile полезен, когда нужна TTL-логика самого файлового маркера или на системе нет flock. Добавьте аккуратное логирование, лимиты времени и вменяемый интервал расписания — и проблема пересечений перестанет существовать.


