Часто бывает так, что приходишь на машину и обнаруживаешь какой-то скрипт, запущенный под системным пользователем неделю назад. Кто его запустил? Где искать этот run.php? Или добавляешь запись в /etc/crontab, а скрипт там падает с ошибкой «command not found». Почему? И что делать? 

У меня есть ответы на эти вопросы.



Переменные окружения


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

Среди прочих есть переменная PATH, которая указывает пути для поиска исполняемых файлов, переменная HOME, которая указывает на домашнюю директорию пользователя, переменные, отвечающие за языковые предпочтения пользователя, и многие другие. 

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

Кто запустил процесс?


Итак, мы обнаружили скрипт, запущенный под системным пользователем неделю назад. Кто его запустил? Зачем? Может, про него просто забыли? Запустить его потенциально могли человек 10–15, всех не опросишь. Как найти, кто же это был? И где лежит этот run.php?

$ ps x  | grep run.php
10684 ?    	Ss   472:25 /local/php/bin/php run.php 

На помощь приходят переменные окружения процесса и особенность sudo. Есть такая переменная PWD, в которой оболочка хранит текущую рабочую директорию; это значение, по сути, сохраняет информацию о текущей директории в момент запуска команды. Также утилита sudo по умолчанию оставляет в переменной окружения процесса информацию о том, из-под какого пользователя была запущена она сама. 

Переменные окружения (и многое другое) для любого запущенного процесса можно посмотреть в /proc. Вуаля:

$ cat /proc/10684/environ | tr '\0' '\n' | grep SUDO_USER
SUDO_USER=alexxz
$ cat /proc/10684/environ | tr '\0' '\n' | grep PWD
PWD=/home/etlmaster

Кхм, сам и запустил. Ну с кем не бывает?.. 

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

Скрипт работает из командной строки, но не работает из cron


Одним из случаев, когда приходится вспоминать о переменных окружения, является ситуация, когда добавленный в /etc/crontab скрипт падает с ошибкой. Заходишь на сервер по SSH, запускаешь команду, всё вроде работает как надо. А при автоматическом запуске показывает что-то типа «hive: command not found». 

Вообще есть хорошая практика прописывать полный путь до исполняемых команд, однако это не всегда возможно. В таких случаях разработчики выкручиваются кто как может. Кто-то добавляет нужный путь в PATH частью команды в кронтабе. Более опытные оборачивают свою команду в bash -l. А наученные горьким опытом крон-бомбы ещё и flock довернуть не забывают. Всё так: сделал, добавил в мониторинг и забыл.

После таких манипуляций в душе настоящего инженера остаётся некий осадочек. Да, задача решена. Но я же ни фига не понял, что происходит! Чем один подход лучше другого? Где все эти настройки хранятся и кем меняются?

Давайте сравним переменные окружения, которые есть у процесса, когда он запускается из крона, и переменные окружения, которые есть у нас в командной строке. Логируем вывод команды env из крона и своё текущее окружение:

$ echo "* * * * * env > ~/crontab.env" | crontab; sleep 60; echo "" | crontab;
$ env > my.env

Смотрим, что там в переменной PATH:

> grep ^PATH= crontab.env my.env
Crontab.env:
PATH=/usr/bin:/bin
My.env:
PATH=/local/hive/bin:/local/python/bin:/local/hadoop/bin:/local/hadoop/bin:/local/hive/bin:/local/hadoop/bin:/usr/local/bin:/usr/bin:/bin



Мама мия! Так там под кроном только самый минимум! Конечно же, надо подгружать нормальные переменные окружения. 

Давайте посмотрим, какое окружение будет, если добавить bash -l:

$ echo "* * * * * bash -l env > ~/crontab.env" | crontab; sleep 60; echo "" | crontab;

alexxz@bi1.mlan:~> grep ^PATH= crontab.env my.env
Crontab.env:
PATH=/local/hive/bin:/local/python/bin:/local/hadoop/bin:/local/hadoop/bin:/local/hadoop/bin:/local/hive/bin:/usr/local/bin:/usr/bin:/bin
My.env:
PATH=/local/hive/bin:/local/python/bin:/local/hadoop/bin:/local/hadoop/bin:/local/hive/bin:/local/hadoop/bin:/usr/local/bin:/usr/bin:/bin

Разница уже не так заметна. Все пути представлены. Некоторые в другом порядке, некоторые повторяются, но это уже намного лучше, чем было. Остальные переменные тоже неплохо настроены. Есть, конечно, небольшая разница в локали, в переменных от SSH, но это уже не должно драматично влиять на работу скрипта. 

Теперь понятно, почему bash -l просто необходим в crontab-записях. И, конечно же, не забываем про flock.

Отлаживаем инициализацию логин-скриптов


Проблема вроде бы решена, всё из крона работает. Но как же получается, что некоторые пути дублируются в переменной PATH? Значит, есть какой-то беспорядок в настройке сервера. Давайте попробуем разобраться. 

