PostgreSQL: Out of Memory — причины и решение
Сервер работал спокойно неделями, а потом PostgreSQL внезапно обрывает соединения, в логе появляется «out of memory» или процесс postgres просто исчезает без внятного сообщения — его тихо прибил Linux. Обычно за этим стоит не баг и не «утечка памяти», а конкретная и предсказуемая арифметика: настройки памяти не соответствуют тому, что реально есть на сервере. Разберём, откуда берётся проблема, как её диагностировать и как посчитать значения, которые не приведут к падению.
Содержание
- Как понять, что дело именно в памяти
- Почему PostgreSQL вообще так активно использует память
- Ошибка №1: настройки «по формуле из интернета» без проверки реальной RAM
- Ошибка №2: слишком много соединений без пулера
- Ошибка №3: один тяжёлый запрос без лимитов
- Как посчитать безопасные настройки под вашу RAM
- Как найти в логах, кто именно убил процесс
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Как понять, что дело именно в памяти
Есть два разных сценария, и их важно не путать, потому что лечатся они по-разному.
Первый — сам PostgreSQL честно говорит, что ему не хватило памяти. В логе postgresql.log (обычно /var/log/postgresql/postgresql-16-main.log или похожий путь в зависимости от версии и дистрибутива) вы увидите что-то вроде:
ERROR: out of memory
DETAIL: Failed on request of size 8388608 in memory context "ExecutorState".
Это значит, что процесс PostgreSQL, обслуживающий конкретный запрос, попросил у операционной системы кусок памяти и получил отказ. Обычно это происходит, когда work_mem выставлен слишком щедро относительно количества одновременных запросов, либо сам запрос устроен так, что требует непропорционально много памяти на сортировку или хеширование.
Второй сценарий страшнее внешне, но диагностируется проще: PostgreSQL вообще ничего не пишет в свой лог, потому что процесс не успевает — его убивает OOM Killer самой операционной системы, когда суммарное потребление памяти всеми процессами превышает физический предел. В логе PostgreSQL в этом случае вы обычно видите только обрыв:
LOG: server process (PID 14221) was terminated by signal 9: Killed
LOG: terminating any other active server processes
Signal 9 — это верный признак того, что процесс не завершился сам, его прибили снаружи. Дальше нужно смотреть не в лог PostgreSQL, а в системный журнал — об этом ниже, в отдельной секции.
Почему PostgreSQL вообще так активно использует память
Чтобы понимать, что крутить, полезно на пальцах представлять модель памяти PostgreSQL — она устроена иначе, чем, скажем, у MySQL.
shared_buffers — это общий кеш страниц данных, один на весь сервер PostgreSQL, выделяется один раз при старте и используется всеми соединениями сразу. Простыми словами: это тот кусок RAM, где PostgreSQL держит «горячие» страницы таблиц и индексов, чтобы не лезть за ними на диск каждый раз. Чем больше эта область — тем меньше дисковых операций, но она статично отъедает память навсегда, даже если сервер простаивает.
work_mem — это не общий, а частный лимит, причём коварный: он выделяется не на соединение, а на каждую операцию сортировки или хеширования внутри запроса. Если запрос делает ORDER BY, JOIN через хеш-таблицу и DISTINCT одновременно — это уже потенциально три независимых выделения по work_mem в рамках одного соединения. При 50 параллельных соединениях с похожими запросами реальное потребление может оказаться не «50 × work_mem», а в разы больше.
maintenance_work_mem — память для обслуживающих операций: VACUUM, создание индексов, REINDEX, ALTER TABLE ... ADD FOREIGN KEY. Она нужна не постоянно, а в моменте, но если у вас запущено несколько параллельных autovacuum-воркеров, каждый может занять по maintenance_work_mem.
Ключевая деталь, которая ломает интуицию новичка: в PostgreSQL каждое соединение клиента — это отдельный процесс операционной системы (не поток, как в большинстве других СУБД). У каждого такого процесса есть собственные накладные расходы памяти плюс потенциальные выделения под work_mem. Отсюда и типичная ловушка — при росте числа подключений память растёт не линейно-предсказуемо, а рывками, в зависимости от того, что именно эти подключения сейчас выполняют.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSОшибка №1: настройки «по формуле из интернета» без проверки реальной RAM
Самая частая причина OOM в продакшене банальна: администратор находит в статье или калькуляторе PostgreSQL готовые рекомендации вида «shared_buffers = 25% RAM, effective_cache_size = 75% RAM, work_mem = RAM / max_connections / 4» — и вставляет цифры, посчитанные для сервера с 32 ГБ, в конфиг сервера с 4 ГБ. Или наоборот: копирует конфиг с прошлого проекта, где было 64 ГБ RAM, на новый VPS с 8 ГБ.
Проверить, сколько реально памяти доступно, — секундное дело:
free -h
total used free shared buff/cache available
Mem: 7.8Gi 1.2Gi 0.3Gi 120Mi 6.3Gi 6.2Gi
Swap: 2.0Gi 0.0Gi 2.0Gi
Если в этом выводе total — 7.8 ГБ, а в postgresql.conf стоит shared_buffers = 16GB — сервер обречён упасть при первой же нагрузке, потому что операционной системе, самому PostgreSQL-процессу-надзирателю (postmaster) и другим приложениям на сервере (nginx, приложение, cron-задачи) память тоже нужна, а PostgreSQL резервирует свою часть сразу и полностью.
Проверить текущие настройки можно прямо из psql:
SHOW shared_buffers;
SHOW work_mem;
SHOW maintenance_work_mem;
SHOW max_connections;
Если хотя бы shared_buffers заметно превышает 25–30% от вывода free -h, это уже повод для аудита.
Ошибка №2: слишком много соединений без пулера
Второй по частоте сценарий — рост числа клиентских подключений без пулера соединений. Каждое новое подключение к PostgreSQL — это новый процесс на уровне ОС с собственным стеком, буферами и потенциальными work_mem-выделениями. Веб-приложение с плохо настроенным пулом на стороне бэкенда (например, каждый запрос Django или Node.js открывает своё соединение вместо переиспользования) легко доходит до max_connections = 200 и выше — а это уже 200 потенциальных процессов, каждый из которых в пике может занять свой work_mem.
Грубая, но полезная прикидка пикового потребления:
max_connections × (несколько МБ на процесс + work_mem × число параллельных операций в запросе)
При max_connections = 300 и work_mem = 64MB даже без экзотических запросов достаточно, чтобы половина соединений одновременно делала сортировку — и вы уже упираетесь в несколько гигабайт только на work_mem, не считая shared_buffers и самой ОС.
Решение здесь не «уменьшить max_connections до неудобного минимума», а поставить пулер соединений между приложением и PostgreSQL — например, PgBouncer. Он держит небольшое число реальных соединений к базе, а клиентским подключениям от приложения выдаёт их по очереди через режим transaction pooling. Это резко снижает количество одновременно живущих postgres-процессов при той же нагрузке от приложения. Подробная установка и настройка разобрана в статье про PgBouncer — там же есть примеры конфигурации пулов под разную нагрузку.
Ошибка №3: один тяжёлый запрос без лимитов
Даже с адекватными базовыми настройками один плохо написанный запрос способен положить сервер в одиночку. Классика: JOIN нескольких больших таблиц без индексов по ключам соединения заставляет планировщик выбрать хеш-джойн или сортировку по огромному объёму строк — и это выделение памяти под work_mem происходит один раз на каждый узел плана, который в ней нуждается.
Проверить, во что PostgreSQL оценивает план запроса, помогает EXPLAIN:
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
В выводе стоит смотреть на строки вида Sort Method: external merge Disk — это признак того, что операции не хватило work_mem и она ушла на диск (не катастрофа, но медленно), либо на резкое расхождение между estimated rows и actual rows — если планировщик ошибся на порядки, он мог выбрать неоптимальную стратегию, которая требует намного больше памяти, чем нужно было бы.
Два простых защитных механизма:
statement_timeout— обрывает запрос, который выполняется дольше заданного времени, не давая одному «зависшему» запросу монопольно держать ресурсы:
ALTER ROLE app_user SET statement_timeout = '30s';
work_memна уровне конкретной сессии или роли — можно занизить дефолт глобально и повышать точечно только там, где это оправдано (например, для аналитических запросов через отдельного пользователя):
ALTER ROLE analytics_user SET work_mem = '256MB';
Так вы не даёте случайному «тяжёлому» запросу от обычного пользователя приложения устроить OOM для всего сервера.
Как посчитать безопасные настройки под вашу RAM
Единой точной формулы, подходящей всем, не существует — слишком многое зависит от профиля нагрузки (много коротких запросов vs редкие тяжёлые аналитические). Но есть рабочий ориентир для старта, от которого можно отталкиваться и потом корректировать по факту наблюдений:
| Параметр | Ориентировочное значение | Что это значит простыми словами |
|---|---|---|
shared_buffers | 25% от RAM (не больше 40%) | Постоянный кеш данных PostgreSQL, отъедается сразу и навсегда, даже в простое |
effective_cache_size | 50–70% от RAM | Не резервирует память — это лишь подсказка планировщику, сколько памяти ОС вероятно доступно под файловый кеш, чтобы он правильно оценивал, выгодно ли читать с диска |
work_mem | (RAM − shared_buffers) / (max_connections × 2-3) | Сколько памяти можно потратить на одну операцию сортировки/хеша, с запасом на несколько таких операций в одном запросе |
maintenance_work_mem | 5–10% от RAM, но не больше 1-2 ГБ | Память под VACUUM и построение индексов — можно давать больше, чем work_mem, потому что таких операций одновременно немного |
max_connections | по факту нагрузки приложения, обычно 100–200 | Сколько процессов-соединений разрешено держать одновременно; чем больше, тем больше потенциальный пик памяти |
Пример для сервера с 8 ГБ RAM, где PostgreSQL — не единственный процесс (рядом крутится приложение и nginx):
shared_buffers = 2GB
effective_cache_size = 5GB
maintenance_work_mem = 512MB
max_connections = 100
work_mem = 16MB
Это заведомо консервативный набор — он не выжимает максимум производительности, зато с высокой вероятностью не уронит сервер. Если после недели наблюдений (free -h, отсутствие OOM в логах) есть запас — можно аккуратно поднимать work_mem шагами по 8–16 МБ и снова наблюдать. Более подробный разбор тюнинга под конкретные сценарии нагрузки — в статье про тюнинг PostgreSQL.
Отдельно стоит проверить, есть ли на сервере swap — он не спасает от OOM Killer полностью (Linux всё равно может решить убить процесс, если считает, что памяти критически мало), но даёт системе больше времени и снижает вероятность попадания в OOM при кратковременных всплесках. Как правильно выбрать размер — в статье про настройку swap.
Как найти в логах, кто именно убил процесс
Если PostgreSQL просто «пропал» без внятной ошибки в своём логе, а в нём видно только terminated by signal 9, нужно идти в системный журнал ядра Linux — именно там OOM Killer оставляет след о своём решении.
dmesg | grep -i kill
или, если dmesg пуст (буфер ядра мог перезаписаться после ребута), через journalctl:
journalctl -k | grep -i "out of memory"
sudo journalctl --since "1 hour ago" | grep -iE "oom|killed process"
Типичная запись выглядит так:
Out of memory: Killed process 14221 (postgres) total-vm:2145728kB, anon-rss:1823456kB, oom_score_adj:0
Здесь postgres — какой именно процесс убили (это может быть не главный postmaster, а один из рабочих процессов под конкретное соединение), anon-rss — сколько реальной памяти он занимал на момент убийства. Полезно посмотреть на соседние строки лога — там обычно перечислен весь список процессов-кандидатов с их oom_score, это позволяет понять, что именно съело память: сам PostgreSQL, другое приложение на сервере или совокупность процессов PostgreSQL при резком всплеске подключений.
Если в логе фигурирует oom-kill:constraint=CONSTRAINT_MEMCG — это означает, что убийство произошло внутри cgroup-лимита (актуально, если PostgreSQL запущен в Docker-контейнере с ограничением памяти) — тогда искать причину нужно не только в postgresql.conf, но и в лимитах контейнера (--memory в docker run, mem_limit в docker-compose).
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Поможет ли просто добавить оперативной памяти на сервер?
Часто да, если проблема именно в нехватке физического объёма под текущую нагрузку. Но если причина — неверно посчитанные настройки или неограниченные соединения без пулера, проблема со временем вернётся на новом потолке: приложение или число подключений вырастет и снова упрётся в лимит.
Можно ли просто выключить OOM Killer, чтобы PostgreSQL не убивало?
Технически можно через oom_score_adj, но это не решает проблему, а прячет её: вместо контролируемого убийства одного процесса система может зависнуть целиком из-за нехватки памяти для всех процессов сразу. Лучше не бороться с симптомом, а привести настройки в соответствие с реальным объёмом RAM.
Почему postgres «ест» больше памяти, чем shared_buffers + work_mem по формуле?
Потому что формула — это ориентир, а не гарантия. В реальности на потребление влияют временные буферы, соединения от мониторинга и служебных задач, autovacuum-воркеры, работающие параллельно, и то, что несколько операций work_mem в одном запросе суммируются, а не берут один и тот же кусок памяти.
work_mem можно менять на лету без перезапуска сервера?
Да, work_mem и maintenance_work_mem можно менять через SET на уровне сессии, роли или базы без перезапуска PostgreSQL — изменение через ALTER SYSTEM или postgresql.conf требует SELECT pg_reload_conf();, тоже без остановки сервиса. А вот shared_buffers и max_connections требуют полного рестарта, потому что память под них резервируется один раз при старте процесса.
Как узнать реальное потребление памяти PostgreSQL прямо сейчас?
ps aux --sort=-%mem | grep postgres покажет потребление по каждому процессу, а SELECT pid, state, query FROM pg_stat_activity; — что именно этот процесс сейчас выполняет, что удобно сопоставить с высоким расходом памяти у конкретного PID.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.