Analizowanie ograniczeń aplikacji internetowej opartej na sondowaniu

Ukończone

Aplikacja internetowa oparta na sondowaniu.

Bieżąca architektura aplikacji zgłasza informacje o zapasach, pobierając wszystkie informacje o cenach akcji z serwera na podstawie czasomierza. Taki projekt jest często określany mianem architektury opartej 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,
        };
    },
});
  • Pobieranie danych: pierwsza sekcja kodu cosmosInput pobiera wszystkie elementy w stocks tabeli z zapytaniem SELECT * from cw stocksdb bazie danych w usłudze Cosmos DB.
  • Zwracane dane: druga sekcja kodu app.http odbiera te dane do funkcji jako dane wejściowecontext.extraInputs, a następnie zwraca je jako treść odpowiedzi z powrotem do klienta.

Klient

Przykładowy klient używa Vue.js do tworzenia interfejsu użytkownika i klienta pobierania 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óra następnie jest wyświetlana 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 startPoll sondowania metoda jest wywoływana update co pięć sekund. update Wewnątrz metody żądanie GET jest wysyłane do punktu końcowego interfejsu /api/getStocks API, a wynik jest ustawiony na app.stockswartość , która aktualizuje interfejs 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 zidentyfikowano niektóre wady tego podejścia do sondowania opartego na czasomierzu.

  • 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 powrocie danych z serwera cała lista akcji jest aktualizowana na stronie internetowej, nawet jeśli żadne dane nie uległy zmianie. Taki mechanizm sondowania to nieefektywne rozwiązanie.

  • Interwały sondowania: wybór najlepszego interwału sondowania dla danego scenariusza jest również wyzwaniem. Sondowanie wymusza dokonanie wyboru między kosztem każdego wywołania zaplecza a szybkością reagowania aplikacji 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ę. Poniższa ilustracja przedstawia ten 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żyć 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 to setki bajtów danych obejmujących plik cookie sesji. Cały ten narzut, szczególnie przy dużym obciążeniu, powoduje marnowanie zasobów i niepotrzebnie obciąża serwer.

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

Obsługa mechanizmu CORS

W pliku local.settings.json aplikacji Host usługi Functions sekcja 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 wykonywanie żądań do aplikacji funkcji uruchomionej pod adresem localhost:7071. Właściwość CORSCredentials informuje aplikację funkcji o zaakceptowaniu plików cookie poświadczeń z żądania.

Mechanizm CORS (udostępnianie zasobów między źródłami) to funkcja protokołu HTTP, która umożliwia aplikacji internetowej działającej w ramach jednej domeny dostęp do zasobów w innej domenie. Przeglądarki internetowe wdrażają ograniczenie bezpieczeństwa nazywane zasadami jednego źródła, które zapobiega wywoływaniu interfejsów API w innych domenach przez strony internetowe. Mechanizm CORS zapewnia bezpieczną metodę umożliwiania jednej domenie (domenie źródłowej) wywoływania interfejsów API z innej domeny.

W przypadku uruchamiania lokalnego mechanizm CORS jest skonfigurowany dla Użytkownika w pliku local.settings.json przykładu, 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.