68  /  97

Стабилизируем PHP на бою

Просмотров: 13448
Дата последнего изменения: 23.09.2021
Сложность урока:
5 уровень - сложно, но не смертельно. Нужно подумать, вспоминать уже пройденный материал, собрать в кучу внимание, немного терпения и всё получится.
1
2
3
4
5

Введение

Веб-проект на 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

Примечание: Если ничего не задать, система создаст файл с именем core.#process_number# в рабочей директории процесса. Только проследите, чтобы процесс apache-PHP имел туда право записи.

Однако, скорее всего, в вашей системе отключена генерация coredump-файлов. Ее можно включить, вставив в начало скрипта запуска веб-сервера строку:

ulimit -с unlimited

Или, чтобы сделать настройку постоянной, отредактировать файлик /etc/security/limits.conf. Туда можно вставить:

apache - core -1

Примечание: Подробности по формату файла: man limits.conf.

Необходимо также для 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. Вы научились получать дамп памяти завершившегося процесса. Теперь ждем, когда процесс будет завершён естественным образом.

  Как толковать coredump?

Примечание: Ошибочно думать, что:
  • если PHP собрана без отладочных символов (ключик --enable-debug, -g для gcc при компиляции), то потеряется много полезной информации. Даже если PHP собран из исходников без этой опции, но исходники лежат рядом, этого может хватить для анализа.
  • отладочная сборка влияет на производительность и потребляемую процессом память (memory footprint). Не влияет, а лишь увеличивается размер исполняемого файла. Поэтому, если не сможете разобраться в причине ошибки без отладочной сборки - попросите сисадмина собрать модуль PHP с отладочными символами.

Открыть coredump можно утилитой gdb. Обычно открывают coredump так:

gdb путь_к_выполняемому_файлу_веб-сервера путь_к_coredump

Разобраться, как работает отладчик, не займет много времени. Можно за пару часиков поглотить один из самых занимательных учебников, а можно попросить это сделать сисадмина. Все уважающие себя разработчики на C в unix умеют пользоваться этим отладчиком. Но, к сожалению, их может не быть в вашей команде. И есть еще одно неприятное "НО".

  Отладка PHP в gdb

Компилированный в байткод скрипт 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 или другой ошибки, "убившей" процесс. И объясните программистам в чем причина, и они ее поправят.

  Частые причины ошибок

Ошибки можно свести в группы:

  1. Проблемы в расширениях PHP. В этом случае либо отключите расширение, либо попробуйте поиграть его настройками. Вы точно знаете, что проблема в нем, дело за малым.
  2. Проблема с рекурсией, стеком. Ошибки, при которых функция библиотеки, например, pcre, входит в рекурсию и вызывает себя несколько тысяч раз. Можно либо настроить параметры библиотеки или добавить процессу побольше стека (/etc/init.d/httpd):
    ulimit -s «ставим значение больше»
    А текущее значение можно посмотреть командой:
    ulimit -a
    Справка по команде man ulimit, далее ищем ulimit.
  3. Проблемы в ядре PHP - сообщите разработчикам PHP.

  Отладка запущенного процесса

Если вы не можете получить coredump, то можно подключиться к запущенному процессу и изучить его. Пока вы внутри процесса, его выполнение приостанавливается (ps aux | grep apache | grep 'T ', Он будет в состоянии трейсинга.). Когда покинете его, он снова продолжит выполняться. Подключиться можно так:

gdb -p ид_процесса

  Чеклист

Составим чеклист для менеджера для борьбы с загадочными серверными ошибками, в которых не могут разобраться ни веб-разработчики, ни сисадмины:

  1. Включить сбор coredump-файлов на сервере (сисадмин)
  2. При необходимости пересобрать Apache-PHP с отладочными символами (сисадмин)
  3. С помощью gdb исследовать причину появления ошибки (сисадмин с веб-разработчиком)
  4. Принять меры по ее устранению или снижению частоты появления: поменять настройки, обновить софт, написать в багтрекер, отключить расширение PHP и т.п.

6
Курсы разработаны в компании «1С-Битрикс»

Если вы нашли неточность в тексте, непонятное объяснение, пожалуйста, сообщите нам об этом в комментариях.
Развернуть комментарии