Как стать автором
Обновить

Комментарии 16

У нас 2PC работает изумительно и имеет нулевую дополнительную сложность для разработчиков бизнес-логики. Пришлось конечно повозиться чтобы реализовать эту транзакционность в платформе (готовые не подошли), но теперь голова о транзакциях в распределенной системе почти не болит.

Было бы интересно почитать подробности :) Обычно же как раз не советуют использовать 2PC.

Обычно же как раз не советуют использовать 2PC.

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

Какого рода подробности интересуют?

  1. Как разработчики пишут транзакции? Какой-то DSL используют? В виде обычного SQL, но 2PC разруливает какой-то прокси?

  2. На какие компромиссы пришлось пойти?

  3. Примерно с какой нагрузкой и каким объёмом данных вы работаете?



По технологиям - используем Spring Boot с Kotlin/Java

  1. Используют статический метод TxnContext.doInTxn {/*действия в рамках транзакции*/}. Есть разные вариации этого метода с флагом readOnly и указанием нужно или не нужно принудительно создавать новую транзакцию, но это уже детали. Есть еще реализация адаптера для транзакционного менеджера спринга и с ней можно использовать штатную аннотацию @Transactional на любом методе. С т.з. разработчиков работа с распределенными транзакциями идет так же как и с локальными. Работа 2PC полностью скрыта в реализации транзакционного менеджера + API либе, которая занимается регистрацией внешних приложений в качестве XA ресурсов для менеджера.

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

  3. Нагрузка пока не сильно большая. Порядка 300-400 одновременных пользователей, которые работают с ~десятком миллионов записей в БД.

Из интересного еще - основная потребность в 2PC возникла из-за работы BPMN движка Camunda с документами, которые находятся в других базах и приложениях. В результате реализации 2PC теперь сценарии завершили_задачу -> поменяли_документ -> что-то_пошло_не_так работают на ура.

Вот более обстоятельная статья, опубликованная четыре дня назад - https://habr.com/ru/articles/769102/

Там как раз про 2pc только вводные

Коротко о транзакциях и их распределении - берёшь и выкидываешь их нафиг. На секунду помешаешь в голову мысль, что человечество их ещё не изобрело. Стало страшно? Поборол страх, поместил эту мысль ещё раз, теперь на минуту. Подумал хм, а может быть собрать всё как-то иначе. И придумал какую-нибудь квантовую асинхронную мульти-супер позицию состояния данных. Родил что-то новое в общем.

Ну а если без фантастики. Ну неужели нет способа без транзакций в реляционках системы строить. На каких нибудь хитрых кэшах или crdt. И как нибудь там шмяк бух и вау чего случилось.

Транзакция здесь — не способ построения, а абстракция от реального бизнес-процесса покупка->товар, например. Без транзакции ты физически можешь потерять данные о покупке или товаре и тогда никакие кеши тебя не спасут— спасёт только философия :) Типа, извини, друг, не повезло — потеряли мы твой "товар" и это есть суть вещей. Низведём же в конструкте человеческого сознания страх потери!

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

Зайдём с другого угла. Ну был у тебя монолит, разделил его на сервисы. Один сервис - одна бд. Вроде всё сделал правильно, НО именно путь один сервис - одна бд и привёл тебя к необходимости распределения транзакций. Ты в итоге не туда свернул и решил проблему У. Вместо решения проблемы Х. То есть тебя что-то не устраивало в монолите и ты его поменял, словил проблему ХУ.

Ты просто вспомни свой монолит. Наверное там не нужны были всякие саги... Но почему не нужны были? Вот и ключ. А сделай ты одну бд и пусть все сервисы пишут туда и смотрят туда. И сделай кое что весьма полезное одна сущность - один запрос на запись в бд. Если запрос не удался, то и ладно ты ничего не потерял. А если удался - то всё круто, все данные за один запрос зашли куда надо.

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

И на любой чих регрессим весь монолит. И релизы собираем полгода.

А как вы распределённую транзакцию будете проверять без общесистемной регрессии? Или типа, сделали её распределённой - ну и бог бы с ней, больше не тестируем? Проблема-то никуда не ушла, тестировать надо.

