Подтвердить что ты не робот

Обработка доктрины Iterate Использование высокой памяти

Я изучал использование итераторов для пакетной обработки в 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
4b9b3361

Ответ 1

Пакетная обработка с доктриной сложнее, чем кажется, даже с помощью iterate() и IterableResult.

Так же, как вы ожидали наибольшего преимущества IterableResult, это то, что он не загружает все элементы в память, а второе преимущество в том, что оно не содержит ссылок на объекты, которые вы загружаете, таким образом IterableResult doesn ' t предотвратить GC от освобождения памяти от вашего объекта.

Однако есть еще один объект Doctrine EntityManager (более конкретно UnitOfWork), который содержит все ссылки на каждый объект, который вы запрашивали явно или неявно (EAGER ассоциации).

Простыми словами, всякий раз, когда вы получаете какой-либо объект (ы), возвращенный findAll() findOneBy() даже через DQL запросы, а также IterableResult, тогда ссылка на каждое из этих объектов сохраняется внутри доктрины. Ссылка просто хранится в массиве-помощнике, здесь псевдокод: $identityMap['Acme\Entities\Image'][0] = $image0;

Итак, поскольку на каждой итерации вашего цикла ваши предыдущие изображения (несмотря на то, что они не присутствуют в области цикла или области IterableResult), все еще присутствуют внутри этого identityMap, GC не может их очистить, а потребление памяти - так же, как когда вы звонили findAll().

Теперь откройте код и посмотрите, что на самом деле происходит

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

//здесь doctrine только создает объект Query, нет доступа db здесь

$iterable = $query->iterate(); 

//в отличие от findAll(), при этом вызове нет доступа к db.  // Здесь объект Query просто завернут в Iterator

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
}

Однако приведенный выше пример имеет несколько недостатков, хотя он представлен в документации по документации для пакетной обработки. Он отлично работает, если ваша организация Image не выполняет загрузку EAGER для любых своих ассоциаций. Но если вы EAGERly загружаете какую-либо из ассоциаций, например.

/*
  @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 `$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.

}

Так надейтесь, что это поможет немного понять итерацию доктрины.

Ответ 2

Я твердо верю, что пакетная обработка с 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

Ответ 3

Если вы комбинируете 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/

Ответ 4

Результаты могут быть похожими, поскольку клиент db может выделять дополнительную память, которую вы не видите. Также в вашем коде используется "IterableResult", который возвращает форму "$ query- > iterate()"; это позволяет обрабатывать большие результаты без проблем памяти. Просто быстрые мысли надеются, что это немного помогло.