Привет, я — Дмитрий Немеш, СТО в компании Lalafo, волонтер в GeekHub, более 7 лет работаю с PHP/Java/Node.js. В этой статье расскажу, как мобильный С2С маркетплейс переходил на микросервисы и с какими вызовами столкнулся.
Lalafo — это приложение для покупки и продажи б/у вещей, авто и поиска недвижимости, работы и услуг. В нем применяется машинное обучение и компьютерное зрение для распознавания товаров на фото, выявления мошенников, улучшения релевантности контента. Сегодня Lalafo активен на 4 рынках, на 3 из них сервис стал мобильным маркетплейсом № 1. Сегодня проект обрабатывает более 900 запросов в секунду, и его машины нагружены на
Изначально проект был построен на монолитной архитектуре. После детального анализа монолита оказалось, что продукт имел ряд проблем. Это проблемы как с самим качеством кода, так и с возможностью прочитать и понять чужой код, так как над проектом последовательно работало две команды.
Почему микросервисы
Перед нами стояла задача объединить 7 баз данных в одну. Изменить базу данных означало полностью переписать проект, потому что при этом меняется структура таблиц, сущностей и сама логика. Прежде чем принять решение, куда двигаться, мы решили проконсультироваться с высококлассными разработчиками из highload проектов. В результате пришли к
- переписать монолит;
- перейти на сервисно-ориентированную архитектуру;
- перейти на микросервисно-ориентированную архитектуру.
Микросервисы импонировали больше остальных. Поэтому мы пообщались с людьми, которые уже внедрили микросервисы, с людьми которым не удалось внедрить микросервисы, и оценили с какими проблемами столкнемся. В результате решили остановиться на микросервисах.
Основные вызовы
Нам нужно было объединить 7 баз данных из
И в дополнение, нам нужно было сохранить SЕО трафик. Это значит, что все проиндексированные ссылки должны были остаться доступными по тем же адресам с теми же ID, что и до миграции.
Какие бывают модели микросервисов
Различают 3 модели микросервисов: звездоподобная, модель Twitter, сервисно-ориентированная. Для Lalafo мы остановились на последней и вот почему.
Звездоподобная— это когда у вас в центре есть остаток монолита, или супермикросервис, который коммуницирует определенную бизнес-логику или задачу на микросервисы. Это самая популярная модель, когда есть остаток или полноценный монолит, но определенную часть логики выносят в микросервисы. Нормальная система, но, к сожалению, она нам не подходила. Нам нужно было все переписывать с нуля.
Модель Twitter— мы ее использовали. У нас весь API версионный: версия 1, версия 2 использует как раз эту модель. Есть фронт-микросервис, он работает как front controller в MVC фреймворках. Он принимает на себя все запросы, и он же их верифицирует, анализирует и перенаправляет на другие микросервисы. Все хорошо, замечательно, нам нравится, но на фронт-микросервис приходилось слишком много трафика. Поэтому возникло желание его переписать. У нас он написан на PHP, оптимально было бы перейти на Rust или Go, чтобы он работал быстрее. Хотя на тот момент проблем с производительностью не было, прежде чем переписывать его мы подумали, а может есть какой-то другой вариант? И попробовали сервисную модель.
Сервисно-ориентированная.Сервисная модель заключается в том, что сам клиент обращается в те микросервисы, которые ему нужны. Эта модель хорошо сработала для нас, правда только ее
- Сложность администрирования.Все наши микросервисы работают в приватной сети, и к ним никак нельзя достучаться извне. Чтобы взаимодействовать с микросервисами, нужно прописать путь на балансере. На наших 24 микросервисах уже более сотни этих роутов. Что значительно усложняет жизнь сисадмину.
- В работе с микросервисом появляется элемент сервиса.Если микросервис занимается непосредственно одной связанной доменной областью или маленькой задачей: объявление, пользователь или еще что-то, то микросервис начинает сам определять наличие доступа пользователя к определенному ресурсу. Для этого микросервис ходит на юзер-микросервис, вытаскивает данные, проверяет их, верифицирует и после этого дает ответ. Из-за такой логики микросервисы начинают постепенно разрастаться в полноценные сервисы и заниматься тем, чем по идее не должны.
Как мы мигрировали
Когда мы решили начать разработку, нам нужно было понять, можем ли мы работать с микросервисной архитектурой. Для этого мы разработали SDK (Logs, InfluxDB, Services, Helpers, HttpClient), который помогал синхронизироваться с разными микросервисами и ускорить разработку. Также мы разработали инструменты, которые помогали работать с микросервисами в виде ORM. Все это было сделано, чтобы разработка осталась максимально похожей на привычную разработку монолита. В результате разработчикам было несложно привыкнуть к ней, так как стиль кода оставался похожим на монолитный.
Мы также адаптировали компоненты фреймворка, которые мы использовали для access control, users, логирования. В итоге разработчик использовал все необходимые компоненты как и раньше. Данный подход помог нам за 3 месяца написать полностью рабочую MVP версию проекта, который писался более
Мы объединили 7 баз данных с помощью микросервиса «migrate», также он умел доливать данные. Этот микросервис отсылал всю историю старых и новых ID на микросервис «map», по которым строилась карта старых и новых идентификаторов. Это решало проблему обратной совместимости и сохранения SEO трафика. Также мы зарезервировали диапазон идентификаторов для старых данных, чтобы можно было определить, где данные с монолита, а где те, которые уже созданы в новой архитектуре. После этого мы создали еще один микросервис, который разнес данные из большой базы по отдельным базам данных микросервисов. В итоге мы получили данные, где все записи сохранили все отношения друг с другом, но имели новые ID и карту для получения данных по старым ID. Диапазон-разделитель ID помог нам понимать, какие данные нужно доставать по карте, а которые оставить как есть.
Процесс перехода
Migrate микросервис запускался для конкретной страны (рынка), переливал данные в общую базу, создавал карту связей и потом разносил данные по базам отдельных микросервисов. Следующим шагом было переключение на балансере трафика с монолита на микросервисы. В финале мы доливали данные, которые создались или изменились во время окончания первой заливки и переключения трафика.
Stack, который мы выбрали:
- PHP 7, Yii2, Codeception;
- Python — data-science стек для компьютерного зрения и машинного обучения;
- NodeJS — comet server для сокетов;
- PostgreSQL — основная база данных,
- MongoDB, Cassandra, Google BigQuery — БД для наших кастомных аналитик;
- Redis — кеш и сессии;
- ElasticSearch — полнотекстовый поиск;
- RabbitMQ, Kafka — очереди и message bus для аналитики;
- InfluxDB+Grafana — метрики;
- Graylog2, Zabbix — логирование и мониторинг системы;
- GitLab, Kubernetes, Docker, CloudFlare;
В результате мы пришли к такому количеству микросервисов:
Core: user, catalog, chat, sender, moderation, payment, security, fraud.
Supplementary: page, location, SEO, translation, Migrate-app, map, mobile-api, cache, analytics, upload, file node.
AI: classify, classify-analytics, duplicates, image processing, content filtering.
После того, как ты всю жизнь писал монолиты, ты садишься за микросервисы и возникают три категории ощущений:
- Идеально! Ты работаешь с некоторыми из них и думаешь: «Круто, это действительно работает!».
- С другими ты думаешь: «Что-то не так, эти несколько микросервисов можно было объединить в один».
- Ты видишь, что микросервис работает плохо. И это не проблема микросервиса.
Идеальные микросервисы: translations, sender, analytics, security, upload, classify.
Микросервисы, которые хочется объединить: user, catalog, location.
Микросервисы, которые хочется переосмыслить: fraud, moderation.
Важность тестирования
С микросервисной архитектурой невозможно жить, не используя автотесты. Мы используем гибридные Codeception тесты, смесь Acceptance (REST Module) с Functional тестами — это оптимальный вариант. У нас получаются легко читабельные Acceptance тесты, простые в написании и поддержке, плюс с фикстурами и моками, как в функциональных тестах. На их прогон нужно немного времени, в связи с чем у нас быстрый continuous integration — у нас маленькие микросервисы, у которых маленькие наборы тестов, непосредственно связанные с этим микросервисом. Как только какое-то изменение произошло, continuous integration сразу же тестит это изменение независимо от ветки.
Ручное тестирование возможно только с дополнительными инструментами, такими как Postman. В последних версиях можно создавать сценарии и импортировать их, что позволяет нашим тестировщикам в сложных кейсах отсылать не шаги воспроизведения в виде таска в Jira, а сценарий для Postman. Также наша QA команда использует JMeter для автоматизации своих тест-кейсов, тестирования публичного API и нагрузочного тестирования.
Вывод:
- автотесты — must have;
- ручное тестирование — боль;
- Acceptance + functional tests (REST Module);
- Fast continuous integration.
Микросервисная коммуникация
- REST API with http-cache;
- Queue Rebbit MQ/Kafka;
- Global events with Kafka (Message Bus/ Event-drive).
REST API позволяет легко организовывать, оптимизировать и версионировать коммуникацию между микросервисами. Плюс у сисадмина появляются новые инструменты для спасения нас и проекта по ночам. Если что-то пошло не так, сисадмин находит проблемный путь и подключает HTTP кэш на этот путь, проблемный микросервис практически не вызывается, но зато ответы отдает (не всегда актуальные, но это другой вопрос). Мы в это время анализируем логи, фиксим, и при этом приложение не падает.
Очереди Rabbit MQ — за год у нас не было ни одной проблемы с Rabbit MQ. Стабильно и надежно работает, только позитивный опыт. Но надо отдать должное — это заслуга того, что у нас не огромные данные в очереди.
Kafka — крутая штука. Очень напоминает очереди, но это не совсем они, это скорее система логов с возможностью обмена сообщениями. Вы подключаетесь как к обычной очереди и можете отрабатывать один и тот же ивент несколько раз, в зависимости от того, какие микросервисы подписаны на этот топик, так как у каждого микросервиса свой оффсет. Для такой реализации в RabbitMQ вам нужно один месседж бросать в несколько очередей для каждого микросервиса или перебрасывать с одной очереди в другую по мере выполнения. Пропускная способность намного выше, чем в Rabbit MQ. Сейчас мы работаем над тем, чтобы все наши ивенты автоматом попадали в Kafka.
Активные и пассивные микросервисы
Когда человек ведет сразу несколько микросервисов, внимание распыляется. Он может забыть или не успеть сделать тест, что-то пропустить. Поэтому мы пришли к выводу, что оптимальные соотношения это:
- 1 разработчик — один активный микросервис;
- 1 разработчик — 3 пассивных микросервиса, которые он знает.
Микросервис — это то же самое, что и монолит, только в других масштабах.Если есть ошибки — они в микромасштабах. Микросервисы разрастаются, после чего их нужно разбивать на несколько микросервисов. Вы не можете наперед сказать, какой микросервис вырастет, а какой нет.
Стабильность микросервисов
Если следовать рекомендациям по созданию микросервисной архитектуры, у вас должно быть много серверов. Например: eсть микросервис 1 и микросервис 2. На каждый тип или группу микросервисов должен быть балансер, а на каждую группу микросервисов должна быть своя база данных в режиме master-slave. И выходит так, что на каждую группу мы должны выделить от 5 серверов для того, чтобы добиться максимальной отказоустойчивости. Если у вас 20 микросервисов, теоретически должно быть более 100 серверов. Это если мы говорим о своем железе.
У нас было 3 мощных сервера, на которых находились все наши микросервисы. В работе с микросервисами лучше иметь много мелких, чем несколько мощных серверов. Высоконагруженные микросервисы мы выносим на отдельные, более мелкие сервера. В результате мы поняли, что это большой перенапряг по обслуживанию и мониторингу. На сегодняшний день мы перешли на Kubernetes. В процессе перехода и работы нам пришлось попотеть. Результаты хорошие. Нужно инвестировать много ресурсов, но оно того стоит. Нужно отметить, что сервера с базами данных живут отдельно.
Существует мнение, что микросервисы — это мегастабильные системы. Я могу сказать, что стабильность у микросервисов действительно высокая. Когда мы запускались, у проектов были проблемы со стабильностью. Но если вы все правильно делаете с самого начала, в результате у вас будет очень стабильное приложение. Для этого нужно следовать подходу, который говорит, что все микросервисы могут падать и не отвечать. Для того чтобы проверить, правильно ли написаны микросервисы, вы должны отключать микросервисы и смотреть, как другие работают без них. Чтобы они умели нормально работать и имели сценарии на тот случай, если один из них упадет или не ответит. Вот когда вы все эти комбинации попробуете, вручную или автоматом, тогда эта система будет работать мегастабильно.
Выводы
Важные моменты при переходе на микросервисы:
- микропроблемы лучше, чем большие проблемы на монолите;
- контейнеризация и автотесты — must have;
- легкое масштабирование как проекта, так и команды;
- легкость в интеграции новых технологий;
- плавная миграция с монолита лучше, чем force;
- мощная и продвинутая система мониторинга;
- хороший системный администратор или DevOps;
- отдельная тема — это контейнеризация, которая будет раскрыта в отдельной статье. Но если коротко сказать, то использование Kubernetes + Docker + GitLab CI выводит управление и мониторинг микросервисов на другой уровень.
Микросервисы — это идеальная экосистема для экспериментов:
- переписывать весь проект не нужно, благодаря консультациям мы за год не переписывали ни один микросервис полностью;
- Fast CI — быстрый CI помог нам избавиться от многих проблем;
- новый функционал появляется быстрее, чем его могут протестить;
- обратная совместимость всех частей приложения: вы всегда можете откатиться до предыдущей версии и делать тестирование на старой или новой версии;
- возможность масштабирования;
- низкая стоимость ошибки — даже если джуниоры что-то испортили, любой микросервис можно переписать в течение
1-3 дней.
«Распределенный монолит»
Недавно я был на ивенте PHP Friends meetup #4 (Tech Leads Panel), на котором я очередной раз рассказывал о нашем опыте и микросервисах. Я познакомился с Максимом Волошиным из OWOX (очень техничный и опытный специалист) и услышал от него фразу «распределенный монолит» — это дало пищу для размышлений. Так как у нас разнообразные микросервисы — некоторые абсолютно самостоятельные, а некоторые плотно завязаны на другие микросервисы — у меня появилось ощущение, что 2 наших микросервиса (User и Catalog) можно справедливо отнести к распределенному монолиту.
Особенно это стало чувствоваться после изменения основного функционала пару недель назад. Хотя мы и научили эти микросервисы жить друг без друга, в случае падения одного из них зависимость данных очень высокая. Бороться с этим можно и нужно с помощью денормализации данных, но это решение проблемы, а суть — что это не совсем микросервисы, и можно их назвать «распределенный монолит».
У меня в голове выработалось правило: если ты денормализируешь данные с другого сервиса с данными твоего микросервиса, то у тебя не микросервис. Его можно по-разному называть: сервис, распределенный монолит, самодостаточное приложение и т. д. Скользкая приставка «микро» дает много поводов к спорам по поводу трактовки. Из-за этого могу с чистой совестью сказать, что за последние 2 года, общаясь с разными ребятами, которые работают или работали с микросервисами, я увидел, что реализации настолько отличаются друг от друга, что кажется что под словом «микросервис» пытаются преподнести все, что не есть монолит.
У меня есть рекомендация — мигрировать постепенно и выводить в микросервис то, что не имеет зависимостей от других микросервисов или монолита (как функционально, так и на уровне данных). Тогда у вас появится понимание и опыт, после чего вы начнете видеть тонкую грань между микросервисом и всем остальным.