PHP: высокое использование памяти с PDO - PullRequest
0 голосов
/ 20 июня 2019

У меня есть база данных MySQL удаленного поставщика, которая содержит данные о розничных товарах. Я создал скрипт php, чтобы выбрать эти данные в одном наборе данных, обработать их и вставить в локальную базу данных с тремя таблицами.

Мой сценарий работает так, как задумано, но к концу сценария использование памяти кажется действительно высоким.

Самая длинная часть скрипта выполняет запрос для выбора удаленных данных. Этот запрос содержит несколько объединений, выполнение которых занимает около 190 секунд, и извлекает около 100 000 строк.

Затраты на запуск сценария и выборку удаленных данных составляют около 35 МБ памяти RES. Когда удаленный запрос завершается, данные обрабатываются и вставляются локально в течение 10 секунд. В течение этих 10 секунд потребление памяти скриптом в конце увеличивается с ~ 35 до 300 МБ.

Это похоже на большое использование памяти для этой простой задачи. Кажется, что сборщик мусора не работает.

Читая о сборке мусора в PHP, я попытался обернуть части своего кода в функции. Было отмечено, что это помогает с сборкой мусора. Это не в моем случае.

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

Я попытался unset() и установить значения на null в конце итераций, но это, похоже, не освобождает память.

Я установил расширение memprof, чтобы взглянуть на то, что потребляет много памяти. Кажется, что explode() и PDOStatement::fetch() используют больше всего. Кажется, память не освобождается при каждой итерации. Как это будет освобождено?

Примечание: в моем скрипте я разделил локальную обработку элементов на группы по 5957, это связано с превышением ограничений с привязкой параметров. Каждый элемент получает привязку к 11 параметрам (5957 * 11 = 65527; чуть ниже предела 65535).

Местное окружение:

Linux 4.4.0-17763-Microsoft #379-Microsoft x86_64 GNU/Linux (DEBIAN WSL)
PHP 7.0.33-0+deb9u3 (cli)
mysqlnd 5.0.12-dev - 20150407

Сценарий:

<?php

ini_set('memory_limit', '-1');
set_time_limit(0);
$start = time();

// Step size for processing local inserts
$items_per_step = 5957;

// PDO options
$db_options = [
    PDO::ATTR_TIMEOUT => 10,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];

// Queries
$fetch_remote_query = file_get_contents(__DIR__ . '/sql/fetch_remote.sql');
$item_query = file_get_contents(__DIR__ . '/sql/add_local.sql');
$about_query = file_get_contents(__DIR__ . '/sql/add_about.sql');
$filters_query = file_get_contents(__DIR__ . '/sql/add_filters.sql');