Открываем какой-нибудь ман по инициализации окружения, вычитываем, какие скрипты и в каком порядке выполняются, с воодушевлением начинаем пробегать их глазами — и через несколько минут приходит чувство отчаяния. Какой-то бесконечный поток условий про какие-то особые случаи архитектур, терминалов и невероятно важных настроек цветов для команды ls. Боль, отчаяние, ненависть! Нас интересует одна чёртова переменная PATH!

На самом деле всё несколько проще. Знакомьтесь:

env -i bash -x -l -c 'echo 123' > login.log 2>&1

Что делает эта команда? Создаёт новый процесс bash с девственно чистым окружением, указывает, что надо запустить скрипты инициализации и всё подробно залогировать в файле login.log. Теперь у нас есть возможность не выполнять в уме все скрипты, а просто прочитать, что, где и когда выполнилось и откуда появилась та или иная настройка окружения.

Я не буду детально разбирать, как читать получившийся лог. Там всё почти тривиально. Упомяну лишь, что одно попадание у меня оказалось из /etc/profile и два — из /etc/bash.bashrc. Да, где-то перемудрили при настройке пакетов в паппете. Ну ничего, мне работать это не мешает.

Зато теперь я знаю и умею!

P. S. В совсем сложных случаях и чтобы разобраться вообще во всём, можно обернуть команду в strace:

 strace -f env -i bash -x -l -c 'echo 123' > login.log 2>&1

