Дата последнего изменения: 23.09.2021
Веб-проект на PHP, добавляются фичи, клиенты довольны. Но нагрузка постоянно растет. В один прекрасный день начинают появляться загадочные ошибки, которые программисты не знают как исправить. "Ломается" серверный софт, например связка Apache-PHP - а клиент получает в ответ на запрос страницу о регламентных работах.
Достаточно часто встречаются проекты, которые сталкиваются с подобным классом "ошибок" серверного софта, и в команде не всегда знают, что делать. В логе Apache часто появляются сообщения о нарушении сегментации (segmentation fault). Клиенты получают страницу об ошибке, а веб-разработчик с сисадмином ломают себе голову, играются с разными версиями PHP/Apache/прекомпилятора, собирают PHP из исходников с разными опциями снова и снова. А это баги не PHP, а их кода.
[Mon Oct 01 12:32:09 2012] [notice] child pid 27120 exit signal Segmentation fault (11)
В данном случае бесполезно искать подробную информацию в логе ошибок PHP - ведь "упал" сам процесс, а не скрипт. Если заранее не сделать на NGINX симпатичную страничку о регламентных работах, то клиенты увидят аскетичную ошибку 50*.
Вспомним теорию. Что такое signal? Это средство, которое операционная система использует, чтобы сказать процессу, что он не прав. Например, берет и, нарушая законы математики, делит на 0, или насильственными действиями вызывает переполнение стека. В данном случае мы видим сигнал с номером 11 и названием SIGSEGV. Список сигналов можно посмотреть, выполнив kill -l
.
Теперь найдем причину, за что же убили процесс PHP? Для этого нужно настроить создание дампа памяти процесса в момент "убийства" или coredump. Как только в следующий раз процесс будет убит операционной системой, ядром будет создан файл. Место размещение и название файла можно настроить. Если вы в консоли, просто наберите man 5 core
.
Например, можно складывать файлы в папочку так:
echo "/tmp/httpd-core.%p" > /proc/sys/kernel/core_pattern
Однако, скорее всего, в вашей системе отключена генерация coredump-файлов. Ее можно включить, вставив в начало скрипта запуска веб-сервера строку:
ulimit -с unlimited
Или, чтобы сделать настройку постоянной, отредактировать файлик /etc/security/limits.conf
. Туда можно вставить:
apache - core -1
Необходимо также для Apache настроить папку для coredump-файлов (/etc/httpd/conf/httpd.conf
):
CoreDumpDirectory /tmp
Перезапустите Apache:
service httpd restart
Тестируем и вручную завершаем процесс:
ps aux | grep httpd … kill -11 12345
Проверка: в файле /var/log/httpd/error_log
должно быть что-то вроде такого:
[Mon Oct 01 16:12:08 2012] [notice] child pid 22596 exit signal Segmentation fault (11), possible coredump in /tmp
В /tmp
теперь видим файл с названием типа /tmp/httpd-core.22596
. Вы научились получать дамп памяти завершившегося процесса. Теперь ждем, когда процесс будет завершён естественным образом.
--enable-debug, -g
для gcc при компиляции), то потеряется много полезной информации. Даже если PHP собран из исходников без этой опции, но исходники лежат рядом, этого может хватить для анализа.Открыть coredump можно утилитой gdb. Обычно открывают coredump так:
gdb путь_к_выполняемому_файлу_веб-сервера путь_к_coredump
Разобраться, как работает отладчик, не займет много времени. Можно за пару часиков поглотить один из самых занимательных учебников, а можно попросить это сделать сисадмина. Все уважающие себя разработчики на C в unix умеют пользоваться этим отладчиком. Но, к сожалению, их может не быть в вашей команде. И есть еще одно неприятное "НО".
Компилированный в байткод скрипт PHP это не совсем программа на C. Нужно, правда совсем немного, разобраться во внутренностях движка Zend. А именно - нужно найти в трейсе последний вызов функции execute, перейти в этот frame стека и исследовать локальные переменные (op_array
), а также заглянуть в глобальные переменные движка Zend:
(gdb) frame 3 #3 0x080f1cc4 in execute (op_array=0x816c670) at ./zend_execute.c:1605 (gdb) print (char *)(executor_globals.function_state_ptr->function)->common.function_name $14 = 0x80fa6fa "pg_result_error" (gdb) print (char *)executor_globals.active_op_array->function_name $15 = 0x816cfc4 "result_error" (gdb) print (char *)executor_globals.active_op_array->filename $16 = 0x816afbc "/home/yohgaki/php/DEV/segfault.php"
В op_array
можно запутаться, поэтому полезна команда просмотра типа этой структуры:
(gdb) ptype op_array type = struct _zend_op_array { zend_uchar type; char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsigned char return_reference; zend_uint *refcount; zend_op *opcodes; zend_uint last; zend_uint size; zend_compiled_variable *vars; int last_var; int size_var; zend_uint T; zend_brk_cont_element *brk_cont_array; zend_uint last_brk_cont; zend_uint current_brk_cont; zend_try_catch_element *try_catch_array; int last_try_catch; HashTable *static_variables; zend_op *start_op; int backpatch_count; zend_bool done_pass_two; zend_bool uses_this; char *filename; zend_uint line_start; zend_uint line_end; char *doc_comment; zend_uint doc_comment_len; void *reserved[4]; } *
Процесс отладки заключается в хождении между фреймами стека (frame N
), переходе в каждый вызов функции execute и исследовании ее локальных аргументов (print name
, ptype name
). Чем меньше номер фрейма, тем вы глубже. Иногда полезно зайти в гости в экстеншн PHP и посмотреть, где произошла ошибка и почему (хотя бы попытаться понять причину).
(gdb) frame #номер# (gdb) print op_array.function_name $1 = 0x2aaab7ca0c10 "myFunction" (gdb) print op_array.filename $2 = 0x2aaab7ca0c20 "/var/www/file.php"
Если разбираться во внутренностях движка Zend особого времени нет, то просто запомните, что переходя между фреймами стека вызовов с помощью команды frame #N#
, нужно смотреть только определенные элементы этой структуры, и вы точно сможете установить в каком файле PHP была вызвана функция PHP, какую функцию она вызвала и т.п. Так можно добираться до причины Segmentation Fault или другой ошибки, "убившей" процесс. И объясните программистам в чем причина, и они ее поправят.
Ошибки можно свести в группы:
pcre
, входит в рекурсию и вызывает себя несколько тысяч раз. Можно либо настроить параметры библиотеки или добавить процессу побольше стека (/etc/init.d/httpd
):
ulimit -s «ставим значение больше»А текущее значение можно посмотреть командой:
ulimit -aСправка по команде
man ulimit
, далее ищем ulimit. Если вы не можете получить coredump, то можно подключиться к запущенному процессу и изучить его. Пока вы внутри процесса, его выполнение приостанавливается (ps aux | grep apache | grep 'T '
, Он будет в состоянии трейсинга.). Когда покинете его, он снова продолжит выполняться. Подключиться можно так:
gdb -p ид_процесса
Составим чеклист для менеджера для борьбы с загадочными серверными ошибками, в которых не могут разобраться ни веб-разработчики, ни сисадмины: