сборщик мусора с ограничением памяти php

3 дня разбился головой о стену.

Я разработал php-скрипт для импорта больших текстовых файлов и заполнения базы данных mysql. Пока я не получу 2 миллиона записей, он работает отлично, но мне нужно импортировать около 10 миллионов строк, разделенных на разные файлы.

Мое приложение сканирует файлы в папке, получает расширение файла (у меня есть 4 вида импорта процедур для 4 разных расширений) и вызывает функцию относительного импорта.

У меня есть структура из этих классов:

CLASS SUBJECT1{ public function import_data_1(){
    __DESTRUCT(){$this->childObject = null;}
    IMPORT SUBJECT1(){
    //fopen($file);
    //ob_start();
    //PDO::BeginTransaction();
    //WHILE (FILE) {
    //PREPARED STATEMENT        
    //FILE READING
    //GET FILE LINE
    //EXECUTE INSERT
    //} END WHILE
    //PDO::Commit();
    //ob_clean(); or ob_flush();
    //fclose($file);
    //clearstatcache();
   }
};}

CLASS SUBJECT2{ same as SUBJECT1;}

CLASS SUBJECT3{ same as SUBJECT1;}

CLASS SUBJECT4{ same as SUBJECT1;}

и основной класс, запускающий процедуру:

CLASS MAIN{
   switch($ext)
     case "ext1":
        $SUBJECT1 = new SUBJECT1();
        IMPORT_SUBJECT1();
        unset $SUBJECT1;
        $SUBJECT1 = null;
        break;
     case "ext2": //SAME AS CASE ext1 WITH IMPORT_SUBJECT2();
     case "ext3": //SAME AS CASE ext1 WITH IMPORT_SUBJECT3();
     case "ext4": //SAME AS CASE ext1 WITH IMPORT_SUBJECT4();

}

Он отлично работает с некоторой настройкой файловых буферов mysql (ib_logfile0 и ib_logfile1 установлены как 512 МБ).

Проблема в том, что каждый раз, когда процедура завершается, php не освобождает память. Я уверен, что вызывается деструктор (я помещаю эхо в метод __destruct), и объект недоступен (var_dump говорит, что это NULL). Я пробовал так много способов освободить память, но теперь я в мертвой точке.

Я также проверил gc_collect_cycles () во многих разных точках кода, и он всегда говорит 0 циклов, поэтому все abject не ссылаются друг на друга. Я пытался даже удалить структуру классов и вызвать весь код последовательно, но всегда получаю такую ​​ошибку:

Неустранимая ошибка: недостаточно памяти (выделено 511180800) (попытка выделить 576 байт) в C: \ php \ index.php в строке 219 (строка 219 выполняет PS в 13-м файле).

Память используется таким образом:

  • php скрипт: 52 МБ
  • конец первого импорта файла: 110 МБ
  • деструкторы и вызов unset: 110 МБ
  • вызов новой процедуры: 110 МБ
  • конец импорта второго файла 250 МБ
  • деструкторы и вызов unset: 250 МБ
  • вызов новой процедуры: 250 МБ

Итак, как вы можете видеть, даже сбрасывая объекты, они не освобождают память.

Я попытался установить размер памяти php ini на 1024 МБ, но он очень быстро растет и вылетает после 20 файлов.

Любой совет?

Большое спасибо!

РЕДАКТИРОВАТЬ 1:

почтовый индекс:

class SUBJECT1{

    public function __destruct()
    {
        echo 'destroying subject1 <br/>';
    }

