7.5. Обнаружение распределённых взаимоблокировок

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

    create table players(id int, username text, pass text) with (distributed_by='id');
    insert into players select id, 'user_' || id, 'pass_' || id from generate_series(1,1000) id;

Предположим, что запись с id=2 принадлежит node1, а запись с id=3 принадлежит node2.

Выполните следующие команды на разных узлах:

    node1=# begin;
    node1=# update players set pass='someval' where id=3;

    node2=# begin;
    node2=# update players set pass='someval' where id=2;

    -- it should stuck because transaction on node1 locked record with id=3
    node2=# update players set pass='someval2' where id=3;

    -- it should stuck because transaction on node2 locked record with id=2
    node1=# update players set pass='someval2' where id=2;

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

Можно представить взаимодействие между процессами во всем кластере в виде графа. Вершина графа представляет собой процесс (сервер), который можно идентифицировать парой атрибутов {rgid; vxid}, где rgid – идентификатор группы репликации, а vxid — виртуальный идентификатор текущей выполняемой транзакции. Рёбра графа представляют собой направленные связи между вершинами. Каждое подключение направлено от заблокированного процесса к блокирующему процессу.

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

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

Детектор распределённых взаимоблокировок в Shardman реализован как отдельная задача внутри Shardman monitor. Если процесс не может получить блокировку в течение заданного периода времени (по умолчанию это одна секунда, но его можно настроить в параметре конфигурации deadlock_timeout), внутренний детектор взаимоблокировок PostgreSQL пытается обнаружить локальную взаимоблокировку. Если локальная взаимоблокировка не обнаружена, активируется детектор распределённых взаимоблокировок.

Детектор распределённых взаимоблокировок строит граф (список) блокировок в кластере. Он запрашивает представления pg_locks и pg_stat_activity на локальном узле и на каждом из удалённых узлов кластера.

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

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

    отмена оператора из-за обнаруженной распределённой взаимоблокировки

В журналы сервера будет записано подробное сообщение об обнаруженной взаимоблокировке:

    LOG:  distributed deadlock detected
    DETAIL:  repgroup 1, PID 95264 (application 'psql'), executed query 'update players set pass='qqq' where id=2;' is blocked by repgroup 1, PID 95283 (application 'pgfdw:2:95278:9/2'), executed query 'UPDATE public.players_0 SET pass = 'qqq'::text WHERE ((id = 2))'
    repgroup 1, PID 95283 (application 'pgfdw:2:95278:9/2'), executed query 'UPDATE public.players_0 SET pass = 'qqq'::text WHERE ((id = 2))' is blocked by repgroup 2, PID 95278 (application 'psql'), executed query 'update players set pass='qqq' where id=3;'
    repgroup 2, PID 95278 (application 'psql'), executed query 'update players set pass='qqq' where id=3;' is blocked by repgroup 2, PID 95267 (application 'pgfdw:1:95264:8/4'), executed query 'UPDATE public.players_1 SET pass = 'qqq'::text WHERE ((id = 3))'
    repgroup 2, PID 95267 (application 'pgfdw:1:95264:8/4'), executed query 'UPDATE public.players_1 SET pass = 'qqq'::text WHERE ((id = 3))' is blocked by repgroup 1, PID 95264 (application 'psql'), executed query 'update players set pass='qqq' where id=2;'