Главная » Хабрахабр » Целостность данных в микросервисной архитектуре — как её обеспечить без распределенных транзакций и жёсткой связности

Целостность данных в микросервисной архитектуре — как её обеспечить без распределенных транзакций и жёсткой связности

Как вы, возможно, знаете, раньше я все больше писал и рассказывал про хранилища, Vertica, хранилища больших данных и прочие аналитические вещи. Всем привет. Сейчас в область моей ответственности упали и все остальные базы, не только аналитические, но и OLTP (PostgreSQL), и NOSQL (MongoDB, Redis, Tarantool).

Единую распределенную гетерогенную базу, состоящую из кучи PostgreSQL, Redis-ов и Монг… И, возможно, из одной-двух баз Vertica. Эта ситуация позволила мне взглянуть на организацию, имеющую несколько баз данных, как на организацию, имеющую одну распределенную гетерогенную (разнородную) базу.

Прежде всего, с точки зрения бизнеса важно, чтобы с данными, движущимися по такой базе, все было нормально. Работа этой единой распределенной базы порождает кучу интересных задач. термин это сложный, и в разных нюансах рассмотрения СУБД (ACID и CAP теорема) он имеет разный смысл. Я специально не использую здесь термин целостность, consistency, т.к.

Под катом я рассказываю, как обеспечить целостность данных в микросервисной архитектуре без распределенных транзакций и жесткой связности. Ситуация с распределенной базой обостряется, если компания пытается перейти на микросервисную архитектуру. (А в самом конце объясняю, почему выбрал для статьи такую иллюстрацию).

Согласно Крису Ричардсону (одному из известнейших евангелистов микросервисной архитектуры), в этой архитектуре есть два подхода к работе с базами данных: shared database и database-per-service.

При этом сам по себе этот паттерн является анти-паттерном с точки зрения микросервисной архитектуры, т.к. Shared database — это неплохой первый шаг, отличное решение для небольшой компании без амбициозных планов роста. Т.е. два сервиса, делящих общую базу, нельзя независимо тестировать и масштабировать. эти сервисы скорее — один сервис, тяготеющий к превращению в монолит.

Сервис может обращаться к данным другого сервиса только через API (в широком смысле), без прямого подключения к его базе. Паттерн database-per-service предполагает, что у каждого сервиса своя база.

Кто-то умеет в MongoDB, кто-то верит в PostgreSQL, кому-то достаточно Redis (риск потери данных при выключении для этого сервиса приемлем), а кто-то вообще хранит данные в CSV-файлах на диске (а почему бы, собственно, и нет?). Паттерн database-per-service позволяет командам соответствующих сервисов выбирать базы, как им нравится.

Работа с подобным «зоопарком» баз данных поднимает задачу наведения порядка в данных на абсолютно новый уровень сложности.

Давайте посмотрим на задачу наведения порядка через призму классического СУБД-шного набора требований ACID: развернем суть каждой буквы аббревиатуры, и проиллюстрируем сложности с этой буквой в микросервисной архитектуре.

Atomicity — все или ничего. (A) CID — Atomicity.

Согласно требованию Atomicity, нужно обязательно выполнить все шаги (с возможными повторами), при отказе важного шага — отменить выполнившиеся.

Все четыре шага должны либо выполниться, либо не выполниться. В приведенной иллюстрации демонстрируется тестовый процесс покупки услуги VIP: резервируются деньги в биллинге (1), для пользователя подключается бонусная услуга (2), тип пользователя меняется на Pro (3), зарезервированные деньги в биллинге списываются (4).

При этом нельзя зависнуть посередине процесса, поэтому предпочтительнее асинхронность, в крайнем случае — синхронность со встроенным таймаутом.

Consistency – каждый шаг не должен противоречить граничным условиям. A(С)ID – Consistency.

Для соблюдения этого требования нужно где-то кодировать условия и проверять данные для условий (в идеале — без дополнительных обращений). Классические примеры условий для, например, отправки денег от клиента А в сервисе 1 клиенту B в сервисе 2: в результате подобной отправки денег не должно стать меньше (деньги при пересылке не должны пропасть) или больше (недопустимо отправить одни и те же деньги двум пользователям одновременно).

