Doctrine Batch Processing迭代高内存使用率

时间:2014-05-08 15:12:59

标签: doctrine batch-processing

我一直在研究在Doctrine(http://docs.doctrine-project.org/en/2.0.x/reference/batch-processing.html)中使用迭代器进行批处理。我有一个包含20,000张图像的数据库,我想迭代一遍。

我知道使用迭代器应该会阻止Doctrine加载内存中的每一行。但是,两个示例之间的内存使用情况几乎完全相同。我正在计算使用(memory_get_usage() / 1024)之前和之后的内存使用情况。

$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i');
$iterable = $query->iterate();

while (($image = $iterable->next()) !== false) {
    // Do something here!
}

迭代器的内存使用情况。

Memory usage before: 2823.36328125 KB
Memory usage after: 50965.3125 KB

第二个示例使用findAll方法将整个结果集加载到内存中。

$images = $this->em->getRepository('Acme\Entities\Image')->findAll();

findAll的内存使用情况。

Memory usage before: 2822.828125 KB
Memory usage after: 51329.03125 KB

5 个答案:

答案 0 :(得分:34)

即使在iterate()IterableResult的帮助下,使用学说进行批处理也比看起来更复杂。

正如您所期望的那样IterableResult的最大好处是它不会将所有元素加载到内存中,第二个好处是它不会保存对您加载的实体的引用,因此{ {1}}不会阻止GC从您的实体中释放内存。

然而,还有另一个对象Doctrine的IterableResult(更具体地说是EntityManager),其中包含您明确或隐含查询的每个对象的所有引用(UnitOfWork协会)。

简单来说,每当您获得EAGER findAll()即使通过findOneBy()查询和DQL返回的任何实体时,也会引用每个实体实体保存在学说中。引用只是存储在一个assoc数组中,这里是伪代码: IterableResult

因此,在循环的每次迭代中,您之前的图像(尽管不存在于循环的范围或$identityMap['Acme\Entities\Image'][0] = $image0;的范围内)仍然存在于此{{1 GC无法清除它们,您的内存消耗与调用IterableResult时的内存消耗相同。

现在让我们浏览一下代码,看看实际发生了什么

identityMap

//这里doctrine只创建Query对象,这里没有数据库访问

findAll()

//与findAll()不同,在此调用时不会发生数据库访问。    //这里Query对象只包含在Iterator

$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i'); 

所以第一个解决方案是实际告诉Doctrine EntityManager将对象与$iterable = $query->iterate(); 分离。我还将while (($image_row = $iterable->next()) !== false) { // now upon the first call to next() the DB WILL BE ACCESSED FOR THE FIRST TIME // the first resulting row will be returned // row will be hydrated into Image object // ----> REFERENCE OF OBJECT WILL BE SAVED INSIDE $identityMap <---- // the row will be returned to you via next() // to access actual Image object, you need to take [0]th element of the array $image = $image_row[0]; // Do something here! write_image_data_to_file($image,'myimage.data.bin'); //now as the loop ends, the variables $image (and $image_row) will go out of scope // and from what we see should be ready for GC // however because reference to this specific image object is still held // by the EntityManager (inside of $identityMap), GC will NOT clean it } // and by the end of your loop you will consume as much memory // as you would have by using `findAll()`. 循环替换为$identityMap以使其更具可读性。

while

然而,上面的例子有很少的缺陷,即使它在doctrine's documentation on batch processing中有特色。如果您的实体foreach未对其中的任何关联执行foreach($iterable as $image_row){ $image = $image_row[0]; // do something with the image write_image_data_to_file($image); $entity_manager->detach($image); // this line will tell doctrine to remove the _reference_to_the_object_ // from identity map. And thus object will be ready for GC } 加载,则效果很好。但是,如果您正在EAGERly加载任何关联,例如。 :

Image

因此,如果我们使用与上面相同的代码,那么

EAGER

可能的解决方案是使用/* @ORM\Entity */ class Image { /* @ORM\Column(type="integer") @ORM\Id */ private $id; /* @ORM\Column(type="string") */ private $imageName; /* @ORM\ManyToOne(targetEntity="Acme\Entity\User", fetch="EAGER") This association will be automatically (EAGERly) loaded by doctrine every time you query from db Image entity. Whether by findXXX(),DQL or iterate() */ private $owner; // getters/setters left out for clarity } 代替foreach($iterable as $image_row){ $image = $image_row[0]; // here becuase of EAGER loading, we already have in memory owner entity // which can be accessed via $image->getOwner() // do something with the image write_image_data_to_file($image); $entity_manager->detach($image); // here we detach Image entity, but `$owner` `User` entity is still // referenced in the doctrine's `$identityMap`. Thus we are leaking memory still. } EntityManager::clear(),这将完全清除身份地图。

EntityManager::detach()

所以希望这有助于理解教义迭代。

答案 1 :(得分:2)

我坚信使用Doctrine进行批处理或使用MySQL(PDO或mysqli)进行任何迭代都只是一种错觉。

@ dimitri-k提供了一个很好的解释,特别是关于工作单元。问题是导致错过:&#34; $ query-&gt; iterate()&#34;它并没有真正迭代数据源。已经完全获取数据源的只是一个\ Traversable wrapper

一个例子表明即使从图片中完全删除Doctrine抽象层,我们仍会遇到内存问题

echo 'Starting with memory usage: ' . memory_get_usage(true) / 1024 / 1024 . " MB \n";

$pdo  = new \PDO("mysql:dbname=DBNAME;host=HOST", "USER", "PW");
$stmt = $pdo->prepare('SELECT * FROM my_big_table LIMIT 100000');
$stmt->execute();

while ($rawCampaign = $stmt->fetch()) {
    // echo $rawCampaign['id'] . "\n";
}

echo 'Ending with memory usage: ' . memory_get_usage(true) / 1024 / 1024 . " MB \n";

<强>输出:

Starting with memory usage: 6 MB 
Ending with memory usage: 109.46875 MB

这里,令人失望的 getIterator()方法:

namespace Doctrine\DBAL\Driver\Mysqli\MysqliStatement

/**
 * {@inheritdoc}
 */
public function getIterator()
{
    $data = $this->fetchAll();

    return new \ArrayIterator($data);
}

您可以使用我的小库来实际使用PHP Doctrine或DQL或纯SQL来传输繁重的表。但是你找到了合适的:https://github.com/EnchanterIO/remote-collection-stream

答案 2 :(得分:0)

如果将原则iterate()与批处理策略结合使用,则应该能够遍历大型记录。

例如:


$batchSize = 1000;
$numberOfRecordsPerPage = 5000;

$totalRecords = $queryBuilder->select('count(u.id)')
            ->from('SELECT i FROM Acme\Entities\Image i')
            ->getQuery()
            ->getSingleScalarResult();   //Get total records to iterate on

$totalRecordsProcessed = 0;

        $processing = true;

        while ($processing) {
            $query = $entityManager->createQuery('SELECT i FROM Acme\Entities\Image i')
                ->setMaxResults($numberOfRecordsPerPage) //Maximum records to fetch at a time
                ->setFirstResult($totalRecordsProcessed);

             $iterableResult = $query->iterate();

            while (($row = $iterableResult->next()) !== false) {
                $image = $row[0];
                $image->updateSomethingImportant();

                 if (($totalProcessed % $batchSize ) === 0) {
                    $entityManager->flush();
                    $entityManager->clear();
                }
                $totalProcessed++;
            }
            if ($totalProcessed === $totalRecords) {
                break;
            }
        }

    $entityManager->flush();


请参见https://samuelabiodun.com/how-to-update-millions-of-records-with-doctrine-orm/

答案 3 :(得分:0)

tl;博士;

运行命令时,使用 --no-debug 或将 Sql 记录器设置为 null 以防止它保存它运行的所有查询。

时不时使用EntityManager::clear(),内存泄漏几乎为零。

答案 4 :(得分:-1)

结果可能类似,因为db客户端可能正在分配您无法看到的额外内存。您的代码也使用'IterableResult',它返回'$ query-&gt; iterate()';这允许处理大的结果而没有内存问题。只是快速的想法希望它有所帮助。