PHP

В поисках утечек памяти

Кто я

Шкарбатов Дмитрий

Руковожу командой web-разработки

 

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

shkarbatov@gmail.com

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

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

Wikipedia

Почему об этом не часто говорят?

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

   

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

Почему от утечек надо избавляться?

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

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

 

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

Вы не правильно используете PHP

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

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

 

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

Как работает механизм

памяти в PHP?

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

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

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


typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

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

Создание array zval

Если у вас установлен » Xdebug, то вы можете вывести структуру переменной,
вызвав функцию xdebug_debug_zval().

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

Результатом выполнения данного примера будет следующее:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>

Результатом выполнения данного примера будет следующее:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

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

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

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>

Результатом выполнения данного примера будет следующее:

a: (refcount=1, is_ref=0)=array (
   'life' => (refcount=1, is_ref=0)='life'
)

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

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

<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>

Результатом выполнения данного примера будет следующее:

a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)

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

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

<?php
unset($a);
?>

Результатом выполнения данного примера будет следующее:

a: (refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

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

Удаление $a

Как же быть

с циклическими ссылками?

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

 

До PHP 5.3 - никак

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

Но в PHP же есть GC!

Работа GC в PHP


xdebug_start_gcstats() - Начало сбора статистики GC
xdebug_stop_gcstats() - Конец сбора статистики GC


// ==================================


Как отдельный php-модуль:
https://github.com/tideways/php_garbage_stats

// ==================================

Является частью Xdebug с версии 2.6

Работа GC в PHP


<?php

class MyClass{}

function buildObjects() {
    $c = new MyClass();
    $b = new MyClass();

    $c->b = $b;
}

$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
    buildObjects();
}

// ==================================

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php

Found 0 garbage collection runs in current script.

Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------

Работа GC в PHP

<?php

class MyClass{}

function buildObjects() {
    $c = new MyClass();
    $b = new MyClass();

    $c->b = $b;
    $b->c = $c;
}

$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
    buildObjects();
}

// ==================================

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php

Found 6 garbage collection runs in current script.

Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------
     9998 |    99.98 % |  0.81 ms |       4646232 |       487560 |    89.51 % | buildObjects
    10000 |   100.00 % |  0.63 ms |       4648848 |       489344 |    89.47 % | buildObjects
    10000 |   100.00 % |  1.11 ms |       4650312 |       490808 |    89.45 % | buildObjects
    10000 |   100.00 % |  1.28 ms |       4651776 |       492272 |    89.42 % | buildObjects
    10000 |   100.00 % |  0.80 ms |       4653240 |       493736 |    89.39 % | buildObjects
    10000 |   100.00 % |  0.69 ms |       4654704 |       495200 |    89.36 % | buildObjects

Работа GC в PHP

<?php
class MyClass{}

function buildObjects() {
    $c = new MyClass();
    $b = new MyClass();

    $c->b = $b;
    $b->c = $c;
}

$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
    buildObjects();

    if ($i%3000 == 0)
        echo number_format(memory_get_usage()) . "\n";
}

// ==================================

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php

365,080
2,918,456
1,322,608
3,818,608
2,156,072
493,536
2,989,536
1,327,000
3,823,000
2,160,464

Работа GC в PHP

<?php
class MyClass{}

function buildObjects() {
    $c = new MyClass();
    $b = new MyClass();

    $c->b = $b;
    $b->c = $c;

    return [$c, $b];
}

$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
    $c = buildObjects();

    unset($c[0]->b);
}

// ==================================

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php

365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832

Работа GC в PHP


<?php

class MyClass{}

function buildObjects() {
    $c = new MyClass();
    $b = new MyClass();

    $c->b = $b;
    $b->c = $c;

    return [$c, $b];
}

$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
    $c = buildObjects();

    unset($c[0]->b);
}

// ==================================

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php

Found 0 garbage collection runs in current script.

Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------

Как PHP очищает память?

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

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

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

Предотвращаем утечки