    public function import_subject1($file,$par1,$par2){
        global $pdo;

        $aux            = new AUX();
        $log            = new LOG();

// ---------------- FILES  ----------------
        $input_file    = fopen($file, "r");

// ---------------- PREPARED STATEMENT  ----------------

$PS_insert_data1= $pdo->prepare("INSERT INTO table (ID,PAR1,PAR2,PARN) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE ID = VALUES(ID), PAR1 = VALUES(PAR1), PAR2 = VALUES(PAR2), PAR3 = VALUES(PAR3), PARN = VALUES(PARN)");

$PS_insert_data2= $pdo->prepare("INSERT INTO table (ID,PAR1,PAR2,PARN) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE ID = VALUES(ID), PAR1 = VALUES(PAR1), PAR2 = VALUES(PAR2), PAR3 = VALUES(PAR3), PARN = VALUES(PARN)");

//IMPORT
if ($input_file) {
  ob_start();
  $pdo->beginTransaction();
  while (($line = fgets($input_file)) !== false) {
  $line = utf8_encode($line);
  $array_line = explode("|", $line);
  //set null values where i neeed
  $array_line = $aux->null_value($array_line);

  if(sizeof($array_line)>32){    
     if(!empty($array_line[25])){
          $PS_insert_data1->execute($array_line[0],$array_line[1],$array_line[2],$array_line[5]);
     }

  $PS_insert_data2->execute($array_line[10],$array_line[11],$array_line[12],$array_line[15]);
  }

$pdo->commit();    
flush();
ob_clean();
fclose($f_titolarita);
clearstatcache();
}

Я делаю это итеративно для всех файлов в моей папке, остальные процедуры имеют ту же концепцию. У меня все еще есть увеличение памяти, и теперь он вылетает с ответом белой страницы: - \


person SBO    schedule 15.12.2014    source источник
comment
php.net/manual/en/function.memory-get-usage. php может быть полезно посмотреть, что происходит с вашей памятью   -  person Jelle Keizer    schedule 15.12.2014
comment
unset () не удаляет их из памяти. Это потому, что unset () не запускает деструктор объекта.   -  person Sunhat    schedule 15.12.2014
comment
@Jelle то, что вы видите в списке, сообщает memory_get_usage ();   -  person SBO    schedule 15.12.2014
comment
@iWontStop да, я знаю об этом, но перед сбросом обязательно вызывается диструктор. В любом случае я пробовал с unset, destruct и null и без него, почему он переходит в переполнение?   -  person SBO    schedule 15.12.2014
comment
Какая у вас версия php?   -  person Jelle Keizer    schedule 15.12.2014
comment
Вы пробовали вызвать $ SUBJECT1 = null; перед сбросом $ SUBJECT1;   -  person Jelle Keizer    schedule 15.12.2014
comment
Да, я пробовал обнулить до того, как сбросить, результат тот же.   -  person SBO    schedule 16.12.2014
comment
Я использую Php 5.4.16 с Mysql 5.5   -  person SBO    schedule 16.12.2014


Ответы (2)


Лично я бы сказал немного иначе. Вот шаги, которые я бы сделал:

  • Откройте соединение PDO, установите PDO в режим исключения
  • Получите список файлов, которые я хочу прочитать
  • Создайте класс, который может использовать PDO и список файлов и выполнять вставки
  • Подготовьте заявление ОДИН РАЗ, используйте его много раз
  • Транзакция Chunk PDO фиксируется до 50 (настраиваемых) вставок - это означает, что каждый 50-й раз, когда я вызываю $ stmt-> execute (), я выполняю фиксацию, которая лучше использует жесткий диск, что делает его быстрее
  • Прочтите каждый файл построчно
  • Разберите строку и проверьте, действительна ли она
  • Если да, добавить в MySQL, если нет - сообщить об ошибке

Теперь я создал 2 класса и пример того, как я буду это делать. Я тестировал только часть чтения, так как я не знаю ни структуру вашей БД, ни то, что делает AUX ().

class ImportFiles
{
    protected $pdo;
    protected $statements;
    protected $transaction = false;
    protected $trx_flush_count = 50; // Commit the transaction at every 50 iterations

    public function __construct(PDO $pdo = null)
    {
        $this->pdo = $pdo;

        $this->stmt = $this->pdo->prepare("INSERT INTO table 
                                                (ID,PAR1,PAR2,PARN)
                                            VALUES 
                                                (?,?,?,?) 
                                            ON DUPLICATE KEY UPDATE ID = VALUES(ID), PAR1 = VALUES(PAR1), PAR2 = VALUES(PAR2), PAR3 = VALUES(PAR3), PARN = VALUES(PARN)");
    }

    public function import($file)
    {
        if($this->isReadable($file))
        {
            $file = new FileParser($file);

            $this->insert($file);
        }
        else
        {
            printf("\nSpecified file is not readable: %s", $file);
        }
    }

    protected function isReadable($file)
    {
        return (is_file($file) && is_readable($file));
    }   

    protected function insert(FileParser $file)
    {
        while($file->read())
        {
            //printf("\nLine %d, value: %s", $file->getLineCount(), $file->getLine());

            $this->insertRecord($file);

            $this->flush($file);
        }

        $this->flush(null);
    }

    // Untested method, no idea whether it does its job or not - might fail
    protected function flush(FileParser $file = null)
    {
        if(!($file->getLineCount() % 50) && !is_null($file))
        {
            if($this->pdo->inTransaction())
            {
                $this->pdo->commit();

                $this->pdo->beginTransaction();
            }
        }
        else
        {
            if($this->pdo->inTransaction())
            {
                $this->pdo->commit();
            }
        }
    }   

    protected function insertRecord(FileParser $file)
    {
        $check_value = $file->getParsedLine(25);

        if(!empty($check_value))
        {
            $values = [ 
                $file->getParsedLine[0],
                $file->getParsedLine[1],
                $file->getParsedLine[2],
                $file->getParsedLine[5]
            ];
        }
        else
        {
            $values = [ 
                $file->getParsedLine[10],
                $file->getParsedLine[11],
                $file->getParsedLine[12],
                $file->getParsedLine[15]
            ];      
        }

        $this->stmt->execute($values);
    }
}

class FileParser
{
    protected $fh;
    protected $lineCount = 0;
    protected $line = null;
    protected $aux;

    public function __construct($file)
    {
        $this->fh = fopen($file, 'r');
    }

    public function read()
    {
        $this->line = fgets($this->fh);

        if($this->line !== false) $this->lineCount++;

        return $this->line;
    }

    public function getLineCount()
    {
        return $this->lineCount;
    }

    public function getLine()
    {
        return $this->line;
    }

    public function getParsedLine($index = null)
    {
        $line = $this->line;

        if(!is_null($line))
        {
            $line = utf8_encode($line);
            $array_line = explode("|", $line);

            //set null values where i neeed
            $aux = $this->getAUX();
            $array_line = $aux->null_value($array_line);

            if(sizeof($array_line) > 32)
            {   
                return is_null($index) ? $array_line : isset($array_line[$index]) ? $array_line[$index] : null;
            }
            else
            {
                throw new \Exception(sprintf("Invalid array size, expected > 32 got: %s", sizeof($array_line)));
            }
        }
        else
        {
            return [];
        }
    }

    protected function getAUX()
    {
        if(is_null($this->aux))
        {
            $this->aux = new AUX();
        }

        return $this->aux;
    }
}

Использование:

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

try 
{
    $pdo = new PDO($dsn, $user, $password);

    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $import = new ImportFiles($pdo);

    $files = ['/usr/local/file1.txt', '/usr/local/file2.txt'];

    foreach($files as $file)
    {
        $import->import($file);
    }

} catch (Exception $e) 
{
    printf("\nError: %s", $e->getMessage());
    printf("\nFile: %s", $e->getFile());
    printf("\nLine: %s", $e->getLine());
}
person N.B.    schedule 16.12.2014
comment
Хм, я делаю то же самое, за исключением того, что использую fopen с fgets для получения строк вместо FileParser. Я использую то же соединение pdo, включенное в индексный файл, и готовлю операторы ONCE, повторяющие выполнение в цикле while. Я не сообщал об исключении из псевдокода и отслеживании журнала, но у меня есть процедуры на случай сбоя. Я не делаю коммиты каждые 50 записей, потому что это медленнее, и проблема не в размере файла, потому что происходит сбой после 30-40 файлов. Я действительно не знаю, почему память растет (теперь медленнее, чем раньше), но мне нужно импортировать ~ 10 миллионов записей. Направляясь к стене. - person SBO; 16.12.2014
comment
Вы читаете, анализируете и фиксируете весь файл. Ваш метод подготовки операторов подготавливает один и тот же запрос дважды. Если вы вызываете import_subject1 для каждого файла, это происходит для каждого файла (подготовка оператора). Теперь, если ваш файл, скажем, 100 МБ - у вас будет столько данных, которые вы хотите сбросить за одну фиксацию. То, что я сделал по-другому, выбрано так, чтобы у меня была скорость, с которой я смываюсь. Я сконструировал код по-другому, чтобы четко разграничить, что и что делает. В любом случае, чтение построчно и сброс сразу 50, вместо этого все экономит память. - person N.B.; 16.12.2014
comment
Почему вы говорите, что я дважды подготовил заявление? Для абстрактных разделений у меня есть другие проблемы из-за другого Prepared_statement, потому что, когда я разбираю каждую строку, один бит говорит мне, какой PS я должен использовать. Да и вообще, можно ли промыть PS вручную? Большое спасибо за вашу помощь! - person SBO; 16.12.2014
comment
Если я не ошибаюсь, вы подготовите то же самое в $PS_insert_data1 и $PS_insert_data2. Эта подготовка выполняется каждый раз, когда вы вызываете метод import_subject1. Заявления должны быть подготовлены один раз, использоваться несколько раз. Вы также используете глобальную переменную для PDO вместо того, чтобы просто вводить ее в класс / метод. В любом случае, вы не можете сбросить подготовленный оператор вручную. Когда я говорю «очистить» для подготовленного оператора, я имею в виду фиксацию. - person N.B.; 16.12.2014
comment
Ну, в псевдокоде PS_insert_data1 и PS_insert_data2 имеют одинаковые данные внутри, но parN означает больше n-параметров, это, очевидно, разные утверждения. Насчет глобальной переменной вы правы, но я всегда видел использование global $ pdo в примерах, включенных в файл подключения. Так что, пожалуйста, не могли бы вы написать мне простой пример, как использовать pdo не как глобальную переменную? потому что от твоих многочисленных слов в ответ мне не выбраться! Большое спасибо за ваше время! - person SBO; 17.12.2014
comment
Что ж, я не могу знать, псевдокод это или нет, если он не упоминается явно :) однако факт остается фактом: для каждого import_subject вызова метода вы готовите два оператора, когда вы этого не делаете. должны - вы должны сделать это только один раз. Кроме того, ваши транзакции не фиксируются, пока файл не будет полностью прочитан. Вы можете себе представить, что произойдет, если у вас есть, скажем, файл размером 100 МБ. PHP не зря выделяет память. Разделение этих сбросов (коммитов) помогает с распределением памяти. Я бы хотя бы попробовал подход с фрагментированной фиксацией, чтобы увидеть, не упадет ли память. - person N.B.; 17.12.2014
comment
Что ж, может быть, я раньше не понимал вашего ответа, но я имею в виду, что для той же записи мне нужно заполнить 4 таблицы, поэтому мне нужно 4 подготовленных оператора, не так ли? - person SBO; 17.12.2014
comment
P.s. Мне нужно получить файлы в порядке пути, я не могу обработать все .ext1, затем .ext2 и так далее. Вот почему я создаю объект и уничтожаю его в каждом случае. - person SBO; 17.12.2014
comment
В вашем вопросе говорится, что вы имеете дело с несколькими файлами. Если эти файлы вставляются в одни и те же таблицы, вам не нужно готовить выписку для каждого файла. Вы даже импортируете объект PDO глобально. Вы можете подготовить операторы перед созданием объекта SUBJECT1, сохранить операторы в массив и передать его как ссылку на свой метод импорта. Ключевым моментом здесь является то, что операторы подготавливаются один раз, выполняются несколько раз - это должно быть вашей мантрой. - person N.B.; 17.12.2014
comment
Нет, у меня есть 4 вида расширений файлов. Каждый файл заполняет 4 разных таблицы, поэтому у меня всего 16 PS. Я заполняю оператор один раз в начале, но объект не был уничтожен, поэтому я поместил его в swith / case, чтобы автоматически очистить его, когда он выходит за пределы области case / switch. Я думаю, что (и вы точно правы) проблема здесь в глобальном pdo, который никогда не сбрасывается. Но я не мог понять ваш код, когда вы помещали его в конструктор. не могли бы вы мне объяснить? Я обязательно пришлю тебе пива! - person SBO; 17.12.2014
comment
Я передал классу объект PDO. Таким образом, вам не нужно использовать global $pdo внутри вашего метода. Смысл класса состоит в том, чтобы назначить ему задание, и он выполняет только одно задание, поэтому у меня есть 2 класса - один для анализа файла, один для вставки в базу данных. Класс импорта зависит от подключения к базе данных, поэтому я ввел pdo через конструктор. Кроме того, я подготовил операторы, которые хочу использовать там - это значит, что я могу подготовить один раз, использовать несколько раз. Это означает, что вам нужен 1 класс для многих файлов, и вам не нужно ничего уничтожать. - person N.B.; 17.12.2014
comment
Теперь происходит то, что у вас есть код, который подключается к базе данных, код, который находит файл, код, который анализирует файл в соответствии с некоторыми правилами, и код, который вставляет данные, если все правила соблюдены. Когда вы отлаживаете или оптимизируете, у вас есть четкое разделение задач (подключение, поиск файлов, синтаксический анализ файлов, вставка в базу данных), и вы можете обрабатывать / отлаживать более простым способом. - person N.B.; 17.12.2014
comment
Ладно все ясно. Но следуя вашему коду, когда я создаю объект = новый объект (); а в __construct (PDO $ pdo = null) {..} $ pdo всегда имеет значение null. Я ищу учебник, и он должен работать, но я могу заставить его работать, только если я создам объект, передающий $ pdo в качестве параметра в object = new object ($ pdo); Кажется, что pdo не работает в файле класса. что я делаю не так ?? - person SBO; 17.12.2014
comment
Нет, это не означает, что PDO имеет значение NULL. Это означает, что если экземпляр PDO не передан, вместо него будет использоваться ноль. Это называется настройкой параметров по умолчанию. - person N.B.; 17.12.2014
comment
Ага, в твоем коде то есть ошибка? не следует ли вам вызывать new ImportFiles ($ pdo); ? - person SBO; 22.12.2014
comment
Вы правы, произошла ошибка - я должен был назвать это так, как вы упомянули. Редактируем ответ, чтобы было понятно, что его надо использовать именно так :) - person N.B.; 22.12.2014
comment
Не все проблемы решил, но придется ненадолго покинуть этот проект. Большое спасибо за вашу помощь, действительно полезно! - person SBO; 22.12.2014

РЕШЕНО:

Я применил этот подход, возможно, он будет полезен тем, у кого есть аналогичная проблема:

Я открыл диспетчер задач и посмотрел на использование памяти процессами apache и mysql в следующих случаях:

  • Пытался читать и обрабатывать файлы без вызова процедур MySql (использование памяти было в порядке)
  • Пытался читать, обрабатывать и вставлять в db только файлы с расширением один за другим (все .ext1, все .ext2, ....)
  • Отлаживал процедуру с большим объемом памяти, изолирующие функции одна за другой находили проблемную.
  • Нашел проблему и решил

Проблема заключалась в том, что я вызвал функцию, передав в качестве параметра подготовленное выражение. Я думал, что после подготовки это будет просто «статический» объект для вызова. Что происходит, так это то, что если вы передаете тот же PS в функции, память увеличивается экспоненциально.

Надеюсь, это кому-то поможет.

До свидания!

person SBO    schedule 16.12.2014
comment
Вы должны передать объект PDOStatement по ссылке, чтобы он не копировался. Поздравляю с тем, что справились с этим самостоятельно. - person N.B.; 16.12.2014
comment
N.B. спасибо за комментарий, как это сделать? вы имеете в виду функцию (& $ PS, $ parameters, ...) - person SBO; 16.12.2014
comment
После просмотра кода сделать предложение проще. В основном это что-то вроде этого: function MyFunction(PDOStatement &$stmt){ } - person N.B.; 16.12.2014
comment
Да, я пробовал. Я не знаю почему, но это сильно увеличивает память, не так сильно, как передача параметра по значению, но наверняка удваивает объем памяти (в отношении не использования функции, а копирования / вставки кода). - person SBO; 16.12.2014
comment
Увеличение памяти всегда является признаком того, что что-то не сбрасывается, независимо от того, отправляет ли он это в БД или на вывод (stdout в случае CLI). Ваш псевдокод выглядит нормально, однако, если сценарий использует так много памяти (более 100 для простой вставки в базу данных), это явный признак того, что вы забыли или что-то не сделали правильно. У меня есть сценарии CLI, выполняющие ту же работу, они никогда не достигают 100 МБ. Логика аналогична: fopen файл, чтение N строк, использование подготовленного оператора + транзакция, вставка N строк, фиксация, начало заново. - person N.B.; 16.12.2014
comment
Я опубликовал код редактирования, подскажите, пожалуйста, если я делаю что-то не так? Благодарность! - person SBO; 16.12.2014