MAATRIX / Блог / PostgreSQL: Out of Memory — причины и решение

PostgreSQL: Out of Memory — причины и решение

PostgreSQL: Out of Memory — причины и решение

MAATRIX

Сервер работал спокойно неделями, а потом PostgreSQL внезапно обрывает соединения, в логе появляется «out of memory» или процесс postgres просто исчезает без внятного сообщения — его тихо прибил Linux. Обычно за этим стоит не баг и не «утечка памяти», а конкретная и предсказуемая арифметика: настройки памяти не соответствуют тому, что реально есть на сервере. Разберём, откуда берётся проблема, как её диагностировать и как посчитать значения, которые не приведут к падению.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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_buffers25% от RAM (не больше 40%)Постоянный кеш данных PostgreSQL, отъедается сразу и навсегда, даже в простое
effective_cache_size50–70% от RAMНе резервирует память — это лишь подсказка планировщику, сколько памяти ОС вероятно доступно под файловый кеш, чтобы он правильно оценивал, выгодно ли читать с диска
work_mem(RAM − shared_buffers) / (max_connections × 2-3)Сколько памяти можно потратить на одну операцию сортировки/хеша, с запасом на несколько таких операций в одном запросе
maintenance_work_mem5–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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.