web-dev-qa-db-ja.com

Doctrineバッチ処理はハイメモリ使用量を繰り返す

Doctrine( http://docs.doctrine-project.org/en/2.0.x/reference/batch-processing)でバッチ処理にイテレータを使用することを検討してきました.html )。繰り返し処理したい画像が20,000あるデータベースがあります。

イテレータを使用すると、Doctrineがメモリ内のすべての行をロードするのを防ぐことが想定されています。ただし、2つの例のメモリ使用量はほぼ同じです。メモリ使用量の前後を計算しています。 (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

この2番目の例は、findAllメソッドを使用して結果セット全体をメモリにロードします。

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

findAllのメモリ使用量。

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

doctrineを使用したバッチ処理は、iterate()IterableResultを使用しても、見た目よりも注意が必要です。

IterableResultの最大の利点は、すべての要素をメモリにロードしないことです。2番目の利点は、ロードするエンティティへの参照を保持しないことです。したがって、IterableResultは、GCがエンティティからメモリを解放することを妨げません。

ただし、別のオブジェクトDoctrineのEntityManager(より具体的にはUnitOfWork)があり、明示的または暗黙的にクエリした各オブジェクトへのすべての参照を保持します(EAGERアソシエーション)。

簡単に言うと、DQLクエリやIterableResultを介しても、findAll()findOneBy()によって返されるエンティティを取得するたびに、それらのエンティティのそれぞれは、教義の中に保存されます。参照は単にassoc配列に格納されます。ここに疑似コードがあります:_$identityMap['Acme\Entities\Image'][0] = $image0;_

したがって、ループが繰り返されるたびに、以前の画像(ループのスコープまたはIterableResultのスコープに存在しないにも関わらず)はこのidentityMap内に依然として存在するため、GCはそれらのイメージとメモリをクリーンアップできません消費は、findAll()を呼び出したときと同じです。

それでは、コードを調べて、実際に何が起こっているのかを見てみましょう。

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

//ここdoctrineはQueryオブジェクトのみを作成し、ここにはデータベースアクセスはありません

_$iterable = $query->iterate(); 
_

// findAll()とは異なり、この呼び出しではdbアクセスは発生しません。 //ここでは、Queryオブジェクトは単純にイテレータでラップされています

_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()`.
_

したがって、最初の解決策は、実際にDoctrine EntityManagerにオブジェクトを_$identityMap_から切り離すように指示することです。また、whileループを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
}
_

ただし、上記の例は バッチ処理に関するdoctrineのドキュメント で取り上げられていますが、欠陥はほとんどありません。エンティティ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()を使用して、IDマップを完全にクリアすることです。

_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.

}
_

したがって、これがdoctrine反復を少し理解するのに役立つことを願っています。

42
Dimitry K

DoctrineまたはMySQL(PDOまたはmysqli)を使用したあらゆる種類の反復によるバッチ処理は単なる幻想であると強く信じています。

@ dimitri-kは、特に作業単位について素晴らしい説明を提供しました。問題は、ミスリードです: "$ query-> iterate()"これは、データソースを実際には反復しません。それは単なる\ Traversableラッパー既に完全にフェッチされたデータソースです。

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

4
Lukas Lukac

doctrine 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/ を参照してください

2
Samuel James