Не успели вы оглянуться, как ваш продукт разросся и стал сложным и неповоротливым. Развиваться прежними темпами становится всё труднее. Значит, пришло время меняться, нужен новый подход к работе. Микросервисы вас ускорят, хоть и придется потрудиться в процессе.

Разрабатывая микросервисы для Enchant, я старался следовать практичным подходам, которые хорошо сочетаются с современными веб-технологиями и облачными сервисами. Я брал пример с тех компаний, которые уже прошли по этому пути (а это, например, Netflix, Soundcloud, Google, Amazon и Spotify) чтобы не повторять чужих ошибок.

TL;DR

Архитектура микросервисов подходит сложным структурам. С её помощью единая многокомпонентная система превращается во множество простых взаимодействующих между собой сервисов. Основная задача – не давать системе чрезмерно усложняться.

Платформа

Основные вопросы

Взаимодействие между сервисами

Разработка

Развертывание

Операции

Люди

Ключевые требования

Архитектура микросервисов вносит свои сложности. Раньше вы работали с парой-тройкой систем, а теперь их стало много. Везде сплошные логи. Когда сервисы разрознены, трудно достичь согласованности. Список проблем можно продолжать бесконечно. Наша задача – прийти в состояние упрощенной сложности, то есть признать, что сложность неизбежна, но иметь под рукой инструменты и процессы, чтобы её контролировать.

Стремитесь выполнить следующие ключевые требования:

  • Предоставьте командам максимальную независимость: создайте среду, в которой команды будут добиваться больших результатов без необходимости координироваться друг с другом.
  • Оптимизируйте скорость разработки: оборудование стоит дешево, а люди – нет. Мотивируйте команды легко и быстро создавать мощные сервисы.
  • Сосредоточьтесь на автоматизации: люди ошибаются. И если разрабатываемых систем становится больше, растёт и вероятность ошибок. Автоматизируйте всё.
  • Обеспечивайте гибкость без ущерба согласованности: предоставьте командам свободу делать то, что нужно для их сервисов, придерживаясь при этом типовых строительных блоков, необходимых для нормальной работы в долгосрочной перспективе.
  • Стремитесь к устойчивости: системы ломаются по массе причин. А если система распределённая, сценариев сбоя ещё больше. Примите меры, чтобы минимизировать негативные последствия.
  • Упрощайте обслуживание: у вас будет не одна база кода, а много. Обеспечьте разработчиков руководствами и инструментами, чтобы их работа была согласованной.

Платформа

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

Платформа – это набор стандартов плюс инструменты, которые упрощают создание и эксплуатацию сервисов, соответствующих стандартам.

Платформе нужен уровень управления

Как команды будут взаимодействовать с платформой? Традиционно существует много разных веб-интерфейсов для непрерывной интеграции, мониторинга, ведения логов и документации. Чтобы выполнять эти задачи, командам в качестве отправного пункта понадобится панель инструментов. Это может быть очень простой элемент, где перечислены все сервисы и даны ссылки на различные внутренние инструменты. Предпочтительно, чтобы панель собирала данные с внутренних инструментов и сразу выдавала выводила дополнительные значения для быстрой оценки.

Разработчики, которые пользуются общими чатами, часто получают общие задачи прямо в интерфейс чата с помощью настраиваемого бота. Такие боты могут пригодиться, чтобы запускать тесты, начинать развертывание или запрашивать статистику про работающий сервис. Логи чата при этом помогают отслеживать прошлые действия.

Основные вопросы

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

Независимая разработка и развёртывание

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

Если вам приходится развертывать сервисы совместно, значит, вы что-то делаете не так.

Если у вас единая база кода для всех сервисов, значит, вы что-то делаете не так.

Если вам приходится рассылать предупреждение, прежде чем развернуть новый сервис, значит, вы что-то делаете не так.

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

Владение данными

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

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

Нужно ли заводить отдельный сервер данных для каждого сервиса?

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

Однако, у общего сервера данных есть свои слабые места. Во-первых, это уязвимое звено, сбой в котором может привести к поломке нескольких сервисов. Это серьезный риск. И во-вторых, в такой ситуации один сервис может непроизвольно воздействовать на другие, забирая слишком много ресурсов.

Определение границ сервисов

Где проходят границы сервисов? Это сложный для понимания момент. Каждый сервис должен быть автономной единицей, которая реализует рабочие задачи.

Сервис должен быть слабо связан с другими. Он должен минимально зависеть от остальных сервисов. Любая коммуникация с другими сервисами должна осуществляться через открытые общедоступные интерфейсы (API, события и т.д.). Эти интерфейсы должны быть разработаны таким образом, чтобы не раскрывать внутренние детали.

Сервис должен обладать высокой внутренней связанностью. Тесносвязанные функции должны находиться в границах одного сервиса. Это сводит к минимуму коммуникацию между сервисами.

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

При идеальном раскладе вы с первого раза справитесь с определением естественным границ сервисов, потому что понимаете, как работает ваш продукт и бизнес. Даже если вы ошибётесь, благодаря слабой связанности сервисов в будущем их будет проще реорганизовать (объединить, разделить или поменять их структуру).

Подождите, а что насчет общих моделей?

Давайте подробнее разберем ограниченные контексты. Постарайтесь не создавать «тупые» сервисы CRUD. Это не приведет ни к чему хорошему: сервис будет сильно зависеть от других и обладать низкой внутренней связанностью. Проблемно-ориентированное проектирование вводит принцип ограниченного контекста, позволяющий правильно определить границы сервисов. Ограниченный контекст инкапсулирует связанные части предметной области (в нашем случае в сервис). Несколько ограниченных контекстов обмениваются данными через строго определенные интерфейсы (в нашем случае – API). Некоторые модели полностью входят в один ограниченный контекст, а другие разбросаны по нескольким контекстам в зависимости от сценария использования (и связанных атрибутов). В последнем случае каждый ограниченный контекст должен обладать атрибутами, присущими модели.

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

  • Хранить статистику в сервисе заявок, потому что в конечном итоге именно этот сервис отвечает за модели и жизненный цикл заявок. При таком подходе сервису отчетов придётся обращаться к сервису заявок всякий раз, когда ему требовались данные. Такие сервисы тесно связаны и постоянно обмениваются данными.
  • Хранить статистику в сервисе отчетов, потому что этот сервис занимается статистическим данными. В этом случае у обоих сервисов есть модель заявок, но сохраняют они разные атрибуты. Данные хранятся там, где они используются. Хранение статистических данных можно оптимизировать для целей отчетности Однако теперь сервис отчетов нужно уведомлять, когда создается новая заявка или в существующие заявки вносят изменения.

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

Насколько большим может быть сервис?

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

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

Экземпляры сервисов без состояния

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

При этом многим сервисам всё-таки нужно сохранять какие-то данные. Их следует отправлять во внешние сервисы, такие как серверы, ограниченные размером диска, или ограниченный памятью кэш.

Конечная согласованность

Вне зависимости от того, как подходить к этой проблеме, в распределённой системе сложно обеспечить согласованность. И лучший вариант для такой системы – прийти к согласованности в конечном счете. То есть у сервисов может быть разное представление о данных в процессе, но в конце они придут к согласованному взгляду.

Если вы правильно смоделировали сервисы (то есть обеспечили низкую зависимость друг от друга и высокую внутреннюю связанность), вы обнаружите, что конечная согласованность по умолчанию подходит для многих рабочих сценариев. Кроме того, распределённые системы, которые достигают согласованности только в конце, отвечают ключевому требованию низкой зависимости. Такие сервисы обычно обмениваются данными асинхронно и защищаются от сбоев в следующих сервисах.

Рассмотрим пример. У Enchant есть сервисы заявок (отвечает за запросы службы поддержки) и отчетов (ведет статистику заявок). Сервис отчетов получает асинхронные обновления от сервиса заявок. Это значит, что всякий раз, когда в сервисе заявок происходит обновление, сервис отчетов узнает об этом только несколько секунд спустя. В течение этих нескольких секунд у сервисов разное представление о ключевых запросах клиентов. Для сбора статистики такое отставание допустимо. Дополнительное преимущество такого подхода в том, что он защищает сервис заявок от сбоев в сервисе отчетов.

Асинхронные процессы

Освоив конечную согласованность, вы обнаружите, что не все задачи должны быть выполнены, пока запрос заблокирован в ожидании ответа. Все, что может подождать (и отнимет много времени или ресурсов), должно быть делегировано асинхронным процессам.

Такой подход:

  1. Ускоряет путь основного запроса, потому что выполняет только часть всей работы, которая должна быть сделана по этому запросу.
  2. Распределяет нагрузку на процессы, работы которых легко масштабировать. Идеально подходит для автоматического масштабирования, когда число процессов динамически меняется в зависимости от объемов работы.
  3. Сокращает число сценариев, при которых выдается ошибка в API основного сервиса. Если происходит сбой работы асинхронных процессов, их можно незаметно перезапустить, не замедляя запрашивающий сервис.

Идемпотентность

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

Документация

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

Что делать, если меняется API?

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

Балансировщики нагрузки

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

Традиционно балансировщики нагрузки располагаются в точке, куда поступает запрос от клиента. При этом клиент видит только один сервер для отправки запроса. Балансировщик получает запрос и распределяет его среди множества скрытых внутренних экземпляров сервиса. Альтернативный подход – расположить балансировщик на стороне клиента, как Netflix сделали c Ribbon. В этом случае клиент знает о нескольких возможных целевых серверах и выбирает подходящий сервер в зависимости от стратегии (например, если надо сократить время ожидания запроса, выбирают сервер в том же дата-центре).

Эти два подхода иногда хорошо работают вместе. Начните с традиционных балансировщиков нагрузки, а когда понадобится усовершенствовать маршрутизацию, добавьте их на стороне клиента. Такие балансировщики являются частью клиентских библиотек.

Агрегационные сервисы на границах сети

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

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

Если перед вами стоят сложные задачи, целесообразно создать несколько агрегационных сервисов, по одному для каждого сценария (публичный API, мобильный и десктопный клиенты и т.д.). Если у вас задачи попроще, хватит и одного сервиса.

Ограничьте бизнес-логику в агрегационном сервисе

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

Упал один из внутренних сервисов. Что делать?

Ответ зависит от конкретной ситуации. Спросите себя:

  • Можно ли незаметно удалить функцию или придётся выдавать ошибку в точке входа?
  • Был ли это настолько важный внутренний сервис, что придётся отключать весь агрегационный сервис?
  • Если функцию незаметно убрать из точки входа, как клиент сообщит пользователю об ошибке?

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

Безопасность

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

Надежная защита – это непросто. Вот несколько принципов, которым стоит следовать:

  • Многослойная безопасность, также известная как «defence-in-depth». Вместо того, чтобы верить, что у вас достаточно хороший файервол по периметру сети, добавляйте новые слои защиты в самых важных местах. Защита становится избыточной, а это замедляет атаку, когда один уровень безопасности выходит из строя или обнаруживается уязвимость.
  • Автоматическое обновление защиты. Во многих случаях плюсы автоматического обновления защиты перевешивают риски сбоя системы в результате него. Совмещайте автоматические обновления с автоматическим тестированием, и вы сможете обновлять защиту с гораздо большей уверенностью.
  • Усильте базовую операционную систему. Сервисам обычно практически не нужен доступ к базовой ОС. Поэтому сделайте так, чтобы ОС устанавливала жесткие ограничения на то, что сервис может и не может делать. Так будет проще взять под контроль безопасность, если в сервисе будет обнаружена уязвимость.
  • Не пишите собственную реализацию криптографических алгоритмов. Это очень трудно сделать без ошибок, а соблазн поверить, что вы написали рабочий код, слишком велик. Всегда используйте хорошо известные и широко используемые реализации.

Взаимодействие между сервисами

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

Протоколы передачи данных

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

Транспортный протокол

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

Единственный минус HTTP в том, что этот протокол производит слишком много информации, постоянно отправляя простые текстовые заголовки и многократно создавая и разрывая соединения. Можно утверждать, что это разумный компромисс, учитывая сколько пользы приносит экосистема HTTP. Однако уже существует вариант получше: HTTP/2. В этой версии успешна решена проблема громоздкости запросов благодаря сжатию заголовков и мультиплексированию запросов в постоянных соединениях. HTTP с нами сегодня и никуда не денется в будущем.

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

