#Article
💀 Как один ORM-запрос съел 300 МБ навсегда
История из нашего проекта: неоптимальный запрос через ORM загрузил ~100К записей из базы. Worker в RoadRunner вырос с 80 до ~500 МБ... и больше никогда не вернулся обратно.
Звучит как утечка памяти? Спойлер: это не баг, а особенность архитектуры PHP.
Zend Memory Manager выделяет память большими блоками — чанки по ~2-4 МБ. Когда вы делаете
Чанки остаются закреплёнными за процессом до его завершения. Это сделано ради производительности: повторное использование уже выделенной памяти быстрее, чем постоянные запросы к ОС.
Почему это критично для RoadRunner, Laravel Queue, демонов?
В PHP-FPM каждый запрос = новый процесс, память автоматически очищается. В long-running процессах один worker обрабатывает тысячи задач — и каждый "пик" памяти становится новым baseline навсегда.
Ну а теперь про то, как себя обезопасить от всего этого?
- Использовать батчинг для выборок
- Ограничивать объём данных в памяти в рамках одной операции
- Обрабатывать данные потоково, без загрузки всего набора сразу
- Выносить тяжёлые операции в отдельные функции для автоматического освобождения локальных переменных
- Применять
- Анализировать участки кода, создающие единовременные всплески в десятки МБ. Лучше не игнорировать всплески памяти, надеясь что “само освободится”
Что можно сделать на уровне PHP воркеров:
1. Можно использовать ротацию по количеству задач, например в RoadRunner
2. В RoadRunner — при необходимости включать soft-лимиты по памяти, при достижении которого воркер перезапускается.
Куда ещё может утекать память:
- Статические переменные, накапливающие данные
- Глобальные массивы, в которых растёт состояние
- Singleton-сервисы с неограниченными кешами
- ORM-кеши, которые не сбрасываются
- Списки подписчиков/слушателей, которые не удаляются
- Циклические ссылки объектов друг на друга.
💀 Как один ORM-запрос съел 300 МБ навсегда
История из нашего проекта: неоптимальный запрос через ORM загрузил ~100К записей из базы. Worker в RoadRunner вырос с 80 до ~500 МБ... и больше никогда не вернулся обратно.
Звучит как утечка памяти? Спойлер: это не баг, а особенность архитектуры PHP.
💡 Неочевидный факт: PHP никогда не возвращает память ОС в long-running процессах
Zend Memory Manager выделяет память большими блоками — чанки по ~2-4 МБ. Когда вы делаете
unset($data) или переменная выходит из scope — память освобождается ВНУТРИ PHP, но операционная система об этом не узнаёт.Чанки остаются закреплёнными за процессом до его завершения. Это сделано ради производительности: повторное использование уже выделенной памяти быстрее, чем постоянные запросы к ОС.
Почему это критично для RoadRunner, Laravel Queue, демонов?
В PHP-FPM каждый запрос = новый процесс, память автоматически очищается. В long-running процессах один worker обрабатывает тысячи задач — и каждый "пик" памяти становится новым baseline навсегда.
Ну а теперь про то, как себя обезопасить от всего этого?
- Использовать батчинг для выборок
- Ограничивать объём данных в памяти в рамках одной операции
- Обрабатывать данные потоково, без загрузки всего набора сразу
- Выносить тяжёлые операции в отдельные функции для автоматического освобождения локальных переменных
- Применять
gc_mem_caches() для очистки внутренних пулов, и молиться, что поможет именно в вашем случае)- Анализировать участки кода, создающие единовременные всплески в десятки МБ. Лучше не игнорировать всплески памяти, надеясь что “само освободится”
Что можно сделать на уровне PHP воркеров:
1. Можно использовать ротацию по количеству задач, например в RoadRunner
pool.max_jobs=1000, Laravel Queue --max-jobs=1000. После выполнения 1000 задач, воркер будет перезапущен. Но не стоит полагаться на ротацию как на замену оптимизации, ведь пик может возникнуть в первых же запросах.2. В RoadRunner — при необходимости включать soft-лимиты по памяти, при достижении которого воркер перезапускается.
Куда ещё может утекать память:
- Статические переменные, накапливающие данные
- Глобальные массивы, в которых растёт состояние
- Singleton-сервисы с неограниченными кешами
- ORM-кеши, которые не сбрасываются
- Списки подписчиков/слушателей, которые не удаляются
- Циклические ссылки объектов друг на друга.
1🔥46🤯10 9🤔2