company_banner

Как дебажить переменные окружения в Linux

    Часто бывает так, что приходишь на машину и обнаруживаешь какой-то скрипт, запущенный под системным пользователем неделю назад. Кто его запустил? Где искать этот 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
    
    Badoo
    457,01
    Big Dating
    Поделиться публикацией

    Комментарии 22

      +12

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

        –5

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

        +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=")
        

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

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

            +3

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

              +4
              А ещё самое время узнать о существовании флага -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) — антипаттерн в шеллах, не надо его использовать.
                0
                Можно пойти дальше, вместо
                cat /proc/self/environ| tr '\0' '\n' | grep 'SOMETHING'

                использовать
                strings -a /proc/self/environ | grep 'SOMETHING'
                  0
                  cat действительно лишний, спасибо
                    +1

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

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

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

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

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

                      Просто если буквально следовать этому, то вместо опций, модифицирующих поведение программ (иногда очень существенно) у нас будет одна программа на каждый вариант поведения/обработки, и придётся их комбинировать для достижения одной функции (причём не факт что позволит достичь результата за один проход) — это разве разумно?
                        +1
                        С другой стороны, авторы find, ИМХО, все-таки зашли слишком далеко, особенно с опциями вроде -delete :)
                          0

                          А с третьей, когда необходимо обработать или удалить парочку сотен тысяч файлов в конкретной директории, то только find и спасает, ибо он применяет -delete (-exec) в цикле по мере нахождения файлов, а все остальные команды (типа rm dir/*, some_command dir/*, etc) вначале пытаются распаковать список аргументов, на чем благополучно зависают.

                            0

                            Ну почему, xargs вроде бы тоже так работает — он накапливает буфер для того количества аргументов, которые вы передали, или что-то около 5000 по умолчанию.

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

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

                    +2

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

                      +1
                      поддерживаю, в crontab больше подходит:
                      SHELL=/bin/bash
                      BASH_ENV=$HOME/.bashrc
                      0

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

                        0
                        Как же это «не стоит впринципе»? Вот ради эксперимента попробовал на том же сервере, где собирал примеры.
                        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

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

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


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


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

                            +2
                            Вообще правильно Вам товарищ говорит, не нужно ничего из крона запускать, когда есть скриптовый фреймворк :). Правда проблему переменных окружения это, конечно же, не решает.
                          0
                          А мне нравится lsof, можно многое узнать о процессе, откуда он запущен, например. Или кто съел место (если какие-то процессы писали лог-файлы, а затем криво сротейтили и пишут в удаленные файлы):
                          lsof +aL1 /

                          (вместо / указать любой раздел, где испаряется место)

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое