Doctrine 条令批处理迭代高内存使用率
我一直在研究在Doctrine()中使用迭代器进行批处理。我有一个包含20000张图片的数据库,我想对它进行迭代 我知道使用迭代器应该可以防止条令加载内存中的每一行。然而,两个示例之间的内存使用几乎完全相同。我正在计算使用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方法将
前后的内存使用量(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来流式处理繁重的表。然而,您觉得合适:如果您将doctrineiterate()
与批处理策略相结合,您应该能够在大型记录上进行迭代
例如:
$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);
}