Полезные php функции

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

 

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

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

Инструменты для поиска утечек

XHProf

 

+

php-meminfo

XHProf

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

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

XHProf

<?php

// Инициализируем профайлер
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);

class MyClassA {
    private $myObjectName;

    public function __construct($name)
    {
        $this->myObjectName = $name;
    }
};

$myString = "My very nice string";
$myFloat = 3.14;
$myInt = 42;
$myNull = null;
$myRootArray = [];

for($i = 0; $i < 100000; $i++) {
    $myRootArray['first level']['second level'][] = new MyClassA('Object '.$i);
};

// Останавливаем профайлер после выполнения программы
$xhprof_data = xhprof_disable();

include_once "/var/www/xhprof-0.9.4/xhprof_lib/utils/xhprof_lib.php";
include_once "/var/www/xhprof-0.9.4/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($xhprof_data, "test");

XHProf

XHProf

XHProf

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

XHProf

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

 

https://github.com/phacility/xhprof/issues/82

mem-info

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

  1. Делаем дамп памяти.
  2. Смотрим общее количество элементов в памяти.
  3. Находим элемент, который больше не должен быть в памяти, но является частью дампа.
  4. Отслеживаем ссылку которая все еще держит объект в памяти.
  5. Исправляем код.
  6. Профит!

mem-info


<?php

class MyClassA {

    private $myObjectName;

    public function __construct($name)
    {
        $this->myObjectName = $name;
    }
};

$myString = "My very nice string";
$myFloat = 3.14;
$myInt = 42;
$myNull = null;
$myRootArray = [];

for($i = 0; $i < 100; $i++) {
    $myRootArray['first level']['second level'][] = new MyClassA('Object '.$i);
};

gc_collect_cycles();
meminfo_dump(fopen('/tmp/php_mem_dump100.json','w'));

mem-info


$ analyzer/bin/analyzer summary /tmp/php_mem_dump100.json 
+----------+-----------------+-----------------------------+
| Type     | Instances Count | Cumulated Self Size (bytes) |
+----------+-----------------+-----------------------------+
| string   | 183             | 8486                        |
| MyClassA | 100             | 7200                        |
| array    | 12              | 864                         |
| integer  | 5               | 80                          |
| float    | 2               | 32                          |
| null     | 1               | 16                          |
+----------+-----------------+-----------------------------+

mem-info


$ analyzer/bin/analyzer query -v -f "class=MyClassA" -f "is_root=0" /tmp/php_mem_dump100.json 
+----------------+-------------------+------------------------------+
| Item ids       | Item data         | Children                     |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f008 | Type: object      | myObjectName: 0x7f4d5c85cde0 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 1  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f028 | Type: object      | myObjectName: 0x7f4d5c85cf20 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 2  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f048 | Type: object      | myObjectName: 0x7f4d5c85cca0 |
|                | Class: MyClassA   |                              |
|                | Object Handle: 3  |                              |
|                | Size: 72 B        |                              |
|                | Is root: No       |                              |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f068 | Type: object      | myObjectName: 0x7f4d5c85d060 |
...

mem-info


$ analyzer/bin/analyzer -v ref-path 0x7f4d5c87f008 /tmp/php_mem_dump100.json
Found 1 paths
Path from 0x7f4d5c8562a0
+--------------------+
| Id: 0x7f4d5c87f008 |
| Type: object       |
| Class: MyClassA    |
| Object Handle: 1   |
| Size: 72 B         |
| Is root: No        |
| Children count: 1  |
+--------------------+
         ^          
         |          
         0          
         |          
         |          
+---------------------+
| Id: 0x7f4d5c85cb60  |
| Type: array         |
| Size: 72 B          |
| Is root: No         |
| Children count: 100 |
+---------------------+
         ^          
         |          
    second level    
         |          
         ^          
         |          
    second level    
         |          
         |          
