Как дебажить переменные окружения в 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