Вытягивание данных из API, рост памяти

Я работаю над проектом, в котором я извлекаю данные (JSON) из API. Проблема, с которой я сталкиваюсь, заключается в том, что память медленно растет, пока я не получаю страшную фатальную ошибку:

Неустранимая ошибка: допустимый объем памяти * байт исчерпан (проверен выделить * байты) в C:... в строке *

Я не думаю, что должен быть рост памяти. Я попытался разобрать все в конце цикла, но никакой разницы. Поэтому мой вопрос: я что-то делаю неправильно? Это нормально? Что я могу сделать, чтобы исправить эту проблему?

<?php

$start = microtime(true);

$time = microtime(true) - $start;
echo "Start: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br/>";

include ('start.php');
include ('connect.php');

set_time_limit(0);

$api_key = 'API-KEY';
$tier = 'Platinum';
$threads = 10; //number of urls called simultaneously

function multiRequest($urls, $start) {

    $time = microtime(true) - $start;
    echo "&nbsp;&nbsp;&nbsp;start function: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

    $nbrURLS = count($urls); // number of urls in array $urls
    $ch = array(); // array of curl handles
    $result = array(); // data to be returned

    $mh = curl_multi_init(); // create a multi handle 

    $time = microtime(true) - $start;
    echo "&nbsp;&nbsp;&nbsp;Creation multi handle: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

    // set URL and other appropriate options
    for($i = 0; $i < $nbrURLS; $i++) {
        $ch[$i]=curl_init();

        curl_setopt($ch[$i], CURLOPT_URL, $urls[$i]);
        curl_setopt($ch[$i], CURLOPT_RETURNTRANSFER, 1); // return data as string
        curl_setopt($ch[$i], CURLOPT_SSL_VERIFYPEER, 0); // Doesn't verifies certificate

        curl_multi_add_handle ($mh, $ch[$i]); // Add a normal cURL handle to a cURL multi handle
    }

    $time = microtime(true) - $start;
    echo "&nbsp;&nbsp;&nbsp;For loop options: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

    // execute the handles
    do {
        $mrc = curl_multi_exec($mh, $active);          
        curl_multi_select($mh, 0.1); // without this, we will busy-loop here and use 100% CPU
    } while ($active);

    $time = microtime(true) - $start;
    echo "&nbsp;&nbsp;&nbsp;Execution: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

    echo '&nbsp;&nbsp;&nbsp;For loop2<br>';

    // get content and remove handles
    for($i = 0; $i < $nbrURLS; $i++) {

        $error = curl_getinfo($ch[$i], CURLINFO_HTTP_CODE); // Last received HTTP code 

        echo "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;error: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

        //error handling if not 200 ok code
        if($error != 200){

            if($error == 429 || $error == 500 || $error == 503 || $error == 504){
                echo "Again error: $error<br>";
                $result['again'][] = $urls[$i];

            } else {
                echo "Error error: $error<br>";
                $result['errors'][] = array("Url" => $urls[$i], "errornbr" => $error);
            }

        } else {
            $result['json'][] = curl_multi_getcontent($ch[$i]);

            echo "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Content: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";
        }

        curl_multi_remove_handle($mh, $ch[$i]);
        curl_close($ch[$i]);
    }

    $time = microtime(true) - $start;
    echo "&nbsp;&nbsp;&nbsp; after loop2: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br>";

    curl_multi_close($mh);

    return $result;
}


$gamesId = mysqli_query($connect, "SELECT gameId FROM `games` WHERE `region` = 'EUW1' AND `tier` = '$tier ' LIMIT 20 ");
$urls = array();

while($result = mysqli_fetch_array($gamesId))
{
    $urls[] = 'https://euw.api.pvp.net/api/lol/euw/v2.2/match/' . $result['gameId'] . '?includeTimeline=true&api_key=' . $api_key;
}

$time = microtime(true) - $start;
echo "After URL array: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br/>";

$x = 1; //number of loops

while($urls){ 

    $chunk = array_splice($urls, 0, $threads); // take the first chunk ($threads) of all urls

    $time = microtime(true) - $start;
    echo "<br>After chunk: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br/>";

    $result = multiRequest($chunk, $start); // Get json

    unset($chunk);

    $nbrComplete = count($result['json']); //number of retruned json strings

    echo 'For loop: <br/>';

    for($y = 0; $y < $nbrComplete; $y++){
        // parse the json
        $decoded = json_decode($result['json'][$y], true);

        $time = microtime(true) - $start;
        echo "&nbsp;&nbsp;&nbsp;Decode: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "<br/>";


    }

    unset($nbrComplete);
    unset($decoded);

    $time = microtime(true) - $start;
    echo $x . ": ". memory_get_peak_usage(true) . " | " . $time . "<br>";

    // reuse urls
    if(isset($result['again'])){
        $urls = array_merge($urls, $result['again']);
        unset($result['again']);
    }

    unset($result);
    unset($time);

    sleep(15); // limit the request rate

    $x++;
}

include ('end.php');

?>

PHP версии 5.3.9 - 100 циклов:

loop: memory | time (sec)
1: 5505024 | 0.98330211639404
3: 6291456 | 33.190237045288
65: 6553600 | 1032.1401019096
73: 6815744 | 1160.4345710278
75: 7077888 | 1192.6274609566
100: 7077888 | 1595.2397520542

EDIT:
Попробовав его с PHP 5.6.14 xampp на окнах:

loop: memory | time (sec)
1: 5505024 | 1.0365679264069
3: 6291456 | 33.604479074478
60: 6553600 | 945.90159296989
62: 6815744 | 977.82566595078
93: 7077888 | 1474.5941500664
94: 7340032 | 1490.6698410511
100: 7340032 | 1587.2434458733

EDIT2: я вижу только увеличение памяти после json_decode

Start: 262144 | 135448
After URL array: 262144 | 151984
After chunk: 262144 | 152272
   start function: 262144 | 152464
   Creation multi handle: 262144 | 152816
   For loop options: 262144 | 161424
   Execution: 3145728 | 1943472
   For loop2
      error: 3145728 | 1943520
      Content: 3145728 | 2095056
      error: 3145728 | 1938952
      Content: 3145728 | 2131992
      error: 3145728 | 1938072
      Content: 3145728 | 2135424
      error: 3145728 | 1933288
      Content: 3145728 | 2062312
      error: 3145728 | 1928504
      Content: 3145728 | 2124360
      error: 3145728 | 1923720
      Content: 3145728 | 2089768
      error: 3145728 | 1918936
      Content: 3145728 | 2100768
      error: 3145728 | 1914152
      Content: 3145728 | 2089272
      error: 3145728 | 1909368
      Content: 3145728 | 2067184
      error: 3145728 | 1904616
      Content: 3145728 | 2102976
    after loop2: 3145728 | 1899824
For loop: 
   Decode: 3670016 | 2962208
   Decode: 4980736 | 3241232
   Decode: 5242880 | 3273808
   Decode: 5242880 | 2802024
   Decode: 5242880 | 3258152
   Decode: 5242880 | 3057816
   Decode: 5242880 | 3169160
   Decode: 5242880 | 3122360
   Decode: 5242880 | 3004216
   Decode: 5242880 | 3277304

Ответ 1

Я проверил ваш script на 10 URL. Я удалил все ваши комментарии, кроме одного комментария в конце script и одного в цикле проблем при использовании json_decode. Также я открыл одну страницу, которую вы кодируете из API, и выглядел очень большим массивом, и я думаю, что вы правы, у вас есть проблема в json_decode.

Результаты и исправления.

Результат без изменений:

код:

for($y = 0; $y < $nbrComplete; $y++){
   $decoded = json_decode($result['json'][$y], true);
   $time = microtime(true) - $start;
   echo "Decode: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "\n";
}

