Предотвратить дублирование выполнения cron-заданий

Вопрос или проблема

Я запланировал cron-задачу, чтобы она выполнялась каждую минуту, но иногда скрипт занимает больше минуты, и я не хочу, чтобы задачи начинали “накладываться” друг на друга. Думаю, это проблема параллелизма – т.е. выполнение скрипта должно быть взаимно-исключающим.

Чтобы решить эту проблему, я сделал так, чтобы скрипт проверял наличие определенного файла (“lockfile.txt“) и завершался, если он существует, или создавал его с помощью touch, если его нет. Но это довольно неэффективный семафор! Есть ли лучшие практики, о которых я должен знать? Может, мне следовало написать демон?

Есть несколько программ, которые автоматизируют эту функцию, избавляют от раздражения и потенциальных ошибок, возникающих при выполнении этого самостоятельно, и также избегают проблемы “зависшего блокирования”, используя flock в фоновом режиме (что является риском, если вы просто используете touch). Я использовал lockrun и lckdo в прошлом, но теперь есть flock(1) (в новых версиях util-linux), который замечателен. Использовать его очень просто:

* * * * * /usr/bin/flock -n /tmp/fcj.lockfile /usr/local/bin/frequent_cron_job

Лучший способ в оболочке – использовать flock(1)

(
  flock -x -w 5 99
  ## Выполняйте ваши действия здесь
) 99>/path/to/my.lock

Фактически, flock -n может использоваться вместо lckdo*, так что вы будете использовать код от разработчиков ядра.

Основываясь на примере пользователя womble, вы могли бы написать что-то вроде:

* * * * * flock -n /some/lockfile command_to_run_every_minute

Кстати, глядя на код, все flock, lockrun и lckdo делают одно и то же, поэтому это просто вопрос, что вам доступнее.

Теперь, когда systemd существует, есть еще один механизм планирования на системах Linux:

systemd.timer

В /etc/systemd/system/myjob.service или ~/.config/systemd/user/myjob.service:

[Service]
ExecStart=/usr/local/bin/myjob

В /etc/systemd/system/myjob.timer или ~/.config/systemd/user/myjob.timer:

[Timer]
OnCalendar=minutely

[Install]
WantedBy=timers.target

Если служебная единица уже активируется, когда таймер активируется в следующий раз, тогда другой экземпляр службы не будет запущен.

Альтернатива, которая запускает задание один раз при загрузке и одну минуту после завершения каждой итерации:

[Timer]
OnBootSec=1m
OnUnitInactiveSec=1m 

[Install]
WantedBy=timers.target

Вы не указали, хотите ли вы, чтобы скрипт ждал завершения предыдущей итерации. По “я не хочу, чтобы задания “накладывались” друг на друга” я предполагаю, что вы подразумеваете, что хотите, чтобы скрипт завершался, если уже выполняется,

Так что, если вы не хотите зависеть от lckdo или подобных, вы можете сделать следующее:


PIDFILE=/tmp/`basename $0`.pid

if [ -f $PIDFILE ]; then
  if ps -p `cat $PIDFILE` > /dev/null 2>&1; then
      echo "$0 уже выполняется!"
      exit
  fi
fi
echo $$ > $PIDFILE

trap 'rm -f "$PIDFILE" >/dev/null 2>&1' EXIT HUP KILL INT QUIT TERM

# выполняйте работу

Я бы рекомендовал использовать команду run-one – она гораздо проще, чем работа с блокировками. Из документации:

run-one – это скрипт-оболочка, который запускает не более одной уникальной копии некоторой команды с уникальным набором аргументов. Это часто полезно с cron-заданиями, когда вы хотите, чтобы не более одной копии выполнялось в одно и то же время.

run-this-one работает так же, как run-one, за исключением того, что он будет использовать pgrep и kill для поиска и завершения всех выполняющихся процессов, принадлежащих пользователю и соответствующих целевым командам и аргументам. Обратите внимание, что run-this-one будет блокировать выполнение при попытке завершить соответствующие процессы, пока все соответствующие процессы не будут завершены.

run-one-constantly работает точно так же, как run-one, за исключением того, что он
воспроизводит “COMMAND [ARGS]” каждый раз, когда COMMAND завершает выполнение (независимо от успешного или неуспешного результата).

keep-one-running является псевдонимом для run-one-constantly.

run-one-until-success работает точно так же, как run-one-constantly, за исключением
того, что он воспроизводит “COMMAND [ARGS]” до тех пор, пока COMMAND не завершится успешно (т.е.
завершается с нулевым кодом).

run-one-until-failure работает точно так же, как run-one-constantly, за исключением
того, что он воспроизводит “COMMAND [ARGS]” до тех пор, пока COMMAND не завершится с ошибкой (т.е.
завершается с ненулевым кодом).

Вы можете использовать файл-блокировку. Создайте этот файл при запуске скрипта и удалите его при завершении. Скрипт перед своим основным циклом должен проверять, существует ли файл-блокировка, и действовать соответственно.

Файлы-блокировки используются инискриптами и многими другими приложениями и утилитами в Unix-системах.

