在循环中执行 Doctrine 查询时的内存泄漏

Posted

技术标签:

【中文标题】在循环中执行 Doctrine 查询时的内存泄漏【英文标题】:Memory leak when executing Doctrine query in loop 【发布时间】:2014-12-24 08:39:59 【问题描述】:

我无法在脚本中找到内存泄漏的原因。我有一个简单的存储库方法,它将我的实体中的“计数”列增加 X 数量:

public function incrementCount($id, $amount)

    $query = $this
        ->createQueryBuilder('e')
        ->update('MyEntity', 'e')
        ->set('e.count', 'e.count + :amount')
        ->where('e.id = :id')
        ->setParameter('id', $id)
        ->setParameter('amount', $amount)
        ->getQuery();

    $query->execute();

问题是,如果我在循环中调用它,每次迭代时内存使用量都会激增:

$entityManager = $this->getContainer()->get('doctrine')->getManager();
$myRepository = $entityManager->getRepository(MyEntity::class);
while (true) 
    $myRepository->incrementCount("123", 5);
    $doctrineManager->clear();
    gc_collect_cycles();

我在这里缺少什么?根据 Doctrine 的 advice on batch processing,我已经尝试过 ->clear()。我什至尝试过gc_collect_cycles(),但问题仍然存在。

我在 php 5.5 上运行 Doctrine 2.4.6。

【问题讨论】:

在你的函数中尝试作为最后一行$query->clear(); 你将无限循环使用 while(true);是否有休息、返回或退出某处? @Mihai 这只是给出了一个Call to undefined method Doctrine\ORM\Query::clear() 错误。 @HalayemAnis 是的,我只是将其从示例中删除以保持我的问题简单。 我认为您可以通过配置与数据库的持久连接来解决您的问题 【参考方案1】:

我遇到过类似的内存泄漏问题。我在 Symfony 5.2 项目中运行 Doctrine。更具体地说,我构建了一个永无止境的命令,它处理一个表中的条目,从另一个表中检索条目,并在其他表中创建 2 个新条目。 (事件处理)

我分两步解决了我的泄漏问题。

    我在运行命令时使用--no-debug(正如 Jonathan 已经建议的那样) 我在循环末尾添加了$this->entityManager->clear();

为了查看和识别泄漏,我使用以下行来输出当前的内存使用情况:

$output->writeln('Memory Usage in MB: ' . memory_get_usage() / 1024 / 1024);

也许这有助于任何仍在与泄漏作斗争的人。

【讨论】:

【参考方案2】:

我刚刚遇到了同样的问题,这些是为我解决的问题:

--无调试

正如 OP 在他们的回答中提到的,设置 --no-debug(例如:php app/console <my_command> --no-debug)对于 Symfony 控制台命令中的性能/内存至关重要。在使用 Doctrine 时尤其如此,因为没有它,Doctrine 将进入调试模式,这会消耗大量额外的内存(每次迭代都会增加)。有关更多信息,请参阅 Symfony 文档 here 和 here。

--env=prod

您还应该始终指定环境。默认情况下,Symfony 使用dev 环境来执行控制台命令。 dev 环境通常没有针对内存、速度、cpu 等进行优化。如果您想迭代数千个项目,您可能应该使用prod 环境(例如:php app/console <my_command> --env prod)。请参阅here 和here 了解更多信息。

提示:我创建了一个名为console 的环境,我专门为运行控制台命令配置了该环境。这是关于how to create additional Symfony environments的信息。

php -d memory_limit=YOUR_LIMIT

如果运行大更新,您可能应该选择可接受的内存消耗量。如果您认为可能存在泄漏,这一点尤其重要。您可以使用php -d memory_limit=x(例如:php -d memory_limit=256M)为命令指定内存。注意:您可以将限制设置为-1(通常是 php cli 的默认值)以让命令在没有内存限制的情况下运行,但这显然很危险。

用于批处理的格式良好的控制台命令

使用上述提示运行大更新的格式良好的控制台命令如下所示:

php -d memory_limit=256M app/console <acme>:<your_command> --env=prod --no-debug

使用 Doctrine 的 IterableResult

在循环中使用 Doctrine 的 ORM 时,另一个重要的问题是使用 Doctrine 的 IterableResult(参见 Doctrine Batch Processing docs)。这在提供的示例中无济于事,但通常在进行这样的处理时,它会超出查询的结果。

定期刷新

