Анализ ограничений веб-приложения на основе опроса

Завершено

Веб-приложение на основе механизма опроса.

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

Сервер

Сведения о ценах на акции хранятся в базе данных Azure Cosmos DB. При активации HTTP-запроса функция getStocks возвращает все строки из базы данных.

import { app, input } from "@azure/functions";

const cosmosInput = input.cosmosDB({
    databaseName: 'stocksdb',
    containerName: 'stocks',
    connection: 'COSMOSDB_CONNECTION_STRING',
    sqlQuery: 'SELECT * from c',
});

app.http('getStocks', {
    methods: ['GET'],
    authLevel: 'anonymous',
    extraInputs: [cosmosInput],
    handler: (request, context) => {
        const stocks = context.extraInputs.get(cosmosInput);
        
        return {
            jsonBody: stocks,
        };
    },
});
  • Получение данных: первый раздел кода cosmosInput получает все элементы таблицы stocks с запросом SELECT * from cв stocksdb базе данных в Cosmos DB.
  • Возвращаемые данные: второй раздел кода app.http получает эти данные в функцию в качестве входных данных, context.extraInputs а затем возвращает его в качестве текста ответа обратно клиенту.

Клиент

Пример клиента использует Vue.js для создания пользовательского интерфейса и клиента получения для обработки запросов к API.

Html-страница использует таймер для отправки запроса на сервер каждые пять секунд для запроса запасов. В ответе возвращается массив акций, который затем показывается пользователю.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" integrity="sha256-8B1OaG0zT7uYA572S2xOxWACq9NXYPQ+U5kHPV1bJN4=" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Stocks | Enable automatic updates in a web application using Azure Functions and SignalR</title>
</head>
<body>
    
    <!-- BEGIN: Replace markup in this section -->
    <div id="app" class="container">
        <h1 class="title">Stocks</h1>
        <div id="stocks">
            <div v-for="stock in stocks" class="stock">
                <div class="lead">{{ stock.symbol }}: ${{ stock.price }}</div>
                <div class="change">Change:
                    <span :class="{ 'is-up': stock.changeDirection === '+', 'is-down': stock.changeDirection === '-' }">
                        {{ stock.changeDirection }}{{ stock.change }}
                    </span></div>
            </div>
        </div>
    </div>
    <!-- END  -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js" integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
    <script src="bundle.js" type="text/javascript"></script>
</body>
</html>
import './style.css';

function getApiUrl() {

    const backend = process.env.BACKEND_URL;
    
    const url = (backend) ? `${backend}` : ``;
    return url;
}

const app = new Vue({
    el: '#app',
    interval: null,
    data() { 
        return {
            stocks: []
        }
    },
    methods: {
        async update() {
            try {
                
                const url = `${getApiUrl()}/api/getStocks`;
                console.log('Fetching stocks from ', url);

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                app.stocks = await response.json();
            } catch (ex) {
                console.error(ex);
            }
        },
        startPoll() {
            this.interval = setInterval(this.update, 5000);
        }
    },
    created() {
        this.update();
        this.startPoll();
    }
});

startPoll После начала опроса update метод вызывается каждые пять секунд. В методе update запрос GET отправляется /api/getStocks в конечную точку API, а результат имеет значение , в котором обновляется app.stocksпользовательский интерфейс.

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

Анализ прототипа решения

Как инженер Tailwind Traders вы определили некоторые недостатки этого подхода на основе таймера опроса.

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

  • Ненужные обновления страниц: после возврата данных с сервера весь список запасов обновляется на веб-странице, даже если данные не изменились. Этот механизм опроса неэффективен.

  • Интервалы опроса. Выбор оптимального интервала опроса для вашего сценария также является проблемой. Опрос заставляет вас выбирать между затратами на каждое обращение к серверной части и скоростью реагирования приложения на новые данные. Задержки часто существуют между временем, когда новые данные становятся доступными и время обнаружения приложения. Эта проблема показана на рисунке ниже.

    Иллюстрация, показывающая временную шкалу и опрашивающий триггер, проверяющий наличие новых данных каждые пять минут. Новые данные становятся доступны через семь минут. Приложение не знает о них до следующего опроса, который происходит через 10 минут.

    В наихудшем случае возможная задержка при обнаружении новых данных равна интервалу опроса. Так почему бы не использовать меньший интервал?

  • Объем данных: по мере масштабирования приложения объем данных, обмен данными между клиентом и сервером становится проблемой. Заголовок каждого HTTP-запроса включает в себя сотни байтов данных, а также файл cookie сеанса. Все эти накладные расходы, особенно при высокой нагрузке, приводят к излишнему расходованию ресурсов сервера.

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

Поддержка CORS

В файле local.settings.json приложения функций раздел Host содержит следующие параметры.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<STORAGE_CONNECTION_STRING>",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "COSMOSDB_CONNECTION_STRING": "<COSMOSDB_CONNECTION_STRING>"
  },
  "Host" : {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:3000",
    "CORSCredentials": true
  }
}

Эта конфигурация позволяет веб-приложению, работающему в localhost:3000, выполнять запросы к приложению-функции, работающему в localhost:7071. Свойство CORSCredentials сообщает приложению-функции принять файлы cookie учетных данных из запроса.

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

При локальном запуске CORS настраивается для вас в файле local.settings.json примера, который никогда не публикуется. При развертывании клиентского приложения (единица 7) необходимо также обновить параметры CORS в приложении-функции, чтобы разрешить доступ из клиентского приложения.