PHP脚本中的内存泄漏问题

4

我有一个 PHP 脚本,它运行了一个 MySQL 查询,然后循环遍历结果,在该循环中还运行了多个查询:

    $sqlstr = "SELECT * FROM user_pred WHERE uprType != 2 AND uprTurn=$turn ORDER BY uprUserTeamIdFK";
    $utmres = mysql_query($sqlstr) or trigger_error($termerror = __FILE__." - ".__LINE__.": ".mysql_error());
    while($utmrow = mysql_fetch_array($utmres, MYSQL_ASSOC)) {
// some stuff happens here    
//  echo memory_get_usage() . " - 1241<br/>\n";
        $sqlstr = "UPDATE user_roundscores SET ursUpdDate=NOW(),ursScore=$score WHERE ursUserTeamIdFK=$userteamid";
        if(!mysql_query($sqlstr)) {
            $err_crit++;
            $cLog->WriteLogFile("Failed to UPDATE user_roundscores record for user $userid - teamuserid: $userteamid\n");
            echo "Failed to UPDATE user_roundscores record for user $userid - teamuserid: $userteamid<br>\n";
            break;
        }
    unset($sqlstr);
    //  echo memory_get_usage() . " - 1253<br/>\n";
// some stuff happens here too
}

更新查询永远不会失败。
由于在两个`memory_get_usage`调用之间添加了一些内存,原因未知。由于大循环运行大约500,000次或更多次,在最后它确实会累积大量内存。我在这里漏掉了什么吗?也许内存实际上并没有在两个调用之间添加,而是在脚本的其他位置添加?
编辑:一些额外的信息: 循环之前大约为5mb,在循环之后为约440mb,每个更新查询添加约250字节的内存(其余的内存在循环的其他地方添加)。 我没有发布更多的“其他内容”的原因是因为它约有300行代码。我发布了这部分内容,因为它看起来是添加最多内存的地方。

我在这里看不到问题。请发布更多的while循环代码,并指出它使用了多少内存。 - Marcus Adams
在第一个查询之前、第一个查询之后和循环之后,memory_get_usage 返回什么? - webbiedave
@webbiedave 在循环之前大约为5MB,在循环之后为约440MB,每个更新查询添加约250字节。(其余的内存在循环的其他位置添加)。 @Marcus 我没有发布更多的“其他内容”的原因是因为它大约有300行代码。我发布了这部分是因为它看起来是添加最多内存的地方。 - Jasper De Bruijn
我知道这是一个老问题,但下次请同时包含PHP的版本。我正在使用mysqli_fetch_array遇到同样的问题,我确定我在一个更新的PHP版本中。(5.5,虽然仍然有点老) - Ben Keene
6个回答

4

只有在出现“内存耗尽”错误时,该内存泄漏才会成为问题。PHP会自动垃圾回收任何未使用的对象/变量,但收集器不会启动直到必须 - 垃圾回收可能是非常昂贵的操作。

即使您不断重用相同的对象/变量,看到内存使用量上升也是正常的 - 直到内存使用量超过某个级别,收集器才会启动并进行清理。

我怀疑如果将用户ID分组并发出较少的更新请求,每次更改更多记录,您可以使事情运行得更快。例如,执行以下操作:

UPDATE user_roundscores SET ursUpdDate=NOW() WHERE ursUserTeamIdFK IN (id1, id2, id3, id4, id5, etc...)

不要为每个用户进行单独的更新操作,这样可以减少对数据库接口层的往返次数,让服务器有更多的时间来运行,从而提高运行速度。

此外,请考虑将其扩展到数百万用户的影响,正如您在评论中所说。一百万个单独的更新操作需要花费相当长的时间来运行,因此NOW()不会是一个“常量”。如果需要5分钟来完成整个操作,则会得到各种不同的ursUpdDate时间戳。您可能需要考虑将单个NOW()调用缓存到服务器端变量中,并针对该变量发出更新操作:

 SELECT @cachednow :p NOW();
 UPDATE .... SET ursUpDate = @cachednow WHERE ....;

感谢您对内存的解释,我试图让这段代码完全不使用内存,但似乎这是不可能的。 我已经更新了我的问题中的更新查询,因为每个用户都会收到自己计算出的分数,所以无法对查询进行分组。 - Jasper De Bruijn

2

最好的方法可能是获取所有的用户ID并将其刷新到文件中。然后运行一个新脚本,使用管道将其分叉到x个工作进程。然后只需给他们一个小的用户ID列表来处理,当他们完成每个列表时。使用多个CPU /核心/服务器可以更快地完成任务。如果一个工人失败了,只需启动一个新的工人。 要将其他服务器用作工作器,您可以从工作线程调用它们,例如curl/fopen/soap等。


从长远来看,这可能是更好的解决方案。该脚本设计了很长时间,仅适用于相对较少的用户数量,现在正在测试可能有数百万用户的情况。 - Jasper De Bruijn

2

来自php.net memory_get_usage手册

参数

real_usage 设置为TRUE以获取从系统分配的内存的实际大小。如果未设置或为FALSE,则仅报告由emalloc()使用的内存。

将此参数设置为true后,脚本没有显示出我预期的内存增加。


1

我认为你应该在循环过程中的某个时刻尝试调用mysql_free_result() — 来自评论:

值得注意的是,mysql_query()仅对SELECTSHOWEXPLAINDESCRIBE查询返回资源。

因此,在更新查询中没有结果需要释放。

无论如何,你的方法并不是最好的。尝试使用mysqli参数化语句,或者(更好的方法)直接在数据库中更新行。看起来循环中的所有SQL都可以通过一个单独的UPDATE语句处理。


循环中发生了很多事情,我只是缩短了查询以便于阅读,并关注我注意到内存增加的部分。 据我所知,我只能在循环后使用mysql_free_result()来释放$utmres,而不需要在更新查询上使用它。 - Jasper De Bruijn
2
mysql_free_result对于更新语句无效。值得注意的是,mysql_query()仅为SELECT、SHOW、EXPLAIN和DESCRIBE查询返回资源。 - OIS
1
@Jasper:当你将循环减少到纯SQL活动时,泄漏是否仍然存在?也许泄漏在其他发生的“东西”中? - Tomalak
我认为我应该开始剥离“其他东西”的部分,看看是否有帮助。但这对我来说毫无意义,因为我在这段代码之前和之后都调用了memory_get_usage函数。 - Jasper De Bruijn
1
此外,在这样的循环中使用unset();只会暂时释放内存,因为变量在每次迭代中被重新分配。你已经验证过垃圾收集器是否在你的服务器上启用了吗? - Benoit
我尝试了谷歌,但有没有简单的方法来检查垃圾收集器是否已启用? - Jasper De Bruijn

1

你可能在每次迭代中看到额外使用的内存,部分原因是PHP尚未对不再引用的内容进行垃圾回收。


好的,这是有用的信息。PHP通常会在什么时候收集它?在整个循环之后,内存使用量约为440mb,但仍在增加。稍后在页面的第二部分中,页面只是超时而没有错误,我读到可能是因为没有足够的内存来显示错误消息,即没有足够的内存。 - Jasper De Bruijn
这很奇怪,通常情况下,PHP会崩溃并显示致命的OOM错误,类似于“尝试分配Y字节内存时超过了X字节的内存限制”。 - Adriano Varoli Piazza
特别奇怪的是这一行代码 "ini_set('memory_limit', '4M');"。每100次循环我使用 "flush(); usleep(50000);",似乎可以防止页面超时。 - Jasper De Bruijn

0

unset 调用是无意义/不相关的。尝试使用 mysql_free_result - 它可能会有一些效果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接