# Блокировки в памяти

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<span id="internal-ru-chapter-15" class="pdf-anchor"></span><span id="internal-ru-page-306" class="pdf-anchor"></span>

<span id="internal-ru-dummy-109" class="pdf-anchor"></span>

## 15.1. Спин-блокировки {#internal-ru-section-15-1}

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

Самые простые из них — *спин-блокировки*, или *спинлоки* (spinlock). Они предназначены для захвата на очень короткое время (несколько инструкций процессора) и защищают отдельные участки памяти от одновременного изменения.

Спин-блокировки реализуются на основе атомарных инструкций процессора, таких как compare-and-swap[^internal-ru-306-1]. Они поддерживают единственный исключительный режим. Если блокировка занята, ожидающий процесс выполняет активное ожидание — команда повторяется («крутится» в цикле, отсюда и название). Если блокировку не удается получить за определенное время, процесс ненадолго приостанавливается и затем снова начинает цикл активного ожидания.

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

Спин-блокировки не позволяют обнаруживать взаимоблокировки. С прикладной точки зрения остается только верить в то, что разработчики PostgreSQL используют эти блокировки правильно.

<span id="internal-ru-page-307" class="pdf-anchor"></span>

## 15.2. Легкие блокировки {#internal-ru-section-15-2}

Следом идут так называемые *легкие блокировки* [^internal-ru-307-1] (lightweight locks, lwlocks). Их захватывают на время, необходимое для работы со структурой данных (например, с хеш-таблицей или списком указателей). Как правило, легкие блокировки удерживаются недолго, но в некоторых случаях они защищают операции ввода-вывода, так что, в принципе, время может оказаться и значительным.

Имеются два режима: исключительный (для изменения данных) и разделяемый (только для чтения). Поддерживается очередь ожидания, но пока блокировка удерживается в разделяемом режиме, другие читающие процессы проходят вне очереди. В системах с высокой степенью параллельности и большой нагрузкой это может приводить к неприятным эффектам[^internal-ru-307-2].

Механизм проверки взаимоблокировок не предусмотрен; корректное использование легких блокировок, как и спин-блокировок, остается на совести разработчиков PostgreSQL.

## 15.3. Примеры {#internal-ru-section-15-3}

Чтобы получить некоторое представление о том, как и где используются спинлоки и легкие блокировки, рассмотрим три примера структур в разделяемой памяти: буферный кеш, буферы журнала предзаписи и кеш SLRU. Я назову далеко не все блокировки; полная картина слишком сложна и представляет интерес только для разработчиков ядра.

<span id="internal-ru-section-star-186" class="pdf-anchor"></span>

### Буферный кеш {#internal-ru-subsection-star-187}

<span id="internal-ru-dummy-110" class="pdf-anchor"></span>

Чтобы обратиться к хеш-таблице, позволяющей найти буфер в кеше, процесс должен захватить легкую блокировку <img src="/images/internal/ru/p307-icon-01.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> BufferMapping: в разделяемом режиме — для чтения, в исключительном — для изменений. (*с.* [183](/ru/09-buffer-cache/#internal-ru-page-184))

<span id="internal-ru-page-308" class="pdf-anchor"></span>

