Автор: Вайбхав Синха

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, реализующее алгоритмы машинного обучения на основе ранжирования элементов канала.
  5. Сервер Websocket: это сервер WebSocket, который используется для связи в реальном времени и push-уведомлений на стороне сервера. Написано на Голанге.

Поток запросов следующий:

  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-сервер и веб-служба являются единственными службами, доступными для клиента.

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

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

Протокол связи, который мы хотим использовать, должен поддерживаться дополнительным прокси. Мы используем HTTP, HTTP / 2 и gRPC для связи типа запрос / ответ. Мы используем Envoy, у которого есть первоклассная поддержка всего этого. Envoy также генерирует множество показателей, которые можно перенести на ваш любимый сервер. Он также может выполнять проверки работоспособности и балансировку нагрузки, которые будут обсуждаться в следующем разделе. Поскольку службы общаются только с дополнительным прокси на своем локальном хосте, им не нужно знать, где находится удаленная служба. Службе нужно только указать, с какой удаленной службой ей нужно взаимодействовать. Мы делаем это, устанавливая заголовок 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, Rotor сгруппирует их и сообщит Envoy, что любой из запросов к службе Feed должен поступать в один из этих экземпляров.

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

Резюме

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