Комментарии (17)


  1. youROCK
    27.08.2019 15:36
    +5

    Ох уж эти админы, никак свой паппет настроить не могут!


    1. mistiman
      27.08.2019 19:56

      Админы тут не при чем. Максимум "devops" стажер.
      Раньше любой, кто написал php скрипт и захостил его в денвере на локалхосте, считал себя программистом. Теперь смог подключиться по ssh и все, ты devops.


  1. dmitryrf
    27.08.2019 17:48
    +2

    Некоторые переменные окружения меняются от сессии к сессии. Захотел я себе сделать автомонтирование сетевых (gvfs) дисков. Вроде, всё просто: gio mount smb://something, но… чтобы это заработало нужна установленная переменная DBUS_SESSION_BUS_ADDRESS. Выцепить её оказалось не совсем просто:

    MATE_PID=$(pgrep mate-session -u $USER)
    DBUS_ADDR=$(cat /proc/$MATE_PID/environ |grep -z "^DBUS_SESSION_BUS_ADDRESS=")
    

    А симптомы были те же — всё работает из консоли, но совсем не работает из крона.


    1. alexxz Автор
      27.08.2019 19:11

      Самое время узнать про существование -z флага у grep. Спасибо!


      1. youROCK
        27.08.2019 21:13
        +1

        В прошлый раз, когда мы использовали этот флаг в сочетании с -E (с -P оно даже в тот момент не работало), оно сломалось после обновления grep и после этого у нас nginx перестал отдавать большие ответы от php-fpm, если ты помнишь :).


      1. bolk
        27.08.2019 21:29
        +1

        А ещё самое время узнать о существовании флага -F (или команды fgrep) и что большинство команд командной строки умеют сами из файла читать. Строка из вашей статьи должна быть переписана так (такой поиск будет быстрее, у вас огромный лог и в нём не будут случайно срабатывать спецсимволы регулярок):

        tr '\0' '\n' < /proc/10684/environ | fgrep SUDO_USER

        Ну или как вам уже подсказали, так:

        grep -Fz SUDO_USER /proc/10684/environ

        И ещё какая-то странная идея экранировать всё подряд, да ещё и двойными кавычками. Если уж использовать кавычки «на всякий случай», надо использовать одинарные, в них интерполяции нет. Строка из комментария выше:

        DBUS_ADDR=$(grep -z ^DBUS_SESSION_BUS_ADDRESS= /proc/$MATE_PID/environ)

        «Бесполезная кошка» (useless cat) — антипаттерн в шеллах, не надо его использовать.


        1. nike38rus
          28.08.2019 11:02

          Можно пойти дальше, вместо

          cat /proc/self/environ| tr '\0' '\n' | grep 'SOMETHING'

          использовать
          strings -a /proc/self/environ | grep 'SOMETHING'


        1. dmitryrf
          28.08.2019 11:30

          cat действительно лишний, спасибо


        1. Goron_Dekar
          28.08.2019 12:01

          А что в бесполезной кошке плохо?
          Используя fgrep и чтение из файла мы делаем совсем не по unix-way, и одна программа делает не одно действие. Для написания шелл-скриптов убирать кошек верно, это и оптимизация, и выразительность. Но при работе из командной строки смысла в этом не вижу.


          1. Tangeman
            28.08.2019 15:25
            +2

            Unix-way — это не «одно действие», это «делать что-то одно, но хорошо», т.е. «одна функция», а количество действий необходимых для этого «одного» может быть разным. Если уж на то пошло, то cat выполняет как минимум два действия — читает и пишет, fgrep — читает и ищет, а если вы посчитаете количество «действий» в самом шелле при выполнении одной команды (а особенно если их несколько, связанных потоками)… вам должно стать страшно.

            Возвращаясь к нашим баранам — (f)grep ищёт что-то в файле/потоке — и неважно откуда этот поток берется — из cat или напрямую из файла, к тому же, без операции чтения тут не обойтись в принципе. Если программа в состоянии читать файл напрямую — ничего плохого в этом нет, даже наоборот, и она всё ещё выполняет только одну функцию — поиск.

            При работе из командной строки это как минимум печатать лишние символы, а вообще (к вопросу о том что плохо в бесполезной кошке) — как это ни удивительно, но это просто бесполезное дополнительное действие и неразумная трата системных ресурсов — создается дополнительный процесс, под него выделяется память, файловые дескрипторы etc — куча дополнительных накладных расходов. Да, на почти любой современной системе это практически незаметно, особенно если не выполняется 1000 раз в секунду — но — зачем?

            И наконец… unix-way — это не догма, не закон и даже не правило, и совсем не отменяет здравого смысла, далеко не всегда имеет смысл сохранять философию «одна программа — одна функция», по соображениям эффективности, целостности и много ещё каким, но это тема для целой статьи…

            Просто если буквально следовать этому, то вместо опций, модифицирующих поведение программ (иногда очень существенно) у нас будет одна программа на каждый вариант поведения/обработки, и придётся их комбинировать для достижения одной функции (причём не факт что позволит достичь результата за один проход) — это разве разумно?


            1. youROCK
              30.08.2019 20:31

              С другой стороны, авторы find, ИМХО, все-таки зашли слишком далеко, особенно с опциями вроде -delete :)


  1. Tangeman
    28.08.2019 00:44

    ls -l /proc/$pid/{exe,cwd} покажут сразу и «настоящий» экзешник и текущий cwd (который часто тот который был в момент запуска, хотя и не всегда), без шаманства с переменными среды.

    К тому же, переменные среды могут быть не совсем верными, а ps покажет то что захочет сам процесс.


  1. Envek
    28.08.2019 14:49
    +1

    bash -l кроме того, что часто чинит env-переменные (отчего в неё любят заворачивать cron-задачи), может и ломать. Лет 5 назад, когда RHEL 7 только появился, а контейнеризация ещё не была так популярна, один заказчик выдал нам пачку виртуалок на RHEL 7 с какими-то приблудами безопасности, на которые вы водрузили разрабатываемый нами веб-портал с помощью паппетов, кривых рук и такой-то матери. В том числе в комплекте была cron-задача, запускавшаяся каждую минуту. Каждый примерно месяц (с поразительной периодичностью) виртуальные машины зависали намертво. Оказывается, что-то там (память, к сожалению, не сохранила, что именно), реагировало на каждый логин в систему и на 65536-м логине вешало всё к чёрту. Убирание bash -l обёртки из crontab'а решало проблему (точнее, делало её крайне редкой). Такая вот прохладная история.


  1. pansa
    29.08.2019 02:15
    -1

    Уже откройте для себя systemd.timers. Там еще 100500 удобных вещей, которые можно делать без баш-портянок. И такой проблемы, как "я запустил руками — работает, а по крону — не работает" не стоит впринципе.


    1. alexxz Автор
      29.08.2019 10:52

      Как же это «не стоит впринципе»? Вот ради эксперимента попробовал на том же сервере, где собирал примеры.
      alexxz@bi1.mlan:~> sudo systemd-run --on-active=1 /bin/sh -c 'env > /tmp/foo'
      Running timer as unit run-r5f95073d0d5a4874832429b0b4168aa5.timer.
      Will run service as unit run-r5f95073d0d5a4874832429b0b4168aa5.service.
      alexxz@bi1.mlan:~> cat /tmp/foo | tr '\0' '\n'
      PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      PWD=/
      SHLVL=1
      LC_CTYPE=en_US.UTF-8
      _=/usr/bin/env

      Я вижу всё такое же неполное окружение, как из крона. Лишь только самую малость получше.


      1. pansa
        29.08.2019 13:38

        Вы немного не поняли. С systemd.timer вы настраиваете юнит, прописывая всё необходимое окружение.
        После этого не имеет значение как и кем этот юнит будет запущен — вами ( systemctl start ) или планировщиком — всё будет выполнено в фиксированном, одинаковом окружении.


        Более того, с кроном есть еще одна очень гадкая проблема, которая обнаружется потом, когда вы будете думать, что всё работает: например, скрипт в кроне делает какие-либо операции, создавая для себя каталоги/файлы. Вы дёрнули этот скрипт из-под другого юзера, а хуже — из-под рута, скрипт создал каталоги с соотв. владельцем. Всё работает корректно, вы радостно идёте домой. Вот только при следующем запуске ваш скрипт обломает зубки, тк прав доступа ему уже не хватит.
        И этой пробемы снова нет с systemd.timer'ами. И там еще куча полезного.


        Но двайте я подслащу хейтерам системд: есть там недостаток, довольно непрятный. Без костылей там нет нормальных почтовых уведомлений о проблемах с запуском юнита, как это есть в cron. Увы и ах. Однако, при всей моей любви к крону, я вижу кучу профита у timer'ов и выбираю их.


      1. youROCK
        30.08.2019 20:43

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