Как стать автором
Обновить

Быстрый путь блокирования в PostgreSQL

Уровень сложностиСложный
Время на прочтение9 мин
Количество просмотров315

В представлении pg_locks есть столбец fastpath, который означает, что блокировка получена по быстрому пути. В документации быстрый путь блокирования описан коротко. В статье рассматривается быстрый путь блокирования и чем он лучше обычного пути.

Быстрый путь блокирования предназначен для уменьшения накладных расходов на получение и освобождение блокировок, которые часто запрашиваются, но редко конфликтуют. При работе с базой данных наиболее часто запрашивают блокировки команды SELECT, INSERT, UPDATE, DELETE, MERGE.

Для создания плана выполнения команды блокируют все relations (таблицы, индексы, секции таблиц) на время формирования плана и оставляют блокировки на тех relations, которые используются в выбранном плане выполнения. С одной таблицей, обычно, одновременно работает много таких команд и они не конфликтуют друг с другом.

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

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

Для чтения таблицы блокировок нужно получить разделяемую блокировку (shared LWLock) на раздел таблицы блокировок, а это замедляет работу. Как можно убедиться в отсутствии конфликтующих блокировок на relation не обращаясь к таблице блокировок и просмотрев только структуры PGPROC?

Блокировки на один и тот же relation всегда попадают в один и тот же раздел таблицы блокировок. Хэш для relations рассчитывается только на основе dboid и reloid. Для этого используется небольшой массив в разделяемой памяти из 1024 разделов (установлен макросом FAST_PATH_STRONG_LOCK_HASH_PARTITIONS), в котором хранятся целые числа. Называние массива "Fast Path Strong Relation Lock Data":

select * from (select *, lead(off) over(order by off) - off as true_size from pg_shmem_allocations) as a where name like 'Fast%';
                name                 |    off    | size | allocated_size | true_size 
-------------------------------------+-----------+------+----------------+-----------
 Fast Path Strong Relation Lock Data | 144058368 | 4100 |           4224 |      4224
(1 row)

 Каждое число в массиве отражает число  "сильных" блокировок (Share, ShareRowExclusive, Exclusive, AccessExclusive), которые способны конфликтовать с теми, которые можно установить по быстрому пути в 1/1024 части таблицы блокировок. Определение какой объект в какую часть попал, происходит быстро - по хэшу. Если число сильных блокировок на раздел равно нулю, то процесс имеет право использовать быстрый путь блокирования relation, хэш которого попадает в этот раздел.

Может возникнуть вопрос - к этому массиву тоже надо получать доступ, так как он в разделяемой памяти, чем он отличается от таблицы блокировок? Отличается тем, что сильные блокировки устанавливаются или должны устанавливаться редко, а слабые блокировки чрезвычайно часто. Поэтому для большинства relations большую часть времени сильных блокировок не будет и процесс будет вправе использовать быстрый путь блокирования. Таблица блокировок разделена на NUM_LOCK_PARTITIONS=16 разделов.

Если число сильных блокировок на разделе больше нуля, то быстрый путь не может использоваться. Если установить сильную блокировку на таблицу, то процессы не смогут использовать быстрый путь блокировки на эту таблицу. То же самое будет, если установить блокировку на relation, хэш от которого попадет в тот же раздел (1/1024) таблицы блокировок. Оценка вероятности такого события: если число сильных блокировок на экземпляре 10, быстрый путь не сможет использоваться примерно в 1% случаев (1/1024*10).

Слабые блокировки

Слабые блокировки - те, которые могут быть получены по быстрому пути:
AccessShare - устанавливает SELECT, COPY TO, ALTER TABLE ADD FOREIGN KEY (PARENT) и любой запрос который читает таблицу.
RowShare - устанавливает SELECT FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE.
RowExclusive - устанавливают INSERT, UPDATE, DELETE, MERGE, COPY FROM.

Блокировка, которая не относится ни к сильной (которая мешает использовать быстрый путь), ни к слабой (которая может использовать быстрый путь): ShareUpdateExclusive. Её устанавливает автовакуум, автоанализ и команды VACUUM (без FULL), ANALYZE, CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, CREATE STATISTICS, COMMENT ON, REINDEX CONCURRENTLY, ALTER INDEX (RENAME), 11 видов ALTER TABLE. Главное, то, что автовакуум и автоанализ не мешают использовать быстрый путь блокирования.

Если автовакуум или автоанализ обрабатывает таблицу и серверный процесс запрашивает блокировку, несовместимую с блокировкой, которую установил автовакуум (ShareUpdateExclusive), рабочий процесс автовакуума прерывается серверным процессом через deadlock_timeout и в диагностический журнал записывается сообщение:

