问题 从API中提取数据,内存增长


我正在开发一个项目,我从API中提取数据(JSON)。我遇到的问题是内存正在慢慢增长,直到我遇到可怕的致命错误:

致命错误:允许的内存大小*字节耗尽(尝试过   分配*字节)在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

编辑:
在Windows上使用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

编辑2:我只看到内存增加后 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

5290
2017-10-27 10:21


起源

如果没有一个真实的例子可以尝试(因为可能难以表示您的实际数据集)。我的建议是:1。使用分析器(即Blackfire)2。如果你不能使用分析器传播更多的memory_get_peak_usage(每行一行就是我要做的),这样你就可以准确地看到内存的增长点。我最好的猜测是CURL泄漏记忆;) - Ricardo Velhote
that's been allocated to your PHP script. 也许是它的一部分,是缓存的内存。因此,即使你很难设置你的代码,其他可以提高内存的行为,但这并不反映实际使用的内存,而是你在处理数据时使用过的最多?例如,您在“操纵数据”中执行的活动可能会提高它,使用该内存但之后不再使用,但系统仍会暂时缓存它 - Prix
顺便问一下,你的PHP版本是什么? - Ricardo Velhote
@RicardoVelhote我使用的是PHP 5.3.9并且几乎达到了5.6.14但差别并不大。见编辑帖子。现在我要尝试一个分析器。 - PHPhil
@prix我正在测试没有操作部分。 - PHPhil


答案:


我在10个URL上测试了你的脚本。我删除了所有注释,除了脚本末尾的一个注释和使用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";
}

在某些情况下,它可以为您提供帮助,但结果可能会导致性能下降。

您可以尝试使用重启脚本 unset,和 unset+gc 如果您在更改后遇到相同的问题,请写在前面。

我也看不到你在哪里使用 $decoded 变量,如果在代码中出错,可以删除json_decode :)


1
2017-11-03 20:05



这是我的真实脚本的简化版本,这就是原因 $decoded 没用过。我会尝试取消设置和gc。 - PHPhil
我的回答对你有帮助吗?如果是,请接受 - Anton Ohorodnyk


你的方法很长,所以我不相信垃圾收集在函数结束之前不会被触发,这意味着你未使用的变量可能会积累。如果它们不再被使用,那么垃圾收集将为您解决这个问题。

您可能会考虑将此代码重构为较小的方法以利用此功能,并考虑使用较小的方法所带来的所有其他好东西,但同时您可以尝试将 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 可能会把东西翻过来,直到循环终止才会清理干净。但这是值得一试的。


4
2017-10-30 11:20



根据我的经验,GC使用PHP 5.4可以更好地处理长函数;在那之前,它确实像那样工作。 - Smar
@smar Baring,记住,我做了双重拍摄。有问题的调用函数不长,但它做了大量的工作。我也会这样做: hackingwithphp.com/18/1/10 - Paul Stanley
嗯,我想这是真的,我在我的PHP脚本上使用了OOP,所以它总是在大多数时间内至少留下一个函数。当它离开PHP的内置函数时,还会留下函数计数吗?我不知道。 - Smar
所以我认为这将是整个脚本在这种情况下退出的时候,我相信,这有点晚了。 - Paul Stanley
是的我没有说你错了(我认为你是对的),我只是不想通过自己从该代码构建可运行的案例来验证它,然后从PHP的源码中读取为什么它实际上做了它做的事情验证,因此我根据我的经验留下了评论。 (出于同样的原因,我不能自己留下答案,因为 我只是不知道 :) - Smar


所以我的问题是:我做错了吗?这是正常的吗?什么可以   我是为了解决这个问题吗?

是的,当您使用所有内存时,内存不足是正常的。您正在请求10个同时发出的HTTP请求,并将JSON响应反序列化到PHP内存中。在不限制响应大小的情况下,您将始终面临内存不足的危险。

你还能做什么?

  1. 不要同时运行多个http连接。转 $threads 降到1来测试这个。如果C扩展调用中存在内存泄漏 gc_collect_cycles() 不会释放任何内存,这只会影响Zend Engine中分配的内存,而这些内存不再可用。
  2. 将结果保存到文件夹并在另一个脚本中处理它们。您可以将处理过的文件移动到子目录中,以便在成功处理json文件时进行标记。
  3. 调查分叉或消息队列以使多个进程同时处理问题的一部分 - 多个PHP进程监听队列桶或父进程的分叉子进程使用自己的进程内存。

3
2017-10-31 18:55





所以我的问题是:我做错了吗?这是正常的吗?我该怎么做才能解决这个问题?

您的代码没有任何问题,因为这是正常行为,您从外部源请求数据,而外部源又被加载到内存中。

当然,解决问题的方法可以简单到:

ini_set('memory_limit', -1);

这允许使用所有需要的内存。


当我使用虚拟内容时,请求之间的内存使用量保持不变。

这是在Windows上的XAMPP中使用PHP 5.5.19。

有一个 cURL内存泄漏相关的bug 版本5.5.4中已修复此问题


1
2017-10-29 13:25



谢谢你的时间,我使用的是php 5.3.9并且我切换到了最新的xampp,但内存使用没有太大变化。见编辑帖子。 - PHPhil
虽然这样可以防止内存错误,但它可能不是一个“明智的”解决方案,除非部署环境完全由您控制。它可能只是解决问题,直到您部署到没有足够内存的服务器,或者您运行其他进程并最终使用PHP占用所有内存来加密服务器。它会“消除”问题,而不是“修复”它 - Jon Story