Архитектура транзакций в Apache Ignite

В этой статье мы рассмотрим, как устроены транзакции в Apache Ignite. Не будем останавливаться на концепции Key-Value хранилища, а перейдем сразу к тому, как это реализовано в Ignite. Начнем с обзора архитектуры, а затем проиллюстрируем ключевые моменты логики транзакций при помощи трейсинга. На простых примерах вы увидите, как работают транзакции (и по каким причинам могут не работать).

Необходимое отступление: кластер в Apache Ignite


Кластер в Игнайте — это множество серверных и клиентских узлов, где серверные узлы объединены в логическую структуру в виде кольца, а клиентские узлы присоединены к соответствующим серверным. Основное отличие клиентских узлов от серверных в том, что первые не хранят данные.

k57nsu9q7y5g1cwrsw2p_g2mmsk.png
Данные, с логической точки зрения, принадлежат партициям, которые в соответствии с некоторой affinity-функцией распределены по узлам (подробнее о распределении данных в Ignite). У основных (primary) партиций могут быть копии (backups).

nvm3ryl71pi31awz3_sowlhjeg0.png

Как устроены транзакции в Apache Ignite


Архитектура кластера в Apache Ignite накладывает на механизм транзакций определенное требование: консистентность данных в распределенной среде. Это означает, что данные, находящиеся на разных узлах, должны изменяться целостно с точки зрения ACID принципов. Существует ряд протоколов, позволяющих реализовать требуемое. В Apache Ignite используется алгоритм на основе двухфазного коммита, который состоит из двух этапов:

  • prepare;
  • commit;


Отметим, что, в зависимости от уровня изолированности транзакции, механизма взятия локов и ряда других параметров, детали в фазах могут изменяться.

Рассмотрим, как происходят обе фазы, на примере следующей транзакции:

Transaction tx = client.transactions().txStart(PESSIMISTIC, READ_COMMITTED);
client.cache(DEFAULT_CACHE_NAME).put(1, 1);
tx.commit();


Prepare фаза


  1. Узел — координатор транзакций (near node в терминах Apache Ignite) — отправляет prepare-сообщение на узлы, содержащие primary-партиции для всех ключей, принимающих участие в данной транзакции.
  2. Узлы с primary-партициями отправляют Prepare-сообщение на соответствующие узлы с backup-партициями, если таковые имеются, и захватывают необходимые локи. В нашем примере backup-партиций две.
  3. Узлы с backup-партициями отправляют Acknowledge-сообщения на узлы с primary-патрициями, которые, в свою очередь, отправляют аналогичные сообщения на узел, координирующий транзакцию.


c-oipwdznnsntazsdo90_zobwos.png

Commit фаза


После получения подтверждающих сообщений от всех узлов, содержащих primary-партиции, узел координатор транзакций отправляет Commit-сообщение, как показано на рисунке ниже.

bsbcw1tlqtrmlykmnh1rqbg6kqo.png

Транзакция считается завершенной в тот момент, когда координатор транзакций получил все подтверждающие (Acknowledgment) сообщения.

От теории к практике


Чтобы рассмотреть логику работы транзакции, обратимся к трейсингу.

Для включения трейсинга в Apache Ignite необходимо выполнить следующие шаги:
  • Включим модуль ignite-opencensus и зададим OpenCensusTracingSpi как tracingSpi посредством конфигурации кластера:
    
        
            
        
    
    

    или
    IgniteConfiguration cfg = new IgniteConfiguration();
    
    cfg.setTracingSpi(
        new org.apache.ignite.spi.tracing.opencensus.OpenCensusTracingSpi());
    

  • Зададим некоторый отличный от нуля уровень сэмплирования транзакций:
    JVM_OPTS="-DIGNITE_ENABLE_EXPERIMENTAL_COMMAND=true" ./control.sh --tracing-configuration set --scope TX --sampling-rate 1
    

    или
    ignite.tracingConfiguration().set(
                new TracingConfigurationCoordinates.Builder(Scope.TX).build(),
                new TracingConfigurationParameters.Builder().
                        withSamplingRate(SAMPLING_RATE_ALWAYS).build());
    

    Остановимся на нескольких моментах чуть подробнее:
  • Запустим PESSIMISTIC, SERIALIZABLE транзакцию с клиентского узла на кластере из трех узлов.
    Transaction tx = client.transactions().txStart(PESSIMISTIC, SERIALIZABLE);
    client.cache(DEFAULT_CACHE_NAME).put(1, 1);
    tx.commit();


Обратимся к GridGain Control Center (подробный обзор инструмента) и взглянем на получившееся дерево спанов:

gtnhuk_azytqtq8zexmhcgcxlgo.png

На иллюстрации мы видим, что корневой спан transaction, созданный в начале вызова transactions ().txStart, порождает две условных группы спанов:

  1. Машинерию, связанную с захватом локов, инициированную put () операцией:
    1. transactions.near.enlist.write
    2. transactions.colocated.lock.map с подэтапами
  2. transactions.commit, созданный в момент вызова tx.commit (), который, как ранее упоминалось, состоит из двух фаз — prepare и finish в терминах Apache Ignite (finish-фаза тождественна commit-фазе в классической терминологии двухфазного коммита).


Давайте теперь рассмотрим детально prepare-фазу транзакции, которая, начавшись на узле координаторе транзакций (near-узел в терминах Apache Ignite), продуцирует спан transactions.near.prepare.

Попав на primary-партицию, prepare-запрос триггерит создание transactions.dht.prepare спана, в рамках которого осуществляется отправка prepare-запросов на бекапы tx.process.prepare.req, где они обрабатываются tx.dht.process.prepare.response и отсылаются обратно на primary-партицию, которая отправляет подтверждающее сообщение на координатор транзакций, попутно создавая спан tx.near.process.prepare.response. Finish-фаза в рассматриваемом примере будет аналогична prepare-фазе, что избавляет нас от необходимости детального ее разбора.

Кликнув по любому из спанов, мы увидим соответствующую метаинформацию:

hmz7yzksuxorzgmob0dzhxrdmfi.png

Так, например, для корневого спана transaction мы видим, что он был создан на клиентском узле 0eefd.

Мы также можем увеличить степень детализации трейсинга транзакций, включив трейсинг коммуникационного протокола.

Настройка параметров трейсинга
JVM_OPTS="-DIGNITE_ENABLE_EXPERIMENTAL_COMMAND=true" ./control.sh --tracing-configuration set --scope TX --included-scopes Communication --sampling-rate 1 --included-scopes COMMUNICATION

или
       ignite.tracingConfiguration().set(
           new TracingConfigurationCoordinates.Builder(Scope.TX).build(),
           new TracingConfigurationParameters.Builder().
               withIncludedScopes(Collections.singleton(Scope.COMMUNICATION)).
               withSamplingRate(SAMPLING_RATE_ALWAYS).build())


zf6xizlii8s0kgdpmjcfoe1qpro.png

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

Обработка исключений и восстановление после сбоев


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

Выше мы говорили, что в контексте транзакций в Apache Ignite можно выделить три типа узлов:

  • Координатор транзакций (near node);
  • Узел с primary-партицией для соответствующего ключа (primary node);
  • Узлы с backup-партициями ключей (backup nodes);


и две фазы самой транзакции:

  • Prepare;
  • Finish;


Путем нехитрых вычислений получим необходимость обработки шести вариантов падений узла — от падения бекапа на prepare-фазе до падения координатора транзакций на finish-фазе. Рассмотрим эти варианты подробнее.

Падение бекапа как на prepare, так и на finish-фазах


Такая ситуация не требует каких-либо дополнительных действий. Данные на новые backup-узлы доедут самостоятельно в рамках ребаланса с primary-узла.

k9hvtfiaxfdbetyvnktolaxw_mo.png

Падение primary-узла на prepare-фазе


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

qexhltrvzdd4ss1difiwdhuskaa.png

Падение primary-узла на finish-фазе


В таком случае координатор транзакции дожидается дополнительных NodeFailureDetection сообщений, после получения которых может принять решение об успешном завершении транзакции, если данные были записаны на backup-партициях.

7qlyebimkd_czbfhzhamgclqydo.png

Падение координатора транзакций


Наиболее интересный случай — потеря контекста транзакции. В такой ситуации primary- и backup-узлы непосредственно обмениваются локальным транзакционным контекстом друг с другом, тем самым восстанавливая глобальный контекст, что позволяет принять решение о верификации коммита. Если, например, один из узлов сообщит, что не получал Finish-сообщение, то произойдет откат транзакции.

hlp6vwyexiykohckid_m7-15gcw.png

Резюме


В приведенных примерах мы рассмотрели flow транзакций, проиллюстрировав его при помощи трейсинга, который в деталях показывает внутреннюю логику. Как видите, реализация транзакций в Apache Ignite близка к классической концепции двухфазного коммита с некоторыми ухищрениями в области производительности транзакций, связанными с механизмом взятия локов, особенностями восстановления после сбоев и логикой таймаута транзакций.

© Habrahabr.ru