Жизнь без распределённых транзакций проще и лучше. И это достижимо во многих кейсах, даже если вдруг одна БД вам претит - чуть более тщательный анализ доменной области скорее всего сожет позволить пилить микросервисы, чтобы одной семантически значимой сущностью владел один микросервис, а не несколько. Тогда весь этот гемморой резко пропадает.

Просто поделюсь опытом (интернет банкинг с бэкендом в виде нескольких систем, в том числе и внешних).

Так как почти все бэкенд системы внешние и наружу выдают http-based API, поэтому никаких двух фазных коммитов и т.д.

Общие идеи такие:

  1. Передача инфы между компонентами - через очереди асинхронно. Это развязывает потенциальные проблемы з отказами отдельных сервисов (а это реально и даже часто), так как очередь скапливается только к ним и никому не мешает. Ну и в целом все остальные плюшки асинхронной системы

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

  3. Все "шаги" транзакции работают по одной статусной модели, условно: init, started, done, error, unknown. Попытка отдельного екземпляра компонеты сделать полезное чего-то предваряется проверкой по базе, а можно ли исходя из статуса. Если низя - то ничего не делается. Так решается проблема дублирования выполнения, так как однократно взятая операция уже другим экземпляром не возьмется

  4. В случае неоднозначности статус unknown и пусть разбирается кто-то другой.

  5. В случае ошибки - статус error и выполнение "компенсации"

Дополнительно есть фоновый процесс, который пересматривает все unknown и решает, что делать (индивидуально). Если есть возможность - можно узнать статус. Если нет - можно передать проблему на решение оператору, который все равно в конечном итоге должен решить, какой статус выставить и "машинка" снова заработает дальше

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

В итоге:

  • слой работы со статусами един и живет как бы сам по себе

  • логика мониторинга "пляшет" от статусной модели, но до уровня "команды" пойди и сделай конкретному типу компоненты

  • реализация больших транзакций вынесена также в отдельный "слой" в каждой компоненте

  • вопросы инфраструктурной надежности сводятся к своей базе, все остальное либо stateless, либо внешнее API (которое напрямую не участвует, так как оно "спрятано" за статусом и компорентой)

  • на случай расхождения по данных с другими базами, что возможно в случае грандиозного шухера, посде которого все восстановились на разное время разработана процедура, которая "условно морозит" все неоднозначное для спокойного точечного разбирательства и позволяет запустить сервис для новых транзакций сразу

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

----------

Не знаю, поможет это автору или нет. Но мне было прикольно вспомнить и кратенько описать

Спасибо! Очень похоже на архитектуру, которую я планировал изначально, но перед внедрением решил сделать ещё круг и потестить https://temporal.io/ — выглядит очень интересно, на след неделе опубликую статью.


  • реализация больших транзакций вынесена также в отдельный "слой" в каждой компоненте

А как конкретно это выглядит? В виде кода? Какая-то декларативная форма?

В реальности это не важно (в этом случае решение было вообще сделано на IBM шине, поэтому вряд ли вам поможет конкретный ответ).

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

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

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

В самом коде - да по фиг. По сути, можно написать большой case оператор, можно много if. Можно красиво замутить таблицу и типа брать из нее. Это чисто стиль, как вам нравится. Любой вариант дает супер читабельность и легкость бытия с этим кодом.

Можно красиво это заворачивать в наследуемость, абстрактные методы - ну блин, это реально примитивщина уровня студента второго курса.

--------------

Фактором принятия решения может быть требование менять ход транзакции low-code подходом. Тогда да, надо чего-то придумывать, делать к нему админку и т.д. Но я имея опыт реализации разных вариантов на разных проектах, в этом пришел к тому, что имея быстрый CI/CD и девелопера на борту, намного эффективнее по затратам просто оставить это в коде. Как минимум, если это просто решение, а не платформа с перспективой 10+ внедрений

Кстати, хорошо залетит сюда любое Business Rules Engine, которыый на борту имеет decision tables. Это если хочется по красоте делать изменения, без привлечения разработчика, и по дешману.

Но это все уже "бантики", которые под капотом имеют свою, иногда неочевидную цену

--------------

Внешние оркестраторы не довелось использовать на полную, когда-то экспериментировали с Camel, но команде не зашло, а когда мы до него "доросли", то было поздно. Плюс в этом проекте была "шина", а она хочешь или нет, дает много из коробки. И не использовать это тупенько было

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации