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

Любая платформа, которую необходимо масштабировать, чтобы удовлетворить потребности миллионов пользователей и обеспечить лучший в своем классе опыт, должна включать в себя сложные функции и функции. В Unacademy мы реализуем разные, логически обособленные части этой системы в виде отдельных микросервисов.

Однако так было не всегда. Как и многие другие стартапы, мы начали с монолита, который позволил нам быстро внедрять новые функции и выполнять развертывание простым и удобным способом. Теперь, когда мы расширили предложения нашей платформы, а также нашу пользовательскую базу, мы перешли к микросервисам, чтобы независимо масштабировать отдельные части системы и создавать слабую связь между ними.

Хотя микросервисы решают целый ряд проблем, они также имеют тенденцию создавать некоторые новые проблемы, которых раньше не было. Среди прочего, вам теперь придется беспокоиться о:

  1. Как развернуть эти сервисы
  2. Как они узнают друг друга
  3. Как они будут общаться друг с другом
  4. Как будет работать управление версиями

В этом посте я дам общий обзор того, как мы решили разные части этой головоломки.

Когда мы приступили к реализации микросервисов, мы хотели, чтобы реализация была такой, чтобы любой сервис:

  • Знает, как делать одно и только одно
  • Четко определяет API для связи с ним
  • Четко определяет свою зависимость от других сервисов
  • Поддерживает собственные ресурсы, такие как базы данных, очереди и т. д.
  • Разрабатывается и развертывается независимо

Обратите внимание, что мы ничего не упомянули о:

  • На каком языке должны быть реализованы сервисы
  • Какой протокол они должны использовать для связи
  • Должны ли они быть синхронными или асинхронными
  • Какую базу данных, брокера, кеш и т. д. они должны использовать

Это одно из преимуществ микросервисов. Разработчик может выбрать набор инструментов, который лучше всего подходит для проблемы, которую он пытается решить. Поэтому у нас есть сервисы, написанные на Python, Go, Java, NodeJS и т. д., которые используют широкий спектр технологий, таких как MySQL, DynamoDB, Redshift, SQS, SNS, Kinesis, X-Ray и т. д.

Как работают фиды

Чтобы продемонстрировать взаимодействие между различными службами, давайте посмотрим, как фиды предоставляются пользователям.

Когда пользователи находятся на своих домашних экранах, они видят ленту уроков и сообщений. Лента отличается для разных пользователей и генерируется с использованием алгоритмов машинного обучения. Таким образом, сообщение, которое может отображаться вверху для одного пользователя, может отображаться намного ниже для другого. Рейтинг этих элементов генерируется для всех пользователей каждые шесть часов. Следовательно, если пользователь открывает приложение каждые шесть часов, он/она увидит разные элементы в ленте. Но мы не хотим, чтобы наши пользователи видели устаревшую ленту, если они просматривают свою ленту с интервалом менее шести часов. Чтобы решить эту проблему, мы предоставляем текущую версию фида пользователю, когда приходит запрос API, но мы также сразу же начинаем вычислять новый фид. Через несколько секунд, когда новый фид будет готов, мы отправляем клиенту сообщение с просьбой снова получить фид.

Теперь, когда мы знаем функциональность высокого уровня, давайте посмотрим, как это работает.

Различные компоненты этой системы:

  1. Сервер API: это наш основной сервис, на котором размещаются различные API. Он реализован на Python.
  2. Служба фидов. Эта служба обрабатывает всю логику, связанную с фидами. Он реализован на Голанге.
  3. Kinesis. Это управляемый сервис AWS для потоковой передачи данных в реальном времени.
  4. Система рекомендаций. Это задание Spark Streaming, которое реализует алгоритмы машинного обучения на основе ранжирования элементов фида.
  5. Сервер веб-сокетов. Это сервер веб-сокетов, который используется для обмена данными в реальном времени и отправки сообщений на стороне сервера. Он написан на Голанге.

