Поиск и отмена блокировок в Postgres
Общее#
Блокировки в Postgres позволяют ему управлять синхронизацией потоков для доступа к общим данным. Клиент, в свою очередь
может подсказывать Postgres о том, с какой целью он пытается получить доступ к определенным данным, что поможет выбрать
правильную блокировку (например, указать FOR UPDATE
, FOR NO KEY UPDATE
или другие модификаторы запроса). Так же,
блокировки можно захватывать явно при помощи оператора LOCK
(блокировка всей таблицы) или через рекомендательные
блокировки - функции pg_advisory_lock
, pg_advisory_lock_shared
, pg_advisory_xact_lock
и другие.
Как и при работе в конкурентной среде, где для определения очередности доступа к данным будут использоваться примитивы
синхронизации типа Mutex
, SpinLock
или другие, так и Postgres использует подобные механизмы у себя. И так же как в
приложении, в Postgres так же может попасть в ситуацию с deadlock - взаимной блокировкой. Если Postgres заметит подобную
ситуацию - он непременно сообщит об этом в лог с указанием того, на каком ресурсе, какие процессы и с какими запросами
были взаимно заблокировны.
При работе приложения, без нагрузки или в условиях, где запросы к СУБД не используют изменение нескольких сущностей в рамках транзакции - вероятность попасть в ситуацию с взаимной блокировкой низкая. Однако в проектах, которые обслуживают большое кол-во запросов такие ситуации могут встречаться чаще.
Для избежания блокировок необходимо правильно проектировать приложение и конкретно - работу с транзакциями. На это уйдет какое-то время, но что делать, если прямо сейчас завис важный процесс, который не позволяет системе функционировать корректно?
Ситуация#
Целью одного из проектов, который мы разрабатываем, была синхронизация данных из удаленного источника и обогащение данных в локальном хранилище. Так как сервис обязан горизонтально масштабироваться, необходимо было предусмотреть механизм, который выбирал бы ответственный инстанс для выполнения операции синхронизации. Для решения этой задачи ( выбора ответственного) было принято решение использовать блокировки специальной записи в СУБД, которая по-сути являлась задачей на синхронизацию данных.
В момент старта, каждый инстанс пытался захватить блокировку на задачу в БД через запрос вида
SELECT *
FROM tasks
WHERE resource = 'sync-leader'
FOR NO KEY UPDATE
Таким образом, если несколько инстансов пытаются получить блокировку на данную строку (resource
- уникальное поле), то
Postgres позволит захватить блокировку только для одного из инстансов. Соответственно, тот инстанс, который первым
получит блокировку, в рамках текущей транзакции, может начать процедуру синхронизации.
Через некоторое время, заметили что процесс синхронизации не стартует, так как зависает в состоянии ожидания захвата блокировки. И так было с каждым из инстансов. Рестарт (k8s - recreate) не помог, пришлось действовать на месте.
Определение проблемы#
В Postgres из коробки существует множество встроенных инструментов для анализа текущего состояния СУБД. И первый из них, кто нас интересует - реестр текущих блокировок.
Все реестры Postgres хранит в системных представлениях. Представление, которое отображает список блокировок
называется pg_locks
.
Вот одна строка из этой таблицы для представления о ее содержимом
\x
select * from pg_locks limit;
+------------------+------------+
|locktype |relation |
+------------------+------------+
|database |16384 |
+------------------+------------+
|relation |2963432 |
+------------------+------------+
|page |NULL |
+------------------+------------+
|tuple |NULL |
+------------------+------------+
|virtualxid |NULL |
+------------------+------------+
|transactionid |NULL |
+------------------+------------+
|classid |NULL |
+------------------+------------+
|objid |NULL |
+------------------+------------+
|objsubid |NULL |
+------------------+------------+
|virtualtransaction|4/32378 |
+------------------+------------+
|pid |71237 |
+------------------+------------+
|mode |RowShareLock|
+------------------+------------+
|granted |true |
+------------------+------------+
|fastpath |true |
+------------------+------------+
Остальные строки особо не отличаются и дают информации о происходящем. Все потому, что почти вся таблица состоит из указателей.
Выполним JOIN
со справочниками, выберем только необходимые поля и получим более наглядное представление.
select pid, relname, mode
from pg_locks l
join pg_class t on l.relation = t.oid
where t.relname = 'tasks'
and t.relkind = 'r';
+-----+-------+-------------+
|pid |relname|mode |
+-----+-------+-------------+
|71237|tasks |RowShareLock |
|63927|tasks |RowShareLock |
|71237|tasks |ExclusiveLock|
+-----+-------+-------------+
Это список всех блокировок на уровне строки (relkind='r'
) в таблице tasks
(t.relname = 'tasks'
). Где pid
-
идентификатор процесса (соединения), mode
- тип блокировки.
На этом этапе можно отменить блокировки завершением процессов (pid
), которые их породили, однако стоит получить чуть
больше информации о том, что привело систему к такому состоянию. Возможно, стоит увидеть запросы, которые привели
систему к такому состоянию.
Обратимся к представлению, в котором находятся текущие запросы тех pid
, которые мы видим в списке блокировок наших
запией.
select pid, state, usename, query, query_start
from pg_stat_activity
where pid in (
select pid from pg_locks l
join pg_class t on l.relation = t.oid
and t.relkind = 'r'
where t.relname = 'tasks'
);
+-----+-------------------+---------------+-------------------------------------------------------------------+--------------------------+
|pid |state |usename |query |query_start |
+-----+-------------------+---------------+-------------------------------------------------------------------+--------------------------+
|71237|active |some-aplication|select * from tasks where resource='sync-leader' for no key update |2020-09-01 12:41:35.757372|
|71684|active |some-aplication|select * from tasks where resource='sync-leader' for no key update |2020-09-01 12:51:00.032879|
|63927|idle in transaction|some-aplication|select * from tasks where resource='sync-leader' for no key update |2020-09-01 12:41:31.380216|
+-----+-------------------+---------------+-------------------------------------------------------------------+--------------------------+
На нашем сетапе было запущено два инстанса, однако в очереди висит 3 процесса, обслуживающих один запрос. Причем он был запущен раньше всех. На момент полуения листинга инстанс, который породил запрос и держал текущее соединение был в состоянии terminating, но не мог корректно завершиться в короткий промежуток времени. Причина - конечно же ошибка, из-за которой процесс graceful shutdown мог начать останов системы только между запросами к бд. В данном случае захват блокировки блокировал останов, а останов заблокировал операцию синхронизации данных, что не давало возможности завершить контекст блокировки. В общем дедлок 🤯.
Блокировку захыватил процесс с pid 63927
Принудительное отключение клиента#
Postgres преоставляет инструменты для управления клиентскими соединениями и внутренними процессами, их обрабатывающими (
называются backend
). Достигается это путем отправки сигналов от одного бекенда(к которому мы подключены) до другого -
которому мы хотим отправить сигнал. Для этого необходимо иметь права суперпользователя. Для начала можно остановить
текущий запрос сигналом pg_cancel_backend
.
select pg_cancel_backend(63927);
Несмотря на название функции - она всег лишь отменяет текущий запрос. Если этого недостаточно - можно завершить обслуживающий процесс, что повлечет отключение клиента от СУБД.
select pg_terminate_backend(63927);