Categories: Алгоритмы

simple-distributed-transactions/

Есть два взгляда на распределенные отказоустойчивые системы — теоретический и практический. В то время, как теоретики (a.k.a distributed systems nerds) пиарят так называемые NewSQL базы данных , рассуждают о Paxos и векторных часах, большинство практикующих программистов относятся к подобным решениям с некоторым скепсисом. Ведь инженерия — это штука про компромиссы. У любого решения всегда есть плюсы и минусы, не исключая NewSQL. Так или иначе, в реальных системах, как правило, все еще используется PostgreSQL и другие традиционные реляционные СУБД.

Вопреки распространенному мнению, такие системы обеспечивают неплохую отказоустойчивость и неплохо масштабируются, в том числе на запись. Увы, от разработчиков требуются дополнительные умственные усилия для того, чтобы обеспечить правильную группировку данных, чтоб большинство транзакций и join’ов выполнялись в рамках одного сервера, написание дополнительного кода для миграции данных между серверами (другими словами, решардинга), а также распихивание повсюду метрик и индикаторов . От админов же тем временем требуется пристально смотреть в мониторинг (например, Zabbix или Nagios ) и иметь под рукой скрипт для быстрого промоутинга слейвов до мастеров . Но в целом это работает. И думается, что NewSQL базы данных, которые в итоге выживут, будут предоставлять все в точности то же самое, только готовое, из коробки.

А пока светлое будущее не наступило, программистам нужно как-то решать стоящие перед ними уже сегодня проблемы. Например, у вас в системе есть некие счета, а на этих счетах лежат некие деньги. В результате неких событий (скажем, пользователи делают ставки на результаты футбольных матчей или играют на форексе) состояние счетов изменяется. Поняв, что упираетесь на запись, вы сделали шардинг по account_id. Теперь счета живут на разных репликасетах. Вы мониторите нагрузку и при необходимости переносите счета с одного репликасета на другой. Все бы хорошо, но иногда изменение состояния счета может влиять на состояние другого счета. Например, если у вас есть реферальная программа. И не факт, что эти два счета находятся в одном репликасете. Что делать?

Типичное решение заключается в том, чтобы использовать 2PC . Но 2PC плох тем, что это блокирующий протокол. И если подумать, он предоставляет слишком строгие гарантии. На практике такие гарантии часто не нужны. Скажем, в нашем примере пользователи не могут видеть состояние счета других пользователей. А значит, нет ничего страшного в том, что реферал получит свой процент от комиссии системы с небольшой задержкой. Вообще, этот процент можно начислять по расписанию, время от времени. Однако в более общем случае состояние счетов требуется обновить одновременно. Хотя атомарность при этом и не обязательна.

Так вот, все это долгое введение было к тому, что на практике условия часто формулируются именно таким образом, и тогда можно обойтись без 2PC. Примем за рабочую теорию, что время на всех серверах синхронизировано при помощи ntpd до определенной точности, 5, 10, 20 миллисекунд — не суть важно. Это вполне реальное условие, которое грамотные админы соблюдают и жестко мониторят в любом случае хотя бы для того, чтобы можно было сопоставить логи или таймстампы, записанные в БД, с нескольких серверов. Теперь рассмотрим следующий алгоритм.

Клиент C хочет выполнить некую транзакцию, затрагивающую счета A и B, хранящиеся на серверах S A и S B соответственно. Скажем, перевести 10 баксов со счета A на счет B. Берется таймаут T, равный 1-ой, 2-м, 5-и секундам — главное, чтобы он был много больше погрешности локального времени. Таймаут может быть захардкожен или прописываться в конфиге приложения.

Тогда, чтобы выполнить транзакцию:

  1. Клиент генерирует UUID транзакции Id и узнает текущее время StartTime;
  2. Клиент пишет в специальную табличку (например, на S A ) запись «транзакция Id с таймаутом T, со счета A переводится 10 баксов на счет B, транзакция начала выполняться в StartTime», говорит commit;
  3. Клиент идет на S A , обновляет состояние счета A, а также делает запись в специальной табличке с финансовыми транзакциями «в результате выполнения транзакции Id со счета A снялось 10 баксов», говорит commit;
  4. Аналогично для S B и других серверов и счетов, если транзакция затрагивает больше двух;
  5. Клиент обновляет запись, созданную на шаге 2, помечая транзакцию, как выполненную, говорит commit;

Шаги 3 и 4 могут выполняться параллельно. Если все шаги выполнены успешно, транзакция считается успешной, возвращается OK. Если во время выполнения любого шага по не важно какой причине произошла ошибка, клиент прекращает выполнение транзакции и возвращает ERROR. Если клиент видит, что выполнение шагов 1-5 заняло больше T единиц времени, транзакция считается неуспешной и возвращается ERROR.

Помимо клиента в системе крутятся специальные процессы, которые что-то делают с неуспешными транзакциями. Каждой транзакции соответствует один процесс. Какой именно — определяется, например, по хэшу от Id транзакции. Если процесс видит незавершенную транзакцию, начавшуюся более T + D единиц времени назад, где D — какая-то дельта того же порядка, что и T, то он откатывает или, наоборот, накатывает транзакцию.

Так что же делать в случае ошибки — откат или накат? Зависит от приложения. Но в общем случае желательнее откатывать. Во-первых, если транзакция не прошла, один из серверов может быть временно недоступен (а админы промоутнут слейва до мастера только через 5 минут) и накатить за разумное время все равно не получится. Быстрее откатить, приведя тем самым систему в согласованное состояние. Во-вторых, возможна ситуация, когда транзакция в принципе не может быть выполнена. В итоге накат не всегда возможен, и система может навсегда остаться в противоречивом состоянии, если не сделать откат. Наконец, в-третьих, откат соответствует всем привычной и понятной традиционной семантике транзакций — если транзакция не прошла, делается rollback. В некоторых случаях траназакцию с ошибкой можно как-то пометить и затем зажечь лампочку в мониторинге. Можно попробовать N раз накатить транзакцию, а затем откатывать. Тут все зависит от ситуации.

Независимо от того, накатываем мы или откатываем, никакие данные не удаляются. Например, если мы делаем откат из-за того, что сервер S B не смог выполнить транзакцию, в финансовых транзакциях аккаунта А делается еще одна запись «на счет А возвращается 10 баксов в результате отката транзакции Id». Понятное дело, перед выполнением любых действий, процесс проверяет, что частичный откат не был выполнен ранее. По выполнении полного отката транзакции в записи, сделанной клиентом на шаге 2, делается пометка, что транзакция была откачена.

Само собой разумеется, мы предполагаем, что (1) распределенные транзакции редки по сравнению с транзакциями в рамках одного сервера (иначе мы плохо пошардили данные), что (2) 99.9% времени система работает в штатном режиме и что большинство межсерверных транзакций выполняются успешно, что (3) если на одном сервере прошел commit, то эти данные уже никогда не потеряются ( Raft , синхронная репликация , …), а также, что (4) в конечном счете все, что падало, поднимается. Если вас беспокоит, что в 0.1% случаев или реже система будет находится в противоречивом состоянии целых T + D единиц времени, транзакции можно либо «смазать», например, делая в базе пометку «эти данные станут видны в момент времени StartTime + T + 2*D», либо, еще лучше, применять изменения только после того, как транзакция уже была помечена, как успешная.

Полагаю, в силу предельной очевидности алгоритма, какое-либо строгое доказательство того, что он работает, не требуется. Идея настолько проста и естественна, что я почти уверен, что она пришла мне в голову далеко не первому. Рабочее название всего этого хозяйства пусть будет ECDT, Eventually Consistent Distributed Transactions. Но если вы вдруг знаете другое, более общепринятое название, сообщите его, плиз, в комментариях.

Дополнение: Еще кое-какая инфа о ручном шардинге и распределенных транзакциях приведена в статье Шардинг, перебалансировка и распределенные транзакции в реляционных базах данных .

Дополнение: По состоянию на октябрь 2021 описанный подход обычно называют распределенные саги / distributed sagas [PDF] (также существуют обычные саги [PDF] ). За прошедшие 7+ лет NewSQL не пришел и никого ни от чего не спас. Все по-прежнему делают application-level sharding и пишут распределенные саги на PostgreSQL.

admin

Share
Published by
admin

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

4 недели ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

4 недели ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

4 недели ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

4 недели ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

4 недели ago

Как управлять журналами событий из командной строки

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

4 недели ago