Правильная блокировка в скриптах оболочки?

66

Иногда необходимо убедиться, что одновременно запущен только один экземпляр сценария оболочки.

Например, задание cron, которое выполняется через crond, которое не обеспечивает самостоятельную блокировку (например, crond по умолчанию Solaris).

Распространенным шаблоном для реализации блокировки является такой код:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Конечно, у такого кода есть условие гонки. Существует временное окно, в котором выполнение двух экземпляров может продвинуться после строки 3, прежде чем один сможет прикоснуться к $LOCKфайлу.

Для задания cron это обычно не является проблемой, потому что у вас есть интервал минут между двумя вызовами.

Но что-то может пойти не так - например, когда файл блокировки находится на сервере NFS - это зависает. В этом случае несколько заданий cron могут блокироваться в строке 3 и помещаться в очередь. Если сервер NFS снова активен, то у вас есть огромное количество параллельно выполняемых заданий.

Выполняя поиск в Интернете, я обнаружил инструмент lockrun, который кажется хорошим решением этой проблемы. С его помощью вы запускаете скрипт, который нуждается в блокировке следующим образом:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

Вы можете положить это в обертку или использовать его из вашего crontab.

Он использует lockf()(POSIX), если доступно, и использует flock()(BSD). И lockf()поддержка по NFS должна быть относительно широко распространена.

Есть ли альтернативы lockrun?

А как насчет других демонов cron? Существуют ли распространенные crond, которые поддерживают блокировку в разумных пределах? Быстрый просмотр справочной страницы Vixie Crond (по умолчанию в системах Debian / Ubuntu) ничего не показывает о блокировке.

Было бы хорошей идеей включить такой инструмент, как lockrunв coreutils ?

На мой взгляд , он реализует тему , очень похожий на timeout, niceи друзей.

maxschlepzig
источник
4
Тангенциально, и для пользы других, которые могут считать ваш начальный паттерн «Достаточно хорошим» (tm), этот код оболочки, возможно, должен перехватывать TERM, чтобы удалить свой файл блокировки при редактировании kill; и кажется хорошей практикой хранить собственный файл в файле замка, а не просто прикасаться к нему.
Ульрих Шварц
@ Шон, не совсем, не упоминает crond и NFS.
maxschlepzig
связанный вопрос по SO: stackoverflow.com/questions/185451/…
maxschlepzig
1
@ Ulrich очень запоздало, хранение PID в файле блокировки NFS добавляет очень мало значения. Даже добавление имени хоста на самом деле не помогает при проверке процесса в реальном времени
roaima

Ответы:

45

Вот еще один способ сделать блокировку в сценарии оболочки, которая может предотвратить состояние гонки, которое вы описали выше, когда два задания могут оба пройти строку 3. noclobberОпция будет работать в ksh и bash. Не используйте, set noclobberпотому что вы не должны писать сценарии в csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV с блокировкой на NFS (вы знаете, когда NFS-серверы недоступны), но в целом он гораздо надежнее, чем раньше. (10 лет назад)

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

У меня нет опыта работы с lockrun, но может помочь предустановленная среда блокировки до запуска сценария. Или это не так. Вы просто устанавливаете тест для файла блокировки вне своего скрипта в обертке, и теоретически, вы не можете просто выполнить одно и то же состояние гонки, если две команды были вызваны lockrun в одно и то же время, точно так же, как и с «inside-» решение сценария?

В любом случае блокировка файлов в значительной степени учитывает поведение системы, и любые сценарии, которые не проверяют существование файла блокировки перед запуском, будут делать то, что они собираются делать. Просто введя тест файла блокировки и правильное поведение, вы решите 99% потенциальных проблем, если не 100%.

Если вы часто сталкиваетесь с условиями гонки в lockfile, это может быть индикатором более серьезной проблемы, например, неправильного определения времени ваших заданий или, возможно, если интервал не так важен, как завершение задания, возможно, ваша работа лучше подходит для того, чтобы ее демонизировать ,


РЕДАКТИРОВАТЬ НИЖЕ - 2016-05-06 (если вы используете KSH88)


Основываясь на комментарии @Clint Pachl ниже, если вы используете ksh88, используйте mkdirвместо noclobber. Это в основном смягчает потенциальное состояние гонки, но не ограничивает его полностью (хотя риск минимален). Для получения дополнительной информации прочитайте ссылку, которую Клинт разместил ниже .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

И, как дополнительное преимущество, если вам нужно создать tmpfiles в вашем скрипте, вы можете использовать lockdirкаталог для них, зная, что они будут очищены при выходе из скрипта.

Для более современного bash, метод noclobber наверху должен быть подходящим.