Результат:

Decode: 3407872 | 2947584
Decode: 3932160 | 2183872
Decode: 3932160 | 2491440
Decode: 4980736 | 3291288
Decode: 6291456 | 3835848
Decode: 6291456 | 2676760
Decode: 6291456 | 4249376
Decode: 6291456 | 2832080
Decode: 6291456 | 4081888
Decode: 6291456 | 3214112
Decode: 6291456 | 244400

Результат с помощью unset($decode):

код:

for($y = 0; $y < $nbrComplete; $y++){
   $decoded = json_decode($result['json'][$y], true);
   unset($decoded);
   $time = microtime(true) - $start;
   echo "Decode: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "\n";
}

Результат:

Decode: 3407872 | 1573296
Decode: 3407872 | 1573296
Decode: 3407872 | 1573296
Decode: 3932160 | 1573296
Decode: 4456448 | 1573296
Decode: 4456448 | 1573296
Decode: 4980736 | 1573296
Decode: 4980736 | 1573296
Decode: 4980736 | 1573296
Decode: 4980736 | 1573296
Decode: 4980736 | 244448

Также вы можете добавить gc_collect_cycles:

код:

for($y = 0; $y < $nbrComplete; $y++){
   $decoded = json_decode($result['json'][$y], true);
   unset($decoded);
   gc_collect_cycles();
   $time = microtime(true) - $start;
   echo "Decode: ". memory_get_peak_usage(true) . " | " . memory_get_usage() . "\n";
}

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

Вы можете попробовать перезапустить script с помощью unset и unset+gc и написать назад, если после изменений у вас будет такая же проблема.

Также я не вижу, где вы используете переменную $decoded, если это ошибка в коде, вы можете удалить json_decode:)

Ответ 2

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

Вы можете подумать о реорганизации этого кода в более мелкие методы, чтобы воспользоваться этим, и со всеми другими хорошими вещами, которые поставляются с меньшими методами, однако в то же время вы могли бы попробовать разместить gc_collect_cycles(); в самом конце вашего чтобы узнать, можете ли вы освободить некоторую память:

if(isset($result['again'])){
    $urls = array_merge($urls, $result['again']);
    unset($result['again']);
}

unset($result);
unset($time);

gc_collect_cycles();//add this line here
sleep(15); // limit the request rate

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

Ответ 3

Итак, мой вопрос: я делаю что-то неправильно? Это нормально? Что может Я исправляю эту проблему?

Да, исчерпание памяти является нормальным, когда вы используете все это. Вы запрашиваете 10 одновременных HTTP-запросов и несериализуете ответы JSON в память PHP. Без ограничения размера ответов вам всегда будет угрожать нехватка памяти.

Что еще вы можете сделать?

  • Не запускайте несколько HTTP-соединений одновременно. Поверните $threads до 1, чтобы проверить это. Если утечка памяти в расширении C, вызывающая gc_collect_cycles(), не освобождает память, это влияет только на память, выделенную в Zend Engine, которая больше недоступна.
  • Сохраните результаты в папке и обработайте их в другом script. Вы можете переместить обработанные файлы в подкаталог, чтобы отметить, когда вы успешно обработали json файл.
  • Исследуйте forking или очередь сообщений, чтобы одновременно работать с несколькими процессами над несколькими проблемами - либо несколькими процессами PHP, которые слушают ведро очереди, либо раздвоенные дочерние элементы родительского процесса с собственной памятью процесса.

Ответ 4

Итак, мой вопрос: я делаю что-то неправильно? Это нормально? Что я могу сделать, чтобы исправить эту проблему?

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

Конечно, решение вашей проблемы может быть таким же простым, как:

ini_set('memory_limit', -1);

Это позволяет использовать всю память.


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

Это использование PHP 5.5.19 в XAMPP в Windows.

Произошла ошибка cURL, связанная с утечкой памяти, которая была исправлена ​​в версии 5.5.4