MAATRIX / Блог / Почему совет «отдать базе 80% памяти» работает не всегда

Почему совет «отдать базе 80% памяти» работает не всегда

MAATRIX

В любом гайде по тюнингу PostgreSQL или MySQL рано или поздно всплывает цифра: «отдайте базе 70-80% оперативной памяти сервера». Совет кочует из статьи в статью, и в нём есть здравое зерно — но применённый не глядя, на любом сервере и с любой нагрузкой, он с равной вероятностью и ускорит базу, и положит её в своп вместе с приложением. Разберём, откуда взялась эта цифра, когда она действительно работает, а когда становится причиной инцидента, и как посчитать правильный размер буферного пула под конкретный сервер, а не под усреднённую рекомендацию из интернета.

Откуда взялся совет про 70-80% и почему он не случайный

Буферный пул — это область памяти, где СУБД держит «горячие» страницы данных и индексов, чтобы не ходить за ними на диск при каждом запросе. Чем больше в пуле помещается рабочих данных, тем выше доля запросов, которые обслуживаются из RAM, а не с диска — а разница между обращением к памяти и обращением к диску (даже к NVMe) остаётся на несколько порядков. Подробнее о механике самого буферного пула и почему он часто важнее скорости диска — в статье про буферный пул базы данных.

Цифра 70-80% появилась из вполне конкретного сценария: выделенный сервер, на котором крутится только СУБД и ничего больше. В таком случае логика простая — вся память сервера так или иначе принадлежит базе, поэтому имеет смысл отдать под буферный пул почти всё, оставив небольшой запас на:

  • служебные процессы самой ОС и сетевой стек;
  • память на соединение (для PostgreSQL — work_mem, maintenance_work_mem, буферы WAL-сендеров; для MySQL — sort_buffer_size, join_buffer_size, read_buffer_size на каждое подключение);
  • фоновые операции: бэкапы, VACUUM, репликацию, пересборку индексов;
  • файловый кеш ОС для файлов, которые СУБД не кеширует сама (логи, временные файлы, бинарники).

Для MySQL/InnoDB, где буферный пул — это фактически единственный слой кеширования данных, доля 70-80% действительно близка к оптимальной на выделенном сервере: InnoDB сам управляет своим кешем, и дублировать те же страницы ещё и в кеше ОС смысла почти нет.

Для PostgreSQL ситуация тоньше. Postgres не работает с диском напрямую (O_DIRECT не используется по умолчанию) и полагается на кеш страниц самой ОС как на второй уровень кеширования. Из-за этого страницы, которые лежат в shared_buffers, зачастую параллельно лежат ещё и в page cache ядра — это называют двойной буферизацией. Поэтому официальная рекомендация PostgreSQL — shared_buffers около 25% RAM (обычно не более 40% даже на выделенном сервере), а роль «отдать почти всю память под кеш данных» на деле играет effective_cache_size (это не выделение памяти, а подсказка планировщику о том, сколько данных вероятно закешировано ОС) — его действительно можно ставить в 50-75% RAM. То есть даже в «идеальном» случае выделенного сервера правило «80% памяти под буферный пул» буквально применимо к MySQL и не применимо к PostgreSQL в лоб.

Когда на сервере не только база — совет ломается первым

Проблема начинается, когда на том же сервере, кроме СУБД, работают приложение, веб-сервер, очередь задач, кеш-слой. Такая конфигурация — норма для небольших и средних проектов на VPS или на одном выделенном сервере, где разносить каждый сервис на отдельную машину экономически не оправдано.

Если в этой ситуации слепо выделить 80% RAM под shared_buffers или innodb_buffer_pool_size, происходит следующее:

  1. СУБД резервирует память под буферный пул при старте (для InnoDB — сразу весь объём innodb_buffer_pool_size, для PostgreSQL — тоже при старте, shared_buffers требует перезапуска процесса).
  2. Оставшихся 20% памяти не хватает приложению — PHP-FPM/Node/Java-процессам, воркерам очередей, самому веб-серверу, Redis, если он тоже локальный.
  3. Ядро начинает вытеснять страницы приложения в своп, как только суммарное потребление превышает физическую память — и делает это на общих основаниях, не зная, что «база важнее». Подробно о том, почему своп для рабочей памяти процессов — это всегда деградация, а не безобидный запасной вариант, разобрано в статье про антипаттерн «своп вместо памяти».
  4. Своп приложения выглядит как «тормозит сайт», хотя причина — неверно посчитанная память базы, а не проблема в коде приложения.

Отдельно стоит учитывать systemd- и cgroup-лимиты, если сервисы запущены как systemd unit'ы или в контейнерах с ограничением памяти: даже если физической RAM формально хватает, буферный пул, вылезший за MemoryMax конкретного юнита, тоже приведёт к OOM или к троттлингу — это отдельная и частая причина инцидентов на серверах с несколькими сервисами.

Правило для совместного сервера простое: 70-80% — это доля от памяти, которая реально принадлежит базе после того, как вычтена память других сервисов, а не доля от общего объёма RAM сервера.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Когда датасет маленький — избыточная память не даёт пользы