Для асинхронной передачи данных следует использовать шаблон публикации-подписки (pub/sub). Здесь есть два основных подхода:

  • Использовать брокер сообщений. Все сервисы отправляют события брокеру. Другие сервисы подписываются на нужные им события. В этом случае брокер сообщений определяет собственный транспортный протокол. Поскольку центральный брокер рискует стать слабым звеном, сбой в котором ведет к сбою всей системы, убедитесь, что система устойчива к ошибкам и масштабируется горизонтально.
  • Использовать вебхуки. Сервис открывает точку входа, с помощью которой другие сервисы могут подписаться на события. Сервис доставляет события в виде вебхуков (то есть в виде HTTP-запроса POST с сериализованным сообщением в теле) назначенному в момент подписки получателю. Вебхуки должны рассылаться асинхронными процессам под управлением сервиса. Это горизонтально масштабируемая функция, и при таком подходе нет единой точки, сбой в которой обвалит всю систему. Встроить эту функцию можно прямо в шаблон сервиса.

А как насчет сервисной шины (ESB) или другой фабрики обмена сообщениями?

Основная проблема с тяжеловесными системами обмена сообщениями в том, что они способствуют вытеснению бизнес-логики из сервисов на уровень сообщений. В результате снижается внутренняя связанность сервисов и появляется ещё один уровень, который со временем незаметно обрастает сложной структурой. Бизнес-логика должна оставаться внутри своего сервиса. И заниматься ей должна команда этого сервиса. Я настоятельно рекомендую придерживаться принципа «умные сервисы, глупые каналы». Так команды сохраняют автономию.

Теперь давайте поговорим о сериализации.

Есть два популярных формата:

  • JSON: обычный текстовый формат, описанный в RFC 7159.
  • Protocol Buffers: созданный Google бинарный формат для описания интерфейсов (IDL).

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

Формат protocol buffers проще анализировать, данные в этом формате легче передавать, и Google обстоятельно обкатал его на практике. Однако для каждого языка понадобятся специальные программы для синтаксического анализа и сериализации, определяющие формат сообщения. В отличие от JSON формат protobuf поддерживает не все языки, охватывая, впрочем, большинство современных языков. Кроме того, серверы должны заранее давать клиентам доступ к файлам, в которых определяется формат сообщения.

JSON распространен шире и освоить его легче. Protocol buffers компактнее и быстрее, но подразумевает дополнительные накладные расходы на составление и обмен .proto-файлами. Оба формата хороши. Выбирайте один и придерживайтесь его.

Что такое сломанный сервис

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

В случае с HTTP-протоколом всё просто. Протокол предполагает, что сервисы будут генерировать коды состояния серий 200, 300 и 400. Поэтому любая ошибка серии 500 или таймаут означают, что в сервисе произошел сбой. Это справедливо и для обратных прокси и балансировщиков нагрузки: они выдают ошибки 502 (Bad Gateway) или 503 (Service Unavailable), если не могут связаться с серверным экземпляром.

Разработка API

Хороший API – простой и понятный API. Пользователь API получает всё, что нужно для работы, и при этом не видит ключевых деталей реализации. Обновления API не доставляют пользователю практически никаких неудобств. Разработка API – это одновременно искусство и наука.

Мы уже выбрали HTTP в качестве протокола передачи данных. Чтобы полность раскрыть потенциал HTTP, к нему нужно добавить принципы REST. REST API использует точки входа, к которым можно применять методы, выраженные глаголами типа GET, POST или PATCH. В статье о разработке практичного REST API описаны основные принципы создания публичных API, которые подходят и для разработки API микросервисов.

Почему API сервисов должны ориентироваться на ресурсы?

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

Поскольку большинство современных публичных API созданы по принципам REST, вокруг сложилась внушительная экосистема проверенных инструментов. Многие из них могут пригодиться, например, клиентские библиотеки, средства автоматизации тестирования и интроспективные прокси.

Обнаружение сервисов

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

Реестр сервисов

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

Как сделать так, чтобы сервисы регистрировались в реестре сервисов? Есть два подхода:

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

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

Обнаружение и балансировка нагрузки

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

  • Интеллектуальные серверы: Клиент отправляет запрос известному балансировщику нагрузки, который в свою очередь получил из реестра данные о расположении экземпляров сервиса. Это традиционный подход, при котором весь трафик проходит через точки входа балансировщика нагрузки. Это стандартная схема для облачных платформ.
  • Интеллектуальные клиенты: С помощью реестра клиент обнаруживает доступные сервисы и решает, к каким присоединиться. При таком подходе отпадает необходимость в балансировщиках нагрузки. Дополнительный бонус в том, что сетевой трафик распределяется более равномерно. По такому принципу устроен Ribbon, созданный Netflix балансировщик на стороне клиента, который также поддерживает расширенную маршрутизацию на основе политик. Для реализации такого подхода в клиентских библиотеках должны быть функции обнаружения и балансировки.

Упрощенный механизм обнаружения с помощью балансировщиков нагрузки и DNS

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

Децентрализованные взаимодействия

Есть два основных способа выполнения сложных рабочих процессов: с помощью централизованной оркестровки или через децентрализованные взаимодействия.

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

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

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

Управление версиями

Перемены неизбежны. Важно то, как вы с ними справитесь. Указывайте версии API и поддерживайте несколько версий одновременно: в конечном счёте это уменьшит головную боль разработчикам других сервисов, ведь у них появится время обновиться по собственному графику. Нумеруйте и публикуйте версии всех API!

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

Быть может, стоит выделять версии в отдельные сервисы?

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

Дальше – ещё сложнее. Если все версии сервиса должны иметь одинаковое представление об основных данных, значит, они должны иметь доступ к общей базе данных. А это ещё одна плохая идея. Все версии будут тесно связаны со схемой хранения данных. Это значит, что любые изменения схемы в любой версии могут привести к сбою в других версиях. В итоге вам придется синхронизировать несколько баз кода.

Так как же поддерживать несколько версий?

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

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

Ставьте лимиты на всё

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

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

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

Пулы соединений

В результате неожиданного роста запросов сервис заваливает следующий за ним сервис, и проблема просто передаётся по цепочке. Пулы соединений помогают сгладить негативные последствия всплесков запросов. Размер пула ограничивает количество запросов, отправляемый в последующий сервис в любой момент времени.

Каждый сервис, с которым происходит обмен данными, должен иметь собственный пул соединений. Так сбой в сервисе не распространится по всей системе, а останется изолированным в отдельной её части.

И помните: быстрый сбой – это хорошо.

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

Короткие таймауты

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

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

При этом просто установить 30-секундный таймаут по умолчанию недостаточно. Таймаут должен чётко соответствовать тому, что ожидается от последующего сервиса. Например, если сервис должен отвечать в течение 10-50 миллисекунд, таймаута более 500 миллисекунд вам хватит с головой.

Игнорируйте несущественные изменения

API сервисов неизбежно будут меняться с течением времени. Выпуск версии с изменениями, требующими координации с пользователями API, занимает намного больше времени, чем в случае с переменами, которые не надо согласовывать. Чтобы свести к минимуму взаимозависимость, сервисы должны игнорировать изменения, не касающиеся обмены данных. Это значит, что если в API сервиса добавили, изменили или удалили поле, на коммуникацию с другими сервисами это влиять не должно.

Если бы все сервисы игнорировали несущественные изменения, небольшие добавки в API можно было бы вообще не согласовывать. А при критических изменениях, не относящихся к обмену данными, команде потребляющего сервиса просто следует запустить набор тестов и убедиться, что у них все работает.

Автоматические выключатели

За каждую попытку связаться с неработающим сервисом приходится платить: клиент расходует ресурсы на попытки сделать запрос, плюс истончаются ресурсы сети и целевого сервиса.

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

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

Идентификаторы корреляции

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