如果您正在做的部分工作是对数据进行更改,则应定期刷新,而不是在每次迭代时刷新。冲洗既昂贵又缓慢。刷新的频率越低,命令完成的速度就越快。但是请记住,Doctrine 会将未刷新的数据保存在内存中。因此,您刷新的频率越低,您需要的内存就越多。

您可以使用类似以下的方法每 100 次迭代刷新一次:

if ($count % 100 === 0) 
    $this->em->flush();

还要确保在循环结束时再次刷新(用于刷新最后

输出内存使用情况

跟踪您的命令在运行时消耗了多少内存非常有帮助。您可以通过输出 PHP 内置的 memory_get_usage() 函数返回的响应来做到这一点。

祝你好运!

【讨论】:

对我来说 $this->entityManager->getConnection()->getConfiguration()->setSQLLogger(null);修复了问题【参考方案3】:

对我来说,这是清除学说,或者如文档所述,分离所有实体:

$this->em->clear(); //Here em is the entity manager.

所以在我的循环中,每 1000 次迭代刷新一次并分离所有实体(我不再需要它们):

    foreach ($reader->getRecords() as $position => $value) 
        $this->processValue($value, $position);
        if($position % 1000 === 0)
            $this->em->flush();
            $this->em->clear();
        
        $this->progress->advance();
    

希望这会有所帮助。

PS:here's the documentation.

【讨论】:

当您没有复杂的引用并且不创建连接到单个对象的实体时,这就像魅力一样。但在清除后,如果这足以进行批处理,您可能总是重新创建参考或部分参考。 这是我所有问题的解决方案。而且我只读取数据,从不写入任何数据。 $this->em->clear(MyClass::class); 有助于仅清除类的实体,因为我想在管理器中保留一些其他类。如其他解决方案中所述,我还需要 --no-debug。【参考方案4】:

Doctrine 会记录您所做的任何查询。如果您进行大量查询(通常发生在循环中),Doctrine 可能会导致巨大的内存泄漏。

您需要禁用 Doctrine SQL Logger 来克服这个问题。

我建议仅对循环部分执行此操作。

在循环之前,获取当前记录器:

$sqlLogger = $em->getConnection()->getConfiguration()->getSQLLogger();

然后禁用 SQL Logger:

$em->getConnection()->getConfiguration()->setSQLLogger(null);

在这里循环: foreach() / while() / for()

循环结束后,放回Logger:

$em->getConnection()->getConfiguration()->setSQLLogger($sqlLogger);

【讨论】:

这就是 --no-debug 选项正在做的事情。【参考方案5】:

我通过在我的命令中添加 --no-debug 解决了这个问题。事实证明,在调试模式下,分析器将有关每个查询的信息存储在内存中。

【讨论】:

每个学说查询都会增加大约 4k 的内存使用量。这为我解决了问题。 我应该在哪里添加--no-debug?能具体说明一下吗? ***.com/a/10913115/3757139 展示了如何使用 $em->getConnection()->getConfiguration()->setSQLLogger(null); 以编程方式禁用 SQL 日志记录【参考方案6】:

每次迭代都在浪费内存。更好的方法是准备查询一次并交换参数多次。例如:

class MyEntity extends EntityRepository
    private $updateQuery = NULL;

    public function incrementCount($id, $ammount)
    
        if ( $this->updateQuery == NULL )
            $this->updateQuery = $this->createQueryBuilder('e')
                ->update('MyEntity', 'e')
                ->set('e.count', 'e.count + :amount')
                ->where('e.id = :id')
                ->getQuery();
        

        $this->updateQuery->setParameter('id', $id)
                ->setParameter('amount', $amount);
                ->execute();
    

正如您所提到的,您可以在此处使用批处理,但先尝试一下,看看性能如何(如果有的话)...

【讨论】:

感谢您的回答,但不幸的是,这对内存泄漏没有影响。我预计这是因为$updateQuery 变量已经被垃圾收集器清理了。尽管如此,您的建议应该会使事情变得更快,所以我会实施它。还有其他想法吗?! 我没想到会这样。我们在这里谈论多少次迭代? 最多 10,000 次迭代。 好的,我认为这需要原生查询(通过普通的Connection 对象)。看到这个:***.com/questions/3325012/…

以上是关于在循环中执行 Doctrine 查询时的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

在 Apache httpd 进程中定位内存泄漏,基于 PHP/Doctrine 的应用程序

Symfony 2 Doctrine 内存使用情况

使用控制台应用程序报告关闭时的内存泄漏

在循环中执行移位操作时C ++中的内存泄漏

python中循环引用导致内存泄漏小案例

QTcpSocket 在工作进程中连续写入。避免内存泄漏的最佳实践