Опыт построения инфраструктуры на микросервисной архитектуре
- суббота, 23 февраля 2019 г. в 00:19:59
За последний год публикаций о микросервисах стало так много, что рассказывать что это и зачем нужно было бы пустой тратой времени, так что дальнейшее изложение будет сконцентрировано на вопросе — каким способом бы реализовали эту архитектуру и почему именно так и с какими проблемами столкнулись.
У нас в небольшом банке были большие проблемы: 3 python монолита связанных чудовищным количеством синхронных RPC взаимодействий с большим объемом legacy. Что бы хотя бы отчасти решить все возникающие при этом проблемы было принято решение перейти на микросервисную архитектуру. Но прежде чем решиться на такой шаг нужно ответить на 3 основных вопроса:
Собственно кратким ответам на эти вопросы и будет посвящена данная статья.
Этот, казалось бы простой вопрос, определил в конечном итоге всю дальнейшую архитектуру.
Мы — банк, соответственно вся система крутиться вокруг операций с финансами и различными вспомогательными вещами. Перенести финансовые ACID транзакции на распределенную систему с сагами безусловно можно, но в общем случае крайне трудно. Таким образом мы выработали следующие правила:
Естественно одновременно и полностью удовлетворить их оказалось невозможным, но даже частичная реализация сильно упрощает разработку.
Вариантов множество, но в конечном итоге их всех можно абстрагировать простым "микросервисы обмениваются сообщениями", но если реализовать синхронный протокол (например RPC через REST) то большинство недостатков монолита сохранятся, а вот достоинств микросервисов почти не появится. Так что очевидным решением было взять любой брокер сообщений и начать работать. Выбирая между RabbitMQ и Кафкой остановились на последней и вот почему:
Дополнительно хочу обратить внимание на очень качественное и детальное сравнение.
Очереди на кафке+асинхронность позволяют нам:
В качестве системы сериализации данных мы выбрали AVRO, почему — описано в отдельной статье.
Но вне зависимости от выбранного способа сериализации важно понимать как будет проходить обновление протокола. Хотя AVRO и поддерживает Schema Resolution мы этим не пользуемся и решаем чисто административно:
Сами же схемы AVRO мы храним в git-субмодулях и подключаем ко всем кафка-проектам. Централизованный реестр схем решили пока не внедрять.
P.S.: Коллеги сделали opensource вариант но только с JSON-schema вместо AVRO.
Это специфика модели взаимодействия Publish–subscribe — будучи подписаны на топик подписчик получит их все. В результате если сервису нужны лишь некоторые из сообщений — ему придется их отфильтровать. Если же это станет проблемой то можно будет сделать отдельный сервис-роутер, который будет раскладывать сообщения по нескольким разным топикам, тем самым реализовывать часть функционала RabbitMQ, отсутствующего в кафке. Сейчас у нас один подписчик на питоне в один поток обрабатывает примерно 7-5 тыс сообщений в секунду, если же запускать с через PyPy то скорость вырастает до 11-15 тыс/сек.
В настройках кафки есть параметр ограничивающие время, которые кафка "помнит" на каком месте читатель остановился — по умолчанию 2 дня. Хорошо бы поднять до недели, чтобы если проблема возникает в праздники и 2 дня не будет решена, то это не привело бы к потере позиции в топике.
Если читатель кафки не подтверждает чтение за 30 сек (настраиваемый параметр) то брокер считает что что то пошло не так и при попытке подтвердить чтение возникает ошибка. Чтобы избежать этого мы при длительной обработке сообщения Отправляем подтверждения чтения без смещения указателя.
Если по-честному нарисовать все взаимосвязи в graphviz то возникает традиционный для микросервисов ёжик апокалипсиса с десятками связей в одном узле. Чтобы хоть как то сделать его (граф связей) читаемым мы договорились о следующей нотации: микросервисы — овалы, топики кафки — прямоугольники. Таким образом на одном графе удаётся отобразить и факт взаимодействия и его тип. Но, увы, становится не сильно лучше. Так что этот вопрос всё ещё открыт.
Ещё в рамках монолита у нас были логи в файлах и Sentry Но по мере перехода на взаимодействие через кафку и развертывания в k8s логи переместились в ElasticSearch и соответственно сначала мониторили читая логи подписчика в Эластике. Нет логов — нет работы.
За тем начали использовать Prometheus и kafka-exporter немного модифицировали его дашборд: https://github.com/kkirsanov/articles/blob/master/2019-habr-kafka/dashboard.json
В результате получаем вот такие картинки:
Срезу видно какой сервис какие сообщения перестал обрабатывать.
Дополнительно все сообщения из ключевых (платежные транзакции, нотификации от партнеров и т.п.) топиков копируются в InfluxDB, заведенную в ту же grafana. Так что мы можем не только фиксировать сам факт передачи сообщений, но делать разнообразные выборки по содержимому. Так что ответы на вопросы вида "каково среднее время задержки ответа от сервиса" или "Сильно ли отличается поток транзакций сегодня от вчерашнего по этому магазину" всегда под рукой.
Так же для упрощения разбора инцидентов мы используем следующий подход: каждый сервис при обработке сообщения дополняет его метаинформацией содержащей UUID выданный при появлении в системе и массив записей типа:
В результате по мере прохождения сообщения через вычислительный граф сообщение обогащается информацией о пройденном на графе пути. Получается аналог zipkin/opentracing для MQ, позволяющий получив сообщение легко восстановить его путь на графе. Особую ценность это приобретает в тех случаях, когда на графе возникают циклы. Помните пример с маленьким сервисом, доля в платежах которого составляет всего 0.0001% Анализируя мета-информацию в сообщении он может определить — являлся ли они инициатором платежа, не обращаясь при этом в БД для сверки.