Блог Amazon Web Services

Создание масштабируемого бессерверного (serverless) веб-приложения с определением местоположения – часть 2

Оригинал статьи: ссылка (James Beswick, Senior Developer Advocate)

В первой части этой серии было представлено веб-приложение Ask Around Me, которое позволяет пользователям в реальном времени отправлять вопросы другим пользователям, находящимся поблизости. Я описал функциональность приложения и объяснил, как использование фреймворков для одностраничных приложений (single-page application, SPA) дополняет бессерверный бэкенд. Я настроил Auth0 для аутентификации и показал, как развернуть фронтенд и бэкенд. Я также рассказал, как SPA-фронтенды могут отправлять и получать данные, используя как традиционный API, так и обмен сообщениями в режиме реального времени с использованием WebSocket.

В этом посте я рассмотрю архитектуру бэкенда, HTTP API в Amazon API Gateway, а также имплементацию гео-хеширования (geohashing). Исходный код и инструкции по установке доступны в репозитории GitHub.

Архитектура

После установки приложения с помощью инструкций из README.md архитектура бэкенда будет выглядеть следующим образом:

Архитектура бэкенда Ask Around Me

Фронтенд, созданный на Vue.js, взаимодействует с бэкендом в основном с помощью HTTP API, используя Amazon API Gateway. Когда пользователи добавляют вопросы или ответы, соответствующие данные отправляются через конечные точки API для метода POST. В свою очередь, получение списка вопросов или ответов на фронтенде происходит с помощью конечных точек API для метода GET.

Входящие вопросы и ответы добавляются в отдельные очереди Amazon SQS. При появлении сообщений в этих очередях происходит вызов соответствующих функций AWS Lambda, которые обрабатывают данные и сохраняют их в таблицах Amazon DynamoDB. В таблице Questions сохраняются данные геолокации и агрегированная статистика по каждому вопросу. В таблице Answers хранятся ID пользователей и их ответы, чтобы убедиться, что каждый пользователь может ответить на вопрос только один раз.

Когда в таблицу Answers добавляются новые ответы, поток DynamoDB Streams осуществляет вызов агрегирующей Lambda-функции. Она подсчитывает среднее значение ответов для каждого вопроса и агрегирует данные координат в тепловую карту, а затем сохраняет результат в основной таблице Questions. Когда происходит обновление таблицы Questions, её поток DynamoDB Streams вызывает публикующую Lambda-функцию. Она в свою очередь публикует обновления в соответствующую тему (topic) AWS IoT Core, на которую подписан фронтенд приложения.

Использование HTTP API

API Gateway часто используется для интеграции между фронтендом и бэкендом бессерверных веб-приложений. Вы можете выбрать между стандартными REST API и более новыми HTTP API. Этот выбор должен основываться на необходимой функциональности и ценовых аспектах вашей рабочей нагрузки.

Приложение осуществляет JWT-аутентификацию с помощью Auth0 и использует интеграцию Lambda-прокси. Обе эти возможности поддерживаются в HTTP API. При этом, нам не требуются многие продвинутые возможности, такие как управление ключами API, интеграция с Amazon Cognito и планы использования (usage plans). Также, важно сравнить цену на каждый из сервисов:

Тип API В час В день В год
Добавление вопросов (PUT) 1 000 24 000 8 760 000
Получение вопросов (GET) 50 000 1 200 000 438 000 000
Добавление ответов (PUT) 10 000 240 000 87 600 000
Итого API-запросов 534 360 000
Цена REST API $1 870,26
Цена HTTP API $534,36

Используя сделанную в первой части статьи оценку количества запросов к API, вы можете сравнить общие затраты на REST API и HTTP API. При оценке в примерно $534 за год, вариант с HTTP API равен приблизительно 30% от цены на REST API.

Шаблон AWS Serverless Application Model (SAM), находящийся в репозитории, определяет ресурс HTTP API и задаёт конфигурацию CORS. Он также включает в себя авторизацию через Auth0 для валидации каждого API-вызова:

  MyApi:
    Type: AWS::Serverless::HttpApi
    Properties:
     Auth:
        Authorizers:
          MyAuthorizer:
            JwtConfiguration:
              issuer: !Ref Auth0issuer
              audience:
                - https://auth0-jwt-authorizer
            IdentitySource: "$request.header.Authorization"
        DefaultAuthorizer: MyAuthorizer

      CorsConfiguration:
        AllowMethods:
          - GET
          - POST
          - DELETE
          - OPTIONS
        AllowHeaders:
          - "*"   
        AllowOrigins: 
          - "*" 

После определения ресурса HTTP API каждая Lambda-функция ссылается на него в конфигурации вызывающего её события. Все функции, ссылающиеся на ресурс HTTP API, автоматически используют авторизацию через Auth0.

  GetAnswersFunction: 
    Type: AWS::Serverless::Function
    Properties:
      Description: Get all answers for a question
      ... 
      Events:
        Get:
          Type: HttpApi
          Properties:
            Path: /answers/{Key}
            Method: get
            ApiId: !Ref MyApi    

Использование гео-хеширования в веб-приложениях

Ключевой функциональностью приложения Ask Around Me является возможность находить и отвечать на вопросы рядом с пользователем. Учитывая ожидаемый объём вопросов в системе, для этого потребуется эффективный способ выборки по местоположению, который будет сохранять производительность при росте трафика.

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

Более эффективным решением является гео-хеширование. В нём географическая область планеты делится на сеть ячеек, каждая из которых идентифицируется буквенно-цифровым хешем. Первый символ хеша указывает на одну из 32 ячеек сети площадью примерно 5000 км на 5000 км. Второй символ задаёт один из 32 квадратов внутри этой ячейки. Таким образом, используя первые два символа, мы можем получить разрешение примерно в 1250 км на 1250 км. К 12-му символу вы сможете определить площадь, занимающую всего несколько квадратных сантиметров. Более детальную информацию можно узнать на сайте о гео-хешировании.

При использовании этого алгоритма важно выбрать правильное разрешение. Для приложения Ask Around Me фронтенд ищет вопросы на расстоянии до 5 миль (~8 км) от пользователя. Такую область можно определить, используя хеш из 5 символов. Это означает, что вы можете сравнить текущее местоположение пользователя, заданное с помощью гео-хеша, и гео-хеш, хранящийся в таблице Questions. Такое сравнение даст возможность сразу отбросить из результатов поиска большинство вопросов и быстро найти необходимые записи в базе.

В решении используется библиотека npm Geo Library for Amazon DynamoDB. И API с методом GET для вопросов, и API с методом POST используют эту библиотеку для подсчёта гео-хеша при чтении и сохранении вопросов. Для работы библиотеки требуется отдельная таблица DynamoDB, поэтому ответы пользователей хранятся отдельно.

Метод GET в API questions использует широту и долготу из параметров запроса для доступа к данным в DynamoDB с помощью этой библиотеки:

const AWS = require('aws-sdk')
AWS.config.update({region: process.env.AWS_REGION})

const ddb = new AWS.DynamoDB() 
const ddbGeo = require('dynamodb-geo')
const config = new ddbGeo.GeoDataManagerConfiguration(ddb, process.env.TableName)
config.hashKeyLength = 5

const myGeoTableManager = new ddbGeo.GeoDataManager(config)
const SEARCH_RADIUS_METERS = 4000

exports.handler = async (event) => {

  const latitude = parseFloat(event.queryStringParameters.lat)
  const longitude = parseFloat(event.queryStringParameters.lng)

  // Get questions within geo range
  const result = await myGeoTableManager.queryRadius({
    RadiusInMeter: SEARCH_RADIUS_METERS,
    CenterPoint: {
      latitude,
      longitude
    }
  })

  return {
    statusCode: 200,
    body: JSON.stringify(result)
  }
}

Паттерн «издатель – подписчик» (pub/sub) для работы веб-приложений в режиме реального времени

Современные веб-приложения часто используют нотификации в режиме реального времени, чтобы информировать пользователей об изменении состояния. Один из способов достичь этого – регулярно опрашивать API на наличие новых данных. Однако такой подход обычно чересчур расточителен как с точки зрения затрат, так и с точки зрения вычислительных ресурсов, потому что большинство вызовов API не будут возвращать новой информации. Кроме того, если обновления данных распределены по времени равномерно, и вы запрашиваете их каждые n секунд, то средняя задержка между доступностью данных и получением их в вашем приложении составит n/2 секунд.

Вместо опроса API для многих веб-приложений более подходящим решением является WebSocket. Доступность данных при этом ближе к реальному времени, а количество сообщений – меньше. Это может быть важно для веб-приложений, используемых на мобильных устройствах, где ненужные сообщения могут повлиять на скорость разряда батареи.

Такой подход использует паттерн «издатель – подписчик». Фронтенд создаёт необходимые подписки на сервис бэкенда, указывая темы (topics) для нотификаций. Бэкенд получает сообщения от издателей, которые представляют собой процессы, посылающие данные в приложение. Он фильтрует сообщения и направляет их соответствующим подписчикам.

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

Сервис AWS IoT Core упрощает этот процесс, а JavaScript SDK обрабатывает стандартные ошибки переподключения. Бэкенд-приложение отправляет сообщения в темы (topics) в AWS IoT Core, а фронтенд-приложение подписывается на необходимые темы. Сервис ведёт список активных издателей и подписчиков и направляет сообщения между ними. Он также обеспечивает копирование сообщения, которое необходимо при наличии нескольких подписчиков на одну тему.

С точки зрения цены, это также экономически эффективный подход. На момент написания этой статьи AWS IoT Core стоит $0,08 за миллион минут подключения и $1,00 за миллион сообщений. Вам не нужно управлять серверами, при этом сервис автоматически масштабируется в соответствии с нагрузкой вашего приложения.

В приложении-примере подключение в режиме реального времени настраивается и управляется в едином компоненте, IoT.vue. Он инициирует соединение с конечной точкой IoT, когда приложение запускается и ожидает сообщений в подписанных темах. Он отправляет данные назад в глобальное хранилище Vuex, чтобы другие компоненты автоматически получали обновлённую информацию без зависимости от компонента IoT.

Выбор тем для pub-sub в веб-приложениях

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

В рассматриваемом веб-приложении есть ряд тем, которые могут быть важны для пользователей. Некоторые темы являются общими для нескольких пользователей, в то время как другие относятся только к отдельным пользователям:

  • Темы на уровне учётной записи: сообщения, которые относятся к отдельному пользователю, такие как биллинг и нотификации. Они предназначаются для любых устройств, где авторизован указанный пользователь.
  • Темы на уровне вопроса: когда пользователь задаёт вопрос, ему нужны оповещения при добавлении новых ответов. Для каждого вопроса создаётся отдельная тема. Любой, кто задаёт вопрос или следит за ним, подписывается на эту тему.
  • Тема оповещений, ограниченная географически: пользователь получает оповещения, когда в локальной области задаются новые вопросы. В таком случае гео-хеш локации является идентификатором темы. Новые вопросы публикуются в темы по гео-хешу, и пользователи, находящиеся в области с таким же гео-хешем, получают эти сообщения.
  • Общесистемная тема: единая тема, на которую подписываются все пользователи. Она зарезервирована для важных сообщений всем пользователям приложения.

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

Заключение

В этом посте была рассмотрена архитектура приложения Ask Around Me. Я сравнил стоимость и функциональность, которую необходимо принять во внимание при выборе между REST API и HTTP API в API Gateway. Я рассказал о гео-хешинге и о библиотеке npm, которая может работать с геолокационными запросами в DynamoDB. Я также показал, как вы можете встроить обмен сообщениями в реальном времени в ваше веб-приложение, используя паттерн «издатель – подписчик», с помощью AWS IoT Core.

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