Комментарии 65
После того, как вы пересели на Kubernetes, пользуетесь ли вы kube-proxy или по-прежнему свою библиотеку для «бана» не отвечающих инстансов?
По-прежнему пользуемся нашей библиотекой. У нас причин для бана сервиса больше, чем просто «не отвечает». Это может быть и не совместимая схема данных или не приемлемое время ответа (метрика приемлемости может варьироваться)
Ага, спасибо. Тогда ещё вопрос — вы бинарно используете/не используете инстансы (для чего достаточно будет хелсчекера, встроенного в kubernetes), или можете плавно изменять трафик, например, обратно пропорционально загрузке конкретного инстанса?
Перечитал дважды, шикарный контент, однозначно в избранное. Продукты Hashicorp удивили, раньше слышал, но обходил стороной, сейчас и Nomad и Vault решено попробовать…
Хорошая статья. Выводы, однозначно, сделаны правильно, но почему они появили так поздно? Ведь это шаблоны проектирования SOA?
Интересно еще узнать какие стандартные поротоколы рассматривались, какие шины? Народ не рекомендует InfluxDB на большой нагрузке как она у вас себя ведет?
Ну выводы тут скорее, не как выводы из всей истории, а как рекомендации в дополнение к остальному повествованию ))

Мы смотрели protobuf, thrift, msgpack. У каждого свои плюшки, но мы в итоге пришли к тому, что с json многие вещи проще, понятнее, прозрачнее.

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

Я бы тоже почитал об этом. Из первого рисунка рождается предположение, что используется NSQ.


Если можно, я бы попросил ещё комментарии о том, как решается вопрос с дедупликацией\идемпотентностью при доставке сообщений. Ну, и отсутствие гарантии порядка доставки сообщений не мешает?

А вы внимательны, да, действительно на первом рисунке в качестве шины NSQ указан. Присоединяюсь к вашим вопросам.

А шины? Я так понимаю, говоря о них вы говорили о MQ. Какие именно реализации пробовали/использовали?

Доклад замечательный.


Хотелось бы от кого-то услышать, как люди решают задачу, когда для рендера страницы пользователю нужна инфа от N сервисов? Синхронные запросы? Или локальный (для микросервиса) кеш + его асинхронное обновление? Что еще?

Хотелось бы от кого-то услышать, как люди решают задачу, когда для рендера страницы пользователю нужна инфа от N сервисов?

ставите над сервисами еще один сервис-агрегатор который делает нужные выборки из других сервисов и склеивает все.


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

За оценку доклада — спасибо!

У нас через микросервисы реализуется только API, прямого рендеринга в HTML — нет. Клиент (не важно будь то single-page application или iOS/android приложение) делает асинхронные вызовы к API и строит результат.

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

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

Да, понимание того какие данные критичны для ответа, а без каких можно обойтись — очень важно — помогает избежать эффекта домино. Хотя это важно в любой распределенной системе и в особенности в системе с внешними ресурсами.
Это трансляции на сайте НТВ?
У вас 80 ГБит каналы?
Для 300к человек нужно ж порядка 300 ГБит при юникасте.

Можно их как-то себе на сайт поставить (трансляции)?
300к человек — это 700-1200 Гбит в зависимости от качества.
По поводу поставить на сайт — напишите мне в личку.
Я просто прочитал
Когда у нас стало уже 80 серверов.

И подумал, что их столько и осталось, ну и что они с 1 ГБит :)
Нет, одногигабитные сервера — это очень дорого :)

Отдающие сервера сейчас более-менее выгодно с 80 Гбит

Уфф, всю статью читал в напряжении, дойдете вы до шины данных с выбрасыванием эвентов, или нет. С этого надо было начинать :)


Респектище за статью и архитектуру, молодцы!

Спасибо, читал немного бегло, но даже так много интересного для себя отметил
Интересно еще узнать как вы АД зависимостей версий сервисов разруливаете? У нас в свое время проблема была, что никто не хотел вкладываться в надежную систему поставок сервисов, с горем пополам сделали, но далеко от идеала. Вообще интересно как у вас система поставок устроена.
А мы как раз делаем все, чтобы от зависимости версий уйти. И переход на json — один из таких шагов. Суть микросервисов как раз в их независимости, в том числе при деплое. Если у вас деплой одного сервиса вызывает каскадный деплой еще 40, то это проблема.

