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

我在脚本中找到内存泄漏的原因时遇到问题。我有一个简单的存储库方法,它将实体中的“计数”列递增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();
}

我在这里错过了什么?我已经尝试过,根据学说关于批处理的建议。我甚至尝试过,但问题仍然存在。->clear()gc_collect_cycles()

我在 PHP 5.5 上运行 Doctrine 2.4.6。


答案 1

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

--无需调试

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

--env=prod

还应始终指定环境。默认情况下,Symfony 将环境用于控制台命令。该环境通常未针对内存,速度,CPU等进行优化。如果要循环访问数千个项目,则可能应该使用环境(例如:)。有关详细信息,请参阅此处此处devdevprodphp bin/console <my_command> --env prod

提示: 我创建了一个名为“我”的环境,该环境是我专门为运行控制台命令而配置的。以下是有关如何创建其他 Symfony 环境的信息。console

php -d memory_limit=YOUR_LIMIT

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

用于批处理的格式正确的控制台命令

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

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

使用学说的可迭代结果

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

定期冲洗

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

您可以使用类似下面的内容每 100 次迭代刷新一次:

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

还要确保在循环结束时再次刷新(用于刷新最后< 100 个条目)。

清除实体管理器

您可能还希望在冲洗后清除:

$this->em->flush();
$em->clear();  // Detach ALL objects from Doctrine.

$this->em->flush();
$em->clear(MyEntity::class); // Detach all MyEntity from Doctrine.
$em->clear(MyRelatedEntity::class); // Detach all MyRelatedEntity from Doctrine.

随时输出内存使用情况

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

$output->writeln(memory_get_usage());

$memUse = round(memory_get_usage() / 1000000, 2).'MB';
$this->output->writeln('Processed '.$i.' of '.$totalCount.' (mem: '.$memUse.')');

滚动您自己的批次

滚动自己的批次也可能有所帮助。您可以通过使用开始和限制来执行此操作,就像使用分页一样。我能够仅使用90Mb的RAM处理400万行。

下面是一些示例代码:


protected function execute(InputInterface $input, OutputInterface $output) {
    /* ... */
    $totalCount = $this->getTotalCount();
    $batchSize = 10000;
    $i = 0;
    while ($i < $totalCount) {
        $i = $this->processBatch($i, $batchSize, $totalCount);
    }
    /* ... */
}

private function processBatch(int $start, int $limit, int $totalCount): int {
    /* @var $q \Doctrine\ORM\Query */
    $q = $this->em->createQueryBuilder()
        ->select('e')
        ->from('AcmeExampleBundle:MyEntity', 'e')
        ->setFirstResult($start)
        ->setMaxResults($limit)
        ->getQuery();

    /* @var $iterableResult \Doctrine\ORM\Internal\Hydration\IterableResult */
    $iterableResult = $q->iterate(null, \Doctrine\ORM\Query::HYDRATE_SIMPLEOBJECT);

    $i = $start;
    foreach ($iterableResult as $row) {
        /* @var $myEntity \App\Entity\MyEntity */
        $myEntity = $row[0];

        $this->processOne($myEntity);

        if (0 === ($i % 1000)) {
            $memUse = round(memory_get_usage() / 1000000, 2).'MB';
            $this->output->writeln('Processed '.$i.' of '.$totalCount.' (mem: '.$memUse.')');
        }
        $this->em->detach($row[0]);
        $i++;
    }

    return $i;
}

private function processOne(MyEntity $myEntity): void {
    // Do entity processing here.
}

private function getTotalCount(): int {
    /* @var $q \Doctrine\ORM\Query */
    $q = $this->em
        ->createQueryBuilder()
        ->select('COUNT(e.id)')
        ->from('AcmeExampleBundle:MyEntity', 'e')
        ->getQuery();

    $count = $q->getSingleScalarResult();

    return $count;
}

祝你好运!


答案 2

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


推荐