Skip to content

Введение в буферы PostgreSQL

Автор: Radim Marek, Introduction to Buffers in PostgreSQL


Работа над RegreSQL заставила меня уделить много внимания буферам. Если вы иногда работаете с PostgreSQL, то наверняка слышали о настройке shared_buffers и следовали старому доброму совету выставить его на уровне 1/4 от доступной оперативной памяти. Но после того как мы немного слишком увлеклись этой темой в недавнем выпуске Postgres FM, меня спросили, что к чему.


Буферы — одна из тех тем, которую легко забыть. И хотя они являются фундаментальным блоком архитектуры производительности PostgreSQL, большинство из нас воспринимает их как чёрный ящик. Эта статья попытается это изменить.

Страница размером 8 КБ


Есть одна концепция, которую нужно разобрать, прежде чем углубляться в буферы. Это концепция страницы размером 8 КБ. Вся информация в PostgreSQL хранится в блоках шириной 8 КБ.


Когда PostgreSQL считывает данные, он читает не отдельные строки. Он читает всю страницу целиком. Когда он что-то записывает — то же самое, ту же страницу. Чтобы получить одну маленькую строку, вы всегда считаете гораздо больше данных вместе с ней. И, если вы внимательно изучали предмет, то же самое относится и к записи.



-- вы можете проверить размер блока (который почти всегда должен быть 8192 байта)
SHOW block_size;

block_size
------------
8192
(1 row)

Каждая таблица и индекс — это коллекция таких страниц. Строка может занимать несколько страниц, если она достаточно велика, но страница остаётся атомарной единицей ввода-вывода.



PostgreSQL vs операционная система


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


Ответ довольно прост. PostgreSQL понимает данные, которые он читает. В то время как операционная система видит только файлы и байты. PostgreSQL видит таблицы, индексы, планы запросов и обладает смысловыми знаниями, чтобы кешировать вещи эффективнее.


Рассмотрим этот пример: запросу необходимо выполнить последовательный просмотр большой таблицы. Операционная система может радостно закешировать все эти страницы, но PostgreSQL знает, что это разовая операция, и использует специальную стратегию (кольцевые буферы), чтобы избежать вытеснения данных из основного кеша.


Второй важный аспект — ACID, или, точнее, гарантия того, что журнал упреждающей записи (WAL) попадёт на стабильное хранилище до изменения страницы данных. Операционная система не делает различий и не может эффективно гарантировать это требование долговечности (только ценой снижения производительности).



shared_buffers


Теперь мы можем перейти к тому, что все знают как основной «кеш» PostgreSQL. Параметр shared_buffers контролирует размер общей памяти, доступной всем фоновым процессам. Если любому фоновому процессу требуется получить страницу, он сначала проверяет общие буферы. Если страница присутствует — это попадание (hit). Дисковый ввод-вывод не нужен. Промах (miss)? Считайте её с диска (или из кеша ОС) и сохраните в общих буферах на будущее.


У WAL есть своя собственная область буферов (wal_buffers). Это разделение существует потому, что записи в WAL являются последовательными и должны быть сохранены до того, как соответствующее изменение данных может считаться завершённым. По умолчанию это 3% от shared_buffers, но не более 16 МБ (один сегмент WAL).



SHOW shared_buffers;

shared_buffers
----------------
128MB
(1 row)

Значение по умолчанию 128 МБ очень консервативно и связано с тем, чтобы установка PostgreSQL по умолчанию работала практически на любой системе (включая системы с ограниченной оперативной памятью). Но в вашей обычной среде это значение, как правило, должно быть намного выше.


Значение 128 МБ относится к фактическому кешируемому содержимому. Если учесть, что одна страница занимает 8 КБ, можно представить это как 16 384 отдельных ячейки для хранения данных.



Интерактивный визуализатор


Изучите, как на самом деле работает пул буферов PostgreSQL. Наблюдайте, как алгоритм циклического просмотра (clock sweep) вытесняет «холодные» страницы, смотрите, как буферы переходят между состояниями «чистый», «грязный» и «закреплённый», и поймите, как хеш-таблица обеспечивает поиск за постоянное время O(1).


Визуализировать процесс


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



  • Блоки буферов — фактические страницы размером 8 КБ, где находятся данные.

  • Дескрипторы буферов — параллельный массив структур размером ~64 байта, по одной на каждую ячейку.

  • Хеш-таблица, используемая для сопоставления идентификаторов страниц с отдельными ячейками буферов.


Каждый дескриптор отслеживает, какая страница закеширована в ячейке (тег), флаги состояния («грязный», «действительный», «идёт ввод-вывод») и счётчики закрепления/использования.


O(1) = постоянное время, независимо от размера пула буферов.


Хеш-таблица обеспечивает быстрый поиск. Когда фоновому процессу нужна определённая страница, он хеширует идентификатор страницы и переходит непосредственно к нужной «корзине» — нет необходимости сканировать все 16 384 ячейки. Это сохраняет поиск буферов на уровне O(1) независимо от размера пула.


Когда фоновому процессу нужна страница N таблицы orders, он хеширует идентификатор, ищет в хеш-таблице — что определяет логику попадания/промаха.



Счётчики закрепления и использования


То, что происходит с каждой ячейкой буфера, регулируется двумя упомянутыми выше счётчиками: закрепления и использования.


Счётчик закрепления отслеживает активные обращения. Когда фоновый процесс (например, выполняющийся запрос) активно читает или изменяет страницу, он закрепляет буфер, чтобы его нельзя было вытеснить. Когда процесс завершает работу, он снимает закрепление.


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


