【问题标题】:Memory leak when executing Doctrine query in loop在循环中执行 Doctrine 查询时的内存泄漏
【发布时间】: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 是的,我只是将其从示例中删除以保持我的问题简单。
  • 我认为您可以通过配置与数据库的持久连接来解决您的问题

标签: php symfony memory-leaks doctrine-orm doctrine


【解决方案1】:

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

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

  1. 我在运行命令时使用--no-debug(正如 Jonathan 已经建议的那样)
  2. 我在循环末尾添加了$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 文档 herehere

    --env=prod

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

    提示:我创建了一个名为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?能具体说明一下吗?
    • stackoverflow.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 对象)。看到这个:stackoverflow.com/questions/3325012/…
    猜你喜欢
    • 1970-01-01
    • 2014-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-08
    • 2013-11-06
    • 1970-01-01
    相关资源
    最近更新 更多