Pull to refresh

Конспект книги «Создание микросервисов»

Reading time 13 min
Views 18K

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


Микросервисы


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


Преимущества подхода:


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

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


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


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


Архитектор развития


При разработке системы состоящей из многих сервисов возрастает роль архитектора.


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


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


Основные отвественности архитектора:


  • определение концептуального технического устройства системы;
  • умение сотрудничать в лидерами отдельных команд для выработки архитектурных решений;
  • коррекция уровня автономности команд.

Как моделировать сервисы


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


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


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


Делить сервисы по ограниченным контекстам (понятие Эрика Эванса из книжки DDD) — хорошая идея.


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


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


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


Интеграция


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


  • возможность вносить не-ломающие изменения в протокол;
  • независимость протокола от языка (технологии);
  • сокрытие деталей реализации сервиса.

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


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


Варианты интеграции


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


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


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


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


Общий код


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


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


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


Первый вариант — обеспечить функционирование конечных точек двух версий (например 1.5.3 и 2.0.0) в рамках одного сервиса и постепенно мигрировать клиентов на новую версию. В этом случае версию можно сообщать внутри URI (например, /v1/createUser/, /v2/createUser/), или, если используется HTTP, сообщать версию в заголовке запроса.


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


Пользовательские интерфейсы


Чтобы микросервисами пользоваться, нужен пользовательский интерфейс. Есть три пути.


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


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


Третий путь — backend-for-frontend (BFF). Каждая команда разработки интерфейса (например, мобильного приложения) создаёт для себя API-шлюз и поддерживает его.


При двух последних вариантах, следует тщательно контролировать отсутствие логики в API-шлюзе.


Интеграция сторонних систем


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


Разбиение монолита на части


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


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


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


В микросервисной архитектуре не так просто реализовать отчеты. Мы не можем просто сделать большой запрос к единой базе данных и получить результат. Есть четыре варианта:


  • каждый сервис может слать свои данные для отчетов в сервис отчетов;
  • сервис отчетов может ходить к остальным сервисам за данными;
  • если сервисы общаются через события, то сервис отчетов может просто подписаться на эти события;
  • сервис отчетов может брать бекапы разных сервисов и генерировать отчеты на основе этих данных (так делает Netflix).

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


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


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


Чтобы упростить доставку сервисов, нужно обеспечить единообразие способа развёртывания. Для этого можно взять систему управления конфигурациями, например Chef, Puppet или Ansible. Этот путь ведёт к нескольким проблемам — повторяемость сборки зависит от текущего состояния системы, подготовка занимает значительное время. Другой хороший вариант — создать артефакт для запуска на конкретной операционной системе, например deb-пакет. Но это приводит к сложностям с разными операционными системами. Чтобы избежать любых проблем с совместимостью, можно распространять сервис в виде образа системы с подготовленным окружением для запуска в виртуальной машине. И тут есть трудности — образы много весят и долго делаются.


LXC-контейнеры — это лучший вариант для большенства задач. Но ими сложно управлять. Docker решает многие проблемы — управляет предоставлением контейнеров, справляется с некоторыми проблемами использования сетей, а чтобы решить остальные придумали Kubernetes.


Тестирование


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


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


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


Юнит-тесты и тесты сервисов пишут владельцы конкретного сервиса. Сквозные же тесты должны разрабатываться совместно всеми командами (чтобы избежать дублирования).


Любые тесты должны быть повторяемыми. То есть при повторных запускать показывать одинаковые результаты.


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


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


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


Мониторинг


Для микросервисной архитектуры критически важен мониторинг.


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


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


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


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


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


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


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


Когда речь заходит о работе сервисов с сервисами, появляются варианты:


  1. Любой сервис может совершить любой вызов внутри системы. Это вполне приемлемый вариант для систем, которые не работают с каким-то супер-важными данными. Нужно учесть, что если злоумышленник попадет внутрь сети — он получит доступ до всего, чего захочет.
  2. Сервисы отправляют друг другу запросы через HTTP(S) Basic Auth. Во-первых, нельзя использовать это без HTTPS, а это вынуждает заниматься управлением SSL-сертификатами. Во-вторых, это дублирующая система (клиенты то не будут приходить с Basic Auth).
  3. Заведение обычных учетных записей для сервисов. Заводить и управлять аккаунтами запарно, зато удобно — везде единая система авторизации, никакого дублирования, все безопасно.
  4. Клиентские сертификаты. Очень сложно, непонятно зачем нужно.
  5. Проверки подлинности сообщений на основе хеш-функции (HMAC). Тело сообщения кешируется, подписывается ключом, принимающая сторона проверяет корректность подписи. Можно использовать готовые протоколы (например JWT) или реализовать свой. В некоторых случая имеет смысл применить асимметричное шифрование — подписывающий знает приватный ключ, а читающий только публичный.
  6. API-ключи. Рассматриваем все сервисы, как сторонние, которые умеют выдавать API-ключи. Удобно, но нужно управлять ключами для каждого сервиса, это накладно.

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


Приложение должно хранить как можно меньше пользовательских данных. Например, зачастую вместо IP-адреса можно сохранить только хеш от него. Это защитит от кражи данных злоумышленниками.


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


Закон Конвея и проектирование систем


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


Обычно, если каждым сервисом владеет конкретная команда, система в целом получается лучше, а её развитие происходит быстрее.


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


Надежность


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


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


При падении отдельных сервисов система в целом должна продолжать работать как можно дольше — деградировать изящно (graceful degradation).


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


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


Переборки — искусственные границы между сервисами, призванные снизить опасность от проблем в одном из них. Например, если два сервиса используют общий пул подключений к базе данных, то при сбое в одном сервисе (если он сожрёт все подключения), второй сервис тоже упадет. Можно разделить пулы и избежать такой ситуации. Переборки можно делать «подвижными», то есть закрывать их, только если обнаружены проблемы в одном из сервисов.


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

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


Масштабирование


Вертикальное масштабирование — заменить сервер на более мощный. Способ решить проблему с производительностью в моменте. Но не работает на большой дистанции.


Горизонтальное масштабирование — запустить несколько экземпляров приложения на разных машинах и балансировать нагрузку между ними. Это правильный путь. Легко масштабировать стейт-лесс сервисы, но с базами данных все усложняется.


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


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


Кеширование


Кешировать результаты можно на клиенте, можно в прокси-сервере, а можно на сервере:


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

В реальных проектах, обычно, используется все три варианта кеширования.


В HTTP встроен мощный механизм кеширования. Можно управлять временем жизни кеша через заголовки cache-control и expires и передавать серверу ETag для получения только новой версии ресурса. Если сервисы общаются по HTTP, то можно смело кешировать данные на стороне клиента, в протокол строен удобный инструментарий для этого.


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


Кеширование — это опасно. Можно легко накосячить (сообщить клиенту, что кеш вечный, например) и потом долго исправлять все это. Поэтому, нужно быть очень осторожным и стараться делать максимально простые для понимания системы кеширования.


Теорема CAP


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


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


Самый простой способ — использовать DNS. Если его возможностей не хватает можно применить штуки вроде Zookeeper, Consul или Eureka.


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


API сервисов нужно документировать. Очень желательно, чтобы код и документация были связаны и нельзя было изменить что-то одно. Например, можно взять Swagger или HAL.


Резюме


Микросервисная архитектура базируется на нескольких принципах:


  • моделирование вокруг бизнес-концепций;
  • культура автоматизации;
  • скрытые подробности внутренней реализации;
  • всесторонняя децентрализация;
  • независимое развёртывание;
  • изолированные сбои;
  • всесторонен наблюдение.
Tags:
Hubs:
+12
Comments 1
Comments Comments 1

Articles