Введение
Тема буферов в PostgreSQL обычно всплывает только в одном месте — когда кто-то советует «подкрутить shared_buffers (например, до 1/4 RAM)».
Но буферы — это не просто настройка. Это фундаментальный слой, через который проходят чтения и записи: какие данные попадут в память, что будет вытеснено, когда и как «грязные» страницы окажутся на диске, почему последовательный скан не должен уничтожать прогретый кэш, и зачем вообще PostgreSQL держит собственный buffer cache при наличии кэша ОС.
Оригинал: https://boringsql.com/posts/introduction-to-buffers/
1) База основ: 8KB страница — атомарная единица I/O
Прежде чем говорить о буферах, важно понять ключевую вещь: PostgreSQL читает и пишет данные блоками фиксированного размера — страницами (pages).
По умолчанию размер страницы почти всегда 8192 байта (8KB). Это означает:
- когда запросу нужна «одна строка», PostgreSQL всё равно читает страницу целиком;
- когда происходит запись, изменяется и записывается страница (а не отдельная строка);
- таблицы и индексы — это просто набор таких 8KB страниц;
- очень большие строки могут занимать несколько страниц, но «атомарность I/O» остаётся на уровне страницы.
Проверить размер страницы можно так:
SHOW block_size;2) Почему PostgreSQL держит свой кэш, если есть кэш ОС
Логичный вопрос: зачем PostgreSQL вообще нужен собственный буферный кэш, если операционная система уже умеет кэшировать файлы в RAM (page cache)?
Причины две:
2.1 Семантика данных
ОС видит «байты в файлах». PostgreSQL видит «таблицы, индексы, планы запросов» — и может принимать более умные решения. Например, при разовом последовательном скане большой таблицы PostgreSQL старается не «вымести» из кэша горячие страницы, используя специальные стратегии (ring buffers).
2.2 Долговечность и WAL (ACID)
PostgreSQL обязан соблюдать правило: сначала WAL должен попасть на стабильное хранилище, и только потом можно считать изменения данных надёжно зафиксированными. ОС не понимает, какие страницы относятся к данным, какие к WAL, и не может так же эффективно гарантировать этот порядок без сильного удара по производительности.
3) shared_buffers: главный буферный пул PostgreSQL
Параметр shared_buffers задаёт размер общей памяти, доступной всем backend-процессам.
Модель простая:
- процессу нужен блок данных → он сперва ищет его в shared_buffers;
- если блок найден — это hit (диска не касаемся);
- если блока нет — это miss: читаем с диска (или из кэша ОС), кладём в shared_buffers для следующих обращений.
Посмотреть текущий размер:
SHOW shared_buffers;Важно: shared_buffers — это не только «страницы данных». Внутри общего пула есть ещё служебные структуры, без которых быстрый кэш невозможен:
- сами буферы (слоты под 8KB страницы);
- дескрипторы буферов (метаданные: что за страница, dirty/clean, счётчики и т.д.);
- хеш-таблица для быстрого поиска «страница → слот» (логика hit/miss).
4) Pin count и usage count: почему «горячие» страницы живут дольше
Поведение буфера во многом определяется двумя счётчиками:
4.1 Pin count (закрепление)
Если страница прямо сейчас читается или модифицируется активным запросом, она «пинится» (pin). Пока pin > 0, страницу нельзя вытеснить из буферного пула — иначе запрос потеряет данные из-под ног.
4.2 Usage count (частота/давность использования)
PostgreSQL хранит небольшую оценку «насколько страница была востребована». При обращениях usage count растёт (с ограничением), а при попытке вытеснения — уменьшается.
Зачем это нужно? Чтобы один большой sequential scan не уничтожил весь прогретый кэш.
5) Как вытесняются страницы: clock sweep вместо LRU
Когда shared_buffers заполнен и нужно загрузить новую страницу, PostgreSQL должен найти место. Полноценный LRU был бы дорогим (поддерживать структуру данных на каждое обращение).
Вместо этого применяется алгоритм clock sweep (по аналогии со стрелкой часов, которая «обходит» круг буферов):
- если буфер pinned — пропускаем;
- если usage count > 0 — уменьшаем и идём дальше (буфер «выжил» этот проход);
- если usage count стал 0 и буфер не pinned — его можно вытеснять.
Итог: «холодные» страницы вылетают быстро, «горячие» держатся дольше.
6) Dirty buffers, background writer и checkpoint: когда запись реально уходит на диск
Когда PostgreSQL модифицирует страницу в shared_buffers, она становится dirty (грязной). Это означает: в памяти есть версия, которую нужно сбросить на диск, но делать это «немедленно» часто невыгодно (страница может измениться ещё много раз).
Как же грязные страницы попадают на диск:
6.1 Checkpoint
Во время checkpoint PostgreSQL сбрасывает грязные страницы, фиксируя согласованное состояние на диске. После успешного checkpoint при аварии нужно будет воспроизвести WAL только «с этого момента».
6.2 Background writer
Фоновый writer старается заранее «подчищать» грязные страницы, чтобы при вытеснении (eviction) backend не упирался в синхронную запись.
6.3 Худший случай: синхронная запись на вытеснении
Если clock sweep упёрся в dirty страницу, которую нужно вытеснить прямо сейчас — придётся синхронно записать её на диск, и запрос может ждать I/O. Это часто воспринимается как «внезапные лаги», особенно под нагрузкой.
7) Ring buffers: защита кэша от «одноразовых» операций
Есть операции, которые по природе «одноразовые» и трогают много страниц подряд: большой sequential scan, VACUUM, bulk операции. Если пустить их в общий shared_buffers, они могут выдавить горячие страницы и «охладить» систему на минуты.
Поэтому PostgreSQL использует ring buffers — маленькие отдельные буферные кольца для bulk-процессов. Смысл: прокрутить поток страниц внутри маленького буфера, не загрязняя основной кэш.
Типовые сценарии:
- большие sequential scans используют отдельный небольшой ring buffer (чтобы не вытеснять основной кэш);
- COPY / CREATE TABLE AS и другие bulk write операции используют ring buffer большего размера для эффективной пакетной записи;
- VACUUM имеет свою стратегию, чтобы не разрушать горячий кэш.
8) Local buffers: временные таблицы и temp_buffers
Временные таблицы (temporary tables) обслуживаются иначе: поскольку они «сессионные», PostgreSQL может использовать локальные буферы backend-процесса (local buffers) вместо общих shared_buffers.
Параметр temp_buffers задаёт размер памяти под временные таблицы на соединение. Плюсы:
- меньше межпроцессной синхронизации (быстрее);
- временные операции меньше «засоряют» shared_buffers;
- часто это хорошая альтернатива сложным CTE/подзапросам для промежуточных расчётов.
Но есть важный момент: temp_buffers — это память на соединение, то есть при большом количестве коннектов суммарное потребление может вырасти очень сильно.
9) OS Page Cache: «второй уровень» кэша и почему правило 25% не из воздуха
PostgreSQL не обходит ОС: чтения и записи проходят через ядро, а ядро держит свой page cache. В итоге возможна двойная буферизация: одна и та же 8KB страница может находиться и в shared_buffers, и в кэше ОС.
Звучит как «трата памяти», но на практике это часто помогает:
- PostgreSQL может вытеснить clean страницу из shared_buffers, но она останется в OS page cache;
- если она снова понадобится — ОС отдаст её из RAM, без реального чтения с диска;
- ОС делает read-ahead для последовательных паттернов доступа, что ускоряет потоковые чтения.
Отсюда и классический компромисс: выделить часть RAM PostgreSQL под shared_buffers и оставить пространство ОС, чтобы page cache работал как «L2 кэш».
effective_cache_size: подсказка планировщику
Параметр effective_cache_size не «выделяет память». Он сообщает планировщику оценку доступного кэша (shared_buffers + OS cache), влияя на выбор планов (например, склоняя к index scan, если ожидается, что данные в кэше).
SHOW effective_cache_size;Практические советы: как использовать знания о буферах в оптимизации
1) Смотрите BUFFERS в плане выполнения
Если вы анализируете производительность, одного EXPLAIN ANALYZE часто мало — важно видеть, сколько было:
- shared hit (попадания в shared_buffers),
- shared read (чтение),
- и не было ли временных файлов/спиллов.
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;2) Избегайте «убийственных» sequential scan, которые охлаждают систему
Большие сканы могут быть допустимы, но важно понимать их последствия и почему PostgreSQL пытается изолировать их ring buffers. Иногда лучше оптимизировать отчёты индексами/партиционированием, чем регулярно прогонять гигантские сканы.
3) Следите за «грязными» страницами и пиками записи
Если система периодически «подвисает» на записи, это может быть:
- агрессивное накопление dirty buffers,
- неудачные checkpoint-параметры,
- ситуации, когда backend вынужден синхронно сбрасывать грязные страницы при вытеснении.
4) Временные таблицы иногда быстрее, чем сложные CTE
Если у вас тяжёлые промежуточные вычисления, временная таблица + увеличение temp_buffers (в разумных пределах) может дать выигрыш и уменьшить давление на shared_buffers.
FAQ
Правда ли, что shared_buffers нужно ставить в 25% RAM?
Это популярная отправная точка, потому что важно оставить место OS page cache. Но идеальное значение зависит от нагрузки, размера данных, типа диска, числа соединений и того, насколько система «cache-friendly». Начинайте с разумной базы, затем измеряйте (BUFFERS, latency, I/O).
Если shared_buffers большой — будет быстрее?
Не всегда. Слишком большой shared_buffers может:
- уменьшить OS page cache;
- изменить поведение планировщика и эффективность read-ahead;
- не дать ожидаемого эффекта, если узкое место — CPU, блокировки или плохие запросы.
Почему иногда запрос “читает с диска”, хотя данных много в памяти?
Потому что «память» бывает двух уровней: shared_buffers и OS page cache. Miss в shared_buffers не означает физический диск — данные могут быть в кэше ОС, и тогда чтение будет быстрым.
Что такое pinned buffers и почему это важно?
Pinned буфер нельзя вытеснить, пока он используется активным запросом. Это защита от неконсистентности, но при высокой конкуренции и давлении на буферный пул pinned страницы могут усложнять eviction и влиять на latency.
Вывод
Буферы — это не «чёрный ящик» и не только shared_buffers. Это система управления страницами, которая включает:
- 8KB страницы как атомарную единицу I/O;
- общий buffer pool (shared_buffers) с быстрым поиском страниц;
- pin/usage счётчики и алгоритм clock sweep;
- механизмы работы с грязными страницами (checkpoint, background writer);
- ring buffers для bulk-операций, чтобы не разрушать прогретый кэш;
- local buffers для временных таблиц;
- и «второй уровень» в виде OS page cache + настройку effective_cache_size.
Понимание этих механизмов помогает точнее диагностировать проблемы производительности и настраивать систему на реальные сценарии нагрузки.