Глобальные блокировки на MongoDB

cluster_iconПредставим себе типичный стартап. Начинается всё с маленького хостинга за пару баксов, потом покупается VPS или даже VDS. Потом убирается первая буква V, и проект переезжает на dedicated а то и colocation. Ну, а через какое-то время у нас уже несколько серверов – один для базы, другой – веб-сервер. А потом….

… приходит тот замечательный момент, когда веб-серверов уже несколько и деплоятся они одновременно. Получается MySQL сервер один(ну или кластер с одним мастером), а вот веб-серверов множество, и миграции на них запускаются одновременно. Что же делать? Как не повредить базу при этом?

active-active-dns-cluster

Задача

Итак, нам необходимо сделать так, чтобы мы могли безболезненно запускать миграции на нескольких веб-серерах при наличии одного сервера баз данных (или кластера с одним мастером).

Решение

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

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

Один вопрос, где мы будем хранить эти глобальные блокировки? К ним ведь есть ряд требований.

  • Глобальная доступность. Мы должны иметь возможность устанавливать и снимать блокировки с любого узла кластера веб-серверов.
  • Атомарность. Операция получения блокировки должна быть атомарна в пределах кластера.

Вначале я обратил свой взгляд собственно на MySQL с его функциями get_lock() и release_lock() . С get_lock() у меня возникла засада. Из консоли MySQL всё работало замечательно, но вот из PHP скрипта на Doctrine 2 ничего не получалось. Я использовал Native Query, но ничего не получилось. Промучавшись с ним какое-то время я решил реализовать хранение блокировок в MongoDB.

В комментах заметили: If you have a lock obtained with GET_LOCK(), it is released when you execute RELEASE_LOCK(), execute a new GET_LOCK(), or your connection terminates (either normally or abnormally) – вот поэтому у тебя через консоль получилось, а через ORM нет.

Храним глобальные блокировки в MongoDB

Интерфейс для использования блокировок не мудрствуя лукаво я взял из MySQL: get_lock(), release_lock(), is_free_lock(). Больше для счастья ничего и не надо) Концептуально штуки, который мы будем реализовывать, называются мьютексы.

Мьютексы — простейшие двоичные семафоры, которые могут находиться в одном из двух состояний — отмеченном или неотмеченном (открыт и закрыт соответственно). Когда какой-либо поток, принадлежащий любому процессу, становится владельцем объекта mutex, последний переводится в неотмеченное состояние. Если задача освобождает мьютекс, его состояние становится отмеченным.
Задача мьютекса — защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. В каждый конкретный момент только один поток может владеть объектом, защищённым мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток засыпает до тех пор, пока мьютекс не будет освобождён.

Создадим в MongoDB колелкци mutexes с документами, в которых будут поля: lockName (имя мьютекса, по смыслу – название блокируемого ресурса), pid (номер процесса, я например использую такой формат – pid@hostname) и createdAt (время создания мьютекса). На основе поля createdAt мы реализуем таймаут для мьютексов. Итак Doctrine 2 ODM Document будет выглядеть так:

My\DefaultBundle\Document\Mutex.php

Тут есть военная хитрость. Мы используем MongoDB TTL Collections, однако в Монге есть ограничение – на одну коллекцию – один общий TTL (time-to-live), т.е. мы не можем настроить разыне TTL для разных блокировок. Но я всё-таки это сделал. Релизовал я это с помощью пересчёта поля createdAt в зависимости от нужного для конкретной блокировки TTL. Получается и волки сыты, и овцы целы. А если вам понадобится реальное время создания блокировки, то можно сохранять его в другом поле. Дальше я покажу вам файл репозитария, в котором реализуется интерфейс работы с блокировками.

My\DefaultBundle\Repository\DMutex.php

А вот простенький пример работы с мьютексами.
MutexDemo.php

Ожидаемый вывод:

Заключение

Получается теперь мы можем блокирвоать глобально-доступный ресурс в кластере. К тому-же мы преодолели ограничение MongoDB “one-ttl per ttl-collection”. В следующей статье, я покажу на практике, как можно использовать глобальные блокировки в веб-проекте.

Ссылки

http://docs.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/indexes.html

http://docs.mongodb.org/manual/tutorial/expire-data/

2 Comments

Leave a Comment