Привет!
Сегодняшний перевод затрагивает не только и не столько микросервисы — тему, которая сегодня у всех на устах — но и напоминает, как важно называть вещи своими именами. Переход на микросервисную архитектуру бывает необходим, но, как лишний раз подчеркивает автор, требует тщательно просчитывать последствия. Приятного и плодотворного чтения!
Время от времени я задаюсь одним и тем же вопросом.
Есть ли такая важная истина, в которой с вами соглашаются лишь немногие? — Питер Тиль
Прежде, чем сесть за этот пост, я долго примерял этот вопрос к одной теме, которая сегодня в серьезном тренде – речь о микросервисах. Думаю, теперь мне есть о чем рассказать; некоторые находки основаны на размышлениях, другие – на практическом опыте. Итак, рассказываю.
Начнем с одной важной истины, которая послужит нам в этом пути ориентиром как полярная звезда.
Большинство реализаций микросервисов – ни что иное как распределенные монолиты.
Эра монолитов
Любая система начиналась как монолитное приложение. Не буду вдаваться здесь в подробности этой темы – о не уже писали многие и много. Однако, львиная доля информации о монолитах посвящена таким вопросам как производительность разработчика и масштабируемость, оставляя при этом за скобками наиболее ценный актив любой Интернет-компании: данные.
Типичная архитектура монолитного приложения
Если данные настолько важны, то почему же все внимание уделяется любым другим темам, но только не им? Ответ, в общем, прост: поскольку они не так болезненны, как вопрос данных.
Пожалуй, монолит – единственный этап в жизненном цикле системы, на котором вы:
- Полностью понимаете вашу модель данных;
- Можете согласованно оперировать данными (предполагается, что ваша база данных правильно подобрана для вашего прикладного случая).
С точки зрения данных монолит идеален. А поскольку данные – самый ценный актив любой компании, лучше монолит не ломать, если только у вас нет на этой очень веской причины, либо совокупности таких причин. В большинстве случаев решающей причиной такого рода становится необходимость масштабироваться (поскольку мы живем в реальном мире с присущими ему физическими ограничениями).
Когда этот момент наступает, ваша система, скорее всего, переходит в новую ипостась: превращается в распределенный монолит.
Эра распределенных монолитов
Допустим, дела в вашей компании идут хорошо, и приложению необходимо развиваться. У вас появляются все более крупные клиенты, а ваши требования к биллингу и отчетности изменились как по набору возможностей, так и по их объему.
Всерьез взявшись за снос монолита, вы, в частности, попробуете реализовать два небольших сервиса, один из которых будет обеспечивать отчетность, а второй – биллинг. Вероятно, эти новые сервисы будут предоставлять HTTP API и иметь выделенную базу данных для долговременного хранения состояния. После множества коммитов у вас, как и у нас в Unbabel, может получиться нечто, напоминающее следующую иллюстрацию.
Обобщенный вид системной архитектуры после открепления сервисов биллинга и отчетности от основного монолитного приложения
Все идет по плану.
- Команда продолжает дробить монолит на более мелкие системы;
- Конвейеры непрерывной интеграции/доставки работают как часы;
- Кластер Kubernetes здоров, инженеры работают продуктивно и всем довольны.
Жизнь прекрасна.
Но что, если я скажу, что прямо сейчас против вас плетутся гнусные заговоры?
Теперь, присмотревшись к вашей системе, вы обнаружите, что данные оказались распределены по множеству разных систем. Вы начинали с этапа, когда у вас была уникальная база данных, где хранились все объекты данных, а теперь ваши объекты данных распространились по разным местам. Возможно, подумаете вы, в этом нет никакой проблемы, поскольку микросервисы для того и нужны, чтобы создавать абстракции и запечатывать данные, скрывая внутреннюю сложность системы.
Вы совершенно правы. Но с увеличением масштабов возникают и более сложные проблемы: теперь в любой момент времени вы вынуждены выполнять бизнес-требования (например, отслеживать некоторую метрику), требующие обращаться к данным, расположенным более чем в одной системе.
Что же делать? На самом деле, вариантов много. Но вы торопитесь, вам же нужно обслужить огромную братию клиентов, которые у вас недавно зарегистрировались, поэтому приходится искать баланс между «быстро» и «хорошо». Обсудив детали, вы решаете соорудить дополнительную систему, которая выполняла бы определенную ETL-работу, способствующую решению конечных задач. Эта система должна будет обладать доступом ко всем репликам на считывание, в которых содержится нужная вам информация. На следующем рисунке показано, как могла бы работать такая система.
Обобщенный пример аналитической ETL-системы (у нас в Unbabel мы назвали ее Automatic Translation Analytics)
В Unbabel мы воспользовались именно таким подходом, так как:
- Он не слишком сильно влияет на производительность каждого микросервиса;
- Он не требует серьезных инфраструктурных изменений (просто добавляем новый микросервис);
- Мы смогли достаточно оперативно удовлетворить наши бизнес-требования.
Опыт подсказывает, что в течение некоторого времени такой подход будет работоспособен – до достижения определенных масштабов. В Unbabel он служил нам весьма хорошо до самого недавнего времени, когда мы начали сталкиваться со все более серьезными вызовами. Вот некоторые вещи, превращавшиеся для нас в головную боль:
1. Изменения данных
Одно из основных достоинств микросервисов – инкапсуляция. Внутреннее представление данных может меняться, а клиентов системы это не затрагивает, поскольку они общаются через внешний API. Однако, наша стратегия требовала непосредственного доступа к внутреннему представлению данных, и поэтому, стоило команде лишь внести какие-то изменения в представление данных (например, переименовать поле или изменить тип с text
на uuid
), нам приходилось также менять и заново развертывать наш ETL-сервис.
2. Необходимость обрабатывать множество разных схем данных
По мере того, как увеличивалось количество систем, к которым нам требовалось подключаться, приходилось иметь дело со все более многочисленными неоднородными способами представления данных. Было очевидно, что мы не сможем масштабировать все эти схемы, взаимосвязи между ними и их представления.
Корень всех зол
Чтобы получить полное представление о происходящем в системе, нам пришлось остановиться на подходе, напоминающем монолит. Вся разница заключалась в том, что у нас была не одна система и одна база данных, а десятки таких пар, каждая – с собственным представлением данных; более того, в некоторых случаях по нескольким системам реплицировались одни и те же данные.
Такую систему я предпочитаю называть распределенным монолитом. Почему? Так как она совершенно не приспособлена для отслеживания изменений в системе, и единственный способ вывести состояние системы – собрать сервис, подключающийся непосредственно к хранилищам данных всех микросервисов. Интересно посмотреть, как многие колоссы Интернета также сталкивались с подобными вызовами в какой-то момент своего развития. Хороший пример в данном случае, который мне всегда нравится приводить – сеть Linkedin.
Именно такую мешанину данных представляли собой информационные потоки Linkedin по состоянию примерно на 2011 год — источник
В данный момент вы, возможно, задумываетесь: «что же вы, ребята, собираетесь со всем этим делать?» Ответ прост: необходимо приступать к отслеживанию изменений и вести учет важных действий по мере того, как они будут совершаться.
Разбиваем распределенный монолит при помощи регистрации событий (Event Sourcing)
Как и практически во всем остальном мире, в Интернете системы работают, реагируя на действия. Так, запрос к API может привести к вставке новой записи в базу данных. В настоящее время такие детали нас в большинстве случаев не волнуют, так как нас интересует, в первую очередь, обновление состояния базы данных. Обновление состояния базы данных – это обусловленное следствие некоторого события (в данном случае – запроса к API). Феномен события прост и, тем не менее, потенциал событий очень велик – ими даже можно воспользоваться для разрушения распределенного монолита.
Событие – ни что иное, как неизменный факт некой модификации, произошедшей в вашей системе. В микросервисной архитектуре события приобретают критическую важность и помогают осмыслить потоки данных, а на их основе – вывести совокупное состояние нескольких систем. Каждый микросервис, совершающий действие, интересное с точки зрения всей системы, должен порождать событие вместе со всей существенной информацией, относящейся к тому факту, который это событие представляет.
Возможно, у вас возникает вопрос:
“Как же микросервисы, порождающие события, помогут мне с решением проблемы распределенного монолита?”
Если у вас есть системы, порождающие события, то может быть и лог фактов, обладающий следующими свойствами:
- Отсутствие привязки к какому-либо хранилищу данных: события обычно сериализуются с использованием двоичных форматов, таких как JSON, Avro или Protobufs;
- Неизменяемость: как только событие порождено, изменить его невозможно;
- Воспроизводимость: состояние системы в любой заданный момент времени может быть восстановлено; для этого достаточно «переиграть» лог событий.
При помощи такого лога можно выводить состояние, пользуясь логикой любого типа на уровне приложения. Вы больше не связаны ни с каким набором микросервисов и N способов, какими в них представлены данные. Единый источник истины и ваше единственное хранилище данных – это теперь тот репозиторий, в котором хранятся ваши события.
Вот несколько причин, по которым лог событий кажется мне тем средством, которое помогает разбить Распределенный Монолит:
1. Единый источник истины
Вместо поддержания N источников данных, которые могут понадобиться для соединения с (многочисленными) разнотипными базами данных, в данном новом сценарии истина в последней инстанции хранится ровно в одном хранилище: логе событий.
2. Универсальный формат данных
В предыдущем варианте системы нам приходилось иметь дело со множеством форматов данных, так как мы были непосредственно связаны с базой данных. В новой компоновке мы можем действовать гораздо более гибко.
Допустим, вам понравилась фотография из Instagram, которую опубликовал кто-то из ваших друзей. Такое действие можно описать: “Пользователю X понравился снимок P”. А вот событие, представляющее этот факт:
Событие, соответствующее подходу AVO (Actor, Verb, Object), моделирующий факт выбора пользователем понравившегося снимка.
3. Ослабление связи между производителями и потребителями
Последний немаловажный момент: одно из величайших преимуществ операций с событиями – эффективное ослабление связи между производителями и потребителями данных. Такая ситуация не только упрощает масштабирование, но и снижает количество зависимостей между ними. Единственный контракт, остающийся между системами в данном случае – это схема событий.
________________________________________
В начале этой статьи был поставлен вопрос: Есть ли такая важная истина, в которой с вами соглашаются лишь немногие?
Позвольте вернуться к нему и в заключении этого экскурса. Полагаю, большинство компаний не считают данные «сущностями первого класса», когда приступают к миграции на микросервисную архитектуру. Утверждается, что все изменения данных по-прежнему можно будет проводить через API, но такой подход в конечном итоге приводит к постоянному усложнению самого сервиса.
Я считаю, что единственный верный подход к захвату изменений данных в микросервисной архитектуре – заставить системы порождать события согласно строго определенному контракту. Имея правильно составленный лог событий, можно выводить множества данных, исходя из любого множества бизнес-требований. В таком случае просто придется применять разные правила к одним и тем же фактам. В некоторых случаях такой фрагментации данных можно избежать, если ваша компания (в особенности – ваши продукт-менеджеры) трактует данные как продукт. Однако, это уже тема для другой статьи.
Автор: ph_piter