Простая защита от двойного запуска заданий cron

    Хочу рассказать о простом скрипте, позволяющем защититься от двойного запуска заданий cron.

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

    Что будет в таком сценарии дальше — вопрос весьма интересный. Велика вероятность, что два процесса будут активно мешать друг другу (они ведь работают с одними и теми же объектами), и их общее время выполнения будет отнюдь не в два раза больше, чем обычно, а если и третий настигнет…

    Описанная ситуация банальна до неприличия, хотя чаще встречается вырожденный вариант: нет никакого крона, просто в полночьв момент истечения срока годности кеша каждый новый посетитель начинает толстый процесс и сервер превращается в тыкву.
    Как разработчик, вы обязаны предусмотреть такой сценарий и защититься от него. Но если Буратино был тупой разработчики вашего ПО не позаботились об этом, спасаться придется самостоятельно.

    Я использую простую и удобную утилиту lockrun. Принцип ее работы прост: для каждого процесса она создает файл и вешает на него lock. Как только процесс завершается, лок пропадает. Лок также пропадает в случае внезапной смерти процесса, и нет необходимости проверять pid на существование или делать другие телодвижения. Если процесс запускается повторно, а лок-файл еще не освободился, работа скрипта прерывается и выдается сообщение в STDERR.

    Утилита написана на C, так что перед использованием ее придется скомпилировать на целевой машине. Итак, качаем, компилируем и кладем куда надо:

    $ wget unixwiz.net/tools/lockrun.c
    $ gcc lockrun.c -o lockrun
    $ sudo cp lockrun /usr/local/bin/


    Если вы не root, последней строчкой придется пренебречь и либо указывать полный путь, либо изменить PATH.

    Пример использования:
    * * * * * /usr/local/bin/lockrun --lockfile=/tmp/megacache.lockrun -- /path/to/megacache/generator
    Собственно команда и параметры lockrun разделяются двумя минусами.

    Принимаются следующие параметры:
    --lockfile=/path/to/file
    Обязательный параметр, задающий имя файла для лока. Если такого файла нет, он создастся автоматически. Разумеется, для каждого задания должен быть свой файл.
    --maxtime=N
    Время в секундах, отводящееся скрипту на «нормальную» работу. Если скрипт работал дольше, в STDERR будет выведено сообщение, которое cron может отправить вам на почту.
    --wait
    Если этот параметр указан, lockrun не отменит выполнение скрипта, а будет ждать, пока предыдущий процесс освободит лок.
    --verbose
    Как всегда, выдача более подробной информации о ходе процесса.
    --quiet
    Не выдавать сообщений об ошибках. Можно включить, если отказ в запуске задания не является серьезной проблемой.

    Вот и все. Как видите, действительно просто и эффективно.

    UPD: В комментариях мне сообщили, что есть также родное средство для Linux.
    Разумеется, никто не утверждал, что описанная утилита — единственное решение.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 71

      +9
      man 1 flock
      функций даже побольше, есть в стандартных конфигурациях в линуксе
        –7
        Прекрасно, а дальше что? Самому еще раз написать эту утилиту? Она как раз флок и использует.
          +5
          flock(1) это и есть утилита.
            0
            И правда.
              0
              PHP вариант:

                0
                // Сохранение в $lock_file обязательно, иначе handle помрёт и файл автоматом закроется
                if(!flock($lock_file = fopen("my_script.lock", 'w'), LOCK_EX | LOCK_NB))
                  die("Already runninng\n");
                  0
                  А вот за это спасибо: замены для lockrun меня лично не особо интересуют, а для скриптов идея хороша. Возьму на заметку.
                    0
                    Огромное спасибо! Очень помогло!
            0
            Ого, а я как дурак для этого скрипт на шелле использовал, вот такой: code.reddit.com/browser/scripts/saferun.sh

            Я туда правда добавил еще проверку load average, чтобы некоторые скрипты могли подождать если в момент, когда крон их решил запустить уже и так нагрузка высокая. Но проверку на существование процесса надо будет на flock переделать на досуге.
            0
            man 2 flock — совсем не тоже самое, что man 1 flock.
            Второе — уже утилита.
              –2
              Нашел на линуксовом сервере. На freebsd найти не удалось, оно там есть?
                +1
                на сколько я знаю, нету.
                для BSD пойдёт :)
                  –2
                  Я тут над flock экспериментирую — никак не могу заставить его ругаться при выходе. Это возможно?
                    0
                    Он выставляет статус, можно на него ругаться вручную через $?
                      –2
                      То есть ему еще обертку писать?
                        0
                        Зависит от ситуации.
                        Часто бывает проще написать
                        $ flock -n /tmp/lock -c mycommand || echo Fail
                        чем что-то качать, собирать и ставить.
                          –1
                          Ну в общем да. Правда, у меня-таки фря на сервере.
                        –1
                        Если обертку, то получится, что flock больше все-таки для встраивания в шелл-скрипты, а lockrun для самостоятельного использования. Я прав?
                          0
                          Так можно сказать, но обычно приходится использовать то, что уже стоит на сервере.
                          Качают и ставят — когда совсем нечем заменить. или нечем заняться 8)
                            +2
                            Отвыкли в наше время качать и ставить-то! %)
                      • UFO just landed and posted this here
                        +1
                        На FreeBSD — lockf
                          0
                          О. А пример использования в описанном контексте не подкинете?
                            0
                            * * * * * /usr/local/bin/lockrun --lockfile=/tmp/megacache.lockrun — /path/to/megacache/generator

                            * * * * * lockf /tmp/megacache.lockrun /path/to/megacache/generator
                              0
                              [mixailo@DTG1127 ~]$ lockf pid.lock sleep 60 &
                              [1] 69177
                              [mixailo@DTG1127 ~]$ lockf pid.lock sleep 60 &
                              [2] 69352

                              Не то что-то.
                                +1
                                Почитайте man. Там написано, что по умолчанию, lockf будет бесконечно ждать освобождения лок-файла.

                                У вас в примере вторая команда ждёт выполнения первой для начала своей работы.
                                А если поставит нулевое время ожидания:

                                [root@bsd ~]# lockf -t 0 pid.lock sleep 60 &
                                [1] 65999
                                [root@bsd ~]# lockf -t 0 pid.lock sleep 60
                                lockf: pid.lock: already locked
                                  0
                                  А, вот оно что.
                    –2
                    Вроде же в линуксе есть нормальные именованые мютексы, которые могут использоваться для этой задачи куда удобнее и эффективнее, чем файлы. Я бы понял, если бы решение было на PHP, но вы используете С (кстати, программы на С не зовутся скриптами) — почему не мютексы?
                      –3
                      Я не автор скрипта, поэтому не могу ответить вам на этот вопрос.
                        +1
                        Ну, файлы — это Unix way. Это даёт возможность для просмотра установленных блокировок использовать ls (dir в windows).
                          –1
                          В этом что-то есть, хотя родной линуксовый flock за собой файл не удаляет.
                            0
                            Я еще не слышал, чтобы Unix way-ем называли использованием неподходящих инструментов при наличии подходящих.
                              0
                              А чем он неподходящий? Кроме того, что мьютекс, условно говоря, находится в пространстве имён недоступном через файловый API.
                                –1
                                В Win32 мютекс от приложения, которое умерло не своей смертью, умирает вместе с ним (точнее, переходит в состояние abadonned) и в результате становятся не нужны экзорцизмы, связанные с удалением файла в случае некорректного завершения работы.
                                  0
                                  >не нужны экзорцизмы, связанные с удалением файла в случае некорректного завершения работы.

                                  они и в юниксах не нужны
                                    0
                                    Это да. Но если есть (я не смотрел) простой способ определить открыт ли кем-то файл, то это решает проблему.
                                      0
                                      Мне концепция Plan 9 покоя не даёт :)
                                        0
                                        Есть, для этого нужно всего лишь открыть файл эксклюзивно (FILE_SHARE_NONE, или аналоги). Только экзорцизмы с файлами мне все равно непонятны. Файл должен служить для того, для чего предназначен.
                              +1
                              Если вы не root, последней строчкой придется пренебречь и либо указывать полный путь, либо изменить PATH.

                              Очень часто в PATH добавлен $HOME/bin (по крайней мере в rhel-based)
                              Не менее часто этого каталога просто нет. Ничего не мешает его создать и использовать.
                                0
                                У меня по дефолту такое:
                                mixailo@db:~$ echo $PATH
                                /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
                                mixailo@db:~$ cat /etc/lsb-release
                                DISTRIB_ID=Ubuntu
                                DISTRIB_RELEASE=10.04
                                DISTRIB_CODENAME=lucid
                                DISTRIB_DESCRIPTION="Ubuntu 10.04.1 LTS"
                                0
                                Вот блин, лок-файл то она не чистит за собой.
                                  0
                                  flock тоже %)
                                    +1
                                    Это не так важно, важно что бы при запущеной программе лок на файл был экслюзивный.
                                    +1
                                    Менее надёжно, но более популярно:

                                    ln -s $$ lock-file || exit
                                    trap "rm -f lock-file" EXIT
                                    trap "trap - EXIT; rm -f lock-file" HUP INT QUIT


                                    Если произошёл коллапс и файл-таки остался, то вместо "|| exit" можно осуществлять проверку существования процесса.
                                      0
                                      Плюс в том, что если cleanup не отработал, то произошло что-то плохое и это дело лучше разгрести руками.
                                        0
                                        Вау. А что делает последняя строчка?
                                        Трап на попытку прибить скрипт ставит трап на EXIT и если получает exit то… вот дальше непонял.
                                          0
                                          Ай, бага ручного ввода, надо было копипастить :( в последней строчке должен быть exit в самом конце обработчика.

                                          Обработчик (на стандартные сигналы завершения, можно при желании пополнить список) сбрасывает хук на выход, освобождает файл и выходит (в этот момент хук на выход уже сброшен, так что файл 2й раз не удаляется).
                                          0
                                          apt-cache show lockfile-progs

                                          cron, между прочим, recommends.

                                          Пример использования в /etc/cron.daily/standard
                                            +2
                                            Спасибо за поднятую тему. Узнал про flock!
                                              –1
                                              Я тоже %) Только мне бестолку — у меня фряха на сервере.
                                                +1
                                                Port: flock-3.4_1
                                                Path: /usr/ports/sysutils/flock
                                                Info: Assert/wait for advisory locks with flock(2)
                                                Maint: ports@FreeBSD.org
                                                B-deps: mkcmd-8.14 msrc0-0.7
                                                  0
                                                  тогда вам man lockf :)
                                                    0
                                                    Упоминалось еще вчера (пруф).
                                                    Хотя я, в общем-то, и не претендовал на то, что описанное решение самое лучшее или единственное.
                                                      0
                                                      я видел. Просто не заметил, что там тоже были вы :)
                                                      Собственно, ничего страшного. Про хорошую вещь можно и повторить
                                                –4
                                                Линуксоиды не умеют мьютексы?
                                                  0
                                                  Я тут немного отрекламирую «замену крона»: http://search.cpan.org/~kohts/snaked-0.07/lib/snaked.pm
                                                  Кому-то может показаться, что это «изобретание велосипеда», но на самом деле — штука очень удобная и умеет справляться с описанной в топике задачей.
                                                  • UFO just landed and posted this here
                                                      0
                                                      Я давно для таких целей использую runit.

                                                      Просто run-скрипт задаю как
                                                      #!/bin/sh
                                                      exec 2>&1
                                                      sleep exec do periodic task
                                                        0
                                                        pidof -s -o '%PPID' -x $( basename $0 ) > /dev/null 2>&1 && exit
                                                          +3
                                                          А что не так с классическим
                                                          */5 * * * * pgrep -f my_command &>/dev/null || my_command
                                                          применительно к конкретной ситуации описанной в начале?
                                                            0
                                                            Строго говоря, race condition'ом — два pgrep'а проверяют, что команда не запущена, и оба синхронно весело запускают два экземпляра.
                                                            0
                                                            А чем плох mktemp? Проверяем не создан ли временный файл, если нет, создаем и запускаем скрипт. Файл прибивается автоматически при завершении скрипта.
                                                              0
                                                              Ничем, скорее всего, не плохо любое решение, к которому вы привыкли.
                                                              Я вот к lockrun привык.
                                                                0
                                                                >Чем плох?
                                                                дополнительными не дающими 100% гарантии костылями для обработки аварийного завершения скрипта?
                                                                0
                                                                По моему вполне хватает создания файла и его удаления после выполнения скрипта, так в большинстве скриптов реализовано в линуксе.
                                                                  0
                                                                  Выше уже писали, что внезапная смерть скрипта создает проблемы в виде неудаленного файла. Можно проверять пид на существование, но это не дает 100% гарантии.
                                                                    0
                                                                    Ну добавьте ещё проверку ps ax|grep имя скрипта на такой случай.
                                                                      0
                                                                      Во-во, пошли костыли.
                                                                        0
                                                                        Где вы увидели костыли? Это простые и надежные юниксвей методы.
                                                                          0
                                                                          Не хватает тега «сарказм».
                                                                  +1
                                                                  Есть еще lckdo — включен в стандартный дебиановский репозиторий.

                                                                  Only users with full accounts can post comments. Log in, please.