Фон

По своей сути Yik Yak - это сервис, в котором пользователи могут создавать сообщения в своем местоположении и спрашивать: «Что люди говорят рядом со мной?» Ответить на этот вопрос и вернуть набор контента, релевантного для пользователя, отвечает служба Feed. Продукт развился из реализации PHP, но в этой статье описывается архитектура микросервисов на основе Go, пришедшая на смену PHP.

Цели дизайна

Мы поставили перед собой задачу разработать систему, удовлетворяющую ряду целей:

  1. Поддержка существующих клиентов (ожидающих интерфейса HTTPS / JSON). Новая система должна заменить старую, возвращая канал, аналогичный предыдущему стеку PHP. Это подразумевает некоторые необходимые специфические функции:
    - Аутентификация пользователей, проверка их состояния верификации (пользовательская служба).
    - Поддержка отключения сообщений и пользователей, о которых сообщают пользователи (пользовательская служба).
    - Поддержка целевого сообщения внедрение, которое реализует объявления (данные инструментов).
    - Рассчитайте соответствующие разрешения на взаимодействие (например, разрешите пользователям удалять свои собственные сообщения, но не другие и т. д.).
    - Поддержка простых блоков геозон (для блокировки использования служба в школах).
  2. Предоставьте современную платформу, которая будет поддерживать сложные запросы и схемы ранжирования, адаптирующиеся к пользователю и местоположению.
  3. Создан как микросервис на Go с использованием GRPC.
  4. Будьте надежными, гибкими и быстрыми.

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

Вот упрощенная схема системы:

Вы заметите, что все интерфейсы являются grpc / proto, за исключением интерфейса API клиента ⟷. Мы решили использовать grpc-gateway для создания интерфейса JSON для наших клиентов, но он делает немного больше, чем просто это, он также сохраняет устаревший API, который мы должны были продолжать поддерживать. Там были некоторые поля, всегда жестко запрограммированные на определенное значение, и некоторые компоненты, которым требовалось сопоставление или перевод из протоинтерфейса под ним в стеке. Мы можем написать еще один пост, чтобы обсудить сам по себе микросервис API.

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

Разделы «подготовки», перечисленные на схеме, довольно просты. Проверка запроса сводится к проверке корректности параметров (не nil, диапазон в разрешенных границах, строки, не включающие запрещенные символы, и т. Д.). Блокировка GeoFence - это основная функция платформы Yik Yak: мы блокируем использование приложения для многих школ (средних школ и т. Д.) И делаем это, определяя геопространственную область, из которой мы отклоняем большинство пользовательских запросов.

Точно так же мы проверяем, был ли пользователь заблокирован или не подтвержден, и можем ограничить или изменить то, что мы возвращаем в этих случаях.

Последний этап подготовки - определение области запроса. В приложении Yik Yak клиент отправляет местоположение точки и запрашивает сообщения «поблизости». Эта часть службы должна интерпретировать, что это значит для пользователя. Это была область, в которую мы вложили немало экспериментов. Рассмотрим несколько способов решения этой проблемы:

  • Используется фиксированный радиус (круг) вокруг пользователя. Пользователь в Манхэттене может найти круг в 500 метров «близко» и может найти много сообщений в этом диапазоне, которые кажутся поблизости.
  • Расширяющийся радиус. В то время как наш пользователь из Нью-Йорка может быть доволен радиусом в 500 метров, другой пользователь в менее плотной зоне, например, на пешеходной тропе, найдет мало сообщений в радиусе 500 метров, если вообще найдет их. Мы могли бы расширить диапазон, если не найдено достаточно сообщений, увеличиваясь с 500 до 1000 до 2000 метров и т. Д., Пока мы не достигнем некоторого порогового количества сообщений. Проблема в том, что это создает множитель нагрузки и приводит к значительному изменению задержки запроса.
  • Адаптивный радиус. Чтобы избежать колебаний задержки и нагрузки, мы решили предварительно рассчитать плотность сообщений в активных областях. Благодаря этому подходу мы смогли выполнить быстрый поиск, чтобы получить «подсказку» в виде радиуса для запроса N сообщений. Это метод, который мы использовали и используем сегодня. Однако есть недостаток: предварительный расчет может задерживаться при внезапной активности. Этот подход (как и все, кроме фиксированного запроса) также страдает от того, что поведение приложения несколько непредсказуемо для пользователей.

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

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

Чтобы изучить этап «выборки ингредиентов», давайте посмотрим, как мы построили параллельные действия в Go. В следующем коде мы создаем четыре канала и выполняем три анонимных вызова функций (go func () {…}), которые работают одновременно. В каждом из них, чтобы код оставался простым и читаемым, вызывается другая функция, которая выполняет основную работу с простым возвратом, затем анонимная функция записывает этот ответ в канал, используя простую структуру, которая несет данные и ошибку. Используемый здесь подход, в отличие от передачи канала в функцию, был для нас эффективным шаблоном, поскольку он поддерживал чистоту интерфейсов функций и изолировал управление параллелизмом.

Затем код попадает в цикл for-select, который выполняет блокировку до тех пор, пока какой-либо из каналов не получит данные. Мы приняли решение потерпеть неудачу, если, например, не смогли получить данные пользовательского блока, и вы можете увидеть это в коде. Вместо этого мы могли бы рассматривать эти данные как необязательные, с ошибкой «открыть» вместо «закрыть». В этом случае раннего отказа вы также видите код, отменяющий контекст (cancelCtx ()). Контекст - еще один мощный шаблон в Go, который я не буду вдаваться в подробности здесь, а просто скажу, что он позволяет коду где-то еще в параллельных операциях определять, необходимо ли то, что они делают. For-select ищет такое условие, проверяя ctx.Done (), что позволяет преждевременно завершить весь этот набор действий.

Каждый раз через for-select код проверяет, закрыты ли каналы (ноль). Если все они закрыты, мы прочитали все необходимые данные и можем продолжить.

Возвращаясь к этапам создания канала, на этапе «Смешивание» (продолжение метафоры запекания) мы выполняем простые операции, обрабатывая набор возвращенных сообщений и удаляя нежелательный контент - сообщения, созданные «заблокированными авторами», сообщения, специально заблокированные пользователями ( «Заблокированные сообщения»), сообщения в состоянии оценки злоупотреблений и т. Д.

Мы также добавляем в любой целевой пост, который возвращается (если есть). Обычно они используются для объявлений в области или для уведомления определенного пользователя с некоторой информацией (например, предупреждением о приостановке за плохое поведение).

На последнем этапе мы «упаковываем» данные для возврата пользователю. На этом этапе действия довольно просты:

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

Резюме

Мы рассмотрели общую структуру и базовый поток системы генерации каналов Yik Yak, а также обсудили некоторые аспекты стратегии проектирования и подхода к запросам. Одна область, которую мы здесь не обсуждали, - это фактическая выборка сообщений из службы GeoIndex, которая должна будет дождаться следующего сообщения в блоге.

В целом я был очень доволен гибкостью, которую дала нам наша архитектура. Наряду с нашей экспериментальной структурой мы могли легко опробовать новые стратегии и расширить функциональность. Функции параллелизма в Go и принятые нами шаблоны позволили легко реализовать сложное «смешивание» различных источников данных. Комбинация Golang + GRPC + Proto - хорошая технология, подходящая для архитектуры системы подачи.