try {

    // Connect to databases
    $remotedw = new PDO('dsn', 'user', 'pass', $db_options);
    $localdw = new PDO('dsn', 'user', 'pass', $db_options);

    // Fetch remote
    echo 'Fetching items from the Remote database...' . PHP_EOL;;
    $items = $remotedw->query($fetch_remote_query);
    $item_count = $items->rowCount();
    echo "$item_count items fetched and ready for caching" . PHP_EOL;;

    // Calculate steps
    $steps_required = ceil($item_count / $items_per_step);
    echo "Processing items in $steps_required steps" . PHP_EOL;;

    // Run steps
    for ($steps_taken = 1, $offset = 0; $steps_taken <= $steps_required; $steps_taken++, $offset += $items_per_step) {

        // Step length
        $length = $steps_taken * $items_per_step > $item_count ? $item_count - $offset : $items_per_step;

        // Initial local query parts for the current step
        $item_rows = '';
        $about_rows = '';
        $filter_rows = '';
        $item_data = [];
        $about_data = [];
        $filter_data = [];

        // Step through items
        for($i = 0; $i < $length; $i++) {

            // Fetch next row
            $item = $items->fetch();

            // Build items
            $item_rows .= '(?,?,?,?,?,?,?,?,?,?,?),';
            $item_data[] = $item['sku'];
            $item_data[] = $item['mfg_number'];
            $item_data[] = $item['handling'];
            $item_data[] = $item['taxable'];
            $item_data[] = $item['price'];
            $item_data[] = $item['qty_available'];
            $item_data[] = $item['department'];
            $item_data[] = $item['class'];
            $item_data[] = $item['description'];
            $item_data[] = $item['sales_to_date'];
            $item_data[] = $item['show_on_web'];

            // Build about
            foreach (explode('*', $item['about']) as $about_entry) {
                if ($about_entry === '') continue;
                $about_rows .= '(?,?),';
                $about_data[] = $item['sku'];
                $about_data[] = $about_entry;
            }

            // Build filters
            if ($item['fineline']) {
                $filter_rows .= '(?,?),';
                $filter_data[] = $item['sku'];
                $filter_data[] = $item['fineline'];
            }

        }

        // Add items
        $localdw
            ->prepare(str_replace('{{rows}}', rtrim($item_rows, ','), $item_query))
            ->execute($item_data);

        // Add about (sometimes items do not have about data, so check if there are rows)
        if ($about_rows) $localdw
            ->prepare(str_replace('{{rows}}', rtrim($about_rows, ','), $about_query))
            ->execute($about_data);

        // Add filters (sometimes items do not have filter data, so check if there are rows)
        if ($filter_rows) $localdw
            ->prepare(str_replace('{{rows}}', rtrim($filter_rows, ','), $filters_query))
            ->execute($filter_data);

    }

} catch (PDOException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

echo 'Script finished in ' . (time() - $start) . ' seconds' . PHP_EOL;

1 Ответ

1 голос
/ 20 июня 2019

Я думаю, rowCount() может вызывать буферизацию всех результатов, как если бы вы вызывали $items->fetchAll().

Вместо использования цикла for используйте цикл while, которыйсобирает результаты и выполняет пакетные запросы, когда переменная итерации кратна размеру шага.

$i = 0;
$item_rows = '';
$about_rows = '';
$filter_rows = '';
$item_data = [];
$about_data = [];
$filter_data = [];

while ($item = $items->fetch()) {
    $item_rows .= '(?,?,?,?,?,?,?,?,?,?,?),';
    $item_data[] = $item['sku'];
    $item_data[] = $item['mfg_number'];
    $item_data[] = $item['handling'];
    $item_data[] = $item['taxable'];
    $item_data[] = $item['price'];
    $item_data[] = $item['qty_available'];
    $item_data[] = $item['department'];
    $item_data[] = $item['class'];
    $item_data[] = $item['description'];
    $item_data[] = $item['sales_to_date'];
    $item_data[] = $item['show_on_web'];

    // Build about
    foreach (explode('*', $item['about']) as $about_entry) {
        if ($about_entry === '') continue;
        $about_rows .= '(?,?),';
        $about_data[] = $item['sku'];
        $about_data[] = $about_entry;
    }

    // Build filters
    if ($item['fineline']) {
        $filter_rows .= '(?,?),';
        $filter_data[] = $item['sku'];
        $filter_data[] = $item['fineline'];
    }

    if (++$i == $items_per_step) {
        $localdw
            ->prepare(str_replace('{{rows}}', rtrim($item_rows, ','), $item_query))
            ->execute($item_data);

        // Add about (sometimes items do not have about data, so check if there are rows)
        if ($about_rows) $localdw
            ->prepare(str_replace('{{rows}}', rtrim($about_rows, ','), $about_query))
            ->execute($about_data);

        // Add filters (sometimes items do not have filter data, so check if there are rows)
        if ($filter_rows) $localdw
            ->prepare(str_replace('{{rows}}', rtrim($filter_rows, ','), $filters_query))
            ->execute($filter_data);

        $i = 0;
        $item_rows = '';
        $about_rows = '';
        $filter_rows = '';
        $item_data = [];
        $about_data = [];
        $filter_data = [];
    }
}
if ($i > 0) {
    // process the last batch
    $localdw
        ->prepare(str_replace('{{rows}}', rtrim($item_rows, ','), $item_query))
        ->execute($item_data);

    // Add about (sometimes items do not have about data, so check if there are rows)
    if ($about_rows) $localdw
        ->prepare(str_replace('{{rows}}', rtrim($about_rows, ','), $about_query))
        ->execute($about_data);

    // Add filters (sometimes items do not have filter data, so check if there are rows)
    if ($filter_rows) $localdw
        ->prepare(str_replace('{{rows}}', rtrim($filter_rows, ','), $filters_query))
        ->execute($filter_data);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...