Обратная ситуация — маленький, полностью помещающийся в память датасет. Если вся база данных вместе с индексами весит 2 ГБ, а сервер располагает 32 ГБ RAM, выделение 80% (25+ ГБ) под буферный пул не ускорит ни одного запроса — просто потому что кешировать уже нечего: все данные и так постоянно находятся в кеше, и дальнейшее увеличение пула не повышает cache hit ratio, который уже близок к 100%.

При этом у избыточного выделения есть реальная цена:

  • память, «замороженная» под буферный пул, недоступна для других процессов на сервере (см. предыдущий раздел);
  • у PostgreSQL слишком большой shared_buffers увеличивает время контрольных точек (checkpoint) и объём данных, которые нужно синхронизировать с диском за один проход, что иногда даже ухудшает задержки на записи;
  • на серверах с ограниченной физической памятью это просто нерациональное использование ресурса, за который вы платите.

Для маленьких датасетов разумная методика — не процент от RAM, а объём данных с запасом. Например, если рабочий датасет весит 2-3 ГБ, разумный буферный пул — 4-6 ГБ (с запасом на рост и на индексы, которые могут пересчитываться), а не 25 ГБ «раз уж память есть». Оставшуюся память логичнее отдать под кеш приложения, очереди, или просто оставить сервер менее нагруженным — это дешевле, чем через полгода упираться в лимиты выросшей базы.

Working set — то, что нужно измерить, а не то, что нужно угадать

Ключевая ошибка в слепом следовании проценту — использование объёма RAM или размера базы на диске вместо реального рабочего набора данных (working set). Working set — это тот объём данных и индексов, к которому идут обращения в обычной рабочей нагрузке, а не весь объём таблиц на диске. У многих проектов таблица логов или архивных заказов весит десятки гигабайт, но 95% запросов идут по последним нескольким неделям данных — именно эти несколько гигабайт и есть реальный working set, под который нужно кеширование.

Разница между «размером базы» и working set — это разница между «настроить память по ощущениям» и «настроить память по факту». Измерять working set нужно через реальные метрики попаданий в кеш, а не через размер файлов на диске.

PostgreSQL — cache hit ratio по всей базе:

SELECT
  sum(blks_hit)  AS hits,
  sum(blks_read) AS reads,
  round(sum(blks_hit) * 100.0 / nullif(sum(blks_hit) + sum(blks_read), 0), 2) AS hit_ratio_pct
FROM pg_stat_database;

Пооперационно по таблицам — какие именно объекты «не помещаются» в кеш:

SELECT
  relname,
  heap_blks_hit,
  heap_blks_read,
  round(heap_blks_hit * 100.0 / nullif(heap_blks_hit + heap_blks_read, 0), 2) AS hit_ratio_pct
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC
LIMIT 20;

MySQL/InnoDB — общий hit ratio буферного пула:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
-- hit_ratio = 1 - (reads / read_requests)

Более детальная картина — блок BUFFER POOL AND MEMORY в выводе:

SHOW ENGINE INNODB STATUS\G

В нём видно текущий размер пула, число «грязных» страниц, ожидающих сброса на диск, и коэффициент попаданий за последний интервал. Собирайте эти метрики не разово, а на протяжении нескольких дней с типичной нагрузкой — пиковый вечер интернет-магазина и спокойная ночь дают разные цифры, и ориентироваться нужно на пиковую нагрузку, а не на усреднённую.

Хороший ориентир: если hit ratio стабильно выше 98-99%, буферного пула достаточно и увеличивать его дальше почти бессмысленно. Если ratio заметно ниже и падает под нагрузкой — working set больше текущего пула, и памяти действительно не хватает.

Как посчитать размер буферного пула под конкретный сервер

