Как найти утечку памяти php

Обновлено: 06.07.2024

В предыдущем разделе нами уже говорилось, что простой сбор корней меньше влияет на производительность. Хотя запись корней в буфер по сравнению с полным отсутствием таковой в PHP 5.2 работает медленней, другие изменения в работе PHP 5.3 сделали эту потерю производительности незаметной.

Есть две основные области влияющие на производительность: уменьшение размера используемой памяти и замедление работы при сборке мусора. Рассмотрим их.

Уменьшение размера используемой памяти

Прежде всего, основной причиной реализации механизма сборки мусора является уменьшение размера используемой памяти с помощью чистки циклических ссылок, которая происходит при достижении соответствующих условий. В реализации PHP это происходит как только заполнится корневой буфер или при вызове функции gc_collect_cycles() . На графике ниже приведено использование памяти скрипта, запущенного в PHP 5.2 и PHP 5.3, без учёта памяти, используемой самим PHP при запуске.

<?php
class Foo
public $var = '3.14159265359' ;
>

for ( $i = 0 ; $i <= 100000 ; $i ++ )
$a = new Foo ;
$a -> self = $a ;
if ( $i % 500 === 0 )
echo sprintf ( '%8d: ' , $i ), memory_get_usage () - $baseMemory , "\n" ;
>
>
?>

Сравнение использования памяти в PHP 5.2 и PHP 5.3

В этом очень академическом примере мы создаём объект, свойство a которого задаётся ссылкой на сам объект. Когда в скрипте в следующей итерации цикла переопределяется переменная $a , то происходит типичная утечка памяти. В данном случае пропадают два контейнера zval (контейнер объекта и контейнер свойства объекта), но определяется только один корень - удалённая переменная. Как только пройдут 10 000 итераций (максимально в корневом буфере будет 10 000 корней), то запустится механизм сборки мусора и память, занимаемая этими корнями, будет освобождена. Этот процесс хорошо виден на графике использования памяти для PHP 5.3: после каждых 10 000 итераций график проседает. Сам по себе механизм в данном примере совершает не так много работы, потому что структура утечек слишком проста. Из графика видно, что максимальное использование памяти в PHP 5.3 составило около 9 Мб, тогда как в PHP 5.2 оно продолжает возрастать.

Замедление работы

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

<?php
class Foo
public $var = '3.1415962654' ;
>

for ( $i = 0 ; $i <= 1000000 ; $i ++ )
$a = new Foo ;
$a -> self = $a ;
>

echo memory_get_peak_usage (), "\n" ;
?>

Мы запустим скрипт два раза: с включённой опцией zend.enable_gc и без неё.

На тестовой машине первая команда примерно выполняется 10.7 секунд, а вторая примерно 11.4 секунды. Это примерно на 7% медленнее. Однако, максимальное использование памяти скриптом уменьшилось на 98% с 931 Мб до 10 Мб. Этот тест не очень научный, но он действительно демонстрирует преимущество по использованию памяти, обеспечиваемое сборщиком мусора. Также хорошо то, что замедление для этого скрипта всегда примерно 7%, тогда как экономия памяти увеличивается все больше и больше при нахождении нового мусора.

Внутренняя статистика сборщика мусора

Можно получить немного больше информации о том, как механизм сборки мусора выполняется в PHP. Но для этого вам необходимо пересобрать PHP для включения теста производительности и кода для дополнительного сбора данных. Необходимо установить переменную окружения CFLAGS в значение -DGC_BENCH=1 до выполнения команды ./configure с вашими параметрами. Следующие команды должны сработать:

При запуске вышеприведённого примера с обновлённым PHP, можно увидеть следующую информацию по завершении работы скрипта:

Наиболее полезная статистика отображена в первом блоке. Можно увидеть, что механизм сборки мусора был запущен 110 раз, и суммарно было освобождено свыше 2 миллионов записей в памяти. Если сборка мусора была запущена хотя бы раз, то максимальное число корней в буфере всегда будет равно 10 000.

Заключение

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

Однако в тех случаях, когда механизм сборки должен срабатывать и в обычных скриптах, снижение используемой памяти позволяет одновременно работать на сервере большему их количеству.

Преимущества наиболее очевидны для долгоработающих скриптов, таких как большие наборы тестов или демоны. Новый механизм также должен снизить утечки памяти для приложений » PHP-GTK, которые выполняются обычно дольше, чем веб-скрипты.

мой выделенный сервер имеет 32GB RAM, и память просто идет вверх и вверх постоянно,и я должен перезагрузить его ежедневно. Это стоит мне клиентов и денег.

Мне трудно найти, где утечка памяти. Все, что я могу найти в интернете, это люди говорят "использовать xdebug", но я не смог найти учебники xdebug по поиску утечек памяти. Я пробовал печатать memory_get_usage до и после вызовов функций, но это правильный способ сделать это?