Идентификаторы генерируются либо пользовательским сервисом агрегации, либо любым сервисом, которому нужно сделать запрос (если этот запрос не является немедленным следствием полученного запроса). Любой достойный генератор случайной строки (например, UUID) справится с задачей.

Поддержка распределённой согласованности

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

Вы можете объединить операции в базе данных в локальную транзакцию и одновременно с этим вести записи в таблице событий. Так издатель событий будет просто считывать информацию из таблицы событий. Также можно сделать так, чтобы издатель событий читал журнал подтверждений базы данных. Но не все базы данных раскрывают такие журналы.

А ещё можно смириться с несогласованностью и исправить её позже.

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

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

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

Что, если вам нужно, чтобы какие-то сервисы были строго согласованы? Для начала я бы проверил, правильно ли определены границы сервисов. Обычно, когда требуется строгая согласованность, имеет смысл объединить данные в один сервис (и единую базу данных). Так будет проще обеспечить транзакционную семантику.

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

Аутентификация

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

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

Автоповтор

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

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

Что если происходит постоянный сбой?

Сбой не всегда происходит потому, что целевой сервис вышел из строя. Иногда причиной становится недопустимый запрос. В этом случае не важно, сколько раз повторить запрос. Он всё равно не увенчается успехом. Такие запросы после нескольких неудачных попыток следует перенаправлять в очередь отвергаемых запросов для изучения проблемы.

Передавайте данные только через открытые API

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

Дополнительный плюс открытых API: если коммуникация между сервисами подчиняется универсальным принципам, все остальные компоненты сервиса можно легко защитить надежным файерволом.

Экономические факторы

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

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

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

Клиентские библиотеки

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

Клиентские библиотеки не должны включать специфическую бизнес-логику. Они ограничиваются вспомогательными функциями, такими как подключение, транспорт, регистрация и мониторинг. Также не забывайте про риски, связанные с общими библиотеками.

Разработка

Управление версиями исходного кода

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

Дополнительный совет: используйте стандартную технологию управления исходным кодом. Так командам будет проще поддерживать непрерывную интеграцию и развертывание.

Среда разработки

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

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

Непрерывная интеграция

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

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

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

В чем выгода:

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

Непрерывная интеграция позволяет разработчикам быстрее создавать качественное ПО.

Непрерывное развертывание

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

Небольшие релизы — это здорово. Их легко тестировать. С ними проще анализировать код. И гораздо легче уверенно выпускать и развертывать небольшие изменения.

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

Как сделать так, чтобы конечные пользователи не видели недоделанные функции?

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

Риски, связанные с общими библиотеками

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

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

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

Шаблоны сервисов

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

Обязательно ли использовать шаблоны?

Шаблоны нужны для ускорения работы, а не навязывания строгой структуры. Тем не менее, некоторые процессы сервис должен выполнять обязательно (в частности, регистрироваться, мониторить и вести журнал). Пусть разработчики сами решают, как соблюсти эти требования: с помощью шаблонов или самостоятельно.

То есть шаблон можно создать для любого распространенного набора инструментов?

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

  • Разработчикам не приходится заново внедрять инструменты для каждого набора, и они могут сконцентрироваться на создании надежных стандартных инструментов.
  • Команды получают возможность проверять код друг друга.
  • И, что особенно важно, разработчикам становится проще перемещаться между командами.

Предоставьте шаблоны для каждого поддерживаемого набора.

Заменяемость сервисов

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

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

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

Развертывание

Пакет развертывания

Стандартный пакет развертывания – важный структурный элемент в процессе автоматического развертывания.

Пакет развертывания должен обладать следующими свойствами:

  • Развертывание где угодно. Один и тот же пакет без изменений должен развертываться в любой среде, будь то разработка, обкатка или эксплуатация.
  • Настройки системы и секретные данные вне пакета. Настройки системы и секретные данные не должны храниться внутри пакета. Пакет получает их в момент установки извне.
  • Изолированное развертывание. Если несколько сервисов используют одни и те же ресурсы, нередко происходит так, что один сервис забирает слишком много ресурсов и тем самым наносит непреднамеренный ущерб работе других сервисов. Чтобы снизить негативные последствия, изолируйте каждый развернутый сервис.

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