Вместо «взять процент от RAM» — пошаговая методика, которая учитывает и совместное использование сервера, и реальный working set.

  1. Зафиксируйте физический объём RAM. free -h или cat /proc/meminfo покажут точный объём и что уже занято на текущий момент.
  1. Вычтите память ОС и служебных процессов. Обычно 1-2 ГБ на небольшом сервере или 5% от RAM на крупном — с запасом на файловый кеш некешируемых СУБД файлов, сетевой стек, планировщик задач, мониторинг-агенты.
  1. Если сервер совместный — явно забюджетируйте память других сервисов. Не «что останется», а конкретная цифра: сколько реально потребляют PHP-FPM/Node-воркеры под пиковой нагрузкой, сколько ест локальный Redis, веб-сервер. Измеряется через ps_mem, systemd-cgtop или через лимиты, уже выставленные в cgroup/systemd (MemoryMax для соответствующих unit'ов) — если лимитов ещё нет, их стоит завести именно на этом шаге, чтобы один сервис не «съедал» бюджет другого динамически. Пошаговая настройка тюнинга под конкретный движок разобрана в статье про тюнинг PostgreSQL на VPS.
  1. Измерьте working set по методике из предыдущего раздела, а не берите размер базы на диске. Working set — это данные, которые реально запрашиваются в типичной нагрузке, с запасом 15-25% на рост.
  1. Посчитайте оставшийся бюджет памяти для буферного пула как: RAM − ОС − другие сервисы − память на соединения (для PostgreSQL — примерно work_mem × ожидаемое число параллельных сложных запросов, для MySQL — сумма per-connection буферов на max_connections).
  1. Возьмите буферный пул как минимум из (working set с запасом) и (оставшегося бюджета). Если working set меньше бюджета — не выделяйте лишнее, оставьте запас другим процессам. Если working set больше бюджета — либо сервера не хватает по памяти и его нужно апгрейдить, либо на этом сервере не стоило совмещать СУБД с другими сервисами.
  1. Проверьте на практике и скорректируйте. После применения новых значений (для PostgreSQL — с перезапуском, shared_buffers не меняется на лету) снова снимите hit ratio через несколько дней реальной нагрузки. Общий ориентир по запасу памяти на сервер в целом, не только под буферный пул, — в статье сколько оперативной памяти закладывать с запасом.

Пример для выделенного сервера 32 ГБ только под PostgreSQL, working set измерен на уровне 10 ГБ: ОС — 2 ГБ, соединения и обслуживание — 4 ГБ, остаётся 26 ГБ бюджета, working set 10 ГБ с запасом до 12-14 ГБ — этого достаточно, оставшуюся память логичнее отдать под effective_cache_size (подсказка планировщику, не выделение) и небольшой запас на рост, а не гнаться за формальными 70-80%.

Пример для совместного сервера 16 ГБ (PostgreSQL + приложение + nginx), working set 6 ГБ: ОС — 1 ГБ, приложение и nginx под пиком — 5 ГБ (измерено, а не угадано), остаётся 10 ГБ бюджета, working set с запасом — 7-8 ГБ, значит shared_buffers в районе 5-6 ГБ (около 30-35% от общего RAM, а не 80%) — и в свопе на пике никто не окажется.

Специфика движков: shared_buffers, effective_cache_size и innodb_buffer_pool_size

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

ПараметрPostgreSQLMySQL/InnoDB
Основной параметр буфераshared_buffersinnodb_buffer_pool_size
Типичная доля от RAM (выделенный сервер)~25%, редко выше 40%70-80%
Требует перезапускаданет (динамическое изменение с MySQL 5.7+, innodb_buffer_pool_size можно менять на лету)
Роль кеша ОСвторой уровень кеша, дублирует часть данныхпочти не используется как кеш данных InnoDB
Подсказка планировщикуeffective_cache_size (50-75% RAM, не выделение памяти)нет прямого аналога

Пример конфигурации PostgreSQL (postgresql.conf) для сервера с явно посчитанным бюджетом, а не с формальным процентом:

shared_buffers = 6GB
effective_cache_size = 18GB
work_mem = 32MB
maintenance_work_mem = 512MB

Пример конфигурации MySQL (/etc/mysql/mysql.conf.d/mysqld.cnf) для выделенного сервера баз:

[mysqld]
innodb_buffer_pool_size = 12G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 1G

innodb_buffer_pool_instances имеет смысл увеличивать при пуле больше нескольких гигабайт — это снижает конкуренцию за внутренние блокировки самого пула на многопоточной нагрузке. Для PostgreSQL похожей ручки нет — там масштабирование параллелизма решается на уровне max_connections и пулера соединений (PgBouncer), а не размера shared_buffers.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

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

Если сервер полностью выделен под базу, можно ли просто ставить 80% и не считать working set?

Для MySQL/InnoDB — в целом да, это близко к оптимальному значению по умолчанию, если нет других процессов на сервере. Для PostgreSQL — нет, там даже на выделенном сервере разумный shared_buffers — около 25-40%, а роль «отдать почти всю память под кеш» играет effective_cache_size, который не резервирует память, а лишь подсказывает планировщику запросов.

Что будет, если буферный пул окажется меньше working set?

Часть «горячих» страниц будет постоянно вытесняться и перечитываться с диска, cache hit ratio упадёт, запросы, которые раньше выполнялись из памяти, начнут упираться в дисковый I/O — это заметно по росту времени ответа под нагрузкой и по падению hit ratio в метриках из раздела про working set.

Можно ли поставить буферный пул больше, чем working set, «про запас на рост»?

Разумный запас — 15-25% сверх измеренного working set. Больший запас на совместном сервере отбирает память у других процессов без пользы; на полностью выделенном сервере вреда меньше, но и толку тоже нет, пока датасет реально не вырастет.

Как понять, что причина тормозов — именно нехватка буферного пула, а не что-то ещё?

Сначала проверьте hit ratio описанными выше запросами. Если он высокий (98%+), а тормоза есть — ищите причину в другом месте: отсутствующих индексах, блокировках, медленном диске под запись WAL/redo-логов, а не в размере буферного пула.

Меняются ли эти расчёты, если СУБД работает в контейнере с лимитом памяти?

Да, и это частый источник ошибок: буферный пул нужно считать от лимита cgroup/контейнера (MemoryMax, лимит Docker), а не от физической RAM хоста — иначе процесс СУБД получает OOM внутри контейнера при формально свободной памяти на хосте.

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

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →