Почему первый запрос к базе всегда медленный: холодный кеш по слоям
Только что подняли базу или перезапустили сервис — и первый же запрос, который обычно выполняется за миллисекунды, тянется секундами. Вы проверяете индексы, план запроса, ресурсы сервера — всё в порядке, а тормозит всё равно. Дело не в запросе: база ещё ничего не держит в памяти и вынуждена читать с диска. Разберём, из каких слоёв кеша состоит путь запроса к данным и что с этим делать на практике.
Содержание
- Путь запроса: через сколько кешей он проходит
- Buffer cache / shared_buffers: кеш самой СУБД
- Page cache ОС: второй слой брони
- Кеш плана запроса: не только данные, но и «как их достать»
- Кеш соединений: сессия тоже стоит времени
- Как прогреть базу после рестарта
- Где смотреть hit ratio и чем опасен рестарт в пик нагрузки
Путь запроса: через сколько кешей он проходит
Когда приложение обращается к базе, запрос проходит несколько уровней, прежде чем данные попадают в ответ. Каждый уровень — либо кеш, либо диск, и от того, где найдутся данные, зависит время ответа на порядки. Цепочка для классической реляционной СУБД (PostgreSQL, MySQL) на Linux:
- Кеш соединения — установлено ли уже TCP-соединение и сессия с базой, или их нужно создавать заново.
- Кеш плана запроса — разбирала ли СУБД уже такой запрос и есть ли готовый план выполнения, или нужно заново парсить SQL и строить план.
- Буферный кеш самой СУБД (
shared_buffersв PostgreSQL, InnoDB buffer pool в MySQL) — лежат ли нужные страницы данных в памяти процесса базы. - Page cache операционной системы — если страницы нет в буфере СУБД, есть ли она хотя бы в кеше страниц ядра Linux.
- Диск — если данных нет нигде выше, их нужно физически прочитать с накопителя.
Каждый шаг вниз по списку на порядок дороже предыдущего: обращение к памяти процесса — микросекунды, page cache ОС — чуть дороже, но всё ещё память, а чтение с диска, даже с быстрого NVMe, — уже другой порядок задержки. Именно поэтому первый запрос после старта сервиса самый медленный: ни один уровень кеша ещё не заполнен, и почти всё приходится читать с диска. Дальше разберём каждый слой отдельно — что он кеширует и как его прогреть.
Buffer cache / shared_buffers: кеш самой СУБД
Это первый и самый важный слой кеша данных — область разделяемой памяти под страницы таблиц и индексов. В PostgreSQL это параметр shared_buffers, в MySQL с InnoDB — innodb_buffer_pool_size. Пока страница лежит здесь, чтение и запись идут в память, минуя диск.
Посмотреть текущее значение в PostgreSQL:
SHOW shared_buffers;
Сразу после старта сервиса эта область пустая. Любой запрос, обращающийся к таблице впервые с момента запуска, обязан сходить либо в page cache ОС, либо на диск, чтобы затащить нужные страницы в буфер. Второй такой же запрос по тем же данным пойдёт уже из памяти — отсюда разница в скорости между «первым» и «вторым» запросом, даже если это буквально один и тот же SQL.
Важный нюанс: shared_buffers — не единственное хранилище кешируемых страниц. Его обычно выставляют заметно меньше объёма RAM, оставляя основную часть памяти под page cache ОС — Linux и так эффективно кеширует файлы. Конкретную долю RAM под shared_buffers считают под свою нагрузку и объём базы — универсального числа тут нет.
Если после перезапуска база стала упираться в память, поможет статья про PostgreSQL out of memory — там разобраны похожие механизмы потребления памяти движком.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSPage cache ОС: второй слой брони
Даже если страницы нет в shared_buffers, есть шанс, что она уже лежит в page cache самой ОС — Linux кеширует в свободной памяти всё, что читает с диска, вне зависимости от процесса. Это касается и файлов данных СУБД: как только PostgreSQL или MySQL один раз прочитал файл с диска, ядро оставляет его содержимое в памяти, пока не понадобится место под что-то другое.
Посмотреть, сколько памяти сервера занято под page cache, можно командой:
free -h
Столбец buff/cache покажет объём, занятый под кеш файловой системы и буферы ядра. Это не «потерянная» память — она свободно освобождается по требованию приложений, поэтому большого числа здесь бояться не стоит: чем больше, тем лучше, значит данные уже в памяти.
Ключевой момент: page cache принадлежит операционной системе, а не процессу СУБД. Если вы перезапускаете саму базу (systemctl restart postgresql), но не перезагружаете сервер целиком, page cache никуда не девается — файлы данных остаются в памяти ядра, и СУБД после рестарта прочитает их оттуда, а не с физического диска. А вот полная перезагрузка сервера обнуляет и этот уровень — тогда холодный старт будет по-настоящему холодным.
Отсюда вывод: перезапуск сервиса СУБД — «тёплый» рестарт (страдает только буфер самой СУБД), а перезагрузка сервера — «холодный» (обнуляются оба уровня кеша данных). Разница может быть существенной, и об этом стоит помнить при планировании обслуживания.
Кеш плана запроса: не только данные, но и «как их достать»
Отдельный от данных слой — кеш плана выполнения запроса. Прежде чем СУБД полезет за данными, она должна разобрать SQL и построить план выполнения (какие индексы использовать, в каком порядке соединять таблицы). Это тоже не бесплатная операция, особенно для запросов с несколькими join и подзапросами.
В PostgreSQL планы обычных запросов не кешируются между соединениями по умолчанию — каждый новый запрос планируется заново. Кеш плана появляется только для подготовленных выражений (PREPARE / параметризованные запросы через драйвер с поддержкой prepared statements) и живёт в рамках одной сессии. То есть за пределами prepared statements выигрыш «второго запроса» обычно не про сам SQL-текст, а про то, что данные уже в буфере.
В MySQL раньше существовал отдельный query cache, кешировавший результат целиком по тексту запроса, но начиная с MySQL 8.0 его убрали из ядра — он плохо масштабировался при частых изменениях данных и создавал больше проблем, чем пользы. Если видите упоминания query cache в старых мануалах — на актуальных версиях этого слоя уже нет.
Практический вывод: если ваш ORM или драйвер поддерживает prepared statements и переиспользует соединения через пул, часть накладных расходов на парсинг и планирование уходит для повторяющихся запросов. Но это отдельная оптимизация, не заменяющая прогрев данных в буфере.
Кеш соединений: сессия тоже стоит времени
Ещё один слой, который часто забывают — установка самого соединения с базой. Каждое новое подключение — это TCP-хендшейк (или unix-сокет), аутентификация, выделение серверного процесса (в PostgreSQL — отдельный процесс на соединение) или потока (в MySQL), инициализация сессионных параметров. Это тоже время, и оно тратится заново на каждое новое соединение, если приложение их не переиспользует.
Если приложение открывает новое соединение на каждый запрос вместо использования пула, оно платит эту цену постоянно, а не только «на холодную». Решение — пул соединений: встроенный в фреймворк или ORM, либо отдельный сервис вроде PgBouncer перед PostgreSQL. PgBouncer держит набор уже установленных соединений к базе и раздаёт их запросам приложения, избавляя от постоянной пересборки сессии.
Базовый конфиг PgBouncer в режиме транзакций:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 20
Это отдельная тема с массой нюансов (режимы pooling, поведение с prepared statements, лимиты), но важно понимать: кеш соединений и кеш данных — разные механизмы, и «холодная» проблема может проявляться на обоих одновременно, особенно если сервис только что перезапущен и клиенты массово переподключаются.
Как прогреть базу после рестарта
Прогрев — это осознанное «предзаполнение» кешей до того, как на сервис пойдёт боевая нагрузка, чтобы не заставлять первых реальных пользователей ждать холодных чтений с диска.
Несколько практических подходов для PostgreSQL:
- Расширение
pg_prewarm— читает страницы таблицы или индекса вshared_buffersзаранее:
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT pg_prewarm('orders');
SELECT pg_prewarm('orders_pkey');
Можно прогреть список часто используемых таблиц и индексов скриптом сразу после старта, до открытия трафика.
- Автосохранение состояния буфера —
pg_prewarmумеет прогревать не только по запросу, но и через фоновый воркерautoprewarm, который периодически сохраняет список страниц из буфера и восстанавливает их при следующем старте СУБД. Снимает часть боли от «тёплого» рестарта самого сервиса.
- Прогон типовых запросов — простой и грубый способ: после старта прогнать по базе набор реальных запросов, не пуская живой трафик. Прогревает и данные, и — при prepared statements — кеш плана.
- Постепенное открытие трафика — если инфраструктура позволяет (балансировщик, canary-деплой), пускайте на свежий инстанс небольшую долю трафика, постепенно увеличивая её, вместо мгновенного переключения на 100%.
Для MySQL похожий инструмент — параметры innodb_buffer_pool_dump_at_shutdown и innodb_buffer_pool_load_at_startup: при штатной остановке сервер запоминает страницы буфера и сам подгружает их обратно при старте.
Прогрев не убирает стоимость чтения с диска полностью — он переносит её на удобный момент, до пуска трафика, а не на первых реальных пользователей.
Где смотреть hit ratio и чем опасен рестарт в пик нагрузки
Чтобы не гадать, насколько база «прогрета», в PostgreSQL есть представление pg_stat_database, которое считает, сколько обращений к страницам данных обслужено из буфера, а сколько потребовало чтения с диска:
SELECT datname,
blks_hit,
blks_read,
round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname = 'mydb';
blks_hit — обращения, найденные в буфере СУБД, blks_read — чтения с уровня ниже (page cache ОС или диск; сама статистика не различает, откуда именно). Отношение blks_hit к сумме обеих величин и есть hit ratio. Сразу после старта оно низкое или нулевое, по мере работы растёт по мере заполнения кеша. Конкретный «здоровый» уровень зависит от объёма базы, паттерна доступа и памяти — не берите чужие цифры как норматив, снимайте показатель на своей базе в спокойном режиме и ориентируйтесь на него.
Смотрите эту статистику не разово, а в динамике — резкое падение hit ratio на работающей базе часто сигнализирует не о рестарте, а о том, что рабочий набор данных перестал помещаться в память (выросла база, изменился паттерн запросов) — повод пересматривать shared_buffers или память сервера.
Почему рестарт в пик нагрузки особенно опасен. Дело не только в даунтайме на время перезапуска. Сразу после старта:
- буфер СУБД пуст — каждый запрос читает данные с диска или из page cache;
- если была полная перезагрузка сервера — пуст и page cache, чтения идут по-настоящему с диска;
- если приложение переподключается массово (например, после разрыва пула), сервер одновременно тратится на установку соединений и на холодные чтения;
- на этом фоне резко растёт нагрузка на диск и CPU, что дополнительно замедляет и без того холодные запросы.
Получается эффект снежного кома: холодная база отвечает медленно → запросы копятся в очереди → растёт число активных бэкендов → это давит на память и диск → база отвечает ещё медленнее. В спокойное время та же последовательность проходит почти незаметно — запросов немного, кеш успевает заполниться постепенно. В пик нагрузки «просто перезапустили сервис для конфига» превращается в полноценный инцидент.
Вывод: плановые рестарты СУБД и перезагрузки серверов лучше делать в окна минимальной нагрузки, вместе с прогревом из предыдущего раздела. Если рестарт аварийный (например, из-за нехватки памяти) — стоит заранее разобраться, почему растёт WAL или база упирается в память, чтобы не доводить до аварии в неудобное время.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько времени нужно, чтобы база «прогрелась» естественным путём?
Зависит от объёма рабочего набора, скорости диска и интенсивности трафика — универсального числа нет. Чем активнее запросы к разным частям данных, тем быстрее заполняется буфер, но и тем заметнее просадка. Прогрев вручную (pg_prewarm, дамп buffer pool) обычно выгоднее ожидания.
Если у меня SSD/NVMe, разве не всё равно, читать с диска или из памяти?
Нет — даже быстрый накопитель на порядки медленнее оперативной памяти по задержке одной операции. Разница не так драматична, как с HDD, но заметна на больших объёмах мелких чтений под нагрузкой.
Помогает ли увеличение shared_buffers полностью избежать холодного старта?
Нет, это не устраняет проблему — просто расширяет ёмкость первого уровня кеша. Пустой буфер остаётся пустым сразу после старта независимо от размера.
Нужно ли прогревать кеш соединений отдельно от кеша данных?
Да, это разные механизмы. Если приложение использует пул (или PgBouncer), можно заранее открыть нужное число соединений к свежезапущенной базе до пуска боевого трафика.
Как понять, что дело именно в холодном кеше, а не в реальной проблеме с запросом?
Проверьте pg_stat_database сразу после инцидента и через несколько минут под нагрузкой. Если hit ratio растёт со временем на том же запросе — дело в прогреве. Если ratio высокий, а запрос всё равно медленный — ищите проблему в плане, индексах или блокировках.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →