Привет, Хабр!

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



Время от времени я задаюсь одним и тем же вопросом.
Есть ли такая важная истина, в которой с вами соглашаются лишь немногие??—?Питер Тиль
Прежде, чем сесть за этот пост, я долго примерял этот вопрос к одной теме, которая сегодня в серьезном тренде – речь о микросервисах. Думаю, теперь мне есть о чем рассказать; некоторые находки основаны на размышлениях, другие – на практическом опыте. Итак, рассказываю.
Начнем с одной важной истины, которая послужит нам в этом пути ориентиром как полярная звезда.

Большинство реализаций микросервисов – ни что иное как распределенные монолиты.

Эра монолитов


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



Типичная архитектура монолитного приложения

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

  • Полностью понимаете вашу модель данных;
  • Можете согласованно оперировать данными (предполагается, что ваша база данных правильно подобрана для вашего прикладного случая).

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

Когда этот момент наступает, ваша система, скорее всего, переходит в новую ипостась: превращается в распределенный монолит.

Эра распределенных монолитов


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

Всерьез взявшись за снос монолита, вы, в частности, попробуете реализовать два небольших сервиса, один из которых будет обеспечивать отчетность, а второй – биллинг. Вероятно, эти новые сервисы будут предоставлять 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, но такой подход в конечном итоге приводит к постоянному усложнению самого сервиса.

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

