Allowed memory size exhausted в PHP: что делать с импортами и cron

Ошибка Allowed memory size exhausted часто появляется в импортах, выгрузках, генерации отчётов и cron-задачах. В браузере её можно увидеть как 500, а в консоли — как fatal error. Первое желание — увеличить memory_limit. Иногда это помогает, но часто только откладывает проблему.

Если скрипт грузит в память всю базу, большой XML, CSV или массив картинок, лимит закончится снова. Лучше найти место, где данные накапливаются без необходимости.

Сначала посмотреть точную ошибку

В сообщении обычно есть размер памяти и файл со строкой. Это важная подсказка.

Allowed memory size of 134217728 bytes exhausted
(tried to allocate 20480 bytes)

Нужно открыть лог и найти полный stack trace.

tail -n 150 runtime/logs/app.log
tail -n 150 /var/log/php-fpm/error.log

Проверить memory_limit

Для CLI и веба memory_limit может отличаться. Импорт из cron может запускаться с одной настройкой, а сайт — с другой.

php -i | grep memory_limit

В коде можно временно вывести значение:

Yii::info(ini_get('memory_limit'), 'memory');

Увеличивать лимит допустимо, если задача действительно требует больше памяти. Но если рост памяти бесконечный, лимит не решит причину.

ActiveRecord all() на больших таблицах

Частая проблема в Yii2 — загрузить всю таблицу через all(), а потом обработать её в цикле.

$products = Product::find()
    ->where(['status' => 'A'])
    ->all();

foreach ($products as $product) {
    // обработка
}

Для больших данных лучше использовать batch() или each().

foreach (Product::find()->where(['status' => 'A'])->batch(500) as $products) {
    foreach ($products as $product) {
        // обработка
    }
}

Если не нужны объекты ActiveRecord, можно использовать asArray(), чтобы снизить накладные расходы.

foreach (Product::find()->select(['id', 'sku'])->asArray()->batch(1000) as $rows) {
    foreach ($rows as $row) {
        // обработка
    }
}

Большие массивы

Иногда память съедает не база, а накопительный массив. Например, скрипт собирает все результаты, а потом записывает их в файл. Лучше писать постепенно.

$handle = fopen($path, 'w');

foreach ($rows as $row) {
    fputcsv($handle, $row);
}

fclose($handle);

Для XML и JSON тоже лучше избегать сборки огромной строки, если файл можно писать частями.

CSV и импорты

При импорте CSV не нужно читать весь файл в массив через file(). Для больших файлов лучше читать построчно.

$handle = fopen($filePath, 'r');

while (($row = fgetcsv($handle, 0, ';')) !== false) {
    // обработка строки
}

fclose($handle);

Если при каждой строке создаётся большой объект и складывается в общий массив, память всё равно будет расти. Обработали строку — сохранили результат — освободили переменные.

unset и сборщик мусора

В долгих задачах иногда полезно явно освобождать крупные переменные после обработки пакета.

foreach ($query->batch(500) as $rows) {
    foreach ($rows as $row) {
        // обработка
    }

    unset($rows);
    gc_collect_cycles();
}

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

Логировать потребление памяти

Чтобы понять, где память растёт, можно временно писать memory_get_usage в лог после крупных этапов.

Yii::info([
    'step' => 'after products batch',
    'memory' => memory_get_usage(true),
    'peak' => memory_get_peak_usage(true),
], 'memory');

Если память растёт после каждого пакета и не снижается, значит где-то остаются ссылки на обработанные данные.

Когда можно увеличить лимит

Иногда увеличение memory_limit нормально: например, генерация крупного отчёта или обработка изображений. Но это должно быть осознанное решение, а не единственный способ скрыть ошибку.

php -d memory_limit=512M yii import/products

Для cron можно указать лимит прямо в команде, но лучше всё равно оптимизировать обработку.

Чек-лист диагностики

  1. Найти точный файл и строку в ошибке.
  2. Проверить memory_limit для CLI и веба.
  3. Найти all() на больших таблицах.
  4. Заменить полную загрузку на batch() или each().
  5. Не собирать огромные массивы без необходимости.
  6. Читать CSV и XML потоково, если возможно.
  7. Логировать memory_get_usage по этапам.
  8. Увеличивать лимит только после понимания причины.

Нехватка памяти почти всегда показывает, что скрипт пытается держать слишком много данных одновременно. Чем раньше обработка станет пакетной или потоковой, тем стабильнее будут импорты, выгрузки и фоновые задачи.

Комментарии (0)

Пока нет комментариев. Будьте первым!