Неизменяемая инфраструктура решает

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

Автоматическое развертывание

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

Старайтесь делать обновления без остановки работы.

Если сервис приходится выключать для внедрения обновлений, то каждое новое обновление будет неизбежно ударять по остальным сервисам. Чтобы избежать таких мини-сбоев, которые которые отобьют у разработчиков желание часто обновляться, найдите изящный способ обновлять сервис без простоя в работе.

Один из возможных вариантов – перезапуск без остановки, при котором обновляется и перезапускается по одному сервису за раз при работающем балансировщике нагрузки. Это основательный подход, но если будет обнаружена ошибка, и потребуется откат назад, вам придется заново проводить полный перезапуск.

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

Флаги функций

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

Командам сервисов пригодится интерфейс, чтобы видеть и управлять флагами функций на платформе. Код для поиска флагов можно включить в общую библиотеку.

Пошаговое добавление функций

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

Короткий жизненный цикл флага

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

Ставьте флаг только в точке входа

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

Управление конфигурацией

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

Популярны подходы к установке конфигурации:

  • Переменные среды: загрузите конфигурацию в переменные среды сервиса.
  • Файловая система: установите на сервис файловую систему с конфигурацией и секретными данными.
  • Хранилище с общим ключом / значением: пусть сервис обменивается данными с таким хранилищем.

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

Эксплуатация

Централизованное ведение журнала

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

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

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

Но как тогда отслеживать последствия запроса в нескольких сервисах?

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

Централизованный мониторинг

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

Чтобы обеспечить бесперебойную работу, почти всегда к балансировщику нагрузки присоединяют несколько экземпляров сервиса. Средство мониторинга должно уметь собирать показатели по всем этим экземплярам. Кроме того, у вас должен быть быстрый доступ к этим агрегированным показателям, чтобы подробно изучить их. Эти меры помогают понять, случился ли сбой во всем сервисе или только в одном его экземпляре.

Какие показатели нужно отслеживать?

Разделим их на несколько категорий:

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

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

Распределённая трассировка поможет полностью оценить ситуацию

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

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

Распределённая трассировка для целей мониторинга работает также, как идентификаторы корреляции – для ведения журнала. Эти два инструмента настолько похожи, что ID, присваиваемые запросу системой отслеживания, также могут быть использованы и как ID корреляции.

Автомасштабирование

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

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

Внешние сервисы

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

Почему бы не предоставить все эти системы в виде сервисов на платформе?

Систему управления базами данных с функцией постоянного хранения информации, естественно, можно создать и интегрировать в системы мониторинга и ведения журнала. Однако это не всегда практично. Некоторые системы предъявляют особые требования к инфраструктуре, особенно если дело касается настроек бесперебойной работы. Другие не способны автоматически перезапускаться после сбоя, и вам придется каждый раз делать это вручную.

Тогда пусть несколько сервисов совместно пользуются системами.

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

Следует сделать оговорку, что совместно используемые ресурсы сложнее изолировать и контролировать по отдельности. В случае с общим сервером данных может случиться так, что один сервис будет использовать чрезмерное количество ресурсов и неосознанно подрывать производительность других сервисов. Если мониторинг недостаточно детализирован, на поиск проблемного сервиса потребуется много времени.

Люди

Команды ведут сервис от начала до конца

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

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

Автономные команды

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

Как быть, когда сотрудники уходят?

Время от времени люди будут покидать проект. Подготовьтесь к этому, чтобы в случае увольнений сервисы всё равно оставались под присмотром. Даже за самым беспроблемным сервисом нужно следить на случай неполадок.

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

Насколько большой должна быть команда?

Чем больше команды, тем больше времени они тратят на коммуникацию друг с другом. Команды должны быть достаточно большими, чтобы сохранять автономию, но при этом не тратить слишком много времени на выяснение рабочих вопросов. Amazon, например, знаменит подходом «команда на две пиццы». Это значит, что достаточно двух пицц, что накормить всю команду.

Ссылки

Я опирался на опыт тех, кто прошел путь к микросервисам до меня:

Перевод статьи «Best Practices for Building a Microservice Architecture» by Vinay Sahni.