![Буферный кеш: buffer strategy; BufferMapping ×128; свободные буферы; «часовая стрелка»; закрепление; буфера; хеш-таблица; buffer header; BufferIO; BufferContent](/images/internal/ru/p308-01.svg)
{#internal-ru-fig-15-1 num="15.1" caption="Буферный кеш" .book-original-figure width=715 height=459}

Обращения к хеш-таблице происходят очень активно, и зачастую эта блокировка становится узким местом. Для увеличения гранулярности она фактически устроена как *транш*, состоящий из 128 отдельных легких блокировок, каждая из которых защищает свою часть хеш-таблицы [^internal-ru-308-1].

> [!NOTE]
> Впервые единичную блокировку, защищающую хеш-таблицу, преобразовали в транш размером 16 в версии PostgreSQL 8.2 в 2006 году. Спустя 10 лет в версии 9.5 размер транша увеличили до 128. На современных многоядерных системах порой и этого значения оказывается мало.

Процесс получает доступ к заголовку буфера, захватывая спин-блокировку <img src="/images/internal/ru/p308-icon-01.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> buffer header [^internal-ru-308-2] (название условное, поскольку спин-блокировки не имеют внешних имен). Отдельные операции, такие как увеличение счетчика обращений, могут выполняться и без явных блокировок с помощью атомарных инструкций процессора.

<span id="internal-ru-page-309" class="pdf-anchor"></span>

Чтобы прочитать содержимое буфера, требуется блокировка <img src="/images/internal/ru/p308-icon-02.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> BufferContent. Она располагается в заголовке данного буфера[^internal-ru-308-3]. Обычно эта блокировка захватывается только на время, необходимое для чтения указателей на версии строк, а дальше достаточно защиты, предоставляемой <img src="/images/internal/ru/p309-icon-01.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> закреплением буфера. Для изменения содержимого буфера блокировка BufferContent должна захватываться в исключительном режиме. (*с.* [189](/ru/09-buffer-cache/#internal-ru-page-190))

При чтении буфера с диска (или записи на диск) захватывается также блокировка <img src="/images/internal/ru/p309-icon-02.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> BufferIO в заголовке буфера; фактически это не настоящая блокировка, а признак, играющий ее роль[^internal-ru-309-1]. Блокировка сигнализирует другим процессам, заинтересованным в этой странице, что необходимо дождаться окончания ввода-вывода.

Указатель на свободные буферы и «часовая стрелка» механизма вытеснения также защищены одной общей спин-блокировкой <img src="/images/internal/ru/p309-icon-03.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> buffer strategy[^internal-ru-309-2].

<span id="internal-ru-section-star-188" class="pdf-anchor"></span>

### Буферы журнала предзаписи {#internal-ru-subsection-star-189}

![Буферы журнала предзаписи: WALBufMapping; WALWrite; хеш-таблица; WALInsert ×8; insert position; PrevBytePos; CurBytePos](/images/internal/ru/p309-02.svg)
{#internal-ru-fig-15-2 num="15.2" caption="Буферы журнала предзаписи" .book-original-figure width=709 height=341}

Для журнального кеша тоже используется хеш-таблица, содержащая отображение страниц в буферы. В отличие от хеш-таблицы буферного кеша, эта таблица защищена единственной легкой блокировкой <img src="/images/internal/ru/p309-icon-04.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> WALBufMapping, поскольку размер журнального кеша гораздо меньше (обычно он равен одному сегменту) и обращение к буферам более упорядочено[^internal-ru-309-3].

<span id="internal-ru-page-310" class="pdf-anchor"></span>

Запись страниц журнала на диск защищена легкой блокировкой <img src="/images/internal/ru/p310-icon-01.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> WALWrite, чтобы в каждый момент времени только один процесс мог выполнять эту операцию.

Чтобы создать журнальную запись, процесс сначала резервирует место внутри страницы WAL, а затем заполняет его необходимыми данными. Резервирование строго упорядочено; процесс должен захватить спин-блокировку <img src="/images/internal/ru/p310-icon-02.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> insert position, защищающую указатель вставки[^internal-ru-310-1]. Но заполнять уже зарезервированное место могут одновременно несколько процессов. Для этого процесс должен захватить *любую* из восьми легких блокировок, образующих транш <img src="/images/internal/ru/p310-icon-03.svg" alt="Условное обозначение блокировки" width="14" height="14" style="display:inline-block;width:1em;height:1em;vertical-align:-.12em"> WALInsert[^internal-ru-310-2].

<span id="internal-ru-section-star-190" class="pdf-anchor"></span>

### Кеш SLRU {#internal-ru-subsection-star-191}

<span id="internal-ru-dummy-111" class="pdf-anchor"></span>

Кеши SLRU (simple least-recently-used) [^internal-ru-310-3] применяются для страниц таких структур данных, как мультитранзакции (multixact), статусы транзакций (clog) или вложенные транзакции (subtrans). В отличие от буферного кеша, кеш SLRU устроен довольно просто и рассчитан на то, что основной поток изменений и чтений приходится на последние страницы: например, завершение транзакции и проверка ее состояния обычно происходят примерно в одно время, после чего к статусу этой транзакции уже никто не обращается. Вытесняются страницы, к которым дольше всего не было обращений. (*с.* [86](/ru/03-pages-and-tuples/#internal-ru-page-87))

![Кеш SLRU: …; Buffer; SLRU × N](/images/internal/ru/p310-03.svg)
{#internal-ru-fig-15-3 num="15.3" caption="Кеш SLRU" .book-original-figure width=720 height=153}

<span id="internal-ru-page-311" class="pdf-anchor"></span>

Кеш поделен на банки по 16 страниц; принадлежность страницы банку определяется четырьмя младшими битами ее номера. Такая схема позволяет обойтись без хеш-таблицы: внутри небольшого банка достаточно линейного поиска. Общий размер кеша, обслуживающего конкретную структуру данных, настраивается индивидуально.

Каждый банк защищен своей легкой блокировкой SLRU (имя транша зависит от структуры данных: например, транш для clog называется XactSLRU). Каждая страница также имеет собственную легкую блокировку Buffer (для clog — XactBuffer). Процессы удерживают блокировку банка для получения или изменения состояния входящих в него буферов. Получить блокировку банка требуется и для захвата блокировки страницы с целью записать ее на диск или прочитать с диска, но на время ввода-вывода блокировка банка освобождается, чтобы процессы могли работать с другими страницами этого банка.

## 15.4. Мониторинг ожиданий {#internal-ru-section-15-4}

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

Самый простой способ получить представление о длительных блокировках в системе — включить параметр *log\_lock\_waits*. В этом случае в журнал сообщений сервера будут попадать подробные сведения о блокировке каждый раз, когда транзакция ждет дольше, чем *deadlock\_timeout*. Эти данные выводятся при завершении проверки на взаимоблокировки, отсюда и имя параметра. (*по умолчанию:* `off`) (*по умолчанию:* `1s`) (*с.* [286](/ru/13-row-level-locks/#internal-ru-page-287))

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

Все ожидания можно разделить на несколько типов[^internal-ru-311-1].

Большую категорию составляют ожидания различных блокировок:

**Lock** — тяжелых блокировок;

<span id="internal-ru-page-312" class="pdf-anchor"></span>

**LWLock** — легких блокировок;

**BufferPin** — закрепленного буфера.

Каждый тип конкретизируется именем ожидания. Например, для легких блокировок это будет имя блокировки или соответствующего транша[^internal-ru-312-1].

Но процессы могут ожидать и других событий:

**IO** — ввода-вывода, когда требуется записать или прочитать данные;

**Client** — данных от клиента (в этом состоянии обычно проводит бóльшую часть времени psql);

**IPC** — данных от другого процесса;

**Extension** — специфического события, зарегистрированного расширением;

**InjectionPoint** — выполнения кода в точке внедрения с целью тестирования PostgreSQL (такое событие не появляется при эксплуатации). (v. 17)

Бывают ситуации, когда процесс просто не выполняет никакой полезной работы. К этой категории относятся ожидания:

**Activity** — фоновых процессов в своем основном цикле;

**Timeout** — таймера.

Как правило, такие ожидания «нормальны» и не говорят о каких-либо проблемах. Однако, например, событие SpinDelay типа Timeout свидетельствует об ожидании спин-блокировки. (v. 17)

Следует учитывать, что представление показывает только те ожидания, которые соответствующим образом обрабатываются в исходном коде[^internal-ru-312-2]. Если имя типа ожидания не определено, процесс не находится ни в одном известном ожидании. Такое время следует считать *неучтенным*, так как это не означает со стопроцентной вероятностью, что процесс ничего не ждет; на самом деле неизвестно, что именно происходит в этот момент.

<span id="internal-ru-page-313" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT backend_type, wait_event_type AS event_type, wait_event
FROM pg_stat_activity
ORDER BY backend_type;
         backend_type         | event_type |     wait_event
------------------------------+------------+---------------------
 autovacuum launcher          | Activity   | AutovacuumMain
 background writer            | Activity   | BgwriterMain
 checkpointer                 | Activity   | CheckpointerMain
 client backend               |            |
 io worker                    | Activity   | IoWorkerMain
 io worker                    | Activity   | IoWorkerMain
 io worker                    | Activity   | IoWorkerMain
 logical replication launcher | Activity   | LogicalLauncherMain
 walwriter                    | Activity   | WalWriterMain
(9 rows)
```

В данном случае во время обращения к представлению все фоновые служебные процессы «бездельничали», а обслуживающий процесс (`client backend`) был занят выполнением запроса и ничего не ждал.

## 15.5. Семплирование {#internal-ru-section-15-5}

К сожалению, представление `pg_stat_activity` показывает информацию об ожиданиях только *на текущий момент*; статистика не накапливается. Единственный способ получить картину ожиданий во времени — *семплирование* состояния представления с определенным интервалом.

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

<span id="internal-ru-page-314" class="pdf-anchor"></span>

Встроенных средств для семплирования не предусмотрено, но в качестве примера мы воспользуемся расширением `pg_wait_sampling`[^internal-ru-313-1]. Необходимо прописать библиотеку в параметр *shared\_preload\_libraries* и перезапустить сервер.

```text {label="psql"}
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling';
postgres$ pg_ctl restart -l /home/postgres/logfile
```

Теперь установим расширение в базе данных:

```text {label="psql"}
=> CREATE EXTENSION pg_wait_sampling;
```

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

Например, посмотрим на ожидания, возникающие при работе эталонного тестирования. Запустим утилиту pgbench и, пока она работает, определим номер процесса:

```text {label="psql"}
postgres$ /usr/local/pgsql/bin/pgbench -T 60 internals
=> SELECT pid FROM pg_stat_activity
WHERE application_name = 'pgbench';
  pid
-------
 71963
(1 row)
```

Профиль ожиданий после окончания работы:

```text {label="psql"}
=> SELECT pid, event_type, event, count
FROM pg_wait_sampling_profile WHERE pid = 71963
ORDER BY count DESC LIMIT 5;
  pid  | event_type |   event    | count
-------+------------+------------+-------
 71963 | Client     | ClientRead |  2601
 71963 | IO         | WalSync    |  2092
 71963 |            |            |  1226
 71963 | IO         | WalWrite   |    22
 71963 | LWLock     | ProcArray  |     3
(5 rows)
```

<span id="internal-ru-page-315" class="pdf-anchor"></span>

С установками по умолчанию (параметр *pg\_wait\_sampling.profile\_period*) значения сохраняются 100 раз в секунду. Поэтому, чтобы оценить длительность ожиданий в секундах, значение `count` надо делить на 100; общая длительность будет примерно соответствовать времени работы процесса. (*по умолчанию:* `10ms`) (v. 1.1.6)

В данном случае бóльшую часть времени утилита выполняла полезную работу. «Непроизводственные» ожидания в основном связаны с синхронизацией журнальных записей с диском. Этот пример хорошо иллюстрирует понятие *неучтенного* времени. Обработка события `WALSync` появилась в версии PostgreSQL 12; на более ранней версии профиль не содержал бы первой строки, хотя само ожидание, разумеется, имело бы место.

Легкая блокировка ProcArray защищает одноименную структуру данных в разделяемой памяти сервера. (*с.* [99](/ru/04-snapshots/#internal-ru-page-100))

А вот как будет выглядеть профиль, если искусственно замедлить работу файловой системы (например, с помощью slowfs [^internal-ru-315-1]), чтобы любая операция ввода-вывода занимала 0,1 секунды:

```text {label="psql"}
postgres$ /usr/local/pgsql/bin/pgbench -T 60 internals
=> SELECT pid FROM pg_stat_activity WHERE application_name = 'pgbench';
  pid
-------
 72414
(1 row)
=> SELECT pid, event_type, event, count
FROM pg_wait_sampling_profile WHERE pid = 72414
ORDER BY count DESC LIMIT 5;
  pid  | event_type |     event      | count
-------+------------+----------------+-------
 72414 | IO         | WalWrite       |  5329
 72414 | LWLock     | WALWrite       |   167
 72414 |            |                |    50
 72414 | IO         | WalSync        |    43
 72414 | IO         | DataFileExtend |    19
(5 rows)
```

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

<span id="internal-ru-page-316" class="pdf-anchor"></span>

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

<span id="internal-ru-page-317" class="pdf-anchor"></span>

[^internal-ru-306-1]: [backend/storage/lmgr/s\_lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/s_lock.c;hb=REL_18_STABLE>).
[^internal-ru-307-1]: [backend/storage/lmgr/lwlock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lwlock.c;hb=REL_18_STABLE>).
[^internal-ru-307-2]: [www.postgresql.org/message-id/flat/CAPpHfdvJhO1qutziOp%3Ddy8TO8Xb4L38BxgKG4RPa1up1L](<https://www.postgresql.org/message-id/flat/CAPpHfdvJhO1qutziOp%3Ddy8TO8Xb4L38BxgKG4RPa1up1Lzh_UQ%40mail.gmail.com>)zh\_ [UQ%40mail.gmail.com](<https://www.postgresql.org/message-id/flat/CAPpHfdvJhO1qutziOp%3Ddy8TO8Xb4L38BxgKG4RPa1up1Lzh_UQ%40mail.gmail.com>).
[^internal-ru-308-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>); [include/storage/buf\_internals.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/storage/buf_internals.h;hb=REL_18_STABLE>), макрос BufMappingPartitionLock.
[^internal-ru-308-2]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция LockBufHdr.
[^internal-ru-308-3]: [include/storage/buf\_internals.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/storage/buf_internals.h;hb=REL_18_STABLE>).
[^internal-ru-309-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция StartBufferIO.
[^internal-ru-309-2]: [backend/storage/buffer/freelist.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_18_STABLE>).
[^internal-ru-309-3]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция AdvanceXLInsertBuffer.
[^internal-ru-310-1]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция ReserveXLogInsertLocation.
[^internal-ru-310-2]: [backend/access/transam/xlog.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlog.c;hb=REL_18_STABLE>), функция WALInsertLockAcquire.
[^internal-ru-310-3]: [backend/access/transam/slru.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/slru.c;hb=REL_18_STABLE>).
[^internal-ru-311-1]: [postgrespro.ru/docs/postgresql/18/monitoring-stats#WAIT-EVENT-TABLE](<https://postgrespro.ru/docs/postgresql/18/monitoring-stats#WAIT-EVENT-TABLE>).
[^internal-ru-312-1]: [postgrespro.ru/docs/postgresql/18/monitoring-stats#WAIT-EVENT-LWLOCK-TABLE](<https://postgrespro.ru/docs/postgresql/18/monitoring-stats#WAIT-EVENT-LWLOCK-TABLE>).
[^internal-ru-312-2]: [include/utils/wait\_event.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/utils/wait_event.h;hb=REL_18_STABLE>).
[^internal-ru-313-1]: [github.com/postgrespro/pg\_wait\_sampling](<https://github.com/postgrespro/pg_wait_sampling>).
[^internal-ru-315-1]: [github.com/nirs/slowfs](<https://github.com/nirs/slowfs>).
