Перейти к содержимому

Allowed memory size exhausted - причины по убыванию частоты

Страница, обмен или консольный скрипт обрывается сообщением об исчерпанной памяти. Разбираем причины по убыванию частоты.

Как проверить

Смотрим, какой лимит действует на самом деле:

Окно терминала
php -i | grep memory_limit # лимит консольного PHP
grep -rn "memory_limit" /etc/php*/ .htaccess bitrix/.settings.php 2>/dev/null
# консоль, веб-сервер и админка нередко работают с разными значениями
# .htaccess на связке nginx с php-fpm не действует вовсе: правку там не увидят

Лимит бывает задан в четырёх местах сразу, и действует не тот, который правили последним. Веб-запрос читает настройку своего пула, консоль - свой файл настроек, а .htaccess на связке nginx с процессами PHP не действует вовсе.

Читаем сообщение целиком, а не первую строку:

Окно терминала
grep -i "allowed memory size" /var/log/php-fpm/error.log | tail -3
# tried to allocate 131426964488 bytes - это не нехватка памяти, а сломанные данные
grep -c "allowed memory" /var/log/php-fpm/error.log # одиночный случай или поток

В сообщении две цифры: предел и запрошенный объём. Запрос в несколько мегабайт означает, что памяти действительно не хватило, а запрос в гигабайты - что код считает длину по испорченному значению или ушёл в рекурсию.

Замеряем пик в подозрительном месте:

echo round(memory_get_peak_usage(true) / 1048576, 1), " МБ\n";
// ставят до и после тяжёлого участка: разница и есть цена этого куска кода
echo round(memory_get_usage(true) / 1048576, 1), " МБ сейчас\n"; // текущий расход

Пик показывает не общий расход, а самую высокую точку. Две отметки вокруг подозрительного цикла отвечают на вопрос точнее, чем любые рассуждения о прожорливости платформы.

Заменяем выборку целиком на порции:

$rs = CIBlockElement::GetList([], $filter, false, ['nTopCount' => 500], $select);
while ($row = $rs->Fetch()) { /* обрабатываем и сразу пишем результат */ }
$count = CIBlockElement::GetList([], $filter, []); // количество без выборки
// накапливать результат в массиве нельзя: он и съедает всю память
unset($row); // в длинном цикле чистят и накопители

Подсчёт записей через их полную выборку - самая частая ошибка в скриптах. Количество платформа отдаёт отдельным вызовом, а обработку строят так, чтобы в памяти одновременно жила одна порция, а не весь инфоблок.

Причины

  1. Скрипт держит в памяти всю выборку примерно 35% случаев

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

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

    Что делатьПереписываем на порции с сохранением результата сразу, а количество записей берём отдельным запросом.

  2. Лимит поднят не в том месте примерно 25% случаев

    ПризнакПамять увеличили, перезапустили сервер, а в сообщении стоит прежнее значение предела.

    ПроверкаСравниваем предел из сообщения с настройками пула процессов, консоли и файла настроек платформы.

    Что делатьПравим настройку там, где выполняется упавший код, и проверяем результат выводом сведений о среде.

  3. Тяжёлая операция платформы примерно 20% случаев

    ПризнакПадает восстановление из копии, установка обновления, обмен с учётной системой или страница админки.

    ПроверкаПроверяем размер обрабатываемых данных: части архива, объём выгрузки, число элементов на странице списка.

    Что делатьПоднимаем предел для конкретной операции и уменьшаем порцию: размер части архива, шаг обмена, число строк в списке.

  4. Зацикливание или испорченные данные примерно 12% случаев

    ПризнакВ сообщении запрошены гигабайты, а падение происходит мгновенно и всегда на одном месте.

    ПроверкаЧитаем трассировку: рекурсивный вызов виден по повторяющимся строкам одного и того же файла.

    Что делатьЧиним данные или логику; поднимать предел здесь бесполезно, память кончится при любом значении.

  5. Утечка в длинном цикле примерно 8% случаев

    ПризнакСкрипт работает минутами и падает не сразу, а после нескольких тысяч итераций.

    ПроверкаПечатаем пик памяти каждые сто шагов: у утечки он растёт равномерно и не возвращается.

    Что делатьОчищаем накопители внутри цикла, отключаем сбор запросов на отладку и не копим вывод в переменной.

Если ничего не помогло

Смотрим, где именно оборвался код. Сообщение называет файл и строку, и если это файл платформы, виноват почти всегда не он, а данные или свой обработчик, который его вызвал.

Запускаем ту же работу в консоли. Консольный запуск не ограничен временем веб-сервера и показывает ошибку целиком, вместе с трассировкой, а не белым экраном.

Проверяем правила корзины и скидки на больших заказах. Условия скидок платформа разбирает на лету, и заказ на сотни позиций с десятком правил - известное тяжёлое место модуля продаж.

Частые вопросы

Поднял memory_limit, а ошибка осталась с прежним пределом.

Значение правили не там, где выполняется код: у процессов веб-сервера, консоли и планировщика настройки разные. Предел из текста ошибки сравнивают с настройкой конкретного окружения, а не с той, которую редактировали.

Сколько памяти нужно сайту на платформе?

Обычной странице хватает 256 мегабайт, тяжёлым операциям - от 512. Постоянная потребность в большем значении означает не требовательность платформы, а выборку без ограничения где-то в своём коде.

Скрипт просит выделить гигабайты - что это значит?

Это не нехватка памяти, а сломанная логика: рекурсия или расчёт длины по испорченному значению. Увеличение предела в таком случае не помогает никогда.

Обмен с учётной системой падает на большом каталоге.

Уменьшают шаг обмена и число элементов в одной части выгрузки, а предел памяти поднимают только для точки обмена. Обмен целиком в одну итерацию не выполняют даже на маленьком каталоге.

Как найти прожорливый участок в своём коде?

Расставить замеры пика памяти вокруг подозрительных участков и сравнить разницу. Профилировщик покажет точнее, но две строки с выводом пика обычно закрывают вопрос быстрее.

Смежное

Первоисточник