Буферный пул базы данных: почему он важнее скорости вашего диска
Когда база данных начинает тормозить, первая мысль обычно — сменить диск на более быстрый NVMe. Иногда это помогает, но чаще проблема не в диске, а в том, что буферному пулу СУБД не хватает памяти под рабочий набор данных. Разберём, как устроен буферный пул в PostgreSQL и MySQL, почему при достаточном объёме памяти скорость физического диска почти перестаёт влиять на производительность запросов, и что из этого следует для выбора сервера.
Содержание
Что такое буферный пул
Буферный пул (в PostgreSQL — область памяти, настраиваемая параметром shared_buffers, в MySQL/InnoDB — innodb_buffer_pool_size) — это выделенная область оперативной памяти внутри процесса СУБД, где хранятся копии страниц данных из таблиц и индексов. Страница — минимальная единица чтения и записи на диск: у PostgreSQL по умолчанию 8 КБ, у InnoDB — 16 КБ.
Логика простая: обращение к оперативной памяти — операция внутри адресного пространства процесса, а обращение к диску — это системный вызов через блочный слой ядра, драйвер устройства и очередь команд контроллера, и только потом собственно чтение. Даже на самом быстром NVMe эта цепочка длиннее и дороже, чем прямое обращение к памяти. Поэтому СУБД старается держать в ОЗУ те страницы, к которым обращаются чаще всего.
Стоит помнить, что буферный пул СУБД — не единственный уровень кеша: операционная система тоже кеширует страницы файлов в своей области, page cache (в Linux видна как buff/cache в free -h). Получается двухуровневая система: буферный пул поверх page cache ядра, и часть данных в них задваивается — это нормальная плата за скорость.
Путь страницы: попадание и промах
Когда приходит запрос, СУБД обращается к менеджеру буферов за нужными страницами таблиц и индексов. Дальше — один из двух сценариев.
Попадание (hit): страница уже в пуле. Менеджер буферов находит её по хеш-таблице в разделяемой памяти, отдаёт указатель на данные — диск в этой цепочке не участвует вообще.
Промах (miss): страницы в пуле нет. Менеджер буферов выбирает слот на вытеснение по алгоритму замещения (в PostgreSQL — модифицированный clock-sweep, в InnoDB — LRU с делением на young/old сегменты, что защищает горячие страницы от вымывания одним большим сканированием), инициирует чтение с диска и ждёт его завершения. Именно здесь запрос физически простаивает — и именно этот шаг СУБД старается делать как можно реже.
С записью логика отложенная: при изменении строки СУБД сначала пишет запись в журнал упреждающей записи (WAL в PostgreSQL, redo log в InnoDB) — это последовательная синхронная запись, гарантирующая durability. Сама страница в пуле помечается «грязной» и физически сбрасывается на диск позже, асинхронно, фоновым процессом (background writer/checkpointer в PostgreSQL, page cleaner в InnoDB). Про то, почему этот фоновый сброс иногда создаёт заметную паузу, — в статье про периодическое замирание базы при checkpoint.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему скорость диска перестаёт быть узким местом
Если буферный пул по объёму покрывает рабочий набор данных (working set — таблицы, индексы и их части, к которым реально идут частые обращения), подавляющее большинство чтений обслуживается прямо из памяти. Диск почти не участвует в цикле обработки читающих запросов — он нужен в основном для записи (WAL/redo log, сброс грязных страниц) и для редких промахов по холодным данным.
В такой конфигурации замена диска на более быстрый почти не ускоряет читающие запросы: они и так не идут на диск на пути горячего исполнения. Эффект от быстрого NVMe в этом сценарии если и есть, то в основном за счёт снижения времени фонового сброса и записи WAL — то есть влияет на пиковую нагрузку по записи и на восстановление после сбоя, а не на типичный SELECT.
Это не означает, что диск не важен в принципе — значит, что при достаточном буферном пуле он перестаёт быть узким местом для чтения. И важен именно рабочий набор, а не размер всей базы: если база весит сотни гигабайт, но реально «горячих» данных на 20 ГБ, буферного пула в 24–32 ГБ может хватить, чтобы почти все запросы обслуживались из памяти — даже на не самом быстром диске.
Что происходит, когда буферного пула не хватает
Обратная ситуация — рабочий набор заметно больше буферного пула — превращает СУБД в машину постоянных промахов: каждая новая загруженная страница вытесняет из пула другую, которая скоро снова понадобится. Это thrashing буферного пула, по аналогии со свопингом ОС. Здесь не спасает даже самый быстрый NVMe: диск может отвечать сколь угодно оперативно, но если каждый читающий запрос вынужден реально идти на диск вместо памяти, суммарная задержка растёт — просто потому что путь стал длиннее на целый уровень, а параллельные промахи ещё и конкурируют за очередь ввода-вывода устройства.
Типичные симптомы на практике: устойчиво высокий iowait в top/vmstat, растущая очередь у диска в iostat -x, и низкое отношение попаданий в статистике самой СУБД. В PostgreSQL это видно через pg_statio_user_tables:
SELECT
sum(heap_blks_hit) AS hits,
sum(heap_blks_read) AS reads,
round(
sum(heap_blks_hit)::numeric /
nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0), 4
) AS hit_ratio
FROM pg_statio_user_tables;
В MySQL с InnoDB — через information_schema:
SELECT POOL_SIZE, DATABASE_PAGES, PAGES_FREE, PAGES_READ, PAGES_WRITTEN
FROM information_schema.INNODB_BUFFER_POOL_STATS;
Смотреть стоит не на разовое значение, а на динамику: если PAGES_READ (число физических чтений с диска) стабильно растёт при спокойной, повторяющейся нагрузке — это сигнал, что рабочий набор не помещается в буферный пул. Универсального порога «хорошего» hit ratio нет — он зависит от характера ваших запросов, ориентируйтесь на динамику по своей базе, а не на чужой бенчмарк.
Как настроить размер буферного пула
В PostgreSQL shared_buffers требует перезапуска сервера при изменении — это область разделяемой памяти, создаваемая при старте процесса:
# postgresql.conf
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 64MB
maintenance_work_mem = 512MB
effective_cache_size при этом не выделяет память сама по себе — это подсказка планировщику о том, сколько памяти суммарно (буферный пул плюс page cache ОС) реально доступно под кеш данных базы, и от неё зависит, насколько планировщик готов выбирать планы в расчёте на закешированные данные.
В MySQL за это отвечает innodb_buffer_pool_size, и современные версии позволяют менять его как resize-параметр без полного перезапуска (сама операция не мгновенная и создаёт временную нагрузку):
[mysqld]
innodb_buffer_pool_size = 8G
innodb_buffer_pool_instances = 8
innodb_buffer_pool_dump_at_shutdown = ON
innodb_buffer_pool_load_at_startup = ON
innodb_buffer_pool_instances делит пул на независимые сегменты, снижая конкуренцию за внутренние блокировки при высокой параллельной нагрузке. А пара dump_at_shutdown/load_at_startup решает проблему холодного старта: без неё после перезапуска пул пустой, и первые минуты работы база заново промахивается по всем горячим страницам — с этими параметрами MySQL сохраняет список горячих страниц перед остановкой и подгружает их сразу при старте.
Отдельный рычаг для больших пулов — huge pages, крупные страницы памяти уровня ядра Linux, снижающие накладные расходы на трансляцию адресов. Механика и практический эффект разобраны в статье почему база без huge pages медленнее.
Практические следствия для выбора памяти сервера
Из этой механики вытекает практический вывод: для типичного OLTP-приложения (веб-сервис, интернет-магазин, CRM, биллинг) объём оперативной памяти сервера баз данных зачастую важнее модели процессора или поколения NVMe — при условии, что рабочий набор данных вообще можно уместить в разумный объём ОЗУ. Порядок действий при подборе конфигурации:
- Оцените реальный рабочий набор — не размер всей базы, а объём данных и индексов, к которым идут частые обращения (можно прикинуть по размеру самых «горячих» таблиц и их индексов:
pg_total_relation_sizeв PostgreSQL,information_schema.TABLESв MySQL). - Заложите буферный пул с запасом на рост рабочего набора, а не строго под текущий объём — база растёт, и то, что сегодня помещается впритык, через несколько месяцев может начать промахиваться.
- Не забывайте о памяти сверх буферного пула:
work_memв PostgreSQL выделяется на каждую операцию сортировки/хеширования в каждом соединении и при большом числе параллельных соединений может суммарно съесть заметную часть ОЗУ сверхshared_buffers— частая причина OOM на серверах, где буферный пул настроили щедро, а про параллельные соединения забыли. - Оставьте памяти операционной системе под её page cache — двухуровневое кеширование означает частичное задваивание данных в памяти, и на это тоже нужно место сверх размера самого буферного пула.
Здесь же уместна оговорка к тезису «чем больше RAM, тем лучше»: сам по себе объём памяти не решает проблему, если он не соответствует характеру нагрузки и не подкреплён правильной настройкой буферного пула — разбор этого мифа в статье чем больше RAM, тем лучше — так ли это.
Когда скорость диска всё-таки решает
Диск не становится полностью неважным — есть сценарии, где его характеристики продолжают напрямую влиять на производительность даже при щедром буферном пуле:
- Нагрузка с большой долей записи. WAL/redo log пишется последовательно и синхронно при коммите (если не отключена соответствующая гарантия durability) — здесь задержка диска напрямую ограничивает пропускную способность транзакций, независимо от объёма буферного пула.
- Холодный старт. Сразу после перезапуска буферный пул пуст (если не восстановлен из дампа, как описано выше), и первое время база активно промахивается, пока не прогреется — в этот период скорость диска ощущается напрямую.
- Аналитика и полные сканы. Отчёты, агрегации по всей таблице, ETL-задачи читают объёмы данных, заведомо большие любого разумного буферного пула — узким местом становится последовательная пропускная способность диска.
- Рабочий набор объективно больше доступной памяти. Иногда база просто большая, а бюджет на память ограничен — тогда работает связка «максимально возможный буферный пул плюс быстрый диск под оставшиеся промахи», а не выбор одного вместо другого.
Более широкий разбор роли диска для баз данных — в статье почему важна скорость диска для баз данных: там ситуация рассматривается без предположения о достаточном буферном пуле, и выводы осторожнее.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько оперативной памяти выделять под буферный пул?
Единого числа нет — отправная точка для PostgreSQL часто звучит как «около четверти оперативной памяти сервера», для MySQL/InnoDB на выделенном под базу сервере — заметно больше. Но правильный ориентир — не доля от RAM, а покрытие реального рабочего набора, который вы оцениваете по своей нагрузке и уточняете по статистике попаданий.
Что будет, если сделать буферный пул больше, чем реально нужно?
Ничего страшного не случится с производительностью чтения, но вы отберёте память у операционной системы, у work_mem-подобных нужд и у других процессов на сервере — вплоть до риска OOM при большом числе параллельных соединений.
Почему после перезапуска базы запросы стали заметно медленнее?
Потому что буферный пул опустел и заново прогревается промахами. Это временное явление — со временем горячие страницы снова окажутся в памяти. Если перезапуски частые и это критично, посмотрите на механизмы сохранения состояния пула вроде dump/load в InnoDB.
Поможет ли переход на более быстрый NVMe, если буферный пул явно мал для рабочего набора?
Поможет, но ограниченно — вы сократите время каждого отдельного промаха, но не устраните сами промахи. Если проблема архитектурная (пул меньше рабочего набора), правильнее сначала увеличить память под буферный пул, а уже потом смотреть на диск как на второй рычаг.
Нужно ли пересматривать размер буферного пула по мере роста базы?
Да, регулярно. Рабочий набор обычно растёт вместе с базой, и конфигурация, подобранная год назад, может со временем перестать его покрывать — стоит периодически возвращаться к статистике попаданий, а не настраивать один раз и забывать.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →