linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть IV — Буферный кеш и журнал

$ глава 17 · 55 минут

Буферный кеш

Диск медленный, память быстрая - на этом простом факте держится вся производительность баз данных. PostgreSQL никогда не работает с данными прямо на диске: он сначала поднимает нужную страницу в память, в общий буферный кеш, и дальше читает и меняет её там. На диск изменения попадают потом, отдельно и не сразу.

Из этого вытекает почти всё, что важно знать о производительности. Запрос «быстрый» или «медленный» чаще всего зависит не от плана, а от того, лежали его данные в кеше или их пришлось читать с диска. А вся механика надёжности (журнал, контрольные точки - следующие главы) существует именно потому, что между изменением в памяти и записью на диск есть зазор, в который может вклиниться сбой.

В этой главе мы посмотрим на буферный кеш как на структуру данных: из чего состоит буфер, как PostgreSQL решает, какой выкинуть, когда место кончилось, и почему большой SELECT не вымывает весь кеш. И всё это - не на словах, а через pg_buffercache.

17.1 Зачем нужен общий кеш

Буферный кеш - это область разделяемой памяти, нарезанная на слоты («буферы») размером ровно со страницу, 8 КБ. Её размер задаёт shared_buffers. Все процессы-бэкенды работают с одним и тем же кешем: если один поднял страницу, второй найдёт её там же, а не будет читать с диска заново.

Логика обращения всегда одна:

  1. бэкенду нужна страница - он ищет её в кеше;
  2. нашёл - работает с ней в памяти (это попадание, hit);
  3. не нашёл - читает с диска в свободный буфер (это промах, read);
  4. если свободных буферов нет - сначала надо кого-то выселить.

Чем больше доля попаданий, тем меньше обращений к диску и тем быстрее работает база. Поэтому shared_buffers - один из первых параметров, который трогают при настройке (об этом - в части про эксплуатацию).

17.2 Четыре состояния обращения к буферу

Удобно смотреть на работу с кешем через четыре исхода. Любое обращение к данным сводится к одному из них:

  • hit - страница уже в кеше, диск не нужен. Самый дешёвый исход;
  • read - страницы в кеше нет, читаем с диска в свободный буфер;
  • dirtied - буфер изменили в памяти; теперь он «грязный» и отличается от того, что на диске;
  • written - грязный буфер сбросили на диск.

Видеть эти исходы числом - значит понимать, что происходит с базой. Простой срез - в pg_statio_user_tables (heap_blks_hit против heap_blks_read); подробный, с PostgreSQL 16, - в pg_stat_io. Разбор состояний - в buffer-states.

17.3 Кто пишет грязные буферы

Грязный буфер не сбрасывается на диск сразу после изменения - иначе терялся бы весь смысл кеша. На диск его относят три участника:

  • bgwriter (background writer) - фоновый процесс, который потихоньку сбрасывает грязные буферы заранее, чтобы бэкендам было куда селить новые страницы;
  • checkpointer - сбрасывает все грязные буферы во время контрольной точки (глава 19);
  • сам бэкенд - если ему нужен свободный буфер, а под рукой только грязные, он вынужден сбросить один сам, прямо в ходе запроса.

Последний случай - худший: запрос пользователя тормозит, потому что ему пришлось писать на диск. Когда таких записей много, это признак, что bgwriter не поспевает или shared_buffers мал. Поэтому за долей буферов, записанных бэкендами, следят.

17.4 Вытеснение: clock-sweep, а не LRU

Когда все буферы заняты, а нужен новый, PostgreSQL должен кого-то выселить. Наивно было бы выкидывать «давно не использованный» (LRU), но честный LRU дорого поддерживать под высокой конкуренцией. Вместо этого используется приближённый алгоритм - clock-sweep («часовая стрелка»).

