Pull to refresh
71.7

Синхронизируем приложения с помощью Advisory Locks (postgresql). Что это, зачем, и нюансы работы с pgBouncer

Reading time4 min
Views12K


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


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


Проблема в том, что такая автоматика с принудительными локами подходит не для всех случаев. Например, вы массово обрабатываете файлы, предполагая, что никакой файл не будет обрабатываться одновременно двумя приложениями сразу, но при этом не хотите создавать для синхронизации полноценную таблицу в БД. В проектах Каруны такие задачи возникают довольно регулярно.


Для решения подобных проблем в PostgreSQL есть так называемые необязательные блокировки (advisory locks), т.е. локи, которые берутся, исходя из логики приложения, а не автоматики хранения/выдачи данных в БД.


Сессионные локи


Сразу скажу, что под сессией здесь мы понимаем tcp-соединение к PostgreSQL, не к pgBouncer, это важно. Чуть ниже расскажу об этом подробнее.


Итак, сессионные advisory locks. Если две программы в разных сессиях одновременно сделают


-- Берём лок, используя некое число (BigInt)
SELECT pg_advisory_lock(123);

то первая продолжит работать, а вторая подвиснет, ожидая, пока первая отпустит лок с помощью команды


-- Снимаем лок (с тем же числом)
SELECT pg_advisory_unlock(123);

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


SELECT pg_advisory_lock(
    ('x' || md5('/path/to/my/file.txt'))::bit(64)::bigint  
);

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


SELECT pg_try_advisory_lock(
    ('x' || md5('create complicated report'))::bit(64)::bigint
);

Эта функция вернёт false, если лок уже взят в другой сессии. А если не взят, то берёт лок и возвращает true. Снимается он точно так же (pg_advisory_unlock)


Сессии и pgbouncer


Если вы новичок в Postgresql, то лучше бы вам сразу узнать про Postgres очень плохую вещь: на каждое соединение он создаёт настоящий процесс ОС. Поэтому в любой маломальски нагруженной системе используется тот или иной пулер соединений (connection pooler). Один из самых распространенных вариантов — это pgBouncer. PgBouncer держит несколько постоянных соединений (сессий) к СУБД, а клиент, подключенный к пулеру, получает то одно из них, то другое — то, которое сейчас больше никем не используется.



PgBouncer может работать в двух режимах: сессионный и транзакционный. Сессионный режим логически почти не отличается от прямого подключения к базе данных, так как пока приложение подключено к pgBouncer, pgBouncer ему предоставляет одно и то же соединение к Postgres. А когда отключается — соединение возвращается в пул.


В транзакционном же режиме pgbouncer'а на каждую транзакцию будет подсовываться разная сессия.


Т.е. вы делаете SELECT — получаете соединение номер 1.
Потом делаете BEGIN; несколько запросов; COMMIT; — получаете соединение, допустим, 15.
Потом делаете ещё один селект — получаете соединение номер 12, а может, и номер 1 — как повезёт. Что свободно на данный момент, то и получите.


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


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


Транзакционные локи


Однако, advisory локи бывают и другого вида — для работы с транзакциями (т.е. подходят для работы с pgbouncer в любом режиме). Они берутся внутри транзакции с помощью pg_advisory_xact_lock()/pg_try_advisory_xact_lock(), а отпускаются автоматически при коммите или откате транзакции.


Пример:


Стартуем транзакцию


BEGIN;

Пробуем взять лок


SELECT pg_try_advisory_xact_lock(
    ('x' || md5('create daily 00:00 report'))::bit(64)::bigint
);

Если этот запрос вернул false, то завершаем транзакцию и не делаем больше ничего. Если вернул true, то делаем что-то полезное, например, запрос на долго вычисляемый ежедневный отчёт на 10 джойнов и 100500 условий, который мы бы не хотели выполнять два раза одновременно.


SELECT ...
  JOIN ...
  JOIN ...
  ....
WHERE
   ...

Отпускаем транзакционный адвизори лок, просто завершив транзакцию:


COMMIT;

Такие *_advisory_xact_lock хорошо работают с pgBouncer, запущенном в транзакционном режиме, однако и тут тоже не надо забывать об одном нюансе.


Не стоит использовать такой лок, например, для часовой обработки видео, если обработчиков много: всё это время транзакция будет висеть, а значит, и соединение к базе — тоже. В этом случае несколько таких видосов просто сожрут все соединения от pgBouncer к базе, и всё остальное замрёт в ожидании соединений.


Еще нюансы


  • Точно так же, как и в других языках и системах, постгресовые advisory locks бывают двух типов: exclusive и shared. Если вы не понимаете разницу, просто загуглите.


  • Если в одной сессии (имеется в виду коннект к самой базе, а не к пулеру) брать сессионный advisory lock с одним и тем же числом два раза, то и отпускать лок нужно будет дважды.


  • Посмотреть, какие адвизори локи взяты, можно с помощью команды


    SELECT objid, "mode" FROM pg_locks where locktype = 'advisory';

    где objid — это число, которое вы указали в скобочках, когда брали лок, а mode — ExclusiveLock или ShareLock.


  • Все блокировки хранятся в памяти, поэтому после перезагрузки сервера ничего не сохранится.



Официальная документация:
общее описание
список функций

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 22: ↑21 and ↓1+20
Comments16

Articles

Information

Website
karuna.group
Registered
Founded
Employees
201–500 employees
Location
Россия