Я пробовал печатать memory_get_usage перед вызовом функции и после, и он поднимается, но затем, если я вызываю функцию более одного раза, она больше не поднимается. Может кто-нибудь объяснить это и сказать мне, как я могу просто и легко сказать, если Функция PHP имеет утечку памяти?

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

позвольте мне уточнить: PHP-это язык сценариев, и он не предназначен для длительных сценариев, поэтому управление памятью не является лучшим на рынке. Но почему так должно быть? Его цель-быть вызванным на уровне запроса, поэтому его рабочая область довольно мала (не более 2 - 3 секунд). Все остальное нужно отодвинуть на задний план.

что могу я сделать против утечек памяти?

если вы находитесь в версии ниже 5.4, вам нужно позаботиться о ссылках круга, так как они не являются собранным мусором.

вы могли бы использовать xdebug профилировать свои скрипты один за другим и узнать, где потребляется много памяти.

вы можете реализовать деструктор для снятия всех ссылок, если класс больше не нужен.

избегайте глобальных переменных, потому что они никогда не собирают мусор и должны быть unset явно. Если вы используете фреймворк, такой как ZF или Symfony, это может быть невозможно, так как вы нарушите функциональность.

Я нашел метод, который работает очень хорошо для меня:

установите "php-memprof" выдвижение. В Ubuntu вы можете запустить:

sudo pecl install memprof

установите "google-perftools". Опять же для Ubuntu:

sudo apt-get install google-perftools

добавьте этот код в начало вашего скрипта:

и это место aroud вы expexct, чтобы найти утечку памяти:

в моем случае это было внутри большого цикла каждые 100 пробегов.

выполнить google-pprof сравнение 2 дампов памяти:

это откроет svg изображение, как это в вашем браузере:

sample from doc

Описание номеров и имен внутри вы можете найти в документация gperftools

П. С. исправление утечек на php-уровне не гарантирует вам, что там нет утечек памяти в интерпретаторе. В моем случае я заканчиваю просто перезапуском sctipt в более длительные периоды.

Я не специалист по памяти, но, возможно, этот метод поможет вам обнаружить проблемные скрипт:

Проверьте время, когда использование памяти идет вверх и сравнить с журналом доступа apache.

Это, по крайней мере, даст вам информацию о том, идет ли использование медленно и постоянно или если это начнется в определенный момент.

К сожалению не могу предоставить исходный код скрипта и тут только примеры когда в которых по моему мнению может быть утечка.

Скрипт на php работает в фоне, обрабатывает все в несколько процессов работа с процессами - есть основной процесс и он только плодит детей с целью обработки, дочерние процессы скачивает zip архивы с ftp, вытаскивает из zip все xml файлы после чего обрабатывает их и записывает в mysql.

И интересен способ нахождения утечи, скрипт работает долго несколько дней от 1 до 3. Спустя какое-то время использует всю память на сервере и сервер перестает отвечать.

Возможные причины утечки (упрощенная версия, отсутствуют проверки что файл был скачен, обработки xml и отправка его в базу и поиск новых zip файлов):

Код пораждения дочерних процессов:

Использую php v5.6, mysql и модуль mysqli.

О сервере: тестировал на VPS SMP Debian v3.16.36, 2 ядра и 58ГБ ОЗУ, и еще на сервере под управлением SMP Debian 3.16.36, 12 ядрами с и 64 ГБ ОЗУ.


Ну вы же понимаете, что тут сам собой напрашивается анекдот "- Мне Карузо совсем не нравится, поет ужасно! - А где ты его слышал? - Да мне Рабинович напел.". Для начала попробуйте помониторьте сервер тупым top , посмотрите какие именно процессы жрут память. @rjhdby как раз речь и идет об скрипте на php, мониторил через htop, жрет дочерние процессы, и код из дочернего процесса в котором может быть проблема я привел в пример. Каким образом запускаются дочерние процессы? Один раз в начале и висят в памяти или при каждой итерации запускаются, отрабатывают и гаснут? Те процессы, которые жрут - время запуска какое, только что или 1-3 дня назад? @rjhdby обновил ответ, но не зависимо от количества дочерних процессов, дочерний процесс умудряется съедать всю память. И все же повторю вопрос, дабы исключить очевидные вещи Те процессы, которые жрут - время запуска какое, только что или 1-3 дня назад?

Для начала вариант определения, куда уходит память: воспользуйтесь каким-нибудь профилировщиком, умеющим мониторить память. Скорей всего включенный профилировщик резко просадит производительность скрипта, да и отчёт использования за 2 дня может быть слишком огромен, так что лучше ограничьте объём работы для запуска с профилировщиком каким-нибудь более компактным значением. enSO советует профилировщики:

xdebug , к сожалению, память не отслеживает.

Казалось, можно бы по старинке натыкать вызовы memory_get_usage или memory_get_peak_usage , но это мало того, что неудобно - эти функции не могут отслеживать память, выделенную для библиотек. Например, выделенную внутри libxml2 , которую использует simplexml .

И вот на simplexml у меня и основное подозрение, что он не возвращает всю занятую память.

Имеет смысл распилить задачу на конвеер. PHP архитектурно слабо заточен для долгоживущих скриптов.

Одна группа скриптов ходит по сети и загружает файлики. Сетевой ввод-вывод обычно штука медленная, в варианте перезаписи потока в файловый дескриптор расход памяти будет довольно мал даже для очень длительных скриптов. Информацию о скачанном файлике публиковать в очередь задач. Если поизвращаться с multicurl 'ом, то это может быть и один скрипт, качающий в много потоков.

вторая группа подписана на очередь новых архивов, распаковывает архив на отдельные xml и кладёт задачи по их обработке в другую очередь. Можно перезапускать воркер, например, после каждых 10 распакованных файлов, если окажется, что течёт ziparchive. Или вообще дёргать системный unzip вместо ziparchive.

третья группа читает очередь xml готовых к обработке и уже читает-пишет mysql. Тоже можно легко и безболезненно перезапускать.

Такая архитектура на первый взгляд более сложна, но позволяет проще отслеживать, на каком этапе затыки, добавлять новых воркеров именно на обработку этого этапа или целиком заменять обработчиков именно этого участка. Обработчик может легко помереть и перезапуститься без необходимости, например, выкачивать крупный архив заново и целиком его обходить, хотя умерли обработав уже 999 из 1000 файлов.

Утечки памяти обычно не беспокоят PHP-разработчиков. Типичное приложение обрабатывает один запрос и работает не больше секунды. После этого вся использованная им память освобождается. Даже если приложение кушает слишком много, максимум, разработчик упирается в memory_limit , выставленный хостером, что решить в общем случае довольно просто: как только переменная становится не нужна, очищаем память, занимаемую ей, при помощи unset .

Однако, при выполнении ресурсоёмких задач (например, обработки большого количества данных) или запуске PHP как демона проблема утечек встаёт очень остро.

В PHP 5.2 нет полноценного сборщика мусора. Вместо него используется подсчёт ссылок.

Все значения переменных хранятся в памяти. И чтобы занимать как можно меньше места, переменные с одинаковыми значениями просто ссылаются на одну и ту же область памяти. При этом количество ссылок подсчитывается и, как только оно становится равно нулю, память освобождается.

В PHP 5.2 причиной утечек являются циклические ссылки:

Исправляется это явным уничтожением ссылки на B при помощи unset:

В PHP 5.3 более умный сборщик мусора, который умеет находить и подчищать последствия использования циклических ссылок. Однако, поиск таких ссылок занимает значительное время и зависит от количества «неподчищенных» ссылок. Плюс к этому работает сборщик не постоянно, а срабатывает только при наполнении буфера ссылок. То есть до его срабатывания какое-то количество памяти всё-таки успевает утекать.

На заметку. Посмотреть, сколько памяти кушает ваше приложение можно при помощи следующих функций:

  • memory_get_usage() — использованная скриптом память в байтах в момент вызова функции.
  • memory_get_usage(true) — использованная скриптом и менеджером памяти PHP память в байтах в момент вызова функции.
  • memory_get_peak_usage() — максимальное количество памяти в байтах, использованной скриптом с запуска скрипта до момента вызова функции.
  • memory_get_peak_usage(true) — максимальное количество памяти в байтах, использованной скриптом и менеджером памяти PHP с запуска скрипта до момента вызова функции.

Комментарии RSS по email OK

Сборщик мусора работает так же, как и в IE для JS, лечение аналогично :)

К сожалению, php течёт не только из-за циклических ссылок. Подтекают некоторые встроенные функции (у меня была проблема с strtotime), текут расширения. Кстати расширения обладают "фичей" и могут грызть память не от текущего процесса, а из shared memory или даже апача. и здесь кроется проблема - вышеописанные функции не показывают эту память, нужно пользоваться утилитами OS а-ля top.

посмотри может пригодиться, например работа с одиночками

AmdY

Да, к сожалению, это так, хоть и не так много встроенного течёт. Я описал то, что реально устранить самостоятельно без патчей к самому PHP.

