На конференции HighLoad++ 2016 руководитель разработки «М-Тех» Вадим Мадисон рассказал о росте от системы, для которой сотня микросервисов казалась огромным числом, до нагруженного проекта, где пара тысяч микросервисов — обыденность.
Тема моего доклада — то, как мы запускали в продакшн микросервисы на достаточно нагруженном проекте. Это некий агрегированный опыт, но поскольку я работаю в компании «M-Tех», то давайте я пару слов расскажу о том, кто мы.
Если коротко, то мы занимаемся видеоотдачей — отдаём видео в реальном времени. Мы являемся видеоплатформой для «НТВ-Плюс» и «Матч ТВ». Это 300 тысяч одновременных пользователей, которые прибегают за 5 минут. Это 300 терабайт контента, который мы отдаем в час. Это такая интересная задача. Как это всё обслужить?
Про что сама эта история? Это про то, как мы росли, как проект развивался, как происходило какое-то переосмысление каких-то его частей, какого-то взаимодействия. Так или иначе, это про масштабирование проекта, потому что это всё — ради того, чтобы выдержать ещё больше нагрузки, предоставить клиентам ещё больше функционала и при этом не упасть, не потерять ключевых характеристик. В общем, чтобы клиент остался доволен. Ну и немного про то, какой путь мы прошли. С чего мы начинали.
Вот такая стартовая точка, некая такая отправная, когда в Docker-кластере у нас было 2 сервера. Тогда базы данных запускались в том же кластере. Чего-то такого выделенного в нашей инфраструктуре не было. Инфраструктура была минимальна.
Если посмотреть на то, что было в нашей инфраструктуре основного, то это Docker и TeamCity как система доставки кода, сборки и так далее.
Следующей вехой — то, что я называю серединой пути — был достаточно серьезный рост проекта. Когда у нас стало уже 80 серверов. Когда мы построили отдельный выделенный кластер под базы данных на специальных машинах. Когда мы стали переходить к распределенному хранилищу на базе CEPH. Когда мы стали задумываться о том, что, возможно, пора пересмотреть то, как наши сервисы взаимодействуют между собой, и вплотную подошли к тому, что нам пора менять нашу систему мониторинга.
Ну и собственно то, к чему мы пришли сейчас. В Docker-кластере уже несколько сотен серверов — сотни запущенных микросервисов. Сейчас мы пришли к тому, что мы начинаем делить нашу систему на некие сервисные подсистемы на уровне шин данных, на уровне логического разделения систем. Когда этих микросервисов стало слишком много, мы стали дробить систему для того, чтобы лучше ее обслуживать, лучше понимать.
Сейчас на экране вы видите схему. Это один небольшой кусок нашей системы. Это штука, которая позволяет нарезать видео. Похожую схему я показывал полгода назад на «РИТ++». Тогда зеленых микросервисов было, по-моему, 17 штук. Сейчас их здесь 28. Если примерно посмотреть, это 1/20 нашей системы. Можно представить себе примерные масштабы.
Подробности
Один из интересных моментов — это транспорт между нашими сервисами. Классически начинают с того, что транспорт должен быть максимально эффективным. Мы тоже про это подумали, решили, что protobuf — это наше всё.
Выглядело это примерно так:
Запрос через Load Balancer приходит на фронтовые микросервисы. Это либо Frontend, либо сервисы, которые предоставляют непосредственно API, они работали через JSON. А ко внутренним сервисам запросы шли уже через protobuf.
Сам по себе protobuf — это такая достаточно неплохая штука. Он действительно дает определенную компактность в messaging. Сейчас уже есть достаточно быстрые реализации, которые позволяют с минимальными накладными расходами сериализовать и десериализовать данные. Можно считать его условно типизированным запросом.
Но если посмотреть в разрезе микросервисов, то заметно, что у вас между сервисами получается некое подобие проприетарного протокола. Пока у вас 1, 2 или 5 сервисов, вы под каждый микросервис можете спокойно выпускать консольную утилиту, которая будет позволять вам обращаться к конкретному сервису и проверять, что она возвращает. Если он что-то затупил — дёрнуть его и посмотреть. Это несколько усложняет работу с этими сервисами именно с точки зрения поддержки.
До какого-то этапа это не было какой-то серьезной проблемой — сервисов было не так много. Плюс ребята из Google зарелизили gRPC. Мы посмотрели, что, в принципе, для наших целей на тот момент он делал всё, что нам было нужно. Мы потихоньку на него мигрировали. И бамс — в нашем стеке появилась ещё одна штуковина.
Здесь тоже достаточно интересная деталь в реализации. Эта штука — на базе HTTP/2. Это вещь, которая реально работает из коробки. Если у вас не очень динамичная среда, если у вас не меняются инстансы, не переезжают по машинам достаточно часто, то это, в общем-то, хорошая штука. Тем более, что в данный момент есть поддержка под кучу языков — как серверных, так и клиентских.
Теперь если взглянуть на это в разрезе микросервисов. С одной стороны, штука хорошая, а с другой — это вещь в себе. Вплоть до того, что когда мы стали стандартизировать наши логи для того, чтобы их агрегировать в единой системе, мы столкнулись с тем, что напрямую в удобном виде из gRPC логи получить нельзя.
В итоге мы пришли к тому, что мы написали собственную систему логирования, подсунули её в gRPC. Она у нас делала парсинг выдаваемых через gRPC сообщений, приводила их к нашему виду, и вот тогда мы могли это засунуть в нашу систему логирования нормально. И плюс ситуация, когда вы описываете сначала сервис и типы к этому сервису, потом их компилируете, повышает зависимость сервисов между собой. Для микросервисов это некая проблема, такая же, как и некая сложность версионирования.
Как вы уже, наверное, догадались, в итоге мы пришли к тому, что мы стали задумываться про JSON. Причём мы сами долгое время не верили в то, что после какого-то компактного, условно бинарного протокола мы вдруг вернёмся в JSON, пока не наткнулись на статью ребят из DailyMotion, которые писали примерно про то же самое: «Блин, мы тоже умеем готовить JSON, его умеют готовить все на свете, зачем мы создаём себе дополнительные сложности?»
В итоге мы постепенно начали мигрировать с gRPC на JSON в некой своей реализации. То есть да, мы оставили HTTP/2, мы взяли достаточно быстрые реализации для работы с JSON.
Получили все те плюшки, которые мы имеем. Мы можем обратиться к нашему сервису через сURL. Наши тестеры пользуются Postman, и у них тоже все хорошо. На любом этапе работы с этими сервисами у нас стало все просто. Это та штука, которая с одной стороны, спорное решение, а с другой — в обслуживании действительно сильно помогает.
По большому счету, если посмотреть на JSON, то единственный реальный минус, который ему можно предъявить прямо сейчас — это недостаточная компактность этого описания. Те 30%, которые, как статистически утверждается, являются разницей между тем же MessagePack или еще чем-то, на самом деле по нашим замерам, разница не настолько большая, а также это не всегда настолько критично, когда мы говорим о поддерживаемой системе.
Плюс с переходом на JSON мы получили дополнительные плюшки. Такие, как, например, версионирование протокола. В какой-то момент у нас начала складываться ситуация, что через тот же protobuf мы описываем какую-то новую версию протокола. Соответственно, клиенты, потребители данного конкретного сервиса, тоже должны на неё переехать. Получается, что если у вас несколько сотен сервисов, даже 10% из них должны переехать. Это уже большой каскадный эффект. Вы в одном сервисе поменяли, а ещё 10 нужно переделать.
В итоге у нас начала складываться ситуация, когда разработчик этого сервиса выпустил уже пятую, шестую, седьмую версию, а реально нагрузка в продакшне идёт до сих пор на четвертую, потому что у разработчиков смежных сервисов свои дедлайны и приоритеты. Они просто не имеют возможности постоянно пересобирать сервис, переезжать на новую версию протокола. Реально получилось, что новые версии выпускаются, но они не востребованы. Зато баги в старых версиях мы должны реализовывать какими-то непонятными путями. Это усложняло поддержку.
В итоге мы пришли к тому, что перестали плодить версии протоколов. Зафиксировали некую базовую версию, в рамках которой мы можем добавлять какие-то property, но в каких-то очень ограниченных пределах. И сервисы потребителей стали пользоваться JSON-схемой.
Вот так примерно она выглядит:
Вместо 1, 2 и 3 у нас стала версия 1 и та схема, которая к ней относится.
Вот типичный ответ одного из наших сервисов. Это Content Manager. Он выдал информацию о трансляции. Вот, например, схема одного из потребителей.
Здесь самая интересная строчка — нижняя, где у нас блок required. Если мы посмотрим, то мы увидим, что этому сервису на самом деле из всех этих данных нужно только 4 поля — id, content, date, status. Если реально применить эту схему, то в итоге сервису потребителя нужны только эти данные.
Они действительно есть в каждой версии, в каждой вариации первой версии протокола. Это упростило переезд на новые версии. Мы стали выпускать новые релизы, и миграция потребителей на них сильно упростилась.
Следующий, немаловажный момент, который возникает, когда мы говорим о микросервисах, да и, в общем-то, о любой системе. Просто в микросервисах это чувствуется сильнее и быстрее всего. Это ситуации, когда система становится нестабильна.
Когда у вас цепочка вызовов 1—2 сервиса, то особых проблем нет. Какую-то глобальную разницу между монолитным и распределённым приложением вы не видите. Но когда у вас цепочка разрастается до 5—7, на каком-то этапе у вас что-то отвалилось. Вы реально не знаете, почему оно отвалилось, что с этим делать. Отлаживать это достаточно сложно. Если на уровне монолитного приложения вы включили debugger, просто прошлись по шагам и нашли эту ошибку, то здесь у вас накладываются такие факторы, как нестабильность сети, нестабильное проведение под нагрузкой и еще что-то. И вот такие вещи — в такой распределенной системе, с кучей таких нод — становятся очень заметны.
Тогда, в начале, мы пошли классическим путём. Мы решили все замониторить, понять, что и где ломается, пытаться с этим как-то оперативно бороться. Мы стали отправлять метрики с наших микросервисов, собирать их в единую базу. Мы через Diamond стали собирать данные по машинам, что на них происходит через сAdvisor. Мы стали собирать информацию по Docker-контейнерам, все это сливать в InfluxDB и строить dashboards в Grafana.
И вот у нас появилось еще 3 кирпичика в нашей инфраструктуре, которая постепенно разрастается.
Да, мы стали больше понимать, что у нас происходит. Мы стали оперативнее реагировать на то, что у нас что-то развалилось. Но разваливаться оно от этого не перестало.
Потому что, как ни странно, основная проблема микросервисной архитектуры — именно в том, что у вас есть сервисы, которые работают нестабильно. То работает, то не работает, и причин тому может быть масса. Вплоть до того, что у вас сервис перегружен, а вы на него дополнительную нагрузку отправляете, он уходит на какое-то время в down. Через какое-то время из-за того, что он всё не обслуживает, нагрузка с него спадает, и он снова начинает обслуживать. Такая чехарда приводит к тому, что такую систему очень сложно и поддерживать, и понимать, что с ней не так.
В итоге мы постепенно пришли к тому, что лучше, чтобы этот сервис падал, чем он вот так туда-сюда скачет. Вот это понимание привело нас к тому, что мы стали менять свой подход к тому, как мы реализуем наши сервисы.
Первый из важных моментов. Мы стали вводить в каждый наш сервис ограничение по входящим запросам. Каждый сервис у нас стал знать сколько он способен обслужить клиентов. Откуда он это знает, я ещё чуть позже расскажу. Все те запросы, что сверх этого лимита или около его границ, он перестаёт принимать. Он выдаёт честные 503 Service Unavailable. Тот, кто к нему обращается, понимает, что нужно выбрать другую ноду — эта неспособна обслужить.
Тем самым мы уменьшаем время запроса в случае, если с системой что-то не так. С другой стороны, повышаем её стабильность.
Второй момент. Если rate limiting на стороне сервиса назначения, то второй паттерн, который мы стали повсеместно вводить — Circuit Breaker. Это паттерн, который мы, грубо говоря, реализуем на клиенте.
Сервис А, у него есть в качестве возможных точек обращения, например, 4 инстанса сервиса B. Вот он сходил в registry, сказал: «Дай мне адреса этих сервисов». Получил, что их 4 штуки. Сходил к первому, тот ему ответил, что всё ок. Сервис пометил «да», к нему можно ходить. По Round Robin он разбрасывает обращения. Пошел ко второму, тот ему не ответил за нужное время. Всё, мы его баним на какое-то время и идём к следующему. Тот, например, у нас возвращает некорректную версию протокола — неважно, почему. Он его тоже банит. Идёт к четвертому.
В итоге получаются 50% сервисов, они реально способны ему помочь обслужить клиента. К этим двум он будет ходить. Те два, которые по какой-то причине его не устроили, он на какое-то время банит.
Это позволило нам достаточно серьезно повысить стабильность работы в целом. С сервисом что-то не так — мы его отстреливаем, поднимается алерт на то, что сервис отстрелили, и мы дальше разбираемся, что могло быть не так.
В ответ на введение паттерна Circuit Breaker у нас появилась еще одна штуковина в нашей инфраструктуре – это Hystrix.
Ребята из Netflix не только реализовали поддержку этого паттерна, но и сделали наглядно, как понять, если с вашей системой что-то не так:
Здесь размер этого кружочка показывает, насколько у вас много трафика относительно других. Цвет показывает, насколько системе хорошо или плохо. Если у вас зеленый кружочек, то, наверное, у вас всё хорошо. Если красный — не всё так радужно.
Примерно вот так выглядит, когда у вас сервис полностью надо отстрелить. На него сработал переключатель.
Мы добились того, что у нас система стало более-менее работать стабильно. У нас у каждого сервиса появилось не меньше двух инстансов, чтобы мы могли переключаться, отстреливая один-другой. Но это не дало нам понимания того, что происходит с нашей системой. Если у нас где-то что-то по ходу дела отвалилось при исполнении запроса, то как это понять?
Вот стандартный запрос:
Такая цепочка исполнения. От пользователя пришел запрос на первый сервис, потом на второй, со второго он разошёлся веткой на третий и четвёртый.
Бац, и у нас одна из веток отпала. Реально непонятно, почему. Когда мы столкнулись с это ситуацией и стали разбираться, что здесь предпринять, как мы можем улучшить видимость ситуации, мы наткнулись на такую штуку, как Appdash. Это сервис трассировки.
Выглядит это вот так:
Сразу скажу, это была штука именно на попробовать, понять, то ли это. Имплементировать её в нашу систему было проще всего, потому что мы к тому моменту достаточно плотно перешли на Go. Appdash имел готовую библиотеку для подключения. Мы посмотрели, что да, эта штука нам помогает, но сама реализация нас не очень устраивает.
Тогда вместо Appdash у нас появился Zipkin. Эта та штука, которую сделали ребята из Twitter. Выглядит это примерно так:
Мне кажется, чуть более наглядно. Здесь мы видим, что у нас есть определённое количество сервисов. Мы видим, как наш запрос проходит по этой цепочке, видим, какую часть в каждом из сервисов этот запрос отъедает. С одной стороны, мы видим некое общее время и деление по сервисам, с другой — никто нам не мешает сюда точно так же добавлять информацию о том, что происходит внутри сервиса.
То есть какая-то полезная нагрузка, обращение к базе, вычитка чего-то там с файловой системы, обращение к кэшам — это всё можно точно так же сюда добавлять и смотреть, что в вашем запросе могло больше всего добавить времени на этот запрос. Та штука, которая позволяет нам этот проброс делать — это сквозной TraceID. Я дальше про него буду немножко говорить.
Вот так мы стали понимать, что у нас происходит в определённом запросе, почему он вдруг падает для какого-то конкретного клиента. У всех всё хорошо и вдруг у кого-то отдельного что-то не так. Мы стали видеть некий базовый контекст и понимать, что у нас происходит с сервисом.
Не так давно на систему трассировки был разработан некий стандарт. Просто некая договорённость между основными поставщиками систем трассировки о том, как нужно реализовывать клиентское API и клиентские библиотеки для того, чтобы можно было эту имплементацию сделать максимально простой. Сейчас уже есть реализация через Opentracing практически под все основные языки. Смело можно пользоваться.
Мы научились понимать, какой из сервисов вдруг не позволил нам обслужить клиента. Мы видим, что какая-то из частей затупила, но не всегда понятно, почему. Контекст недостаточен.
У нас есть логирование. Да, это достаточно стандартная штука, это ELK. Может быть, в небольшой нашей вариации.
Мы не собираем напрямую через кучу forward в виде Logstash. Мы сначала передаём это в Syslog, с помощью Syslog мы это агрегируем на собирающих машинах. Оттуда уже через forward кладём в ElasticSearch и в Kibana. Относительно стандартная штука. В чём фишка?
В том, что везде, где это возможно, где мы реально понимаем, что это реально относится к именно этому конкретному запросу, мы в эти логи стали добавлять тот самый TraceID, который я показывал на скрине с Zipkin.
В итоге, мы в логах видим на Dashboard в Kibana полный контекст исполнения по конкретному пользователю. Очевидно, что если сервис попал в prod, то он условно уже рабочий. Он прошёл автотесты, на него уже посмотрели тестировщики, если надо. Он должен работать. Если он в какой-то конкретной ситуации не работает, то, видимо, были какие-то предпосылки. Эти предпосылки в этом подробном логе, который мы видим при такой фильтрации по конкретному trace для конкретного запроса, помогают гораздо быстрее понять, что конкретно в этой ситуации не так. В итоге у нас достаточно серьёзно сократилось время понимания причин проблемы.
Следующий интересный момент. Мы ввели динамический debug mod. В принципе, у нас сейчас не такое дикое количество логов — порядка 100—150 гигабайт, не помню точную цифру. Но это — в базовом режиме логирования. Если бы мы писали вообще супер-подробно, это были бы терабайты. Обрабатывать их было бы безумно дорого.
Поэтому, когда мы видим, что у нас проявилась какая-то проблема, мы заходим на конкретные сервисы, включаем на них через API debug mod и смотрим, что происходит. Иногда мы сначала смотрим, что происходит. Иногда мы отстреливаем сервис, который создает у нас проблему, не выключая его, включаем на нем debug mod и тогда уже разбираемся, что с ним было не так.
В итоге это довольно серьёзно нам помогает с точки зрения ELK-стека, который достаточно прожорлив. На некоторых критичных сервисах мы дополнительно делаем агрегацию ошибок. То есть сервис сам понимает, что для него очень критичная ошибка, что — средне критичная, и сбрасывает это всё в Sentry.
Она достаточно умно умеет агрегировать эти ошибки, сводить по определенным метрикам, делать фильтры по базовым вещам. На ряде сервисов мы это используем. Причем мы это начали использовать со времён, когда у нас были монолитные приложения, которые и сейчас есть. Сейчас вводим на каких-то сервисах именно на микросервисной архитектуре.
Самая интересная штука. Как мы всю эту кухню масштабируем? Здесь нужно рассказать некую вводную. К каждой нашей машине, которая обслуживает проект, мы относимся как некому чёрному ящику.
У нас есть система оркестрации. Мы начали с Nomad. Хотя нет, на самом деле мы начинали с Ansible, со своих скриптов. В какой-то момент этого стало не хватать. К тому времени уже была какая-то версия Nomad. Мы посмотрели, она подкупила нас своей простотой. Мы решили, что это та штука, на которую сейчас можем переехать.
Попутно с ней появился Consul, как registry для service discovery. Также Vault, в котором мы храним секретные данные: пароли, ключи, всё секретное, что нельзя хранить в Git.
Таким образом у нас получилось, что все машины стали условно одинаковы. На машине есть Docker, на ней есть Consul-агент, Nomad-агент. Это, по большому счёту, готовая машина, которую можно брать и копировать один в один, в нужный момент вводить в строй. Когда они становятся не нужны, можно выводить из эксплуатации. Тем более, если у вас cloud, то вы в пиковые моменты машину заранее можете подготовить, включить. А когда нагрузка упала обратно, выключить. Это достаточно серьезная экономия.
В какой-то момент Nomad мы переросли. Переехали на Kubernetes, а Consul стал играть роль системы центральной конфигурации для наших сервисов со всеми вытекающими.
Мы подошли к тому, что у нас сложился какой-то стек для того чтобы автоматически масштабироваться. Как мы это делаем?
Первый шаг. Мы ввели некоторые лимиты по трём характеристикам: память, процессор, сеть.
Мы зафиксировали три градации по каждой из этих величин. Нарезаем некие кирпичики. Как пример:
R3-C2-N1. Мы некий сервис ограничили, дали ему совсем чуть-чуть сети, чуть больше процессора и много памяти. Там какой-то прожорливый сервис.
Мы вводим именно мнемоники, потому что конкретные значения мы можем динамически подкручивать достаточно в широком диапазоне уже в нашей системе, который мы называем decision service. На текущий момент эти значения примерно такие:
На самом деле у нас есть еще C4, R4, но это те значения, которые совсем выходят за рамки вот этих стандартов. Они оговариваются отдельно.
Это выглядит примерно так:
Следующий подготовительный этап. Мы смотрим, какой тип масштабируемости у этого сервиса.
Самый простой — это когда у вас сервис полностью независим. Вы этот сервис можете линейно клепать. Пришло в 2 раза больше пользователей — вы в 2 раза больше инстансов запустили. У вас снова всё хорошо.
Второй тип — это когда у вас масштабируемость зависит от внешних ресурсов. Грубо говоря, этот сервис входит в базу. У базы есть определенная возможность обслужить какое-то количество клиентов. Вы должны это учитывать. Либо вы должны понимать, когда у вас начнётся деградация системы и больше инстансов вы не сможете добавлять, либо просто каким-то образом понимать, насколько вы в это уже сейчас можете упереться.
И третий, самый интересный вариант — это когда вы ограничены какой-то внешней системой. Как пример — внешний биллинг. Вы знаете, что более 500 запросов он никак не обслужит. И хоть ты 100 своих сервисов запусти, всё равно 500 запросов в биллинг, и привет!
Эти лимиты мы тоже должны учитывать. Вот мы поняли, к какому типу у нас относится сервис, поставили соответствующий тэг и дальше смотрим, как это проходит по-нашему пайплайну.
Стандартно мы на CI-сервере собрали, запустили какие-то юнит-тесты. На тестовом окружении у нас у нас прошли интеграционные тесты, у нас тестировщики что-то проверили. Дальше мы перешли к нагрузочному тестированию в пре-продакшне.
Если у нас сервис первого типа, то мы берём инстанс, его запускаем в этой изолированной среде и даём на него максимальную нагрузку. Делаем несколько раундов, берём минимальное число из полученных значений. Кладём его в InfluxDB и говорим, что это тот предел, который в принципе возможен для этого сервиса.
Если у нас сервис второго типа, то здесь мы запускаем эти инстансы в прирастании в каком-то количестве, пока не увидим, что началась деградация системы. Мы оцениваем, насколько она быстрая или медленная. Здесь мы делаем выводы, если мы знаем какую-то определенную нагрузку на наши системы, то достаточно ли этого вообще? Есть ли запас, который нам нужен? Если его нет, то мы уже на этом этапе ставим алерт и этот сервис не выпускаем в продакшн. Мы говорим разработчикам: «Ребята, вам либо нужно что-то шардировать, либо еще вводить какой-то инструментарий, который позволил бы более линейно масштабировать этот сервис».
Если же мы говорим о сервисе третьего типа, то мы знаем его предел, мы запускаем одну копию нашего сервиса, точно так же даём нагрузку и смотрим, сколько этот сервис может обслужить. Если мы знаем, например, что предел того же биллинга — 1000 запросов, 1 инстанс обслуживает 200, то мы понимаем, что 5 инстансов — это тот максимум, который сможет это корректно обслужить.
Всю эту информацию в InfluxDB мы сохранили. Появляется Decision service. Он смотрит 2 границы: верхнюю и нижнюю. По переходу за верхнюю границу он понимает, что нужно добавлять инстансы, а может, даже машины для этих инстансов. Также верно и обратное. Когда нагрузка падает (ночь), нам не нужно так много машин, мы можем на каких-то сервисах уменьшить количество инстансов, выключить машины и тем самым сэкономить немножко денежек.
Общая схема выглядит примерно так:
Каждый сервис через свои метрики регулярно говорит, какая на нём текущая нагрузка. Она уходит в ту же InfluxDB. Когда Decision service видит, что для этой конкретной версии этого конкретного инстанса мы подходим к порогу, он уже даёт команду Nomad или Kubernetes на то, что нужно добавлять новые инстансы. Возможно, он перед этим инициирует новый сервис в cloud, возможно, ещё какую-то подготовительную работу делает. Но суть в том, что он инициирует то, что нужно поднять новый инстанс.
Если видно, что мы скоро достигнем предела по каким-то лимитированным сервисам, то он поднимает соответствующий алерт. Да, мы условно с этим ничего не можем делать, кроме как копить очередь или еще что-то, но по крайней мере мы знаем, что у нас скоро может быть такая проблема и уже можем начинать к ней готовиться.
Это вот то, что касается масштабирования в каких-то общих вещах. И вся эта петрушка с кучей сервисов, она в итоге привела к тому, что мы посмотрели на еще такую штуку немного сбоку — это Gitlab CI.
Традиционно мы собирали наши сервисы через TeamCity. В какой-то момент мы поняли, что у нас есть один шаблон на все сервисы, потому что каждый сервис уникален, он сам знает, как себя закатать в контейнер. Плодить вот эти проекты стало достаточно сложно, их стало много. А описать это в yml-файле и положить вместе с самим сервисом оказалось достаточно удобно. Поэтому эту штуку мы постепенно внедряем, пока на совсем чуть-чуть, но перспективы интересные.
Ну собственно то, что хотелось бы сказать себе, когда мы начинали всю эту штуку.
Во-первых, это то, что если мы говорим о разработке именно микросервисов, то первое, что я бы посоветовал — это начинать сразу с какой-то системы оркестрации. Пусть самой простой, как тот же Nomad, который вы запускаете с командой nomad agent -dev
и получаете полностью готовую систему оркестрации, сразу с поднятым Consul, с самим Nomad и всей этой кухней.
Это даёт понимание, что вы работаете в некоем чёрном ящике. Вы стараетесь сразу отойти от того, чтобы завязываться на конкретную машину, привязываться к файловой системе на конкретной машине. Что-то такое, это сразу несколько перестраивает ваше
И, естественно, сразу на этапе разработки у вас должно быть заложено, что у каждого сервиса — не меньше двух инстансов, иначе вы не сможете так легко и безболезненно отстреливать какие-то проблемные сервисы, какие-то вещи, которые создают вам сложности.
Следующий момент — это, конечно же, некие архитектурные вещи. В рамках микросервисов одна из самых важных таких вещей — это шина сообщений.
Классический пример: у вас есть регистрация пользователя. Как её сделать самым простым образом? Для регистрации нужно создать аккаунт, завести пользователя в биллинге, нужно сделать ему аватарку и что-то ещё. Вот у вас некое количество сервисов, у вас приходит запрос в некий такой суперсервис, а он уже начинает раскидывать запросы всем своим подопечным. В итоге он каждый раз все больше и больше знает о том, какие сервисы ему нужно дернуть для того, чтобы полностью сделать регистрацию.
Гораздо проще, надёжнее и эффективнее это сделать по-другому. Оставить 1 сервис, который делает регистрацию. Он зарегистрировал пользователя. Затем вы в эту общую шину выбрасываете event «я зарегистрировал пользователя, ID такой-то, минимальная информация такая-то». И это получат все сервисы, которым эта информация полезна. Один пойдёт аккаунт в биллинге сделает, другой приветственное письмо отошлёт.
В итоге у вас система потеряет такую жёсткую связность. У вас не будут появляться такие суперсервисы, которые знают про все и про всех. Это на самом деле очень упрощает оперирование с такой системой.
Ну и то, что я уже упоминал. Не нужно пытаться чинить эти сервисы. Если у вас с каким-то конкретным инстансом проблема, постарайтесь её локализовать, перевести трафик на другие, может быть, только что поднятые инстансы. А затем уже разбираться, что не так. Жизнеспособность системы от этого значительно улучшится.
Естественно, для того, чтобы понять, что происходит с вашей системой, насколько она эффективна, нужно собирать метрики.
Здесь важный момент: если вы не понимаете какую-то метрику, если вы не знаете, как её использовать, если она вам ни о чем не говорит, то не надо её собирать. Потому что в какой-то момент этих метрик становится миллиард. Вы тратите кучу процессорного времени просто на то, чтобы в них выбрать то, что вам нужно, тратите тонны времени на то, чтобы отфильтровать то, что вам не нужно. Оно лежит мертвым грузом.
Вы понимаете, что какая-то метрика вам нужна — начинайте её собирать. Что-то не нужно — не собирайте. Это очень сильно упрощает оперирование этими данными, потому что их реально очень быстро становится безумно много.
Если вы видите какую-то проблему, то не нужно на каждый чих бросаться что-то делать. В большинстве случаев система должна сама как-то отреагировать. Вам реально нужен алерт только в той ситуации, когда он требует какого-то вашего действия. Если вам не нужно среди ночи бежать что-то там делать, то значит это не алерт, а некий такой warning, который вы приняли к сведению и в каком-то условно-штатном режиме сможете обработать.
Ну собственно всё. Спасибо.
Микросервисы: опыт использования в нагруженном проекте
Автор: TM_content