Счётчик использования важен, чтобы избежать ситуации, когда один последовательный просмотр мог бы очистить весь пул буферов. Представьте чтение всей таблицы размером 1 ГБ. Без этой защиты он вытеснил бы всё из общих буферов, игнорируя часто используемые данные. Мы также коснёмся этого конкретного поведения позже, в разделе о кольцевых буферах.


PostgreSQL предоставляет вам функциональность для проверки того, что происходит в кеше общих буферов в реальном времени, с помощью расширения pg_buffercache.



Алгоритм циклического просмотра (clock sweep)


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


Простая проверка LRU потребовала бы гораздо больше обслуживания; обновление связного списка при каждой загрузке буфера значительно увеличило бы сложность.


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



  1. Закреплён ли буфер? Пропускает его, так как он используется.

  2. Счётчик использования > 0? Уменьшает его на единицу и переходит к следующей ячейке (т.е. он доживёт до следующего раунда).

  3. Если счётчик использования достиг 0 и буфер не используется, он будет вытеснен.


Циклический просмотр — это простой способ гарантировать, что «холодные» страницы быстро вытесняются, в то время как «горячие» страницы имеют тенденцию сохраняться и переживать несколько циклов.



Грязные буферы и фоновый процесс записи


Не забывайте, что изменение всегда сначала записывается в WAL.


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


Вместо этого грязные страницы накапливаются до тех пор, пока не произойдёт одно из следующих событий.


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


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


И, как предполагалось, последний шанс записать грязные страницы на диск — когда циклический просмотр находит грязный буфер. Он должен записать данные на диск, чтобы ячейку можно было повторно использовать для новой страницы. Это худший случай, так как он прибегает к синхронной операции ввода-вывода.


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



Кольцевые буферы


Как упоминалось выше, существуют также специальные типы буферов. Мы уже затрагивали сценарий, когда запрос выполняет просмотр большой таблицы.


В наивной реализации последовательный просмотр загрузил бы все данные в общие буферы, вытеснив по пути всё остальное. Ваш прогретый кеш исчез бы, и последующие запросы страдали бы в течение нескольких минут.


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


Отдельные случаи:



  • Последовательные просмотры больших таблиц размером более 1/4 от shared_buffers используют выделенный кольцевой буфер размером 256 КБ. Страницы циклически проходят через это крошечное кольцо и никогда не касаются основного кеша.



-- выполните последовательный просмотр большой таблицы
EXPLAIN (ANALYZE, BUFFERS) SELECT count(1) FROM ring_buffer_test;

Что покажет вам Buffers: shared read=127285 вместо hit. Я расскажу об этом в отдельной статье о том, как читать буферы в планах выполнения.



  • Массовые операции записи (COPY, CREATE TABLE AS) используют кольцевой буфер, ограниченный 16 МБ, достаточно большой для эффективного пакетного выполнения, но достаточно маленький, чтобы не загрязнять общий пул буферов.

  • Поскольку VACUUM касается каждой страницы и не должен вытеснять «горячие» данные, он использует свой собственный выделенный кольцевой буфер. Исторически он был установлен в 256 КБ, но начиная с PostgreSQL 17 его можно настраивать с помощью vacuum_buffer_usage_limit (в то время как предыдущие два заданы).



Локальные буферы


Вторым видом исключения из общего пула буферов являются временные таблицы на уровне сеанса. В этом случае, так как вопрос о параллельном доступе не стоит, каждый фоновый процесс имеет свой собственный локальный пул буферов, управляемый параметром temp_buffers (по умолчанию 8 МБ).


Локальные буферы быстрее, чем общие буферы, потому что у них более простая блокировка. Нет необходимости в сложной координации между процессами, требуемой для основного кеша.


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


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



Кеш страниц операционной системы


PostgreSQL не обходит операционную систему. Каждое чтение и запись проходит через ядро, которое поддерживает свой собственный кеш страниц. Это создаёт двойное кеширование — одна и та же страница размером 8 КБ может одновременно существовать как в общих буферах PostgreSQL, так и в кеше операционной системы.


Кеш операционной системы действует как «кеш второго уровня» — когда PostgreSQL вытесняет страницу, она часто всё ещё остаётся в памяти ОС.


Это звучит расточительно, но на самом деле это особенность. Когда PostgreSQL вытесняет чистую страницу, чтобы освободить место, эта страница попадает в кеш ОС, а не исчезает. Момент спустя, если она снова понадобится, операционная система предоставит её из оперативной памяти — без необходимости в дисковом вводе-выводе.


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


Это взаимодействие объясняет классический совет: установите shared_buffers на 25% от оперативной памяти. Вы сознательно оставляете пространство для операционной системы, чтобы она действовала как страховочная сетка.



-- на выделенных серверах с большим объёмом оперативной памяти 40% может хорошо работать
-- но всегда оставляйте место для кеша ОС и других процессов
ALTER SYSTEM SET shared_buffers = '8GB';

Этот параметр не выделяет память — это лишь подсказка для оценки стоимости.


PostgreSQL должен знать об этом комбинированном кеше для планирования запросов. Для этого существует параметр effective_cache_size — он сообщает планировщику, какой общий объём кеша (общие буферы + ОС) предполагать при оценке стоимости.



-- оценка общего доступного кеша (общего + ОС)
SHOW effective_cache_size;

Более высокое значение побуждает планировщик отдавать предпочтение сканированию по индексу, предполагая, что данные, вероятно, закешированы где-то, даже если не в общих буферах.



Заключение


Буферы — один из краеугольных камней внутреннего устройства PostgreSQL. Они определяют, попадёт ли запрос в быструю оперативную память или на медленный диск; и в то же время они играют решающую роль в хрупком балансе грязных страниц, WAL и долговечности.


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



Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry

Add Comment

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Submitted comments will be subject to moderation before being displayed.