Требование Durability означает, что последствия операций не пропадают. ACI(D) — Durability.

Подобный фокус можно получить даже от солидных баз наподобие PostgreSQL, если там включена асинхронная репликация. В условиях Polyglot persistence сервис может работать на базе данных, которая штатно может «потерять» записанные в нее данные. Для обеспечения требования Durability требуется уметь штатно диагностировать и восстанавливать подобные потери. На иллюстрации демонстрируется, как изменения, записанные в Master, но не доехавшие в Slave по асинхронной репликации, могут быть уничтожены сгоранием сервера Master.

А где же I, спросите вы?

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

Наиболее широко известен алгоритм распределенных транзакций, обеспечиваемых так называемым двухфазным коммитом (2PC). Существует много подходов, позволяющих добиться соблюдения перечисленных выше требований. И самое серьезное: этот алгоритм не очень производителен. К сожалению, реализация двухфазных коммитов требует переписывания всех вовлеченных сервисов. Приведенные иллюстрации из недавних исследований показывают, что этот алгоритм показывает определенную производительность на распределенной базе из двух серверов, но при росте количества серверов производительность растет не линейно… А точнее, практически совсем не растет.

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

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

VLDB 2017) утверждают, что помочь может так называемый «оптимистический подход». Современные исследования (например, An Evaluation of Distributed Concurrency Control. В магазине с прилавком каждый покупатель считается подозрительным, и обслуживается с максимальным контролем. Разницу между двухфазным коммитом и обобщенным «оптимистическим подходом» можно проиллюстрировать разницей между старым советским магазином (с прилавком), и современным супермаркетом, вроде Ашана. А в супермаркете покупатель по умолчанию считается честным, ему дают возможность самому подходить к полкам и набивать тележки. Отсюда очереди и конфликты. Конечно, есть средства мониторинга для ловли жуликов (камеры, охрана), но большинству покупателей никогда не приходится с ними сталкиваться.

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

К «оптимистическому подходу» относится несколько алгоритмов. Важно. Я хотел бы рассказать вам про сагу — алгоритм поддержания распределенной целостности, рекомендуемый Крисом Ричардсоном.

Поэтому сначала я хотел бы универсально описать требуемые элементы алгоритма, чтобы описание подходило для обоих вариантов. Алгоритм саг имеет два варианта.

Т.е. Элемент 1. Надежный персистентный канал доставки событий между сервисами, гарантирующий «at least once delivery». «Персистентный» означает, что канал должен хранить извещения какое-то время (2-3 дня, неделю), чтобы сервис, потерявший последние изменения из-за потери базы (см. если шаг 2 процесса успешно завершился, то извещение (событие) об этом должно достигнуть шага 3 как минимум однажды, повторные доставки допустимы, но потеряться ничего не должно. пример про Durability, на иллюстрации это шаг 2), мог восстановить эти изменения, «перепроиграв» события из канала.

Представим, что я (пользователь) инициирую процесс покупки VIP-пакета (см. Элемент 2. Идемпотентность вызовов сервисов за счет использования уникального ключа идемпотентности. В начале процесса мне выдается уникальный ключ, ключ идемпотентности, например, 42. пример для Atomicity). В пункте выше упоминается возможность повторного прихода одинакового сообщения в сервис (в шаг). Далее вызов каждого из шагов (1→2→3→4) должен выполняться с указанием ключа идемпотентности. Т.е., если все сервисы (шаги процессов) идемпотентные, то для обеспечения требований Atomicity и Durability достаточно переотправить в шаги, соответствующие событиям из каналов. Сервис (шаг) должен автоматически уметь игнорировать повторный приход обработанного события, проверяя повторность по ключу идемпотентности. Шаги, пропустившие события, выполнят их, а шаги, уже выполнившие события, проигнорируют их из-за идемпотентности.

Элемент 3. Отменяемость вызовов сервисов (шагов) по ключу идемпотентности.

пример), если процесс с ключем идемпотентности 42, например, остановился/упал на шаге 3, то необходимо отменить успешные выполнения шагов 1 и 2 для ключа 42. Для обеспечения Atomicity (см. Реализация компенсирующих вызовов — это тяжелый, но необходимый элемент доработки сервисов в рамках внедрения алгоритма саг. Для этого каждый обязательный шаг процесса должен обладать «компенсирующим» шагом, API-методом, отменяющим выполнение обязательного шага для указанного ключа идемпотентности (42).

Перечисленные выше три элемента актуальны для обоих вариантов реализации «cаг»: оркестрируемых и хореографических.

В своей отличной статье kevteev описал алгоритм и процесс реализации механизма оркестрируемых саг в Авито. Более простой и очевидный алгоритм оркестрируемых саг проще для понимания и реализации. Этот же контролирующий сервис может обладать собственной базой данных (например, PostgreSQL), выступающей в роли надежного персистентного канала доставки событий (элемент 1). Их алгоритм предполагает существование контролирующего сервиса, «оркестрирующего» вызовы сервисов в рамках обслуживаемых бизнес-процессов.

Тут в качестве надежного персистентного канала должна выступать шина данных, реализующая следующие требования: fire-and-forget publishing, publish-subscribe event delivery, at least once delivery. С хореографической сагой хитрее. каждый шаг каждого процесса должен получать команду на срабатывание из шины, и кидать туда же сообщение об успешном выполнении, о старте следующего шага, чтобы тот тоже прочитал его из шины и продолжил выполнение процесса. Т.е. При этом на каждое сообщение может быть несколько подписчиков.

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

Сага — это не обязательно линейная цепочка событий, этом может быть направленный граф: событие регистрации нового пользователя может породить несколько шагов в параллель (отправка смс, регистрация логина, генерация пароля, отправка письма), часть из которых может являться необязательными. Один из самых важных нюансов саг, отличающих их от классических транзакций, является отход от линейности, последовательности, обязательности каждого шага. В первом приближении кажется, что в подобной «разветвленной» саге с необязательными шагами тяжело определить завершение саги (процесса), но, на самом деле, все просто: сага (процесс) завершена, когда завершены все обязательные шаги, в любом порядке.

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

Очевидный, но старомодный ответ заключается в том, что регистрация процесса должна выполняться централизованно, в сервисе саг. Кто зарегистрировал этот процесс, указал все шаги, расставил зависимости и обязательность шагов? В микросервисной архитектуре более перспективно, более производительно и более быстро регистрировать процессы «снизу вверх». Но этот ответ не очень соответствует микросервисной архитектуре. не прописывать все нюансы процесса в сервисе саг, а дать возможность отдельным сервисам самостоятельно «вписываться» в существующие процессы, указывая свою обязательность/необязательность и обязательных предшественников. Т.е.

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

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

Вот ссылка на презентацию этого материала, доклад на эту тему я делал на Highload Siberia 2018.

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

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

И довольно часто в сюжете внезапно возникают новые участники, решившие поучаствовать (прилетают драконы ;))… а кое-кто из старых участников внезапно умирает. События идут долго, доходят из разных источников, действия происходят параллельно, а не строго последовательно.

Вроде бардак и хаос, но все работает, внутренняя согласованность мира не нарушается, сюжет развивается и непротиворечив… Хотя иногда и непредсказуем. Такие дела.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

9 кругов автоматизации склада Lamoda

Наш склад размером с две Красные площади и высотой в 5 этажей работает круглый год и никогда не спит — 24/7 364 дня в году (единственный выходной — 1 января). У нас хранится и обслуживается более 8 000 000 товаров, ...

[Перевод] Каскадные SFU: улучшаем масштабируемость и качество медиа в WebRTC-приложениях

В развертывании медиасерверов для WebRTC есть две сложности: масштабирование, т.е. выход за рамки использования одного сервера и оптимизация задержек для всех пользователей конференции. В то время как простой шардинг в духе «отправить всех юзеров конференции X на сервер Y» легко ...