Всем привет. Уже в сентябре OTUS открывает набор в новую группу курса «Highload Architect». В связи с этим я продолжаю серию своих публикаций, написанных специально для этого курса, а также приглашаю вас на свой бесплатный вебинар, в рамках которого я подробно расскажу о программе курса и формате обучения в OTUS. Записаться на вебинар можно тут.
Как известно, переход от монолита к микросервисной архитектуре вызывает ряд сложностей, связанных как с технической частью проекта, так и с человеческим фактором. Одной из самых сложных технических проблем вызывает обеспечение согласованности в распределенной системе.
Достаточно тонким моментом является то, что согласованность в контексте распределенных систем отличается от согласованности в контексте баз данных. Далее под согласованностью мы будем понимать именно первое: незавершенная (ошибочная) операция не вносит никаких эффектов и не меняет данные, при конкурентном доступе к данным все операции рассматриваются как атомарные (нельзя увидеть промежуточный результат операции), если у данных имеется несколько копий (репликация), то последовательность применения операций на всех копиях одна и та же. То есть на самом деле мы хотим получить ACID транзакцию, но только распределенную.
Почему обеспечение согласованности затруднено именно в микросервисной архитектуре? Дело в том, что данный архитектурный стиль зачастую предполагает применение паттерна database per service. Позволю себе напомнить, что этот паттерн заключается в том, что у каждого микросервиса своя независимая база или базы (базы, потому что помимо первичного источника данных может использоваться, например, кеш). Такой подход позволяет с одной стороны не добавлять неявные связи по формату данных между микросервисами (микросервисы взаимодействуют только явно через API), с другой стороны по максимуму использовать такое преимущество микросервисной архитектуры как technology agnostic (мы можем выбирать подходящую под особую нагрузку на микросервис технологию хранения данных). Но при всем при этом мы потеряли гарантию согласованности данных. Посудите сами, монолит общался с одной большой базой, которая предоставляла возможности по обеспечению ACID — транзакций. Теперь баз данных стало много, а вместо одной большой ACID — транзакции у нас много небольших ACID — транзакций. Нашей задачей будет объединение всех этих транзакций в одну распределенную.
Первое что может прийти в голову — это концепция оптимистичной согласованности: мы совершаем столько транзакций сколько хотим на столько механизмов хранения сколько нужно. При этом мы ждем, что все будет хорошо, а если все плохо, то говорим, что все будет хорошо в итоге. Если все плохо в итоге, то говорим: «Да, такое случается, но с крайне низкой вероятностью».
Если без шуток, то пренебрежение обеспечением согласованности в случае ее некритичности для бизнеса является хорошим решением, особенно если учесть то, каких усилий нам будет стоить ее обеспечение (в чем, как я надеюсь, вы убедитесь несколько позже).
Если для бизнеса все-таки согласованность является критичной, можно попытаться ее обеспечить несколькими способами. Если мы говорим о ситуации, когда данные обновляются одним сервисом (например имеет место репликация базы данных), то можно применить стандартные алгоритмы обеспечения консистентности такие как Paxos или Raft. Такие транзакции называются гомогенными. Если данные обновляются несколькими сервисами (то есть имеет место гетерогенная транзакция), то как тут как раз и начинаются сложности, о которых мы говорили выше.
С одной стороны мы можем все-таки обойти необходимость обеспечения распределенной транзакции путем стремления к service-based архитектуры (объединяем сервисы таким образом, чтобы транзакция была гомогенная). Такое решение не очень каноничное с точки зрения принципов микросервисной архитектуры, но зато оно технически намного проще из-за чего часто применяется на практике. С другой стороны мы можем оставить каноничные микросервисы, но при этом применить один из механизмов обеспечения распределенных транзакций: двухфазный коммит или сагу. В этой статье будет изучен первый вариант, а второй мы обсудим в следующий раз.
Механизм предельно прост: есть некоторый transaction manager, который собственно оркестрирует транзакцию. На первом этапе (prepare) transaction manager подает соответствующую команду для resource manager'ов, по которой они в свои журналы записывают данные, которые будут закоммичены. Получив подтверждение ото всех resource manager'ов об успешном завершении первого этапа, transaction manager начинает второй этап и подает следующую команду (commit), по которой resource manager'ы применяют принятые ранее изменения.
Несмотря на кажущуюся простоту, такой подход обладает рядом недостатков. Во-первых, если хотя бы один resource manager даст сбой на втором этапе, вся транзакция должна быть отменена. Таким образом, нарушается один из принципов микросервисной архитектуры — устойчивость к отказам (когда мы приходили к распределенной системе, мы сразу закладывались на то, что отказ в ней является нормой а не исключительной ситуацией). Более того, если отказов будет много (а их будет много), то процесс отмены транзакций необходимо будет автоматизировать (в том числе и писать транзакции, откатывающие транзакции). Во-вторых, сам transaction manager является единой точкой отказа. Он должен уметь транзакционно выдавать id-шники транзакциям. В-третьих, поскольку хранилищу подаются специальные команды, логично предположить, что хранилище должно уметь это делать, то есть соответствовать стандарту XA, а не все современные технологии ему соответствуют (такие брокеры как Kafka, RabbitMQ и NoSQL решения как MongoDB и Cassandra не поддерживают двухфазные коммиты).
Вывод, напрашивающийся из всех этих факторов, был отлично сформулирован Крисом Ричардсоном: «2PC not an option» (двухфазный коммит — не вариант).
Мы разобрались почему распределенные транзакции являются основной технической болью микросервисной архитектуры и поговорили о различных вариантах решения данной задачи, детально обсудили механизм двухфазного коммита.
Приглашаю всех записаться на свой вебинар о курсе, в рамках которого я подробно расскажу о формате обучения и ознакомлю всех желающих с программой обучения.
Введение
Как известно, переход от монолита к микросервисной архитектуре вызывает ряд сложностей, связанных как с технической частью проекта, так и с человеческим фактором. Одной из самых сложных технических проблем вызывает обеспечение согласованности в распределенной системе.
Согласованность
Достаточно тонким моментом является то, что согласованность в контексте распределенных систем отличается от согласованности в контексте баз данных. Далее под согласованностью мы будем понимать именно первое: незавершенная (ошибочная) операция не вносит никаких эффектов и не меняет данные, при конкурентном доступе к данным все операции рассматриваются как атомарные (нельзя увидеть промежуточный результат операции), если у данных имеется несколько копий (репликация), то последовательность применения операций на всех копиях одна и та же. То есть на самом деле мы хотим получить ACID транзакцию, но только распределенную.
Причина проблемы
Почему обеспечение согласованности затруднено именно в микросервисной архитектуре? Дело в том, что данный архитектурный стиль зачастую предполагает применение паттерна database per service. Позволю себе напомнить, что этот паттерн заключается в том, что у каждого микросервиса своя независимая база или базы (базы, потому что помимо первичного источника данных может использоваться, например, кеш). Такой подход позволяет с одной стороны не добавлять неявные связи по формату данных между микросервисами (микросервисы взаимодействуют только явно через API), с другой стороны по максимуму использовать такое преимущество микросервисной архитектуры как technology agnostic (мы можем выбирать подходящую под особую нагрузку на микросервис технологию хранения данных). Но при всем при этом мы потеряли гарантию согласованности данных. Посудите сами, монолит общался с одной большой базой, которая предоставляла возможности по обеспечению ACID — транзакций. Теперь баз данных стало много, а вместо одной большой ACID — транзакции у нас много небольших ACID — транзакций. Нашей задачей будет объединение всех этих транзакций в одну распределенную.
Оптимистичная согласованность
Первое что может прийти в голову — это концепция оптимистичной согласованности: мы совершаем столько транзакций сколько хотим на столько механизмов хранения сколько нужно. При этом мы ждем, что все будет хорошо, а если все плохо, то говорим, что все будет хорошо в итоге. Если все плохо в итоге, то говорим: «Да, такое случается, но с крайне низкой вероятностью».
Если без шуток, то пренебрежение обеспечением согласованности в случае ее некритичности для бизнеса является хорошим решением, особенно если учесть то, каких усилий нам будет стоить ее обеспечение (в чем, как я надеюсь, вы убедитесь несколько позже).
Варианты обеспечения консистентности
Если для бизнеса все-таки согласованность является критичной, можно попытаться ее обеспечить несколькими способами. Если мы говорим о ситуации, когда данные обновляются одним сервисом (например имеет место репликация базы данных), то можно применить стандартные алгоритмы обеспечения консистентности такие как Paxos или Raft. Такие транзакции называются гомогенными. Если данные обновляются несколькими сервисами (то есть имеет место гетерогенная транзакция), то как тут как раз и начинаются сложности, о которых мы говорили выше.
С одной стороны мы можем все-таки обойти необходимость обеспечения распределенной транзакции путем стремления к service-based архитектуры (объединяем сервисы таким образом, чтобы транзакция была гомогенная). Такое решение не очень каноничное с точки зрения принципов микросервисной архитектуры, но зато оно технически намного проще из-за чего часто применяется на практике. С другой стороны мы можем оставить каноничные микросервисы, но при этом применить один из механизмов обеспечения распределенных транзакций: двухфазный коммит или сагу. В этой статье будет изучен первый вариант, а второй мы обсудим в следующий раз.
Двухфазный коммит
Механизм предельно прост: есть некоторый transaction manager, который собственно оркестрирует транзакцию. На первом этапе (prepare) transaction manager подает соответствующую команду для resource manager'ов, по которой они в свои журналы записывают данные, которые будут закоммичены. Получив подтверждение ото всех resource manager'ов об успешном завершении первого этапа, transaction manager начинает второй этап и подает следующую команду (commit), по которой resource manager'ы применяют принятые ранее изменения.
Несмотря на кажущуюся простоту, такой подход обладает рядом недостатков. Во-первых, если хотя бы один resource manager даст сбой на втором этапе, вся транзакция должна быть отменена. Таким образом, нарушается один из принципов микросервисной архитектуры — устойчивость к отказам (когда мы приходили к распределенной системе, мы сразу закладывались на то, что отказ в ней является нормой а не исключительной ситуацией). Более того, если отказов будет много (а их будет много), то процесс отмены транзакций необходимо будет автоматизировать (в том числе и писать транзакции, откатывающие транзакции). Во-вторых, сам transaction manager является единой точкой отказа. Он должен уметь транзакционно выдавать id-шники транзакциям. В-третьих, поскольку хранилищу подаются специальные команды, логично предположить, что хранилище должно уметь это делать, то есть соответствовать стандарту XA, а не все современные технологии ему соответствуют (такие брокеры как Kafka, RabbitMQ и NoSQL решения как MongoDB и Cassandra не поддерживают двухфазные коммиты).
Вывод, напрашивающийся из всех этих факторов, был отлично сформулирован Крисом Ричардсоном: «2PC not an option» (двухфазный коммит — не вариант).
Вывод
Мы разобрались почему распределенные транзакции являются основной технической болью микросервисной архитектуры и поговорили о различных вариантах решения данной задачи, детально обсудили механизм двухфазного коммита.
Приглашаю всех записаться на свой вебинар о курсе, в рамках которого я подробно расскажу о формате обучения и ознакомлю всех желающих с программой обучения.
maxim_ge
Тут не совсем понял. Против «единой точки отказа» разве нет «приемов»?
И зачем «выдавать id-шники» транзакционно, есть требование монотонного возрастания номера транзакции?
PrinceKorwin
Думаю под «транзакционно выдавать id-шники» имелось ввиду выдавать их уникально даже если сам TM горизонтально масштабируется для обеспечения отказоустойчивости.
maxim_ge
Тогда непонятно, в чем проблема выдавать уникальные ID…
PrinceKorwin
Если тупо в лоб, то проблемы есть. Но решений тьма конечно.