Меня зовут Семен Катаев, я работаю в Авито над процессом перехода от монолитной архитектуры к микросервисам. Переход у нас все еще продолжается, но мне уже есть чем с вами поделиться. Это краткий обзор того, с чем придётся столкнуться, если вы задумались над созданием надежного, масштабируемого, распределённого приложения.
Нам пришлось поменять практически все процессы разработки, провести реорганизацию в компании, освоить новые для нас паттерны проектирования и начать использовать незнакомые инструменты для перехода к микросервисной архитектуре. Об инструментах сегодня и пойдёт речь.
До того как мы стали задумываться о микросервисной архитектуре, у нас было классическое веб-серверное приложение с горизонтальным масштабированием. Весь пользовательский трафик встречала серия Load Balancers, которые решали задачи L3-L4 по модели OSI. Application Layer или L7 по модели OSI мы вынесли на отдельный пул серверов, за которыми находилось приложение, формирующее ответ на пользовательский запрос. За ним стояли базы данных:
Отсюда мы начали двигаться в микросервисы. Так как бизнес развивается, в какой-то момент у нас стало 400+ инженеров, которые ежедневно пишут код, делают задачи и запускают процессы CI/CD. И с ростом сложности проекта начала падать производительность отдельно взятого инженера. Хотя микросервисы — это не единственный путь для развития компании, мы решили использовать его из-за модульного подхода к разработке ПО, когда приложение дробится на много независимых слабо связанных модулей (микросервисов). Ниже классическая схема приложения на микросервисной архитектуре.
Мы начали активно делать новые независимые микроприложения и пришли к тому, что у нас сейчас больше 1500 микросервисов (около 200 из них критичных), 2500 баз данных и 3000 git-репозиториев на стеке Atlassian-продуктов и Bitbucket. Мы проводим до 200 деплоев каждый день.
Сложно найти единое правило или критерий, до какой степени гранулярности дробить приложение. У нас пока ещё есть монолит, работающий с разными инструментами (Postgres, Sphinx, Redis, MongoDB, Nginx). Новые фичи мы делаем в микросервисах: например, реализуем API для мессенджера на всех платформах (десктоп, мобильные устройства), бэк-офис со своими независимыми кронами и демонами. У каждого микросервиса может быть своя база данных, своя команда инженеров, свой техдолг, техлид, бэклог, capacity planning и план развития:
Глядя на эту схему, может показаться, что всё очень просто — делай отдельные микро-приложения и получишь микросервисную архитектуру. Но, на самом деле, всё гораздо сложнее.
Один из вызовов микросервисной архитектуры — сделать так, чтобы не потерять комфортность разработки на таких масштабах, чтобы объем техдолга не рос экспоненциально, и количество «грязной» работы держался на одном уровне. Под «грязной» работой я подразумеваю задачи, когда, например, в Golang v1.15 нашли уязвимость и нам надо обновить 500 микросервисов. Или в 400 микросервисах надо поменять SDK для сбора логов. Всю эту «грязную» работу надо как-то автоматизировать. Не говоря уже про управление 10 тысячами микросервисов.
Runtime для 10 000+ приложений
Откуда взялась цифра 10 тысяч, если до этого я говорил про тысячу микросервисов? У нас все микросервисы скейлятся горизонтально: для слабонагруженных сервисов это минимум 3 экземпляра, а для высоко нагруженных (поиск, рекомендации, сервис пользователей) может быть до 50-100. В среднем получается примерно 10 инстансов на микросервис, и это дает цифру в 10000 микроприложений.
А еще у них есть вспомогательные компоненты. У каждого микросервиса может быть свой pgBouncer, stats-daemon, HAProxy, nginx. Эта цифра может спокойно подняться до десятков тысяч микроприложений, и все их нужно как-то запускать.
Есть похожая алгоритмическая задача по наполнению рюкзака.
По условию у вас рюкзак заданной вместимости и грузы, которые надо в него уложить максимально эффективно. Рюкзаки — это наши сервера, их тысячи. А грузы — это наши микросервисы, и их десятки тысяч. Нужно их так разложить, чтобы не было переполненных рюкзаков, и все они были загружены на одном уровне.
Задача может усложниться правилами: 1) в один рюкзак нельзя положить больше, чем два зеленых груза, 2) к одному синему грузу необходимо добавлять оранжевый и 3) в каждом рюкзаке должен быть один серый груз. Эти правила называются политиками Affinity и Anti-Affinity, и они делают задачу практически нерешаемой вручную. Найти ответ можно перебором и оптимизациями из динамического программирования.
Когда у нас было всего 5-10 микросервисов, DevOps’ы пытались подбирать сервера под каждый микросервис с определенным профилем нагрузки. Но с ростом количества микросервисов делать это вручную стало слишком долго, поэтому нужны инструменты scheduling & Orchestration.
Scheduling & Orchestration
Если вы только хотите идти в микросервисы, обратите внимание на фонд Cloud Native Computing Foundation. Он был анонсирован в 2015 году вместе с Kubernetes 1.0. Его цель — развитие технологий контейнеризации приложений, и его поддерживают множество компаний. Участники фонда развивают те самые инструменты оркестрации и шедулинга, из которых вы можете подобрать себе подходящий:
Я не буду сравнивать, чем они отличаются и какая у них архитектура, а только скажу что верхнеуровнево, все они решают общую задачу. Они все состоят из N серверов, на которых запускаются микросервисы:
Например, в Авито около тысячи серверов в кластере Kubernetes. Есть несколько мастеров, которые управляют этим кластером. У мастеров есть свой storage. В Kubernetes по умолчанию используется etcd (disributed key-value storage). Также мастера предоставляют API для работы command-line interface (CLI) утилит и UI-дашбордов.
Мастер Kubernetes решает за вас, на каких нодах запустить микросервисы. Ноды могут появляться и исчезать, то есть вы можете вводить в эксплуатацию и выводить из эксплуатации новые сервера и микросервисы сами станут «перезапускаться» на наиболее свободных. У вас не будет привязки к конкретному железу. Это особенно эффективно на масштабах от тысячи серверов/приложений.
Container Runtime
Когда у вас вместо одного монолита сотни независимых приложений на одном сервере, надо как-то распределять ресурсы. Чтобы не было «плохих» соседей, когда один микросервис начинает утилизировать все CPU сервера, забивает всю сеть или память, не давая работать другим микросервисам.
Чтобы этого избежать, нужна контейнеризация приложения. Это метод виртуализации, когда приложение запускается в изолированных пространствах пользователей. Вы можете для каждого микросервиса указать, сколько ресурсов CPU, сети, памяти он может использовать. Если сервис пытается использовать, например, больше CPU, то он ограничивается, чтобы оставалось процессорное время для работы других микросервисов.
В Cloud Native Computing Foundation есть множество инструментов для контейнеризации:
Фонд пропагандирует модульный подход — делать так, чтобы каждый компонент можно было заменить. Поэтому все инструменты для Container Runtime заменяемые, верхнеуровнево похоже работают и предоставляют одинаковый API (container runtime interface).
Сам микросервис в контейнере может быть не только одним запущенным приложением, а, например, представлять собой собранный бинарник Golang или php-fpm демон. Также в инстанс микросервиса может входить nginx, haproxy для high availability до внешних ресурсов, pgBouncer, Redis как key-value storage (для каждого экземпляра микросервиса тоже можно так делать), rsyslog для сбора логов. И все эти N контейнеров будут одним инстансом приложения.
Как я уже говорил, мы масштабируемся горизонтально, равномерно распределяя по ним. На схеме 6 реплик одного микросервиса, по которым равномерно будет распределять нагрузка.
Так как микросервисы — это, по сути, наботы контейнеров, а контейнер — это запущенное приложение на базе какого-то образа (image), то нужно где-то хранить собранные образы приложений.
Container Runtime/Registry
У фонда для хранения образов контейнеров тоже большой выбор инструментов:
Они все взаимозаменяемые, поэтому вы можете подобрать любой Registry, с которыми будет комфортно работать. Мы остановились на Harbor. Это Cloud Native Registry. У него есть ролевая модель доступа, политики репликации, RESTful API, дашборд, бэкапы и достаточно мощное решение.
API Gateway
Теперь представим, что вы сделали два микросервиса, например, Product Page и Messenger. У каждого своя база данных, своя команда, свой бэклог — все идеально. За исключением того, что пользователи не знают, какие у вас есть микросервисы. Пользователь просто делает запрос по url вам нужно научиться как-то роутить трафик на конкретный микросервис. Эту задачу решает API Gateway:
Но роутинг — это не единственная его задача. Есть еще сотни инфраструктурных задач, которые можно положить в API Gateway:
Application firewall/Rate limits;
Authentication;
Metrics/monitoring/logging;
SSL termination;
Secure breaker;
Retry;
Caching и т.д.
Вы можете подобрать себе API Gateway на любом стеке технологий, который вам нравится. В фонде Cloud Native Computing Foundation есть пул опенсорсных проектов API Gateway, которые всё это уже умеют делать:
Например, Kong Gateway — это платформо независимый инструмент. У него есть готовые сборки для Kubernetes, Docker Swarm, Mesos и его можно запускать на bare metal серверах. В нем есть динамический алгоритм балансировки трафика, встроенные secure breaker, healthcheck, активный и пассивный мониторинг upstream, интеграции с разными 3rd party DNS-резолверами (Consul), встроенный auth2.0), механизмы авторизации пользователя (от jwt-токенов до обычной сессионной куки). А на GitHub вы можете найти много плагинов, которые расширят его возможности.
Или вы можете взять Sentinel, написанный на Java, KrakenD, написанный на Golang, либо Ambassador, там смесь Golang и Python. Из коробки вы получаете сотни возможностей для ваших API Gateway, поэтому не изобретайте велосипед — догнать эти готовые решения очень сложно.
У нас API gateway самописный, но это потому, что мы начали переход к микросервисам очень давно и тогда еще не знали, что есть коробочные решения. Первый commit примерно совпадает па дате с первыми комитами на гитхабе в Kong gateway. Пользовательский трафик с внешних балансеров L3-L4 проксируется в k8s на ingress API-Gateway, который и является балансером L7 или application layer:
Три нижних микросервиса могут быть написаны на разных языках (Python, Golang, PHP). И, если, к примеру, им всем нужна аутентификация пользователей, то эту логику придется копировать и портировать на разные языки, а можно сделать так, что API Gateway будет делать это самостоятельно. То есть, по пользовательскому запросу поймет, кто пришел, разберет сессионную куку, сходит с ней в storage и проверит активность пользователя, а также не заблокирован ли он.
API Gateway подмешивает заголовок userID в нижестоящие сервисы, поэтому они ему доверяют. И, конечно, API Gateway фильтрует заголовки, которые пользователь не может поставить сам.
Межсервисные взаимодействия
Помимо прямого пользовательского трафика у вас будет много межсервисных взаимодействий. На один пользовательский запрос в среднем порождается примерно 5-10 каскадных запросов. У нас, например, есть тяжелые страницы типа карточки объявления или подачи объявления, где для полноценной отрисовки страницы задействованы 50 микросервисов.
Поэтому межсервисные взаимодействия выходят на первое место и важно сразу договорится о следующем:
Какой у вас будет протокол взаимодействия между сервисами?
Как вы будете подписывать запросы?
Использовать готовый sdk/client для сервисов или кодогенерация клиентов, чтобы не делать прямых curl-запросов из кода вашего приложения.
Договоритесь о кодах ошибок и отделите ошибки сетевого уровня от ошибок доменного уровня у микросервисов.
Создайте в компании документ, описывающий принципы межсервисных взаимодействий.
Имейте в виду, что после того, как вы выберете протокол общения между микросервисами, вы, скорее всего, уже не сможете его поменять. Поэтому зайдите на страницу фонда Cloud Native Computing Foundation и посмотрите готовые RPC фреймворки с поддержкой разных языков программирования:
Так вы из коробки получите поддержку для Java, С++, Golang, Python и других языков. Обратите внимание на gRPC либо на DUBBO — это хорошие RPC-фреймворки из коробки. У них есть механизмы для тестирования, отладки, faild injection, мониторинга и сбора трейсов.
Service proxy
Предположим, протокол для общения между микросервисами вы выбрали. И один ваш микросервис запрашивает второй по API:
Конечно, мы можем договориться и надеяться, что все инженеры будут честно прокидывать trace ID, покрывать межсервисные взаимодействия метриками и мониторингами, настраивая алерты. Но если у вас 400 инженеров, то договоренности быстро забываются, теряются или ломаются. Ещё их невозможно обновлять и менять, потому что придется вносить изменения в тысячу микросервисов — это дорого и долго.
С помощью паттерна Service proxy у каждого инстанса микросервиса поднимается sidecar proxy, и весь трафик роутится через него. То есть у каждого экземпляра сервисов этот sidecar proxy локально собирает информацию, агрегирует и сбрасывает в какое-нибудь хранилище. Например, централизованно собирает у каждого сервиса все трейсы межсервисных взаимодействий и отправляет их в систему хранения трейсов по Open Tracing протоколу. Не изобретайте свои Service proxy. Посмотрите на фонд Cloud Native Computing Foundation и вы увидите готовые Service proxy, которые легко встраиваются в любую инфраструктуру, будь то Kubernetes, Mesos или Docker Swarm:
Мне больше всего нравится Envoy (на С++) и Traefik (на Golang). Это самое правильное место для выполнения retry, secure breaker, tracing, metrics, и monitoring.
Tracing
Для трейсинга Фонд Cloud Native Computing Foundation также предлагает много инструментов, которые также легко интегрируются между всеми вышеперечисленными:
Например, мы у себя взяли Jaeger. Он из коробки подключился к Service proxy и зашел в Kubernetes. Это готовые кирпичи, из которых можно усилить вашу микросервисную архитектуру.
Так выглядят трейсы у нас:
По request ID мы можем не только восстановить хронологию, какие сервисы были задействованы (у нас, например, на карточке объявления 50 микросервисов). Трейсы можно расширять из приложения. То есть дополнять их из самого кода приложения. Например, обернуть запрос в БД таймингом либо в походы во внешние ресурсы, либо просто оборачивать таймером тяжелые задачи, помечать их Request ID и отправлять в систему хранения tracing.
Service mesh
Так как межсервисные взаимодействия вышли на первое место, то мы, конечно, можем договориться, что в каждом из тысячи микросервисов будем использовать одну Service proxy. И надеяться, что инженеры будут это делать. Но можно пойти дальше и использовать пул инструментов Service mesh, который будет централизованно оркестрировать всеми прокси:
В Авито 10 тысяч прокси, и мы из командной строки управляем ими для всех сервисов. Например, можем запретить ходить в этот микросервис всем, кроме одного микросервиса, либо централизованно управлять их расположением. Я могу сделать А/В тесты между микросервисами или Canary релизы для них. Это централизованный пульт управления для всех прокси.
Шаблоны сервисов
Итак, мы с вами посмотрели множество инструментов для построения микросервисной архитектуры.
И мы начали создавать шаблоны микросервисов на php, go, python, nodeJS — основных языках из нашего техрадара. Мы сделали их максимально платформонезависимыми. В коде шаблона микросервиса нет ни слова про Kubernetes, Service proxy или Service mesh. Есть только app.toml — описание микросервиса:
app.toml
name = "new-awesome-service"
description = "ведите краткое описание сервиса"
kind = "business"
replicas = 3
[env_vars]
# Clients
SERVICE_ITEM_TOKEN = "my_private_token"
SERVICE_ITEM_TIMEOUT = "2"
SERVICE_GEOGRAPH_TIMEOUT = "2s"
[engine]
version = "1.17"
size = "medium"
name = "golang"
[postgresql]
enabled = true
version = "12.1"
size = "large"
use_sample = true
dwh.tables = [
{ schema = "lf", name = "waiting_items" },
]
[postgresql.data_bus]
batch_size = 10
schemas = [
"billing.service_applied",
"billing.service_cancelled",
"listing_fees.item_refund",
"lf_waiting.items",
"lf.tariff-contract.create",
]
[redis]
enabled = true
version = "5.0"
size = "medium"
[[workers]]
replicas = 1
name = "limitsd"
command = "limitsd"
size = "small"
[envs.prod]
replicas = 10
[envs.dev]
replicas = 1
[envs.test]
replicas = 1
Здесь есть название микросервиса, по которому определяется контроль доступа, и всё это идет в метрики и мониторинг. В сервисе engine описывается, на каком языке написан микросервис и его размер, чтобы понимать, сколько ему выделять CPU или памяти. Команда может выбирать ресурсы для своего микровервиса из стандартных коробочных решений (большой, средний или маленький). Мы автоматизировали и создание БД, теперь можно записать секцию Postgres, и в продакшене автоматически поднимется база данных, также как и в dev/ staging окружениях.
Пока остановимся
Я не рассмотрел еще PaaS, database, streaming & messenging, cloud native storage, logging и многое другое, потому что для этого потребуется еще несколько статей. Например, над PaaS у нас работает целая отдельная команда. Но есть более подробный доклад про наш Platform as a Service от Саши Лукьянченко.
Скажу только, что инструменты streaming & messenging вам понадобятся, чтобы построить надежную распределенную масштабируемую архитектуру. В микросервисах вы теряете возможность легко поддерживать консистентность данных, поэтому приходится идти в eventually consistency, и без готовых инструментов для streaming & messenging вы не построите надежное приложение. У себя мы используем Kafka и Pulsar.
Здесь тоже есть готовые сборки под БД для построения надежных распределенных масштабируемых решений. Есть и коробочные PaaS, например, Heroku. Есть Cloud Native storage (S3, CF, MinIO), инструменты для логирования, которые также легко встраиваются в экосистему из коробки и интегрируются со всеми остальными инструментами. Всё это вы тоже сможете найти в фонде Cloud Native Computing Foundation.
В общем, микросервисы — это дорогая технология, и, если вы хотите туда идти, вам придется начать использовать множество разных инструментов. Это хороший вызов для ваших DevOps и архитекторов, и я бы советовал несколько раз подумать, прежде чем туда отправляться.
Видео моего выступления на HighLoad++ 2021:
В рамках Highload++ Foundation 2022 пройдет трек Яндекса — 2 полных дня, объединенных разными темами. 17 марта мы погрузимся в надежность и отказоустойчивость, решая проблемы больших систем. А 18 марта обсудим Machine learning в Highload. Расписание зала Трантор уже опубликовано.
А еще сейчас идет открытое голосование по Open Source трибуне, где определятся 5 лучших решений. Отдайте свой голос за то, что вам нравится и помогите определить лучших!
До встречи на HighLoad++ 17 и 18 марта в Москве. Информацию о докладах и билетах смотрите на сайте конференции.