Комментарии (20)


  1. algotrader2013
    26.05.2019 09:56
    +3

    Как-то ну слишком уж для детей… Вроде ж прописная истина всех апологетов микросервисов, что в одну базу ходит 1 сервис. И очевидно, что архитектора, которому пришло в голову на постоянной лазить централизированно в базы всех микросервисов, надо гнать сс… ми тряпками. Ведь, во первых, команды не отвечают ни перед кем за схему своей базы (но это проблема команды, делающей ETL, и это описано в статье), а во-вторых, даже безобидное неблокирующее чтение может весьма неожиданно влиять на перфоманс и стабильность базы (я этого говна наелся немало: из недавнего, одна из утилит по репликации базы монго в постгрес, работающая с монго только на чтение, может укладывать кластер монго из-за бага; если через pyodbc подключится к MSSQL без autocommit=True, то сама собой открывается транзакция, и куча других приколов), и никакие автотесты и стресс тесты не помогут, ведь разработчики микросервиса, описывая тест кейсы, и подумать не могли, что какие-то уроды из вне будут непредсказуемую нагрузку создавать на базу.

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


    1. epishman
      27.05.2019 23:16

      Тогда вопрос — а зачем нам такие данные, которые нельзя использовать помимо микросервиса? Жизнь требует обратного. Горячее зеркало есть у MS SQL Ent, у Оракла тоже, и проблема чтения решается. Log Shipping тоже есть.


  1. NoRegrets
    26.05.2019 10:07

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


    1. epishman
      27.05.2019 23:17

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


  1. gfarniev
    26.05.2019 10:58

    Очень интересная статья, спасибо. Кстати, в mongodb слушание событий можно эффективно реализовать через Change Stream. Какие ещё есть практические способы? Хотелось бы второй части, которая была бы ближе к практике. Чтобы узнать как эффективно реализовать такие паттерны.


    1. ipodman
      26.05.2019 21:56

      Брокер сообщений kafka/rabbit/activeMq


  1. epishman
    26.05.2019 15:51

    Похоже, производители СУБД еще лет 50 назад осознали проблему, и изобрели журнал транзакций. Проблема — насколько удобен такой журнал для непосредственного построения отчетности, и что дешевле — строить отчетность простым перебором событий, или сначала развернуть журнал в нормализованную структуру данных, и уже затем использовать например SQL. Частично эту проблему я поднимал здесь, но никто пока не заинтересовался.


    1. algotrader2013
      28.05.2019 01:08
      +1

      С интересом почитал по ссылке. К сожалению, комментировать там не могу по сроку давности)


      Но, мне кажется, что MSSQL с CCI на хорошей железке с достаточным объемом RAM и ядер (и ручным указанием maxdop) решает процентов 90 описанных проблем. Возможно, clickhouse тоже, но у меня он споткнулся на весьма простых запросах типа join таблицы на саму себя по условию, перейдя в однопоточный просчет, что совсем уж печально (возможно, я дзен не познал, или руки кривые). И, предполагаю, что Google big query тоже решает проблему, но я исторически скептик облаков)


      Касательно M$:
      1) очень хитрожопые группировки выполняются на таблице размером 2млрд?60 за пару минут. Если через where отсекаешь партиции, пропорционально быстрее.
      2) новые столбцы добавляются мгновенно
      3) на время запроса влияют только столбцы, которые в нем участвуют
      4) распараллеливание реально работает
      5) если работать в append-only режиме, то тысячи TPS тянет.
      6) пролицензировать все ядра для мощного сервера лицензией по ядрам ОЧЕНЬ дорого, а лицензия не по ядрам позволяет задействовать 20 логических ядер, что неактуально для современного железа.


      Поэтому, берется делается большая колоночная партиционированная таблица. На каждый чих добавляется по колонке. Любые коррекции пишутся, как +1 строка с дельтой, и, соответственно, почти любое чтение содержит group by id. Ну как-то так)


      1. epishman
        28.05.2019 01:29

        Да, РСУБД удовольствие дорогое, а всякие ERP используют лишь подмножество функций, в результате чего колонку на горячее не добавишь, хинтов нет, сбор статистики и репликация сильно ухудшают производительность прода, и так далее. А самое главное — я не понял как распараллелить эту схему хранения, где все жестко связано со всем. В начале своей трудовой деятельности застал биллинговую систему на мейнфрейме IBM, там даже таблиц не было, просто текстовые звонковые файлы. Справочники затягивались в память в хэшмапы, и цикл по звонкам. А потом пришли гиганты, подсадили всю отрасль на дорогие технологии, а сами в том же гугле SQL небось не используют. Для меня сейчас SQL это игрушка типа 1С, а серьезный хайлоад с миллиардами записей (был у меня интернет-биллинг давно) — все равно фулскан как ни крути.
        PS
        > Поэтому, берется делается большая колоночная партиционированная таблица. На каждый чих добавляется по колонке
        =======================
        Кстати, да, но коррекции писать лучше в отдельную таблицу, ибо оптимизаторы не любят само-джойн до сих пор, да и на мутированность постоянно нарываешься.


        1. algotrader2013
          28.05.2019 09:47

          Так а чем вызвано неприятие связки full scan + SQL?


          1. epishman
            28.05.2019 10:00

            Если full scan, то зачем SQL, с таким успехом данные можно положить в документную базу.


            1. algotrader2013
              28.05.2019 10:47

              SQL это лишь способ формально описать преобразование данных. И он реально хорош, иначе бы люди не использовали SQL движки поверх hadoop, и не делали бы его поддержку в clickhouse. Если движок умеет качественно разложить запрос в мап редьюс хотя бы на все ядра одного сервера (а указанные мною MS, CH, BQ это делают весьма неплохо), то что еще нужно?

              И да, из документных только монго приходит на ум, и делать на нем аналитику — это для любителей бдсм) Да и вообще, из реального опыта, мерзкая вещь для почти всех сценариев кроме 2-3 специфических.


              1. epishman
                28.05.2019 10:55

                Спор же не по поводу SQL, а по поводу формата данных. Автор статьи предлагает хранить данные в виде журнала событий. Его можно обработать мэп/редъюсом, но при чем тут SQL тогда?


                1. algotrader2013
                  28.05.2019 10:59

                  Ну ок, значит мы поняли друг друга)


  1. powerman
    26.05.2019 16:09
    +1

    Описанное — это только первый шаг. Вы уже поняли, почему плохо лазить напрямую в БД другого сервиса — это хорошо.


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


    Потом вы осознаете, почему дублировать полностью все изменения БД одного сервиса в публичный лог событий предназначенный для других сервисов — плохо, и сделаете следующий шаг: будете стараться включать в события только тип события и ID связанной с событием сущности, добавив каждому сервису API, позволяющее получить практически сырые данные из его БД по этому ID.


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


    Уже много-много раз говорилось: микросервисная архитектура просто переносит часть сложности из кода сервисов на архитектора, чем сильно повышает требования к квалификации архитектора. Если у вас пока нет действительно сильного архитектора, с серьёзным опытом разработки микросервисных архитектур, то лучше бы вам не мучить свой проект, а постараться вернуть его в состояние монолита, и для масштабирования использовать более традиционные для монолитов подходы (шардинг, вынесение тяжёлых операций во внешние сервисы-воркеры, etc.).


    В качестве хорошего примера, как надо подходить к разработке микросервисной архитектуры, посмотрите на ютубе выступления Udi Dahan. Например (правда, это не для начинающих), очень показательный кейс с медициной, как иллюстрацию ситуации в которой традиционные подходы не работают, и как при этом приходится выкручиваться: Finding Service Boundaries – illustrated in healthcare by Udi Dahan.


    1. JeStasG
      27.05.2019 09:14

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


      1. powerman
        27.05.2019 10:06

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


        Бизнес-аналитику не важно, используется ли монолит или микросервисы. Кроме того, даже если архитектор видит "полную картину", это не сильно облегчает ему жизнь — требования постоянно меняются, и эта "полная картина" через месяц-два может сильно мутировать, так что требовать от бизнес-аналитика супер точную и полную картину менее полезно, чем быть готовым адаптировать архитектуру под новые требования.


        1. JeStasG
          27.05.2019 12:28
          +1

          требовать от бизнес-аналитика супер точную и полную картину менее полезно

          Команда либо есть, либо ее нет, а от нее зависит очень многое. На одном мастерстве архитектора в прямой постановке вопроса, далеко не уедешь, максимум плавание по течению.


          1. powerman
            27.05.2019 13:43

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


            1. epishman
              27.05.2019 23:20

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