Поток запросов выглядит следующим образом:

  1. Клиент подключается к серверу Websocket и подписывается на каналы.
  2. Затем он отправляет запрос на сервер API с просьбой показать каналы пользователю.
  3. Сервер API передает этот запрос службе Feed.
  4. Служба веб-каналов отвечает текущей версией веб-канала, имеющейся в ее базе данных.
  5. Сервер API выполняет некоторое обогащение и отправляет ответ обратно пользователю.
  6. Служба каналов также отправляет в Kinesis сообщение о том, что для этого пользователя необходимо создать новый канал.
  7. Система рекомендаций считывает событие и вычисляет новую ленту.
  8. Система рекомендаций публикует сообщение на сервер Websocket о том, что для пользователя доступен новый канал.
  9. Сервер Websocket отправляет сообщение клиенту с просьбой снова получить канал.
  10. Клиент снова делает запрос API к серверу API.
  11. Сервер API передает запрос службе Feed.
  12. Служба каналов отвечает последним каналом.
  13. Сервер API обогащает ответ и отправляет его обратно клиенту, тем самым доставляя недавно вычисленный фид.

В приведенном выше описании не учитываются некоторые важные детали, такие как

  1. Есть ли один экземпляр различных сервисов или их много?
  2. Если их много, кто решает, сколько?
  3. Как взаимодействуют службы?
  4. Как между ними выполняется балансировка нагрузки?
  5. Как обнаруживаются различные экземпляры?

Попробуем ответить на эти вопросы. Вот более подробная картина, которая даст больше информации о нашей инфраструктуре.

Высокая доступность и автомасштабирование

Поскольку наши сервисы работают на AWS EC2, мы используем функции автоматического масштабирования, предоставляемые AWS, для запуска или закрытия сервисов в зависимости от нагрузки. Для этого наша установка развертывания создает AMI, который используется для создания группы автоматического масштабирования. Всякий раз, когда необходимо запускать новые экземпляры, они используют этот AMI, на котором уже установлены наша служба и другие агенты. Мы разрабатываем наши услуги таким образом, чтобы их всегда можно было масштабировать по горизонтали. А для обеспечения высокой доступности у нас есть как минимум два запущенных экземпляра всех наших служб. Следовательно, в нашей конфигурации группы автоматического масштабирования мы указываем минимальное количество экземпляров равным 2. Для фактического автоматического масштабирования AWS требуются предупреждения и политики. Например, мы можем захотеть запустить новый экземпляр Feed Service, когда среднее количество запросов на экземпляр превышает 200 в секунду. Когда это происходит, поднимается тревога, которая проверяется автоматическим масштабированием, и запускается новый экземпляр, тем самым уменьшая среднее количество запросов на экземпляр. Эти оповещения основаны на метриках, которые публикуются в AWS Cloudwatch. Каждый из наших сервисов публикует множество показателей, которые можно использовать как для автоматического масштабирования, так и для оценки производительности сервиса. Наши сценарии развертывания позволяют разработчикам предоставлять простые конфигурации, на основе которых во время развертывания автоматически создаются предупреждения и политики. Разработчики могут выбрать автоматическое масштабирование своих сервисов на основе использования ЦП, средней скорости запросов, задержек запросов, длины очереди SQS и т. д.

Связь между службами

Перед любой службой, доступной извне, стоит AWS ALB (Application Load Balancer). Следовательно, клиентам нужно знать только домен DNS, чтобы служба могла с ним взаимодействовать. В случае нашей системы фидов API-сервер и служба Websocket являются единственными службами, доступными для клиента.

Для связи между службами мы используем прокси-сервер. Sidecar — это отдельный процесс, который работает вместе с вашей службой и действует как прокси для всех входящих и исходящих запросов. Sidecar может выполнять некоторые общие функции, такие как ведение журнала запросов, метрики, маршрутизация, балансировка нагрузки, проверки работоспособности, разрыв цепи, преобразование протокола и т. д. Выгодно, чтобы эти функции были реализованы вне нашего собственного приложения, чтобы наши приложения нужно только иметь дело с логикой нашего приложения. Итак, если мы посмотрим на общение между двумя сервисами, оно будет выглядеть следующим образом.

Связь между службами с использованием прокси-сервера Sidecar

