Analizowanie ograniczeń aplikacji internetowej opartej na sondowaniu

Ukończone

aplikacji internetowej opartej na sondowaniu.

Bieżąca architektura aplikacji raportuje informacje o akcjach, pobierając wszystkie informacje o cenach akcji z serwera z użyciem zegara. Ten projekt jest często nazywany projektem opartym na sondowaniu.

Serwer

Informacje o cenach akcji są przechowywane w bazie danych usługi Azure Cosmos DB. Po wyzwoleniu przez żądanie HTTP funkcja getStocks zwraca wszystkie wiersze z bazy danych.

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,
        };
    },
});
  • Pobierz dane: pierwsza sekcja kodu cosmosInputpobiera wszystkie elementy z tabeli stocks, używając zapytania SELECT * from c, w bazie danych stocksdb w Cosmos DB.
  • Zwracanie danych: druga sekcja kodu app.httpodbiera te dane do funkcji jako dane wejściowe w context.extraInputs następnie zwraca je jako treść odpowiedzi z powrotem do klienta.

Klient

Przykładowy klient używa Vue.js do komponowania interfejsu użytkownika i Fetch client do obsługi żądań do interfejsu API.

Strona HTML używa czasomierza do wysyłania żądania do serwera co pięć sekund w celu żądania zasobów. Odpowiedź zwraca tablicę akcji, które są następnie wyświetlane użytkownikowi.

<!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();
    }
});

Po rozpoczęciu sondowania metody startPoll metoda update jest wywoływana co pięć sekund. W metodzie update wysyłane jest żądanie GET do punktu końcowego API /api/getStocks, a wynik jest przypisywany do app.stocks, co powoduje aktualizację interfejsu użytkownika.

Kod serwera i klienta jest stosunkowo prosty: pobierz wszystkie dane, wyświetl wszystkie dane. Jak dowiemy się w naszej analizie, ta prostota wiąże się z pewnymi ograniczeniami.

Analiza prototypowego rozwiązania

Jako inżynier firmy Tailwind Traders zidentyfikowałeś pewne wady podejścia sondowania opierającego się na mierniku czasu.

  • niepotrzebne żądania interfejsu API: w prototypie sondowania opartego na czasomierzu aplikacja kliencka kontaktuje się z serwerem, czy istnieją zmiany w danych bazowych.

  • Niepotrzebne odświeżanie strony: Po otrzymaniu danych z serwera cała lista akcji jest aktualizowana na stronie internetowej, nawet jeśli żadne dane nie uległy zmianie. Ten mechanizm sondowania jest nieefektywnym rozwiązaniem.

  • interwały sondowania: wybór najlepszego interwału sondowania dla danego scenariusza jest również wyzwaniem. Sondowanie wymusza wybór między kosztami poszczególnych wywołań zaplecza a tym, jak szybko aplikacja ma reagować na nowe dane. Opóźnienia często występują między czasem, przez jaki nowe dane staną się dostępne, a czasem wykrycia przez aplikację. Na poniższej ilustracji przedstawiono problem.

    Ilustracja przedstawiająca oś czasu i wyzwalacz sondowania sprawdzający nowe dane co pięć minut. Nowe dane staną się dostępne po siedmiu minutach. Aplikacja nie wie o nowych danych do momentu następnego sondowania, które nastąpi o 10 minutach.

    W najgorszym przypadku potencjalne opóźnienie wykrywania nowych danych jest równe interwałowi sondowania. Dlaczego więc nie używać mniejszego interwału?

  • ilość danych: w miarę skalowania aplikacji ilość danych wymienianych między klientem a serwerem staje się problemem. Każdy nagłówek żądania HTTP zawiera setki bajtów danych wraz z plikiem cookie sesji. Wszystkie te obciążenia, zwłaszcza w przypadku dużego obciążenia, tworzą zmarnowane zasoby i niepotrzebnie podatkujemy serwer.

Teraz, gdy znasz prototyp, nadszedł czas, aby aplikacja była uruchomiona na maszynie.

Obsługa mechanizmu CORS

W pliku local.settings.json aplikacji usługi Functions sekcja Host zawiera następujące ustawienia.

{
  "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
  }
}

Ta konfiguracja umożliwia aplikacji internetowej działającej pod adresem localhost:3000 wysyłanie żądań do aplikacji funkcji działającej pod adresem localhost:7071. Właściwość CORSCredentials informuje aplikację funkcyjną o zaakceptowaniu plików cookie poświadczeń z żądania.

Współużytkowanie zasobów między źródłami (CORS) to funkcja PROTOKOŁU HTTP, która umożliwia aplikacji internetowej działającej w jednej domenie uzyskiwanie dostępu do zasobów w innej domenie. Przeglądarki sieci Web implementują ograniczenie zabezpieczeń znane jako zasady tego samego źródła, które uniemożliwia stronie internetowej wywoływanie interfejsów API w innej domenie; Mechanizm CORS zapewnia bezpieczny sposób zezwalania jednej domenie (domenie pochodzenia) na wywoływanie interfejsów API w innej domenie.

W przypadku uruchamiania lokalnie, CORS jest skonfigurowany dla ciebie w pliku przykładu local.settings.json, który nigdy nie jest publikowany. Podczas wdrażania aplikacji klienckiej (lekcja 7) należy również zaktualizować ustawienia mechanizmu CORS w aplikacji funkcji, aby zezwolić na dostęp z aplikacji klienckiej.