Тим Кеннеди
источник
1
Нет, с lockrun у вас нет проблем - когда сервер NFS зависает, все вызовы lockrun будут зависать (по крайней мере) в lockf()системном вызове - когда он резервируется, все процессы возобновляются, но только один процесс блокирует блокировку. Нет расы. Я не сталкиваюсь с такими проблемами с cronjobs много раз - дело обстоит наоборот - но это проблема, когда она бьет вас, она может причинить много боли.
maxschlepzig
1
Я принял этот ответ, потому что метод является безопасным и на сегодняшний день самым элегантным. Я предлагаю небольшой вариант: set -o noclobber && echo "$$" > "$lockfile"получить безопасный запасной вариант, когда оболочка не поддерживает опцию noclobber.
maxschlepzig
3
Хороший ответ, но вы также должны 'kill -0' значение в файле блокировки, чтобы убедиться, что процесс, который создал блокировку, все еще существует.
Найджел Хорн
1
noclobberВариант может быть склонным к условиям гонки. См. Mywiki.wooledge.org/BashFAQ/045, чтобы найти пищу для размышлений.
Клинт Пахл
2
Примечание: использование noclobber(или -C) в ksh88 не работает, потому что ksh88 не использует O_EXCLдля noclobber. Если вы работаете с более новой оболочкой, вы можете быть в порядке ...
jrw32982 поддерживает Monica
14

Я предпочитаю использовать жесткие ссылки.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

Жесткие ссылки атомарны по NFS, и по большей части, mkdir также . Использование mkdir(2)или link(2)примерно то же самое, на практическом уровне; Я просто предпочитаю использовать жесткие ссылки, потому что больше реализаций NFS допускают атомарные жесткие ссылки, чем атомарные mkdir. С современными выпусками NFS вам не нужно беспокоиться об их использовании.

Arcege
источник
12

Я понимаю, что mkdirэто атомное, так что, возможно,

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15
Гленн Джекман
источник
Хорошо, но я не смог найти информацию о том, стандартизировано ли для mkdir()NFS (> = 3) атомарное.
maxschlepzig
2
@maxschlepzig RFC 1813 явно не призывает mkdirк атомарности (это делает для rename). На практике известно, что некоторые реализации не являются. Связанный: интересная тема, в том числе вклад автора GNU Arch .
Жиль "ТАК - перестань быть злым"
8

Самый простой способ - использовать lockfileприходящий обычно с procmailпакетом.

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"
jofel
источник
5

semкоторый входит в состав parallelинструментов GNU , может быть тем, что вы ищете:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

Как в:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

Вывод:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Обратите внимание, что порядок не гарантируется. Также вывод не отображается, пока не закончится (раздражает!). Но даже в этом случае я знаю, что это самый лаконичный способ защиты от одновременного выполнения, не беспокоясь о файлах блокировки, повторных попытках и очистке.

Переменная облачность
источник
Срабатывает ли блокировка, предлагаемая semручкой, в середине исполнения?
Ройма
2

Я использую dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1
AndresVia
источник
1

Я использую инструмент командной строки «flock» для управления блокировками в моих скриптах bash, как описано здесь и здесь . Я использовал этот простой метод из справочной страницы flock, чтобы запустить некоторые команды в подоболочке ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

В этом примере происходит сбой с кодом выхода 1, если он не может получить файл блокировки. Но flock также можно использовать способами, которые не требуют запуска команд в под-оболочке :-)

dru8274
источник
3
flock()Системный вызов не работает через NFS .
maxschlepzig
1
У BSD есть похожий инструмент "lockf".
dubiousjim
2
@dubiousjim, BSD lockf также вызывает flock()и, таким образом, проблематично по NFS. Между тем, flock () в Linux теперь возвращается к тому моменту, fcntl()когда файл находится на монтировании NFS, таким образом, в среде NFS только для Linux flock()теперь работает поверх NFS.
maxschlepzig
1

Не используйте файл.

Если ваш скрипт выполняется так, например:

bash my_script

Вы можете определить, работает ли он, используя:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi
frogstarr78
источник
Хм, код проверки ps запускается изнутри my_script? В случае, если другой экземпляр работает - не running_procсодержит две совпадающие строки? Мне нравится идея, но, конечно, - вы получите ложные результаты, когда другой пользователь запускает скрипт с таким же именем ...
maxschlepzig
3
Это также включает условие гонки: если 2 экземпляра выполняют первую строку параллельно, то ни один из них не получает «блокировки» и оба выходят со статусом 6. Это было бы своего рода взаимное голодание в один раунд . Кстати, я не уверен, почему вы используете $!вместо $$своего примера.
maxschlepzig
@maxschlepzig действительно извините за неправильный $! против $$
frogstarr78
@maxschlepzig для обработки нескольких пользователей, выполняющих скрипт, добавьте euser = в аргумент -o.
frogstarr78
@maxschlepzig для предотвращения нескольких строк вы также можете изменить аргументы на grep или дополнительные «фильтры» (например grep -v $$). В основном я пытался предложить другой подход к проблеме.
frogstarr78
1

