ポーリングベースの Web アプリの制限事項を分析する

完了

ポーリングベースの Web アプリケーション。

アプリケーションの現在のアーキテクチャでは、タイマーに基づいてサーバーからすべての株価情報をフェッチすることで、株式情報をレポートします。 この設計は、ポーリングベースの設計とよく呼ばれます。

[サーバー]

株価情報は、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 は、クエリ SELECT * from c を使用して、Cosmos DB の stocksdb データベースにある stocks テーブル内のすべての項目を取得します。
  • データを返す: コードの 2 番目のセクションである app.http は、そのデータを関数で context.extraInputs の入力として受け取り、それを応答本文としてクライアントに返します。

クライアント

サンプル クライアントでは Vue.js を使用して、API への要求を処理するように UI と Fetch クライアントを構成します。

HTML ページではタイマーを使用して、5 秒ごとにサーバーに要求を送信し、株式を要求します。 ユーザーに表示される、株式の配列が応答で返されます。

<!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 メソッドがポーリングを開始すると、5 秒ごとに update メソッドが呼び出されます。 update メソッド内で、GET 要求が /api/getStocks API エンドポイントに送信され、結果は、UI を更新する app.stocks に設定されます。

サーバーとクライアントのコードは比較的簡単です。すべてのデータを取得し、すべてのデータを表示します。 分析からわかるとおり、このシンプルさにより、いくつかの制限が発生します。

プロトタイプ ソリューションの分析

あなたは Tailwind Traders のエンジニアとして、このタイマー ベースのポーリング アプローチの欠点をいくつか特定しました。

  • 不要な API 要求: タイマーベースのポーリング プロトタイプでは、基になるデータに対する変更が存在するかどうかに関係なく、クライアント アプリケーションはサーバーに接続されます。

  • 不要なページの更新: サーバーからデータが返されると、データが変更されていない場合でも、Web ページで株式のリスト全体が更新されます。 このポーリング メカニズムは非効率的なソリューションです。

  • ポーリング間隔: ご自分のシナリオに最適なポーリング間隔を選択することも困難です。 ポーリングでは、バックエンドへの各呼び出しのコストと、アプリでの新しいデータへの応答速度のどちらかを選ぶよう強制されます。 多くの場合、新しいデータが使用可能になり、アプリで検出されるまでには遅延が発生します。 以下の図にこの問題を示します。

    タイムラインと共に、5 分ごとに新しいデータの有無をチェックするポーリング トリガーを示す図。7 分後に新しいデータが使用可能になっています。新しいデータは、10 分後に行われる次のポーリングまでアプリでは認識されません。

    最悪の場合、新しいデータを検出するときの潜在的な遅延が、ポーリング間隔と同じになります。 それにもかかわらず、なぜより短い間隔を使用しないのでしょうか。

  • データの量: アプリケーションのスケーリング時に、クライアントとサーバー間で交換されるデータの量が問題になります。 各 HTTP 要求ヘッダーには、セッションの Cookie と共に、数百バイトのデータが含まれます。 このすべてのオーバーヘッドにより、特に大きな負荷がかかったときに、無駄なリソースが作成され、サーバーに不必要に負担がかかります。

これで、アプリケーションの開始点についてより理解できたため、次はコンピューター上でアプリケーションを実行します。

CORS のサポート

Functions アプリの 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 で実行されている Web アプリケーションで、localhost:7071 で実行されている関数アプリへの要求を行うことができます。 CORSCredentials プロパティによって、要求からの資格情報 Cookie を受け入れるように関数アプリに指示されます。

クロス オリジン リソース共有 (CORS) は、1 つのドメインの下で実行される Web アプリケーションが別のドメインのリソースにアクセスできるようにする HTTP 機能です。 Web ブラウザーには、Web ページで別のドメインの API を呼び出すことができないようにする同一呼び出し元ポリシーと呼ばれるセキュリティ制限が実装されています。CORS を使用すると、あるドメイン (元のドメイン) から別のドメインの API を安全に呼び出すことができます。

ローカルで実行する場合、CORS はサンプルの local.settings.json ファイルで構成されています。これは公開されません。 クライアント アプリ (ユニット 7) を展開する場合は、関数アプリの CORS 設定も更新して、クライアント アプリからのアクセスを許可する必要があります。