Протокол связи, который мы хотим использовать, должен поддерживаться прокси-сервером sidecar. Мы используем HTTP, HTTP/2 и gRPC для нашего типа связи запрос/ответ. Мы используем Посланник, который имеет первоклассную поддержку для всего этого. Envoy также выдает множество метрик, которые можно передать вашему любимому бэкенду. Он также может выполнять проверки работоспособности и балансировку нагрузки, которые будут обсуждаться в следующем разделе. Поскольку службы взаимодействуют только с прокси-сервером sidecar на своем локальном хосте, им не нужно знать, где находится удаленная служба. Службе нужно только указать, с какой удаленной службой ей необходимо взаимодействовать. Мы делаем это, устанавливая заголовок Host для HTTP-запроса или заголовок Authority для запроса HTTP/2. gRPC использует HTTP/2 внизу и, следовательно, также использует заголовок Authority. Ответственность за знание того, где сейчас находятся удаленные сервисы, ложится на коляску.

Мы предпочитаем использовать gRPC для связи из-за безопасности типов, которую он обеспечивает, и из-за преимуществ в производительности, которые он обеспечивает за счет установления долгоживущего соединения, по которому мультиплексируются несколько запросов. Поскольку клиенты gRPC создаются автоматически, мы получаем преимущества безопасности типов на разных языках без необходимости писать клиенты вручную. Protobuf, используемый gRPC, также служит хорошо документированными и версионными контрактами, предоставляемыми службой.

Для асинхронной связи мы обычно используем SQS. Службы помещают сообщения в определенную очередь, которая считывается несколькими экземплярами службы-потребителя.

Обнаружение служб, балансировка нагрузки и проверка работоспособности

Чтобы иметь возможность взаимодействовать с одним из экземпляров службы, нам нужно знать IP-адрес и порт машины, на которой запущена служба. Кроме того, поскольку наши службы являются частью автомасштабирования, экземпляры приходят и уходят, и мы не можем поддерживать статический список IP-адресов, на которых размещена конкретная служба. Что необходимо, так это динамическое обнаружение служб.

В случае внешних сервисов текущий список экземпляров сервиса поддерживается AWS. Каждый раз, когда новый экземпляр запускается с помощью автомасштабирования, он добавляется в целевую группу для этой группы автомасштабирования. Точно так же экземпляры удаляются из целевой группы, когда они завершаются. ALB направляет запросы всем экземплярам в целевой группе. Следовательно, сервисное обнаружение уже решено для нас.

Для внутренних сервисов мы не используем ALB. Поэтому нам каким-то образом нужно знать обо всех машинах, на которых размещена конкретная служба в любой момент времени. Получив эту информацию, мы должны сообщить об этом Envoy, чтобы он мог направлять запросы к этим службам. Envoy не знает, как выполнять обнаружение сервисов. Он полагается на внешние сервисы, чтобы предоставить ему необходимую информацию. Он предоставляет контракт gRPC, который может реализовать любой сервис, и Envoy попросит этот сервис сообщить ему подробности любого сервисного кластера. Конечно, можно написать реализацию обнаружения сервисов самостоятельно, но уже есть отличная реализация под названием Rotor. Rotor может использовать несколько методологий для обнаружения сервисов. Мы используем их поддержку для сканирования экземпляров EC2 в нашей учетной записи AWS и группировки экземпляров на основе тегов. Следовательно, если у вас есть три экземпляра с тегами, указывающими, что они размещают Feed Service, то Rotor сгруппирует их и сообщит Envoy, что любой из запросов к Feed Service должен направляться в один из этих экземпляров.

Envoy заботится о балансировке нагрузки между этими сервисами. Существуют различные политики балансировки нагрузки, которые можно настроить для Envoy. Для балансировки нагрузки знания о том, где находятся службы, недостаточно. Нам также нужно знать, исправны ли сервисы. Например, если служба Feed Service может работать на трех машинах, она может зависнуть на одной из них и, следовательно, не отвечать на запросы. Мы не хотим, чтобы Envoy отправлял запросы к этому экземпляру, даже если Rotor сообщает, что служба там запущена. Для этого Envoy поддерживает проверки работоспособности сервисов и направляет трафик только на исправные экземпляры.

Резюме

В этом посте мы подробно рассмотрим, как микросервисы разрабатываются в Unacademy. Мы разработали универсальные библиотеки и сценарии развертывания, чтобы дополнить выбранный нами дизайн. Это позволяет разработчикам мгновенно запускать и запускать свои приложения. Переход на микросервисы помог нам справиться с большинством проблем, которые мы пытались решить. Это наши первые шаги в этом направлении, и мы ожидаем еще много экспериментов и разработок в этом направлении. Если вам это интересно, напишите нам на [email protected].