Для фактического использования, вы должны использовать верхний проголосовавший ответ .

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

Этот ответ действительно является ответом «Почему бы не использовать psи grepобрабатывать блокировку в оболочке?

Сломанный подход № 1

Во-первых, подход, приведенный в другом ответе, который имеет несколько положительных отзывов, несмотря на тот факт, что он не работает (и не мог) и явно никогда не проверялся:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Давайте исправим синтаксические ошибки и неверные psаргументы и получим:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Этот скрипт всегда будет выходить из 6 каждый раз, независимо от того, как вы его запустите.

Если вы запустите его ./myscript, то psбудет просто вывод 12345 -bash, который не соответствует требуемой строке 12345 bash ./myscript, поэтому произойдет сбой.

Если вы запустите его bash myscript, все станет интереснее. Процесс bash разветвляет запуск конвейера, а дочерняя оболочка запускает psи grep. И исходная оболочка, и дочерняя оболочка будут отображаться в psвыводе, что-то вроде этого:

25793 bash myscript
25795 bash myscript

Это не ожидаемый результат $$ bash $0, поэтому ваш скрипт завершится.

Неправильный подход № 2

Теперь, честно говоря, для пользователя, который написал неправильный подход № 1, я сам сделал нечто подобное, когда впервые попробовал это:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Это почти работает. Но сам факт разветвления, чтобы запустить трубу, отбрасывает это. Так что и этот всегда будет выходить.

Ненадежный подход № 3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

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

Это может сработать ... при условии, что ни у какого другого процесса нет командной строки, совпадающей с $0, и при условии, что скрипт всегда вызывается одинаково (например, если он вызывается с относительным путем, а затем с абсолютным путем, последний экземпляр не заметит первый ).

Ненадежный подход № 4

Так что, если мы пропустим проверку полной командной строки, так как это может не указывать на фактическую работу скрипта, и lsofвместо этого проверим, чтобы найти все процессы, у которых этот скрипт открыт?

Ну да, такой подход на самом деле не так уж и плох

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Конечно, если запущена копия скрипта, то новый экземпляр запустится просто отлично, и у вас будет две запущенные копии .

Или, если работающий скрипт изменен (например, с помощью Vim или с помощью a git checkout), тогда «новая» версия скрипта запустится без проблем, так как и Vim, и git checkoutрезультатом будет новый файл (новый inode) вместо Старый.

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

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

Но помните, не используйте этот подход, если скрипт может быть отредактирован или скопирован, так как вы получите ложные негативы, т.е. несколько экземпляров, запущенных одновременно - поэтому тот факт, что редактирование с помощью Vim не дает ложных срабатываний, не должен иметь значения тебе. Я упоминаю это, хотя, потому что подход # 3 делает ложные срабатывания (т.е. не запускается) , если у вас есть сценарий открытого с Vim.

Так что же делать тогда?

Топ проголосовали ответ на этот вопрос дает хороший твердый подход.

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

Wildcard
источник
0

С помощью инструмента FLOM (Free LOck Manager) сериализация команд становится такой же простой, как и запуск

flom -- command_to_serialize

FLOM позволяет вам реализовать более изощренные варианты использования (распределенная блокировка, считыватели / записи, числовые ресурсы и т. Д.), Как описано здесь: http://sourceforge.net/p/flom/wiki/FLOM%20by%20examples/

tiian
источник
0

Вот кое-что, что я иногда добавляю на сервер, чтобы легко обрабатывать условия гонки для любой работы на машине. Это похоже на пост Тима Кеннеди, но таким образом вы получаете обработку гонки, добавляя только одну строку в каждый скрипт bash, который в этом нуждается.

Поместите содержимое ниже, например, в / opt / racechecker / racechecker:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Вот как это использовать. Обратите внимание на строку после Шебанга:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

Он работает так, что вычисляет имя основного файла bashscript и создает pid-файл в «/ tmp». Это также добавляет слушателя к сигналу финиша. Слушатель удалит pid-файл, когда основной скрипт завершит работу.

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

Что делать, если скрипт вылетает

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

В случае сбоя системы

Хорошей идеей будет сохранить pidfile / lockfile, например, в / tmp. Таким образом, ваши сценарии будут обязательно выполняться после сбоя системы, поскольку pid-файлы всегда будут удаляться при загрузке.

ziggestardust
источник
В отличие от анзаца Тима Кеннеди, ваш сценарий содержит условие гонки. Это потому, что ваша проверка наличия PIDFILE и его условного создания не выполняется в элементарной операции.
maxschlepzig
+1 на это! Я приму это к сведению и изменю свой сценарий.
ziggestardust
-2

Проверьте мой сценарий ...

Вы можете любить это ....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$
user54178
источник
-3

Я предлагаю следующее решение в сценарии с именем «flocktest»

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
Ньютон Т Хаммет младший
источник