Привет! Меня зовут Константин Евтеев, я работаю в Авито руководителем подразделения DBA. Наша команда разрабатывает системы хранения данных Авито, помогает в выборе или выпуске баз данных и сопутствующей инфраструктуры, поддерживает Service Level Objective для серверов баз данных, а также мы отвечаем за эффективность использования и мониторинга ресурсов, консультируем по проектированию и, возможно, разрабатываем микросервисы, строго привязаны к системам хранения или услугам по разработке платформ в контексте хранения.
Я хочу рассказать вам, как мы решили одну из задач микросервисной архитектуры — проведение бизнес-транзакций в инфраструктуре сервисов, построенной с использованием паттерна Database per Service. Я выступал с докладом на эту тему на конференции Highload++ Сибирь 2018 .
Теория.
Как можно короче Я не буду подробно описывать теорию саг.
Я дам лишь краткое введение, чтобы вы поняли контекст. Как было раньше (с момента запуска Авито до 2015–2016 годов): мы жили в монолитной среде, с монолитными базами данных и монолитными приложениями.
В определенный момент эти условия стали мешать нам расти.
С одной стороны, мы ограничены в производительности сервера с основной базой данных, но это не основная причина, так как вопрос производительности можно решить, например, с помощью шардинга.
С другой стороны, у монолита очень сложная логика, и на определенном этапе роста доставка изменений (релизов) становится очень долгой и непредсказуемой: много неочевидных и сложных зависимостей (все тесно связано), тестирование тоже трудоемко, вообще проблем много.
Решение — перейти на микросервисную архитектуру.
На этом этапе у нас возник вопрос с бизнес-транзакциями, сильно завязанными на ACID, предоставляемыми монолитной базой данных: было не понятно, как мигрировать эту бизнес-логику.
При работе с Авито возникает множество различных сценариев, реализуемых несколькими сервисами, когда очень важна целостность и согласованность данных, например, покупка премиум-подписки, списание денег, применение услуг пользователю, покупка VAS-пакетов - на всякий случай.
из-за непредвиденных обстоятельств или случайностей все может неожиданно пойти не по плану.
Решение мы нашли в сагах.
Мне нравится техническое описание сага, которая была поднята в 1987 году Кеннетом Салемом и Гектором Гарсиа-Молиной, одним из нынешних членов совета директоров Oracle. Как была сформулирована задача: существует относительно небольшое количество долгоживущих транзакций, которые длительное время препятствуют выполнению мелких, менее ресурсоемких и более частых операций.
В качестве желаемого результата можно привести пример из жизни: наверняка многие из вас стояли в очереди на ксерокопирование документов, а ксерокс, если перед ним стояла задача скопировать всю книгу или просто много копий, время от времени сделал копии других членов очереди.
Но использование ресурсов — это только часть проблемы.
Ситуацию усугубляет долговременная блокировка при выполнении ресурсоемких задач, каскад которых будет встроен в вашу СУБД.
Кроме того, при длительном выполнении транзакции могут возникнуть ошибки: транзакция не завершится и начнется откат. Если транзакция была длинной, то и откат будет длиться долго, и наверняка еще будет повтор со стороны приложения.
В общем, «все довольно интересно».
Решение, предложенное в техническом документе SAGAS: разбить длинную транзакцию на части.
Мне кажется, многие подошли к этому, даже не читая этот документ. Мы уже неоднократно рассказывали о нашем defproc (отложенных процедурах, реализованных с помощью pgq).
Например, когда пользователя блокируют за мошенничество, мы быстро выполняем короткую транзакцию и отвечаем клиенту.
В этой короткой транзакции, помимо всего прочего, мы ставим задачу в очередь транзакций, а затем асинхронно, небольшими партиями, например, по десять объявлений, блокируем ее рекламу.
Мы сделали это, используя реализацию очереди транзакций из Skype .
Но наша сегодняшняя история немного другая.
Нам нужно посмотреть на эти проблемы с другой стороны: разрезать монолит на микросервисы, построенные с использованием базы данных по сервисному шаблону.
Одним из важнейших параметров для нас является достижение максимальной скорости резания.
Поэтому мы решили перенести старый функционал и всю логику как есть на микросервисы, вообще ничего не меняя.
Дополнительные требования, которые нам нужно было выполнить:
Решение в виде оркестровой саги лучше всего соответствует описанным выше требованиям.
- обеспечивать зависимые изменения данных для критически важных для бизнеса данных;
- уметь установить строгий порядок;
- поддерживать 100%-ную согласованность – сверять данные даже в случае аварий;
- гарантировать проведение транзакций на всех уровнях.
Реализация оркестрованной саги в виде сервиса PG Saga.
Вот так выглядит сервис PG Saga.PG в названии, поскольку в качестве хранилища сервисов используется синхронный PostgreSQL. Что еще внутри:
- API;
- исполнитель;
- шашка;
- проверка здоровья;
- компенсатор
У них могут быть разные складские помещения.
Как это работает
Рассмотрим на примере приобретения VAS-пакетов.VAS (Values-Added Services) — платные услуги по продвижению объявления.
Сначала служба владельца саги должна зарегистрировать создание саги в службе саги.
После этого генерирует класс саги уже с Payload.
Далее в сервисе саги исполнитель забирает из хранилища ранее созданный вызов саги и начинает пошагово его выполнять.
Первый шаг в нашем случае — покупка премиум-подписки.
В этот момент деньги зарезервированы в биллинговом сервисе.
Затем операции VAS применяются в пользовательском сервисе.
Далее работают VAS-сервисы и создаются VAS-пакеты.
Дальше возможны и другие шаги, но они для нас не столь важны.
Несчастные случаи
Несчастные случаи могут случиться в любой службе, но существуют общеизвестные приемы, как к ним подготовиться.В распределенной системе эти методы важно знать.
Например, одним из наиболее важных ограничений является то, что сеть не всегда надежна.
Подходы, которые позволят решить проблемы взаимодействия в распределенных системах:
- Давайте повторим попытку.
- Мы помечаем каждую операцию идемпотентным ключом.
Это необходимо, чтобы избежать дублирования операций.
Подробнее об идемпотентных ключах можно прочитать в этот материал.
- Компенсируем сделки — действие, характерное для саг.
Компенсация транзакции: как это работает
Для каждой положительной транзакции мы должны описать обратные действия: бизнес-сценарий шага на случай, если что-то пойдет не так.В нашей реализации мы предлагаем следующий сценарий компенсации: Если какой-то шаг саги не удался, и мы сделали много повторов, то есть вероятность, что последняя повторная попытка операции прошла успешно, но мы просто не получили ответа.
Попробуем компенсировать транзакцию, хотя в этом шаге нет необходимости, если сервис-исполнитель проблемного шага действительно сломан и полностью недоступен.
В нашем примере это будет выглядеть так:
- Отключите пакеты VAS.
- Отменяем операцию пользователя.
- Мы отменяем резервирование средств.
Что делать, если компенсация не работает
Очевидно, что нам нужно действовать примерно по такому же сценарию.Опять же используйте retry, идемпотентные ключи для компенсации транзакций, но если в этот раз ничего не работает, например, сервис недоступен, то нужно обратиться к сервису, владеющему сагой, сообщив, что сага не удалась.
Дальше идут более серьезные действия: эскалировать проблему, например, на ручное расследование или запуск автоматизации для решения таких проблем.
Что еще важно: представьте, что какой-то шаг сервиса саги недоступен.
Наверняка инициатор этих действий сделает какую-то повторную попытку.
И в результате ваш сервис саги делает первый шаг, второй шаг, а его исполнитель недоступен, вы отменяете второй шаг, отменяете первый шаг, а также могут возникнуть аномалии из-за отсутствия изоляции.
В общем, сервис саги в такой ситуации занимается бесполезной работой, которая тоже генерирует нагрузку и ошибки.
Как я должен это делать? Healthchecker должен опросить сервисы, выполняющие шаги саги, и посмотреть, работают ли они.
Если сервис стал недоступен, то есть два пути: действующие саги - для компенсации, и новые саги - либо предотвратить создание новых экземпляров (вызовов), либо создать их без принятия их в работу исполнителем.
, чтобы сервис не занимался ненужными действиями.
Еще один сценарий аварии
Представьте, что мы снова делаем ту же премиальную подписку.
- Покупаем VAS-пакеты и резервируем деньги.
- Обращаемся к пользователю услуги.
- Мы создаем VAS-пакеты.
Выглядит неплохо.
Но внезапно, когда транзакция завершается, оказывается, что пользовательский сервис использует асинхронную репликацию и на главной базе данных произошел сбой.
Причин зависания реплики может быть несколько: наличие специфической нагрузки на реплику, которая либо снижает скорость воспроизведения реплики, либо блокирует воспроизведение репликации.
Кроме того, источник (мастер) может быть перегружен, а при отправке изменений на стороне источника наблюдается задержка.
В общем, реплика почему-то отставала, и изменения на успешно пройденном шаге после краша вдруг исчезли (результат/состояние).
Для этого реализуем в системе еще один компонент — используем чекер.
Чекер проходит все этапы успешных саг через время, заведомо большее, чем все возможные лаги (например, через 12 часов), и проверяет, действительно ли они все еще успешно завершены.
Если шаг вдруг терпит неудачу, сага откатывается назад.
Также могут быть ситуации, когда через 12 часов уже нечего отменять – все меняется и движется.
В этом случае вместо сценария отмены решением может быть сигнализация службе владельца саги о том, что эта операция не удалась.
Если операция отмены невозможна, скажем, вам нужно отменить после зачисления денег пользователю, но его баланс уже равен нулю, и деньги не могут быть списаны.
Для нас такие сценарии всегда решаются в пользу пользователя.
У вас может быть другой принцип, это будет согласовано с представителями продукта.
В итоге, как вы могли заметить, в разных местах для интеграции с сервисом саги нужно реализовать очень много разной логики.
Поэтому, когда клиентские команды захотят создать сагу, перед ними встанет очень большой набор весьма неочевидных задач.
Прежде всего, мы создаем сагу, чтобы не возникало дублирования; для этого мы работаем с какой-то идемпотентной операцией создания саги и ее отслеживания.
Сервисам также необходимо реализовать возможность отслеживать каждый шаг каждой саги, чтобы, с одной стороны, она не выполнялась дважды, а с другой стороны, чтобы иметь возможность ответить, действительно ли она была завершена.
И все эти механизмы надо как-то поддерживать, чтобы хранилище сервиса не переполнялось.
Кроме того, существует множество языков, на которых можно писать сервисы, и огромный выбор репозиториев.
На каждом этапе нужно разбираться в теории и реализовывать всю эту логику на разных частях.
Если этого не сделать, то можно совершить целую кучу ошибок.
Правильных способов много, но ситуаций, когда можно «отстрелить конечность», не меньше.
Чтобы саги работали корректно, вам необходимо инкапсулировать все описанные выше механизмы в клиентские библиотеки, которые будут прозрачно реализовывать их для ваших клиентов.
Пример логики генерации саги, которую можно спрятать в клиентской библиотеке
Можно поступить по-другому, но я предлагаю следующий подход.- Получаем ID запроса, по которому мы должны создать сагу.
- Заходим в саг-сервис, получаем его уникальный идентификатор, сохраняем в локальном хранилище совместно с идентификатором запроса из шага 1.
- Запускаем сагу с полезной нагрузкой в сервис саги.
Важный нюанс: я предлагаю в качестве первого шага саги спроектировать локальные операции сервиса, создающего сагу.
- Идет своеобразная гонка, когда сервис саги может выполнить этот шаг (пункт 3), и наш бэкенд, инициирующий создание саги, тоже его выполнит. Для этого мы везде проделываем идемпотентные операции: один человек их выполняет, а второй вызов просто получит «ОК».
- Мы вызываем первый шаг (пункт 4) и только после этого отвечаем клиенту, который инициировал это действие.
Вы можете отправить запрос, и тогда соединение может прерваться, но действие будет выполнено.
Здесь примерно такой же подход.
Как все это проверить
Необходимо покрыть тестами весь провисший сервис.Скорее всего, вы внесете изменения, а написанные на старте тесты помогут избежать неожиданных сюрпризов.
Кроме того, необходимо проверять и сами саги.
Например, как мы тестируем сервис саги и тестируем последовательность саги в рамках одной транзакции.
Здесь есть разные блоки тестов.
Если мы говорим о сервисе саги, он может выполнять положительные транзакции и компенсационные транзакции; если компенсация не сработала, сервис сообщает об этом владельцу саги.
Пишем тесты в общем виде, для работы с абстрактной сагой.
С другой стороны, положительные транзакции и компенсационные транзакции на сервисах, выполняющих шаги саги, — это простой API, и за тестирование этой части отвечает команда, владеющая этим сервисом.
А затем команда владельцев саги пишет сквозные тесты, где проверяет правильность работы всей бизнес-логики при выполнении саги.
Сквозное тестирование происходит на полноценной среде разработки, поднимаются все экземпляры сервиса, включая сервис саги, и там уже тестируется бизнес-сценарий.
Общий:
- писать больше модульных тестов;
- писать интеграционные тесты;
- писать сквозные тесты.
Следующий шаг – CDC. Микросервисная архитектура влияет на специфичность тестов.
В Авито мы приняли следующий подход к тестированию микросервисной архитектуры: Consumer-Driven Contracts. Такой подход помогает, прежде всего, выявить проблемы, которые можно выявить с помощью сквозных тестов, но сквозные тесты «очень дорогие».
А есть еще один сервис, который вызывает API, то есть использует контракт — потребительский.
Потребительский сервис пишет тесты для контракта провайдера, а тесты, которые будет проверять только контракт, не являются функциональными тестами.
Нам важно убедиться, что при изменении API мы не нарушаем шаги в этом контексте.
После того, как мы написали тесты, появляется еще один элемент сервисного брокера — он регистрирует информацию о тестах CDC. Каждый раз, когда сервис-провайдер меняется, он создает изолированную среду и запускает тесты, написанные потребителем.
Что в результате: команда, генерирующая саги, пишет тесты для всех шагов саги и регистрирует их.
Фрол Крючков рассказал в РИТ++ о том, как на «Авито» реализован подход CDC для тестирования микросервисов.
Тезисы можно найти на сайте Бэкенд.конф - Рекомендую посмотреть.
Виды саг
По порядку вызова функций
а) неупорядоченные — функции саги вызываются в любом порядке и не ждут завершения друг друга; б) упорядоченные — функции саги вызываются в заданном порядке, одна за другой, следующая не вызывается, пока не завершится предыдущая; в) смешанный – для некоторых функций указан порядок, а для других нет, но указывается, до или после каких этапов они должны выполняться.Давайте рассмотрим конкретный сценарий.
В том же сценарии покупки премиум-подписки первым шагом будет резервирование денег.
Теперь мы можем вносить изменения в пользователя и параллельно создавать премиум-пакеты, а уведомления пользователю будем отправлять только после выполнения этих двух шагов.
После получения результата вызова функции
а) синхронный – результат выполнения функции известен сразу; б) асинхронный — функция сразу возвращает «ОК», а результат возвращается позже, через обратный вызов API сервиса sag от клиентского сервиса.Хочу предостеречь вас от ошибки: лучше не делать синхронных шагов саг, особенно при реализации оркестрованной саги.
Если вы выполняете синхронные шаги саги, служба саги будет ждать завершения этого шага.
Это лишняя нагрузка, лишние проблемы для сервиса саг, так как сервис один, а участников саг много.
Сага Масштабирование
Масштабирование зависит от размера планируемой системы.Рассмотрим вариант с одним экземпляром хранилища:
И только тогда, если вы заранее знаете, что будете ограничены производительностью одного сервера в СУБД, нужно делать шардинг — n экземпляров базы данных, которые будут работать со своим набором данных.
- один процессор шагов Saga, мы обрабатываем шаги пакетно;
- n обработчиков реализуем «гребенку» — делаем шаги по остатку деления: когда каждый исполнитель получает свои шаги.
- n обработчиков и пропуск заблокированных - будет еще эффективнее и гибче.
Шардинг можно скрыть за API-интерфейсом сервиса Saga.
Больше гибкости
Кроме того, в этом шаблоне, по крайней мере теоретически, клиентский сервис (выполняющий этап саги) может обращаться к сервису саги и вписываться в него, а участие в саге также может быть необязательным.Возможен и другой сценарий: если вы уже отправили письмо, компенсировать действие невозможно — вернуть письмо обратно невозможно.
Но можно отправить новое письмо, что предыдущее было неправильным, а это выглядит так себе.
Лучше использовать сценарий, в котором сага будет развиваться только вперед, без какой-либо компенсации.
Если не играет вперед, то нужно сообщить о проблеме владельцу сервиса саги.
Когда необходим замок?
Небольшое отступление о сагах вообще: если вы можете сделать свою логику без саги, то сделайте это.Саги сложны.
Примерно то же самое и с блокировкой: лучше всегда избегать блокировки.
Когда я пришел в биллинговую команду поговорить о сагах, они сказали, что им нужна блокировка.
Я смог объяснить им, почему лучше обойтись без этого и как это сделать.
Но если замок все-таки понадобится, то это следует предусмотреть заранее.
До сервиса саги мы уже реализовывали блокировки внутри одной СУБД.
Пример с defproc и сценарий асинхронной блокировки рекламы и синхронной блокировки аккаунта, когда мы сначала синхронно делаем часть операции и ставим блокировку, а потом асинхронно завершаем остальную работу пакетами в фоне.
Как это сделать? В рамках одной СУБД можно сделать некую таблицу, в которой вы будете сохранять записи о блокировке, а затем в триггере при выполнении операций над объектом этой блокировки заглядывать в эту таблицу, и если кто-то попытается изменить ее в ходе блокировка, генерирование исключений.
Примерно то же самое можно сделать и в сервисе Саги.
Главное – поддерживать порядок.
Предлагаю следующий подход: сначала делаем блокировку в сервисе саги, если хотим реализовать сагу с блокировкой, а затем опускаем ее на клиентский сервис, используя подход, описанный выше.
Можно сделать по-другому, но важно, чтобы порядок был правильный.
И нужно понимать, что если у вас есть блокировки, то будут и тупики.
Если появляются тупики, то нужно сделать детектор тупиков.
Блоки также могут быть эксклюзивными или общими.
А вот многоуровневую блокировку планировать не рекомендую — это достаточно сложная история, и сервис должен быть простым, ведь это единственная точка отказа всех ваших транзакций.
КИСЛОТА – без изоляции
У нас есть атомарность, потому что все шаги либо выполняются, либо отменяются.Существует согласованность благодаря службе саги и локальному хранилищу в службе саги.
И устойчивость — благодаря локальному хранению и механизмам долговечности.
У нас нет изоляции.
В отсутствие изоляции мы будем испытывать различные аномалии.
Они произойдут, когда мы можем потерять обновления.
Вы прочитаете какие-то данные, затем кто-то другой что-то напишет, а ваша исходная транзакция примет и перезапишет эти изменения.
Может быть, происходит грязный чтение - когда вы находитесь в процессе проведения какой-то саги, вы сделали одно дело, записали, кто-то уже прочитал эти изменения, но ваша сага еще не закончена.
Вы записываете еще раз, что-то меняете, а кто-то другой читает не те состояния.
Случаться неповторяющийся чтения - когда в течение одной и той же саги вы будете получать разные состояния вашего объекта.
Как этого избежать:
- Работайте с версией объекта, сохраняйте определенную версию, например у пользователя, и увеличивайте ее при каждом изменении.
- Убедитесь, что вы все еще работаете с ним.
Или посмотрите состояние, которое вы хотите изменить, например, статус, и убедитесь, что вы применяете его к тому самому статусу, который вы ранее хотели изменить.
- Вы можете создавать блокировки и сериализовать все изменения вокруг основного объекта саги.
- Передавайте в полезную нагрузку саги только события и не работайте с состоянием.
Это история о конечной согласованности — если вы передаете состояние объявления пользовательскому сервису, оно может уже измениться к моменту, когда событие достигнет получателя.
Необходимо передать информацию о том, что произошло, например, регистрация пользователя или мы применили к пользователю премиум-сервис.
Мониторинг
Необходимо следить за выполнением саг с разбивкой по всем шагам и по всем статусам.Мы собираем всю телеметрию, в том числе информацию о том, сколько времени занимает выполнение каждого этапа саги, а также сами саги.
То же самое нам следует рассмотреть и в отношении компенсационных транзакций.
Также не забывайте о чекере.
Также было бы неплохо охватить провисающий сервис метриками на каждом этапе.
Вот примеры диаграмм, которые мы собираем.
Прежде всего, мы смотрим на процентили (50%, 75%, 95%, 99%), потому что это первое, что вы узнаете, если что-то пошло не так.
Как определить место поломки, если сага сломана — как я уже говорил, собираем метрики с разбивкой по шагам и далее.
Мы можем поставить оповещения обо всех этих шагах саги.
Если определенные этапы саги накапливаются, значит, что-то пошло не так.
Но возможно, что сага вообще еще не сломалась - просто произошел всплеск нагрузки в одном из сервисов исполнителей саги.
Другая ситуация.
Как определить, что какой-то шаг саги (сервис вышел из строя) вообще не работает. В этом случае HealthChecker проверяет все конечные точки информационных (keep-alive) клиентских служб.
Ну и третий пример.
Возможна авария из бизнес-сценария.
Ответственность бизнес-сценария за правильное выполнение вашей бизнес-транзакции полностью лежит на команде владельцев саги и командах владельцев сервисов, выполняющих этапы саги.
В этой ситуации владелец одиночной саги, когда он ее проектирует, должен покрывать ее тестами, в том числе сквозными.
Далее нам нужен мониторинг различных бизнес-показателей саги.
Команда, создавшая эту сагу, должна отслеживать метрики — это ее зона ответственности.
Также мы тщательно следим за самим сервисом Saga. Также было бы неплохо реализовать автоматическое аварийное переключение для локального хранилища сервиса саги.
На что следует обратить внимание:
Избегайте паразитных нагрузок
Выше я уже говорил, что нужно построить Healthchecker, и если какая-то нода выйдет из строя, нужно прекратить выполнение этих саг.Потому что сервис один, а клиентов много.
Вы просто перегрузите свой сервис саги без необходимости.
Избегайте сложной логики и избыточных функций в сервисе провисания.
Как только вы втянетесь в эту историю, сервис Saga станет самой важной точкой вашей инфраструктуры.
Если он откажется, последствия могут быть точно такими же, как и тот функционал, который вы на него заложили.
И мы хотим прикрепить к нему самый критический функционал.
Поэтому схема провисания хореографии выглядит более выгодной — сервис провисания включается только тогда, когда что-то идет не так.
В общем, даже если у вас сломается служба саги в шаблоне хореографии, у вас все продолжит работать.
Служба саги в хореографии критически необходима, например, при выполнении отката.
Если мы создаем организованную сагу, то если сервис саги выйдет из строя, все потерпит неудачу.
Соответственно, чем меньше логики вы в него ввернете, тем проще и быстрее он будет работать, и тем надежнее будет вся система.
Интеграция с клиентами
Обучите все свои команды работе с сервисом Sag. Этот пласт теории необходимо прочитать, так как не всем может быть очевидно, как правильно работать с персистентными системами в контексте саг.Подумайте, как сделать удобной работу с локальным хранилищем + работу с разными языками в контексте саг и как все это скрыть в клиентских библиотеках.
Управление версиями API
В нашей реализации, когда мы хотим что-то изменить в службе исполнителя шагов клиента (новая версия API службы), мы создаем новую сагу, в которой используем новую версию API. После этого переносим всех на новую сагу и старую сагу можно удалить, а затем и старый метод API. Здесь нужно быть осторожным — изменения также могут неявно повлиять на логику компенсации.В том числе перед удалением старых классов саги и методов API. Теги: #postgresql #Системный анализ и проектирование #проектирование и рефакторинг #микросервисы #сага #распределенные системы #распределенные транзакции
-
Вот Оно, Наше Лето: Гаджеты Для Отдыха
19 Oct, 24 -
Патч Прошивки Android За 5 Минут
19 Oct, 24 -
Обзор Материалов Esun Для 3D-Печати
19 Oct, 24 -
1-Й Конкурс Инновационных It-Проектов
19 Oct, 24