Это также может быть признаком того, что вы делаете что-то неправильно. Если ваши задания выполняются так часто и так часто, возможно, вам следует рассмотреть возможность отказаться от cron и сделать это в виде программы в стиле демона.

Ваша демона cron не должна запускать задания, если предыдущие экземпляры все еще выполняются. Я являюсь разработчиком одного из демонов cron dcron, и мы стараемся этого избежать. Я не знаю, как Vixie cron или другие демоны это делают.

Я создал один jar для решения такой проблемы, как выполнение дублирующихся cron-заданий, которые могут быть в Java или shell cron. Просто передайте имя cron в Duplicates.CloseSessions(“Demo.jar”), и это будет искать и завершать существующий PID для этого cron, кроме текущего. Я реализовал метод для выполнения этой задачи. Строка proname=ManagementFactory.getRuntimeMXBean().getName();
String pid=proname.split(“@”)[0];
System.out.println(“Current PID:”+pid);

            Process proc = Runtime.getRuntime().exec(new String[]{"bash","-c"," ps aux | grep "+cronname+" | awk '{print $2}' "});

            BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            String s = null;
            String killid="";

            while ((s = stdInput.readLine()) != null ) {                                        
                if(s.equals(pid)==false)
                {
                    killid=killid+s+" ";    
                }
            }

А затем завершите killid строкой с помощью команды оболочки

Ответ @Philip Reynolds начнет выполнение кода после истечения времени ожидания 5 секунд в любом случае без получения блокировки.
Следуя Flock doesn’t seem to be working, я изменил ответ @Philip Reynolds на

(
  flock -w 5 -x 99 || exit 1
  ## Выполняйте ваши действия здесь
) 99>/path/to/my.lock

так, чтобы код никогда не выполнялся одновременно.
Вместо этого после ожидания 5 секунд процесс завершится с кодом 1, если к тому моменту он не получит блокировку.

Если кто-то ищет валидацию, что выполнение двух скриптов с flock с одним и тем же файлом блокировки будет работать, как ожидалось.

Я это проверил, запустив два скрипта с тем же файлом блокировки с flock. Вот пример.

https://gist.github.com/msankhala/5e3923d7481cd13a5dc5fd8e5e0a132a

Ответ или решение

В мире информационных технологий часто возникает задача предотвращения параллельного выполнения задач cron. Это особенно актуально, когда скрипт, запланированный на выполнение каждую минуту, может выполняться дольше этого периода, вызывая накопление задач. Давайте более детально рассмотрим методики решения такой проблемы, прежде чем высказать рекомендации.

Теория

Проблема параллельного запуска cron-задач возникает, когда предыдущая задача не завершила выполнение до начала следующей. Это может привести к нежелательным последствиям, например, к избыточной загрузке системы или нарушениям в работе приложений. Важно обеспечить взаимное исключение выполнения задачи. Основная цель — не допустить одновременного выполнения нескольких экземпляров одного и того же скрипта.

Пример

На начальном этапе можно использовать простое создание файла блокировки (lockfile), где скрипт перед началом своей работы проверяет наличие такого файла: если файл существует, скрипт завершает работу; если его нет, скрипт создает файл и далее выполняет основные операции, после чего удаляет файл. Однако данный метод может быть уязвим для возникновения "зависших" файлов блокировки, если, например, скрипт завершится аварийно.

Применение

Существует несколько более надежных решений:

  1. Использование flock:
    Этот инструмент использует файловые блокировки, что делает его более надежным, чем ручное создание файла. Формат команды в cron для использования flock следующий:

    * * * * * /usr/bin/flock -n /tmp/fcj.lockfile /usr/local/bin/frequent_cron_job

    Основное преимущество flock — возможность блокировки на уровне ядра, что уменьшает риск конкуренции между процессами.

  2. Использование systemd timer:
    Современные Linux-системы поддерживают systemd, который предлагает систему таймеров вместо традиционного cron. Пример конфигурации:

    • myjob.service:
      [Service]
      ExecStart=/usr/local/bin/myjob
    • myjob.timer:
      [Timer]
      OnCalendar=minutely
      [Install]
      WantedBy=timers.target

      При использовании systemd.timer, если задача уже запущена, повторно она не будет стартовать.

  3. Средства вроде run-one:
    Этот инструмент обеспечивает запуск только одного экземпляра команды с определенными параметрами. Это полезно, если нужно точно контролировать запуск задач:

    run-one /path/to/your/script

Заключение

Каждый из упомянутых подходов обладает своими достоинствами и может быть выбран в зависимости от конкретных требований и ограничений. Использование flock рекомендовано как баланс между лёгкостью внедрения и надёжностью. Systemd.timer — предпочтительный выбор, если вы работаете в современном дистрибутиве Linux и можете отойти от классического cron. Средства как run-one добавляют дополнительный функционал, полезный для проведения тестирования и контроля уникальности выполнения задач.

В любом случае, выбор инструмента должен быть продиктован конкретными условиями эксплуатации и требованиями к выполнению задач. Построение надежной системы управления задачами — ключевой элемент в поддержании работоспособности и стабильной производительности сред с высокой нагрузкой.

Оцените материал
Добавить комментарий

Капча загружается...