Doctrine 条令批处理迭代高内存使用率

Doctrine 条令批处理迭代高内存使用率,doctrine,batch-processing,Doctrine,Batch Processing,我一直在研究在Doctrine()中使用迭代器进行批处理。我有一个包含20000张图片的数据库,我想对它进行迭代 我知道使用迭代器应该可以防止条令加载内存中的每一行。然而,两个示例之间的内存使用几乎完全相同。我正在计算使用前后的内存使用量(memory\u get\u usage()/1024) 迭代器的内存使用情况 Memory usage before: 2823.36328125 KB Memory usage after: 50965.3125 KB 第二个示例使用findAll方法将

我一直在研究在Doctrine()中使用迭代器进行批处理。我有一个包含20000张图片的数据库,我想对它进行迭代

我知道使用迭代器应该可以防止条令加载内存中的每一行。然而,两个示例之间的内存使用几乎完全相同。我正在计算使用
前后的内存使用量(memory\u get\u usage()/1024)

迭代器的内存使用情况

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

结果可能类似,因为db客户端可能正在分配您看不到的额外内存。您的代码还使用了“IterableResult”,它是从“$query->iterate()”返回的;这允许在没有内存问题的情况下处理大型结果。快速思考一下,希望能有所帮助。

使用条令进行批处理比看起来要复杂,即使使用
iterate()
IterableResult
也不例外

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

但是,还有另一个对象原则的
EntityManager
(更具体地说是
UnitOfWork
),它保存对您显式或隐式查询的每个对象的所有引用(
EAGER
关联)

简单地说,只要您获得
findAll()
findOneBy()
返回的任何实体,即使是通过
DQL
查询和
IterableResult
,对这些实体的引用也会保存在条令中。引用仅存储在assoc数组中,以下是伪代码:
$identityMap['Acme\Entities\Image'][0]=$image0

因此,因为在循环的每次迭代中,您以前的映像(尽管不在循环的作用域或
IterableResult
的作用域中)仍然存在于
identityMap
中,GC无法清理它们,并且您的内存消耗与调用
findAll()
时相同

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

$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i'); 
//这里只创建查询对象,这里没有数据库访问

$iterable = $query->iterate(); 
//与findAll()不同,在调用时不会发生数据库访问。 //这里,查询对象被简单地包装在一个迭代器中

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()`.
然而,上面的示例几乎没有缺陷,尽管它在。如果您的实体
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()
完全清除标识映射

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->clear();
    // now ``$identityMap` will be cleared of ALL entities it has
    // the `Image` the `User` loaded in this loop iteration and as as
    // SIDE EFFECT all OTHER Entities which may have been loaded by you
    // earlier. Thus you when you start this loop you must NOT rely
    // on any entities you have `persist()`ed or `remove()`ed 
    // all changes since the last `flush()` will be lost.

}

因此,希望这有助于理解条令迭代。

我坚信使用条令进行批处理或使用MySQL进行任何类型的迭代(PDO或mysqli)都只是一种幻觉

@dimitri-k提供了一个很好的解释,特别是关于工作单元的解释。问题在于未命中引导:“$query->iterate()”,它实际上不会在数据源上进行迭代。它只不过是一个\Traversable wrapper环绕已完全获取的数据源

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

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原则或DQL或纯SQL来流式处理繁重的表。然而,您觉得合适:

如果您将doctrine
iterate()
与批处理策略相结合,您应该能够在大型记录上进行迭代

例如:


$batchSize=1000;
$numberOfRecordsPerPage=5000;
$totalRecords=$queryBuilder->select('count(u.id'))
->from('从Acme\Entities\Image i'中选择i')
->getQuery()
->getSingleScalarResult()//获取要迭代的总记录
$totalRecordsProcessed=0;
$processing=true;
while($processing){
$query=$entityManager->createQuery('从Acme\Entities\Image i'中选择i')
->setMaxResults($numberOfRecordsPerPage)//一次获取的最大记录数
->setFirstResult($totalRecordsProcessed);
$iterableResult=$query->iterate();
while(($row=$iterableResult->next())!==false){
$image=$row[0];
$image->updateMethingImportant();
如果($totalProcessed%$batchSize)==0){
$entityManager->flush();
$entityManager->clear();
}
$totalProcessed++;
}
如果($totalProcessed===$totalRecords){
打破
}
}
$entityManager->flush();

请参见

谢谢您的详细解释。根据我的经验,我将添加
$iterable=$query->iterate()也会消耗大量内存@dimitri__k感谢您提供的详细信息,顺便问一下,有没有一种方法可以将Hyde_标量与迭代器一起使用?如果是,那么这是否是处理此
identityMap
问题的另一个备选方案?@Stphane这是一个使用HYDRATE\u SCALAR的有趣建议。起初,我认为这不是一个可行的选择,因为当你使用Hydrou标量的时候,你不会这么做
Starting with memory usage: 6 MB 
Ending with memory usage: 109.46875 MB
namespace Doctrine\DBAL\Driver\Mysqli\MysqliStatement

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

    return new \ArrayIterator($data);
}