У нас практически каждый сервис в любой момент времени может быть выкачен в продакшен с новой версией.

Каждый сервис сообщает о себе в том числе версию базового протокола в рамках которой у сервиса гарантированно есть в наличии нужные данные, если же это не так, то ошибка проверки по json-схеме у получателя включит механизм circuit breaker и для конкретного клиента этот инстанс уйдет в бан.
Здравствуйте.
А были случат когда для клиента все инстансы нужного микросервиса уходили в бан?
Насколько я понимаю, от этого должен защитить механизм circuit breaker.
Но он работает «с лагом», поэтому возможна ли такая ситуация.
Да, были. В случае, когда инстансев определенного типа становится доступно меньше определенного порога поднимается алерт — это нештатная ситуация близкая к аварийной.

От такой ситуации не защищает паттерн circuit breaker (предохранитель) — он как раз косвенный виновник этой ситуации.

Смотрите — есть сервис А, который должен обратиться к сервису B. Он идет в реестр сервисов и просит адреса доступных инстансев сервиса В. Получает, например, список из 2-х адресов. Далее вступает в игру реализация предохранителя. Мы обращаемся к первому инстансу — он не отвечает за нужное нам время и мы в сервисе А делаем пометку — к этому не обращаться 1 мин — это и есть бан сервиса В в сервисе А. Далее мы идем ко второму инстансу — он ответил, но схема данных не подходит, мы его так же баним по причине несовместимости данных. В итоге получаем, что у нас для сервиса А нет доступных инстансев сервиса B. При этом с сервисом C инстансы сервиса В могут быть прекрасно совместимы и для него с сервисом В будет все в порядке.
Интересен как раз вопрос с зависимыми сервисами и проверка, что измененный сервис по прежнему «независим» или отвечает заявленной спецификации. Мы, например, гоняем BDD интеграционные тесты.
Мы в любой момент времени исходим из того, что сервисы у нас независимы. Поэтому у нас гарантированно нет завязки на версию самого сервиса. С версией протокола данных, поставляемых сервисом мы делаем так:
1. Сервис при деплое поставляется с мажорной версией протокола данных
2. Сервис-потребитель имеет json-схему проверки корректности данных с его точки зрения
3. Новый релиз сервиса мы выкатываем не как замену текущей версии, а как дополнение. Т.е. в какой-то момент времени у нас работает и версия сервиса 1.2 с протоколом данных версии 6 и версия 1.3 с протоколом данных версии 7.
4. Если новый релиз не совместим по схеме в сервисе-потребителе, то мы в потребителе его баним через паттерн circuit breaker
5. И мы мониторим переезд на новые релизы от каждого сервиса

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

В итоге никакого ада с версиями у нас нет, он намечался когда у нас было все на protobuf/grpc, но мы от этого ушли перейдя на json и введя схему данных для каждого сервиса-потребителя, который теперь проверяет только, что в ответе есть те данные которые нужны именно ему, а на остальное он не обращает внимание
Вот то, что хотел услышать: «в какой-то момент времени у нас работает и версия сервиса 1.2 с протоколом данных версии 6 и версия 1.3 с протоколом данных версии 7». Круто — как и должно быть.

Вы в итоге остановились на использовании opentracing api + zipkin'овская реализация? Какой транспорт от zipkin-reporter'а до сервера используете?


Попробовал вариант с libthrift и очень удивился пачке ошибок при работе Senderа на localhost'е (версия libthrift, естественно, одинаковая на обоих сторонах).

Большое спасибо за статью. Очень полезна.

Есть пара вопросов:

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

Точную цифру не скажу, но много :)
— Есть ли у этих микросервисов общий код (какой-нибудь общий фреймворк)?

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

А что в него входит (примерно)?
Общие для всех сервисов компоненты:
  • конфигурация приложения/обращение к сервису единой конфигурации
  • хелсчек/рэдичек
  • логгирование
  • трассировка
  • дебаг
  • работа с метриками
  • реализация circuit breaker
  • реализация rate limit
  • + специфичные для нас штуки


Инфраструктурные штуки:
  • болванка сборки приложения в контейнер
  • болванка документации сервиса
  • тестирование


Может быть что-то еще, но это основное
— Как распространяются изменения в шаблоне? Допустим, исправили критическую ошибку в логировании, каким образом изменения появятся в зупущенных сервисах и появятся ли они вообще?
— Как появился этот шаблон: вырос самостоятельно, введён директивно мейнтейнером, etc?
— Как распространяются изменения в шаблоне? Допустим, исправили критическую ошибку в логировании, каким образом изменения появятся в зупущенных сервисах и появятся ли они вообще?

Если прям критическое, то выпускается патч и рассылается всем командам, если не критичное, то каждая команда принимает решение об апдейте самостоятельно
— Как появился этот шаблон: вырос самостоятельно, введён директивно мейнтейнером, etc?

Самостоятельно, по мере накопления опыта выделились общие части каждого микросервиса, которые и стали сборкой для шаблона
Нескромный вопрос — не будете ли Вы так любезны поделиться болванкой сервиса в OpenSource? :)
Расскажите, пожалуйста, про тот момент, когда Nomad вы переросли и переехали на Kubernetes. Как раз сейчас переезжаем на Nomad.
Для нас основной аргумент для миграции был в том, что на тот момент Nomad не умел работать с внешними volume и имел серьезные проблемы с запуском/остановкой сервисов в случае переименования job-а
Спасибо за доклад.

Не могли бы вы рассказать о некоторых тонкостях, в частности интересует:

1. Как собираете образы? На каждый микросервис свой образ или же вся кодовая база в одном образе и при старте контейнера передаете аргументом название микросервиса? Или как-то еще?

2. Как выполняете, например, миграции? При деплое стартует микросервис, который выполняет необходимые действия и затем умирает?

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

Спасибо.
1. Как собираете образы? На каждый микросервис свой образ или же вся кодовая база в одном образе и при старте контейнера передаете аргументом название микросервиса? Или как-то еще?

1 микросервис — 1 репозиторий — 1 контейнер. Каждый репозиторий самодостаточен, у каждого своя кодовая база.

2. Как выполняете, например, миграции? При деплое стартует микросервис, который выполняет необходимые действия и затем умирает?

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

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

Есть шаблон микросервиса — из него генерится новый микросервис на старте разработке, далее у него полностью своя кодовая база, которая развивается самостоятельно. Если какой-то функционал из шаблона должен получить критическое обновление — выпускается общий патч.
Очень интересный материал и подача, наверно лучшее из перепечаток HL++ из тех что читал, спасибо!
Спасибо за доклад. А с помощью как у вас реализована шина сообщений?
Пожалуйста! Для шины используется NSQ, RabbitMQ — как реализация event bus и Kafka для агрегированного сбора данных в аналитические БД
Не совсем понятно, NSQ (nsqd) вроде как сам по себе демон для events — вы дополнительно к нему пристегиваете Rabbit сбоку, или вы их как-то подружили по другому? Или вообще используются отдельно, каждый для своей задачи?
Нет, как я говорил в докладе — у нас не одна шина на все, а есть шина общего обмена и шины групп сервисов.
Ага, т.е. Rabbit отдельно, NSQ отдельно. Не поделитесь Rabbit для каких типов задач? И что более нагружено в плане кол-ва сообщений, Rabbit или NSQ?
Там не конкретные типы задач, просто каким-то командам ближе NSQ, каким-то RabbitMQ, микросервисная архитектура и кластеризация это позволяют.

По нагрузке точно не скажу кто нагружен сильнее, но могу сказать, что в ситуации обмена сообщения между датацентрами мы используем NSQ.
Еще, в приведенном Вами примере о создании аккаунта, в ситуации с АПИ — обычно нужно иметь на выходе результат — причем интерактивно (аккаунт создан, или произошла ошибка — пароль плохой, мейл уже используется и т.д.). При использовании NSQ — мы отправляем эвент и, поскольку это не RPC — мы не знаем какие микросервисы отработали, и создан ли был аккаунт в конце концов. Возможно вы применяете NSQ там где надо именно notify сделать, без опоры на результат? В таких случаях возможно лучше применять Rabbit или, допустим, ZMQ. Интересно как в Вашем случае?
Event Bus — это по определению notify, в противном случае вы создаете сильную связанность между сервисами и теряете возможность их независимой разработки и поддержки. Поэтому речь тут идет именно о NSQ и RabbitMQ, а не, скажем, о Gearman.

А ситуацию с контролем нужно рассматривать несколько иначе: входная точка — это корректное создание аккаунта. Если создание прошло, значит есть отправная точка и предварительная проверка корректности данных. Далее у нас есть UserID к которому делается привязка остальных частей — заведение в биллинге, подготовка аватары и тп. И тут вступает в игру такой паттерн как «eventual consistency» — т.е. мы даем системе какое-то время, чтобы собраться к конечному состоянию в правильном виде и получить итоговый статус «регистрация полностью завершена». Если же этого не происходит, то сервис контроля инициирует процедуру отката. И вот наличие этой процедуры отката — один из вопросов при проектировании распределенной системы. Эта ситуация характерна не только для микросервисов — это вопрос именно к любой распределенной многокомпонентной системе.

Ну в таком случае можно генерить чтото типа taskid и некий сервис проверки результата может откатить если статус fail… Спасибо за статью :)

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


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


Спасибо.

Event Bus — это по определению notify, в противном случае вы создаете сильную связанность между сервисами и теряете возможность их независимой разработки и поддержки.
Но вроде никто не жалуется когда используют БД в режиме RPC. Очень часто в проектах нужен как раз RPC вместо event/notify (хоть event тоже часто нужен, но это не значит что надо RPC заменить на event везде).
Например, создание аккаунта, через RPC или event, связность одинаковая, а сложность с RPC ниже.
И еще вопрос, а зачем было использовать NSQ, если так-же используете Rabbit? (в смысле того что NSQ по функционалу типа как сабсет раббита, и раббите есть то что есть в NSQ). Производительность в привязке к задаче PUB/SUB?
Спасибо за полезный материал, особенно за рекомендации по декомпозиции задач на различные микросервисы (про регистрацию пример) и за советы по аналитическому подходу к построению архитектуры проекта.
Стал замечать по своему опыту, что хочется относительно маленький проект сразу превратить в google|twitter|uber — way и т.п., а потом после изучения и детального тестирования тех или иных решений приходишь к выводу что всему свое время! :)
Очень интересный доклад, спасибо!
Не могли бы вы рассказать чуть подробнее про использование Hystrix:
  1. Реализуете ли вы каждый раз новую Hystrix-команду для обращения к микросервису в конкретном месте кода, или circuit breaker «вшит» где-то на инфраструктурном уровне?
  2. Как вы управляете конфигурацией circuit breaker'ов: список инстансов микросервиса, допустимые таймауты и процент ошибок, время бана микросервиса и т.п.
  3. Как вы «подружили» Hystrix с кодом на Go?
Реализуете ли вы каждый раз новую Hystrix-команду для обращения к микросервису в конкретном месте кода, или circuit breaker «вшит» где-то на инфраструктурном уровне?
Не уверен, что правильно понял вопрос, но… каждый вызов внешнего сервиса делается посредством обертки хистрикса

Как вы управляете конфигурацией circuit breaker'ов: список инстансов микросервиса, допустимые таймауты и процент ошибок, время бана микросервиса и т.п.
У нас для всех сервисов единая точка конфигурирования на базе KV-хранилища Consul-а. В нем хранится вообще все, что касается конфигурации сервисов в целом и каждого в частности. Каждый сервис на момент инициализации знает только свой ID, версию и то, как достучатся до Consul-а, чтобы забрать из него конфигурацию

Как вы «подружили» Hystrix с кодом на Go?
Использовали вот эту библиотеку: https://github.com/afex/hystrix-go
Некровопрос: я правильно понимаю, что у вас может быть несколько схем, допустим, по схеме на endpoint? Для создания нового объекта — одна схема, для запроса — другая?
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.