У каждого буфера есть счётчик популярности usage_count. При каждом обращении он растёт (до небольшого потолка). Стрелка ходит по буферам по кругу:

  • если у буфера usage_count > 0 - стрелка уменьшает счётчик на единицу и идёт дальше («ты популярен, поживёшь ещё круг»);
  • если usage_count = 0 и буфер никем не «прибит» (не pinned) - его выселяют, на его место читают новую страницу.

Так часто используемые страницы переживают много кругов, а одноразовые быстро вылетают - почти как LRU, но без дорогой книжной записи о порядке. Если выселяемый буфер был грязным, его сначала записывают на диск.

17.5 Заглянем в кеш через pg_buffercache

Кеш - не чёрный ящик. Расширение pg_buffercache показывает каждый занятый буфер: какой странице какого отношения он принадлежит, грязный ли он, какой у него usage_count.

sql
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
-- сколько буферов занимает таблица flights
SELECT count(*) AS buffers,
       count(*) FILTER (WHERE isdirty) AS dirty
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('flights');

Прогрейте таблицу запросом - и её доля в буферах вырастет. Повторный запрос пойдёт уже из кеша (hit), и счётчик чтений с диска расти перестанет.

В PostgreSQL 16 появился дешёвый сводный вид pg_buffercache_summary

  • он не сканирует весь кеш построчно, а отдаёт агрегат (сколько буферов занято, сколько грязных, средний usage_count). Удобно для мониторинга. Детали - в buffer-cache.

17.6 Буферное кольцо: почему большой SELECT не вымывает кеш

Представьте SELECT count(*) по таблице в десятки гигабайт. Если бы он читал все страницы в обычном порядке, он вытеснил бы из кеша вообще всё полезное, набив его данными, которые после прохода больше не нужны. Один отчёт - и кеш «холодный» для всех.

Чтобы этого не было, для больших последовательных сканов PostgreSQL использует не весь кеш, а маленькое буферное кольцо (ring buffer)

  • несколько сотен килобайт, которые сканирование переиспользует по кругу. Прочитал страницу, обработал, в том же кольце читаешь следующую. Основной кеш при этом не трогается.

Кольцо включается, когда таблица больше четверти shared_buffers. Свои кольца есть и у VACUUM, и у массовых записей (COPY). Это объясняет неожиданное на первый взгляд: большой отчёт может быть медленным каждый раз, потому что его данные намеренно не оседают в кеше. Подробнее про устройство кольца - в buffer-ring.

Копнуть глубже: сканы умеют объединяться. Если две сессии одновременно последовательно сканируют одну большую таблицу, PostgreSQL может пристроить вторую к уже идущему скану первой - они читают страницы «паровозиком» с одной позиции. Один проход по диску обслуживает оба запроса. Управляется это параметром synchronize_seqscans.

17.7 SLRU-кеши: маленькие кеши для служебных данных

Буферный кеш хранит страницы таблиц и индексов. Но у PostgreSQL есть и служебные структуры - статусы транзакций (clog), multixact, subtransactions. Их держат отдельные маленькие кеши - SLRU (Simple Least Recently Used).

Они невелики и обычно не привлекают внимания, но на специфических нагрузках (много мелких транзакций, обилие блокировок строк) могут стать узким местом - так называемое «SLRU contention». В PostgreSQL 17 их размеры стали настраиваемыми (*_buffers-параметры), а pg_stat_slru показывает по ним статистику. Знать про их существование полезно: когда обычные метрики кеша в норме, а база тормозит, иногда причина именно здесь.

17.8 Как это связано с надёжностью

Главное следствие всей этой главы - зазор между памятью и диском. Изменённая страница какое-то время живёт только в кеше; на диске пока старая версия. Если сервер упадёт в этот момент, изменение, казалось бы, потеряно.

Но мы знаем, что PostgreSQL переживает сбои без потери закоммиченных данных. Значит, есть что-то, что фиксирует изменение раньше, чем оно доходит до файла данных. Это журнал предзаписи - WAL, и именно он закрывает зазор. Следующая глава - о нём.

Уроки в sandbox

lab-17.1. Что лежит в кеше