+--------------------+
| Id: 0x7f4d5c85ca20 |
| Type: array        |
| Size: 72 B         |
| Is root: No        |
| Children count: 1  |
+--------------------+
         ^          
         |          
    first level     
         |          
         |          
+---------------------------+
| Id: 0x7f4d5c8562a0        |
| Type: array               |
| Size: 72 B                |
| Is root: Yes              |
| Execution Frame: <GLOBAL> |
| Symbol Name: myRootArray  |
| Children count: 1         |
+---------------------------+

mem-info

Причина, по которой наш объект все еще находится в памяти, состоит в том, что он находится в массиве, который сам по себе находится в другом массиве, который находится в конечном массиве. И последний массив напрямую связан с переменной, объявленной в <GLOBAL>, и называется myRootArray.


В более короткой нотации PHP это может быть написано так:

$myRootArray ['first level'] ['second level'] [0] = 0x7f4d5c87f008;

Как предотвратить появление утечек?

  • Закрывайте файловые дескрипторы;
  • Контролируйте работу скрипта во время выброса Exception;
  • Не забывайте очищать entityManager в Doctrine;
  • Будьте внимательны со слушателями;
  • Удаляйте не используемые переменные и объекты.

Отключение GC ускорило

composer от 50% до 90%


$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 bin/composer update
Found 157 garbage collection runs in current script.

Collected | Efficency% | Duration | Reduction% | Function
----------|------------|----------|------------|---------
      796 |     7.96 % |  2.59 ms |     0.63 % | [..]::loadProviderListings
        0 |     0.00 % |  0.91 ms |    -0.00 % | [..]::loadProviderListings
        0 |     0.00 % | 17.19 ms |    -0.01 % | [..]::parseConstraints
        0 |     0.00 % | 19.95 ms |    -0.03 % | ArrayLoader::load
        0 |     0.00 % | 22.36 ms |    -0.02 % | Pool::computeWhatProvides
        0 |     0.00 % | 30.40 ms |    -0.01 % | ArrayLoader::parseLinks
        0 |     0.00 % | 29.05 ms |    -0.00 % | Rule2Literals::equals
        0 |     0.00 % | 29.00 ms |    -0.01 % | [..]::createRule2Literals
        0 |     0.00 % | 32.90 ms |    -0.09 % | RuleWatchNode::__construct
        0 |     0.00 % | 35.08 ms |    -0.09 % | Solver::solve
        0 |     0.00 % | 44.10 ms |    -0.09 % | makeAssertionRuleDecisions
        0 |     0.00 % | 53.28 ms |    -0.01 % | Solver::runSat
        0 |     0.00 % | 24.21 ms |    -0.00 % | Transaction::findUpdates
      183 |     1.83 % | 31.34 ms |     0.01 % | Installer::doInstall
        0 |     0.00 % | 24.10 ms |    -0.00 % | Installer::doInstall

// Более детальное описание
https://blog.ircmaxell.com/2014/12/what-about-garbage.html

P.S.:

  • Перед съемом данных для анализа рекомендуется всегда запускать gc_collect_cycles();
  • Всегда делайте ограничение по памяти для процессов;
  • Переносите существующие обработчики на PHP7, там PHP много что (не все), подумает за вас;

  • Возможно в некоторых случаях стоит пересмотреть работу GC в сторону его отключения.

Полезная литература

  • http://rmcreative.ru/blog/post/utechki-pamjati-v-php
  • https://speakerdeck.com/bitone/hunting-down-memory-leaks-with-php-meminfo
  • http://php.net/manual/ru/features.gc.php
  • https://ruhighload.com/index.php/2009/08/21/xhprof-профилирование-php-от-facebook/
  • https://www.youtube.com/watch?v=wZjnj1PAJ78
  • https://github.com/BitOne/php-meminfo
  • https://habrahabr.ru/post/134784/
  • https://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html
  • https://blog.ircmaxell.com/2014/12/what-about-garbage.html

Всем спасибо!

PHP В поисках утечек памяти

By James Jason

PHP В поисках утечек памяти

  • 1,425