Спасибо за подробный обзор и полезные ссылки. Столкнулся с серьезными утечками в системе расчета написнной как CLI приложение на ZendFramwork'е, особенно Zend_Db и конкретно Zend_Db_Table Relationships текли. Проблема решилась переходом с 5.2 на 5.3, все таки там этот механизм был значительно улучшен. Обычные unset'ы не помогали (

Если работаете с Yii, будьте внимательны при созданием моделей в цикле. Behaviors как раз и будут оставаться такими подвешенными ссылками. Их надо отцеплять вручную через detachBehavior() в конце итерации.

С PHP 5.3 не проверял на сколько эффективно будет очищаться память в этом случае.

Exel

Да, в Yii поведения подтекают. Тут, к сожалению, пока придумать ничего не удалось. С PHP 5.3 будет чистится ступеньками, как показано в мануале PHP по ссылкам в заметке.

То есть до его срабатывания какое-то количество памяти всё-таки успевает утекать.

Во-первых, вместо unset лучше использовать

особенно в объекте.

Во-вторых, "Исправляется это явным уничтожением ссылки на B при помощи unset". Вы вызываете unset в деструкторе, вам не кажется это странным? Ведь если вызван деструктор, то нафиг не надо очищать свойства объекта - они и так очистятся.

А вот если бы был вызван какой-нить метод, в котором мы явно делаем для свойства unset - тогда циклические ссылки нам были бы не страшны. Тогда можно было бы делать unset и переменной, которая ссылается на объект.

Выполнял скрипт (там где Class A и B), тестировал на PHP 5.2.17 (cli) (built: Jan 6 2011 17:28:41) Zend Engine v2.2.0 в 1-м скрипте memory_get_usage() выдает в начале: 59880 в конце: 452176


Проекты: Касса, ПриватМаркет, Международные переводы, .


Что такое утечка памяти?

Утeчка памяти (англ. memory leak) — процесс неконтролируемого уменьшения объёма свободной памяти компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих ненужные уже участки памяти, или с ошибками системных служб контроля памяти.

Утечки памяти обычно не беспокоят PHP-разработчиков. Типичное приложение обрабатывает один запрос и работает не больше секунды. После этого вся использованная им память освобождается.

Даже если приложение кушает слишком много, максимум, разработчик упирается в memory_limit , что решить в общем случае довольно просто: как только переменная становится не нужна, очищаем память, занимаемую ей, при помощи unset .

Основными последствиями утечки памяти являются:

  • увеличение использования памяти;
  • снижение производительности.

Если вы разрабатываете ресурсоёмкие задачи (например, обработки большого количества данных) или стоит вопрос запуска PHP как демона, то проблема утечек встаёт очень остро.

PHP предназначен для работы не более 1 сек.

Для длительных процессов нужно использовать Python/Java/Rust/Go.

PHP рожден что бы умереть!



Как работает механизм памяти в PHP?

Zval структура в PHP 5

объявляется следующим образом:

Как работает механизм памяти в PHP?

Создание array zval



Как работает механизм памяти в PHP?

Добавление уже существующего элемента в массив

Как работает механизм памяти в PHP?

Удаление элемента из массива

Как работает механизм памяти в PHP?

Добавление массива новым элементом в самого себя


Как работает механизм памяти в PHP?












Как же быть с циклическими ссылками?

До PHP 5.3 - никак


Как же быть с циклическими ссылками?







Работа GC в PHP

Работа GC в PHP

Работа GC в PHP

Работа GC в PHP

Работа GC в PHP

Работа GC в PHP

Если переменная вне области действия и больше не используется в каком-либо другом месте кода, то сборка мусора будет произведена автоматически. Вы можете принудительно выполнить это раньше, используя метод unset() для выведения переменной из области действия.

Если переменная является частью циклической ссылки, то она может быть очищена только посредством PHPs cycle garbage collector. Он запускается всякий раз, когда 10000 возможных циклических объектов или массивов на данный момент в памяти.

Если вы вызываете функцию gc_collect_cycles().


memory_get_usage() — возвращает количество памяти, выделенное для PHP на момент запуска скрипта.

memory_get_peak_usage() — возвращает пиковое значение объема памяти, выделенное PHP.

gc_collect_cycles() — принудительный запуск сборщика мусора.

Обзор процесса работы

  1. Собираем лог процесса.
  2. Смотрим на необходимые нам параметры (длительное время выполнения, большое количество итераций, большое потребление памяти).
  3. Исправляем код.
  4. Профит!



  • Calls — количество и процентное соотношение вызовов функции.
  • Incl. Wall Time — время выполнения функции с вложенными функциями.
  • Excl. Wall Time — время выполнения функции без вложенных функций.
  • Incl. CPU — процессорное время с вложенными функциями.
  • Excl. CPU — процессорное время без вложенных функций.
  • Incl. MemUse — потребление памяти с вложенными функциями.
  • Excl. MemUse — потребление памяти без вложенных функций.
  • Incl. PeakMemUse — максимальное потребление памяти с вложенными функциями.
  • Excl. PeakMemUse — максимальное потребление памяти без вложенных функций.

Для PHP7 стоит использовать не родной XHProf, а его форк, так как в стандартном расширении нет всех метрик.

Читайте также: