Долгоживущий CLI-скрипт импорта падает по memory_limit. Определите причину.
Ночной импорт падает на середине прогона с "Allowed memory size exhausted".
Ограничения: в исходной таблице 5 млн строк, на машине 2 ГБ RAM, memory_limit равен 1G. Тот же скрипт спокойно доходит до конца на staging-наборе в 10 тыс. строк. Печатаемое потребление растёт монотонно и назад не откатывается.
$stmt = $pdo->query('SELECT * FROM orders');
$rows = $stmt->fetchAll();
$processed = [];
foreach ($rows as $i => $row) {
$entity = $hydrator->hydrate($row);
$repo->save($entity);
$processed[] = $entity; // нужен для итогового отчёта в конце
if ($i % 1000 === 0) {
echo round(memory_get_usage(true) / 1048576), " MB\n";
}
}
// 214 MB ... 447 MB ... 806 MB ... Fatal error: Allowed memory size exhausted
Определите причину.
Это удержание, а не утечка. fetchAll() материализует все 5 млн строк ещё до входа в цикл, а $processed[] держит живую ссылку на каждую сущность, поэтому refcount не падает до нуля и освобождать нечего. Нужно стримить: fetch() в цикле или генератор, и не копить.
- ✗Вызывать
gc_collect_cycles()— он не освободит то, на что скрипт всё ещё держит ссылку - ✗Ставить диагноз «утечка», когда скрипт просто удерживает выборку и каждую сущность
- ✗Поднимать
memory_limitвместо стриминга, что лишь откладывает ту же фатальную ошибку
- →Почему
fetchAll()даёт пик памяти ещё до первой итерации цикла? - →Как небуферизованный запрос в расширении для БД
PDOменяет то, что PHP держит в памяти?
fetchAll() — это не «чтение построчно». Он материализует весь результат в PHP-массив ещё до того, как цикл начнётся: 5 млн строк лежат в памяти сразу. Дальше $processed[] = $entity; держит живую ссылку на каждую созданную сущность, поэтому её refcount никогда не падает до нуля и движку нечего освобождать.
Это удержание, а не утечка. gc_collect_cycles() здесь бесполезен: сборщик циклов собирает только недостижимые циклы, а $rows и $processed достижимы из кода.
Лечение — стримить, а не копить: небуферизованный fetch() в цикле (или генератор поверх него), никакого накопителя, и ссылки на строку не переживают итерацию.
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
function streamOrders(PDO $pdo): Generator {
$stmt = $pdo->query('SELECT * FROM orders');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row; // строка живёт ровно одну итерацию
}
}
$summary = new Summary(); // агрегат, а не список сущностей
foreach (streamOrders($pdo) as $row) {
$entity = $hydrator->hydrate($row);
$repo->save($entity);
$summary->add($entity->id); // копим итог, а не объекты
unset($entity);
}
Отчёт собираем в агрегат (счётчики, суммы, идентификаторы), а не в список объектов. В долгоживущем воркере вдобавок отключите лог запросов фреймворка — он удерживает каждый выполненный statement и течёт ровно так же.