ERROR:  canceling autovacuum task
DETAIL: automatic vacuum of table 'имя'

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

Сильные блокировки

Сильные блокировки если присутствуют, то мешают устанавливать слабые блокировки по быстрому пути. Их список:
Share - CREATE INDEX (без CONCURRENTLY);
ShareRowExclusive - устанавливает CREATE TRIGGER и некоторыми видами ALTER TABLE;
Exclusive - устанавливает REFRESH MATERIALIZED VIEW CONCURRENTLY;
AccessExclusive - устанавливает DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL и REFRESH MATERIALIZED VIEW (без CONCURRENTLY), ALTER INDEX, 21 вид ALTER TABLE.
Команды, устанавливающие сильные блокировки, нежелательно выполнять в то время, когда экземпляр испытывает большую нагрузку. 

Разделы таблицы блокировок

Таблица блокировок разделена на 16 частей (2^4). Степень двойки установлена макросом:

/* Number of partitions the shared lock tables are divided into */
#define LOG2_NUM_LOCK_PARTITIONS  4
#define NUM_LOCK_PARTITIONS  (1 << LOG2_NUM_LOCK_PARTITIONS)

В структуре PGPROC каждого процесса сохраняется список частей, которые он блокирует:

/* All PROCLOCK objects for locks held or awaited by this backend are linked into one of these lists, according to the partition number of their lock. */
dlist_head	myProcLocks[NUM_LOCK_PARTITIONS];

В PostgreSQL LOG2_NUM_LOCK_PARTITIONS - константа. В форках PostgreSQL встречается параметр конфигурации log2_num_lock_partitions. Доступ к структурам PGPROC идет очень часто (вся структура является "горячей"). Если "горячие" структуры перестанут помещаться в кэши процессоров, производительность начнет уменьшаться. Увеличение значения этого параметра увеличивает размер структуры PGPROC. Насколько критично увеличение рассматривается дальше.

Также по обычному пути только 16 процессов могут одновременно находиться в процессе получения блокировки на объекты. Из-за этого, при увеличении значений log2_num_lock_partitions разработчикам форков приходится менять логику LWLocks "утяжеляя" легковесные блокировки, добавляя параметр lwlock_shared_limit ( https://pgconf.ru/media/2020/02/06/Korotkov_pgconfru_2020_Bottlenecks_2.pdf ).

Скрытый текст

> We have increased LOG2_NUM_LOCK_PARTITIONS to 8
> and ... clients start to report "too many LWLocks taken" error.
> There is yet another hardcoded constant MAX_SIMUL_LWLOCKS = 200
> which relation with NUM_LOCK_PARTITIONS  was not mentioned anywhere.

Seems like self-inflicted damage. I certainly don't recall anyplace in the docs where we suggest that you can alter that constant without worrying about consequences.

> So looks like NUM_LOCK_PARTITIONS and MAXNUMMESSAGES 
> constants have to be replaced with GUCs.

I seriously doubt we'd do that.
   regards, tom lane
https://www.postgresql.org/message-id/flat/da3205c4-5b07-a65c-6c26-a293c6464fdb%40postgrespro.ru

В форках также встречается параметр fastpath_num_locks вместо макроса FP_LOCK_SLOTS_PER_BACKEND. Увеличение FP_LOCK_SLOTS_PER_BACKEND с 16 до 64 ещё больше приводит к увеличению структуры PGPROC в разделяемой памяти. Структура PGPROC занимает 880 байт, что равно 14 cache lines (блоков данных), добавление 48 xid увеличит ее на 192 байта (3 cache lines). "Линии кэша" - блоки размером 64 байт, которыми передаются данные между кэшем процессора и памятью, содержит копию данных из основной памяти.

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

Скрытый текст

As for turning the parameter into a GUC, that has a cost too. Either direct - a compiler can do far more optimizations with compile-time constants than with values that may change during execution, for example. Or indirect - if we can't give users any guidance how/when to tune the GUC, it can easily lead to misconfiguration (I can't even count how many times I had to deal with systems where the values were "tuned" following the logic that more is always better).
which just leads back to (1) and (2) even for this case.

regards
--
Tomas Vondra
EnterpriseDB
https://www.postgresql.org/message-id/flat/779f2bd6-00f3-4aac-a792-b81f47e41abd%40enterprisedb.com

Пример блокировок по быстрому и обычному пути

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

echo "explain (analyze, costs false) select abalance from pgbench_accounts where bid=1 limit 1;" > fast.sql

Создадим таблицу с 7 секциями и выполним тест:

pgbench -i -s 1 --partitions=7
pgbench -T 100 -P 5 -f fast.sql
pgbench (17.4 (Ubuntu 17.4-1.pgdg22.04+2))
starting vacuum...end.
progress: 5.0 s, 2610.9 tps, lat 0.376 ms stddev 0.194, 0 failed
progress: 10.0 s, 2598.8 tps, lat 0.378 ms stddev 0.212, 0 failed
progress: 15.0 s, 2616.6 tps, lat 0.376 ms stddev 0.169, 0 failed
progress: 20.0 s, 2582.6 tps, lat 0.381 ms stddev 0.152, 0 failed
progress: 25.0 s, 2578.2 tps, lat 0.382 ms stddev 0.171, 0 failed

Создадим таблицу с 8 секциями и выполним тест:

pgbench -i -s 1 --partitions=8
pgbench -T 100 -P 5 -f fast.sql
pgbench (17.4 (Ubuntu 17.4-1.pgdg22.04+2))
starting vacuum...end.
progress: 5.0 s, 2471.2 tps, lat 0.398 ms stddev 0.115, 0 failed
progress: 10.0 s, 2497.9 tps, lat 0.394 ms stddev 0.139, 0 failed
progress: 15.0 s, 2495.8 tps, lat 0.394 ms stddev 0.182, 0 failed
progress: 20.0 s, 2506.8 tps, lat 0.393 ms stddev 0.189, 0 failed
progress: 25.0 s, 2489.0 tps, lat 0.395 ms stddev 0.244, 0 failed

tps немного уменьшился. Увеличилось время планирования, время выполнения осталось тем же (limit 1).
Проверим, по какому пути были получены блокировки:

postgres=# begin;
BEGIN
postgres=*# explain (analyze, costs false) select abalance from pgbench_accounts where bid=1 limit 1;
                                      QUERY PLAN                                      
--------------------------------------------------------------------------------------
 Limit (actual time=0.034..0.055 rows=1 loops=1)
   ->  Append (actual time=0.025..0.036 rows=1 loops=1)
         ->  Seq Scan on pgbench_accounts_1 (actual time=0.018..0.021 rows=1 loops=1)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_2 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_3 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_4 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_5 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_6 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_7 (never executed)
               Filter: (bid = 1)
         ->  Seq Scan on pgbench_accounts_8 (never executed)
               Filter: (bid = 1)
 Planning Time: 1.604 ms
 Execution Time: 0.233 ms
(20 rows)
postgres=*# select pid, relation::regclass::text, mode, granted, fastpath from pg_locks where locktype='relation' and database = (select oid from pg_database where datname=current_database()) order by pid, fastpath desc;
  pid  |        relation         |      mode       | granted | fastpath 
-------+-------------------------+-----------------+---------+----------
 16065 | pgbench_accounts_7_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_7      | AccessShareLock | t       | t
 16065 | pgbench_accounts_6_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_6      | AccessShareLock | t       | t
 16065 | pgbench_accounts_5_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_5      | AccessShareLock | t       | t
 16065 | pgbench_accounts_4_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_4      | AccessShareLock | t       | t
 16065 | pgbench_accounts_3_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_3      | AccessShareLock | t       | t
 16065 | pgbench_accounts_2_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_2      | AccessShareLock | t       | t
 16065 | pgbench_accounts_1_pkey | AccessShareLock | t       | t
 16065 | pgbench_accounts_1      | AccessShareLock | t       | t
 16065 | pgbench_accounts_pkey   | AccessShareLock | t       | t
 16065 | pgbench_accounts        | AccessShareLock | t       | t
 16065 | pgbench_accounts_8_pkey | AccessShareLock | t       | f
 16065 | pg_locks                | AccessShareLock | t       | f
 16065 | pgbench_accounts_8      | AccessShareLock | t       | f
(19 rows)

16 блокировок: на таблицу, первые 7 секций и индексы на них блокировки получены по быстрому пути.
На 8 секцию и индекс блокировки получены по обычному пути.

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

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

В статье был рассмотрен быстрый путь блокирования relations. Если при выполнении SELECT  все блокировки получены по быстрому пути, то этот запрос не испытывает замедления, связанного с конкурентным доступом. Если команда использует обычный путь блокирования relations, то при большой нагрузке замедление может стать заметным из-за конкуренции за легковесные блокировки типа LockManager (до 13 версии PostgreSQL этот тип блокировок назывался lock_manager). Автовакуум и автоанализ не мешают использовать быстрый путь блокирования.

Теги:
Хабы:
0
Комментарии0

Публикации

Истории

Работа

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область