Заглянем в буферный кеш вживую. Прогреем таблицу запросом, посмотрим её долю в буферах через pg_buffercache, сравним попадания и чтения до и после прогрева. Перед каждым шагом предскажи, что покажет счётчик.

  1. Подключи pg_buffercache и посмотри, сколько буферов сейчас занимает flights.

  2. Сбрось статистику и сделай первый SELECT по таблице - предскажи: будет это read или hit?

  3. Повтори тот же SELECT и сравни heap_blks_hit и heap_blks_read в pg_statio_user_tables.

  4. Посчитай долю грязных буферов после UPDATE по таблице.

  5. Посмотри pg_buffercache_summary - сводку по всему кешу одним запросом.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Буферный кеш - область разделяемой памяти (`shared_buffers`), нарезанная на буферы по 8 КБ; все бэкенды работают с одним кешем.
  • Любое обращение - один из четырёх исходов: hit (в кеше), read (промах, чтение с диска), dirtied (изменён в памяти), written (сброшен на диск).
  • Грязные буферы пишут bgwriter (заранее), checkpointer (в контрольной точке) и сам бэкенд (когда нет свободных) - последнее тормозит запрос.
  • Вытеснение - приближённый алгоритм clock-sweep по счётчику `usage_count`, а не честный LRU; грязный буфер перед выселением записывается.
  • `pg_buffercache` показывает каждый буфер (отношение, грязный ли, usage_count); `pg_buffercache_summary` (PG16) отдаёт дешёвый агрегат.
  • Большие последовательные сканы и VACUUM используют буферное кольцо в несколько сотен КБ, чтобы не вымывать основной кеш.
  • Зазор между изменением в памяти и записью на диск - причина существования WAL, который фиксирует изменение раньше файла данных.

Контрольные вопросы

  1. Опишите четыре состояния обращения к буферу.

    Показать ответ

    Hit - нужная страница уже в кеше, диск не трогается, самый дешёвый исход. Read - страницы в кеше нет, её читают с диска в свободный буфер (промах). Dirtied - буфер изменили в памяти, и теперь он «грязный», отличается от версии на диске. Written - грязный буфер сбросили на диск. Соотношение hit к read показывает эффективность кеша, а объём записей - нагрузку на запись.

  2. Почему PostgreSQL использует clock-sweep, а не честный LRU?

    Показать ответ

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

  3. Зачем нужно буферное кольцо и когда оно включается?

    Показать ответ

    Чтобы один большой последовательный скан или VACUUM не вытеснил из кеша все полезные страницы, набив его данными, которые после прохода не нужны. Вместо всего кеша такой проход использует маленькое кольцо в несколько сотен килобайт и крутит страницы по нему. Включается оно, когда таблица больше четверти shared_buffers. Побочный эффект: большой отчёт может быть медленным каждый раз, потому что его данные намеренно не оседают в кеше.

  4. Почему запись грязного буфера самим бэкендом - плохой признак?

    Показать ответ

    Потому что это означает: бэкенду понадобился свободный буфер, а все подходящие оказались грязными, и ему пришлось самому записать один на диск прямо посреди обслуживания запроса. Пользовательский запрос тормозит на операции записи, которую в норме должен был сделать заранее фоновый bgwriter. Много таких записей - сигнал, что bgwriter не поспевает или shared_buffers слишком мал.

  5. Как буферный кеш связан с необходимостью WAL?

    Показать ответ

    Изменённая страница какое-то время существует только в кеше - на диске пока её старая версия. Между изменением в памяти и записью на диск есть зазор, и сбой в этот момент грозил бы потерей данных. Чтобы этого не происходило, PostgreSQL фиксирует изменение в журнале предзаписи (WAL) раньше, чем оно доходит до файла данных. Поэтому после сбоя закоммиченные изменения можно восстановить из журнала, даже если сами страницы данных на диск ещё не попали.

← Предыдущая16-autovacuum-bloatСледующая →18-wal-basics
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки